diff --git a/src/core/process/order.test.ts b/src/core/process/order.test.ts index 989e2230..d4103c8c 100644 --- a/src/core/process/order.test.ts +++ b/src/core/process/order.test.ts @@ -75,7 +75,7 @@ describe("Test processOrder", () => { takeOrder: { id: "0xid", quote: { maxOutput: 1000000000000000000n, ratio: 2000000000000000000n }, - takeOrder: {}, + struct: { order: { owner: "0xowner" } }, }, }, signer: {}, @@ -107,8 +107,11 @@ describe("Test processOrder", () => { expect(result.value.spanAttributes["details.order"]).toEqual("0xid"); expect(result.value.spanAttributes["details.pair"]).toBe("BUY/SELL"); expect(result.value.spanAttributes["details.orderbook"]).toEqual("0xorderbook"); - expect(result.value.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.quoteOrder"]).toHaveLength(2); + expect(result.value.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.value.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); expect(result.value.endTime).toBeTypeOf("number"); // ensure pair maps are updated on quote 0 @@ -136,8 +139,11 @@ describe("Test processOrder", () => { expect(result.error.spanAttributes["details.pair"]).toBe("BUY/SELL"); expect(result.error.spanAttributes["details.orderbook"]).toEqual("0xorderbook"); expect(result.error.endTime).toBeTypeOf("number"); - expect(result.error.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.error.spanAttributes["event.quoteOrder"]).toHaveLength(2); + expect(result.error.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.error.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); // ensure pair maps are updated on quote failure expect(mockOrderManager.removeFromPairMaps).toHaveBeenCalledWith(mockArgs.orderDetails); @@ -172,16 +178,23 @@ describe("Test processOrder", () => { expect(result.value.spanAttributes["details.inputToEthPrice"]).toBe("100"); expect(result.value.spanAttributes["details.outputToEthPrice"]).toBe("no-way"); expect(result.value.endTime).toBeTypeOf("number"); - expect(result.value.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.quoteOrder"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getPairMarketPrice"]).toBeGreaterThan( + expect(result.value.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.value.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getPairMarketPrice"]).toBeGreaterThan( 0, ); - expect(result.value.spanAttributes["event.getPairMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getEthMarketPrice"]).toBeGreaterThan( - 0, - ); - expect(result.value.spanAttributes["event.getEthMarketPrice"]).toHaveLength(2); + expect(result.value.spanEvents["getPairMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getEthMarketPrice"]).toBeGreaterThan(0); + expect(result.value.spanEvents["getEthMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it('should set outputToEthPrice to "0" if getMarketPrice returns undefined for output and gasCoveragePercentage is "0"', async () => { @@ -213,16 +226,23 @@ describe("Test processOrder", () => { expect(result.value.spanAttributes["details.inputToEthPrice"]).toBe("100"); expect(result.value.spanAttributes["details.outputToEthPrice"]).toBe("0"); expect(result.value.endTime).toBeTypeOf("number"); - expect(result.value.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.quoteOrder"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getPairMarketPrice"]).toBeGreaterThan( + expect(result.value.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.value.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getPairMarketPrice"]).toBeGreaterThan( 0, ); - expect(result.value.spanAttributes["event.getPairMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getEthMarketPrice"]).toBeGreaterThan( - 0, - ); - expect(result.value.spanAttributes["event.getEthMarketPrice"]).toHaveLength(2); + expect(result.value.spanEvents["getPairMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getEthMarketPrice"]).toBeGreaterThan(0); + expect(result.value.spanEvents["getEthMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it('should return FailedToGetEthPrice if getMarketPrice returns undefined and gasCoveragePercentage is not "0"', async () => { @@ -248,12 +268,18 @@ describe("Test processOrder", () => { expect(result.error.spanAttributes["details.pair"]).toBe("BUY/SELL"); expect(result.error.spanAttributes["details.orderbook"]).toEqual("0xorderbook"); expect(result.error.endTime).toBeTypeOf("number"); - expect(result.error.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.error.spanAttributes["event.quoteOrder"]).toHaveLength(2); - expect(result.error.spanAttributes["details.duration.getPairMarketPrice"]).toBeGreaterThan( + expect(result.error.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.error.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.error.spanAttributes["events.duration.getPairMarketPrice"]).toBeGreaterThan( 0, ); - expect(result.error.spanAttributes["event.getPairMarketPrice"]).toHaveLength(2); + expect(result.error.spanEvents["getPairMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it('should set input/outputToEthPrice to "0" if getMarketPrice returns undefined and gasCoveragePercentage is "0"', async () => { @@ -281,16 +307,23 @@ describe("Test processOrder", () => { expect(result.value.spanAttributes["details.pair"]).toBe("BUY/SELL"); expect(result.value.spanAttributes["details.orderbook"]).toEqual("0xorderbook"); expect(result.value.endTime).toBeTypeOf("number"); - expect(result.value.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.quoteOrder"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getPairMarketPrice"]).toBeGreaterThan( - 0, - ); - expect(result.value.spanAttributes["event.getPairMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getEthMarketPrice"]).toBeGreaterThan( + expect(result.value.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.value.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getPairMarketPrice"]).toBeGreaterThan( 0, ); - expect(result.value.spanAttributes["event.getEthMarketPrice"]).toHaveLength(2); + expect(result.value.spanEvents["getPairMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getEthMarketPrice"]).toBeGreaterThan(0); + expect(result.value.spanEvents["getEthMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it("should return ok result if findBestTrade throws with noneNodeError", async () => { @@ -323,18 +356,28 @@ describe("Test processOrder", () => { expect(result.value.spanAttributes["details.noneNodeError"]).toBe(true); expect(result.value.spanAttributes["details.test"]).toBe("something"); expect(result.value.endTime).toBeTypeOf("number"); - expect(result.value.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.quoteOrder"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getPairMarketPrice"]).toBeGreaterThan( - 0, - ); - expect(result.value.spanAttributes["event.getPairMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getEthMarketPrice"]).toBeGreaterThan( + expect(result.value.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.value.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getPairMarketPrice"]).toBeGreaterThan( 0, ); - expect(result.value.spanAttributes["event.getEthMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.findBestTrade"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.findBestTrade"]).toHaveLength(2); + expect(result.value.spanEvents["getPairMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getEthMarketPrice"]).toBeGreaterThan(0); + expect(result.value.spanEvents["getEthMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.findBestTrade"]).toBeGreaterThan(0); + expect(result.value.spanEvents["findBestTrade"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it("should return ok result if findBestTrade throws without noneNodeError", async () => { @@ -367,18 +410,28 @@ describe("Test processOrder", () => { expect(result.value.spanAttributes["details.noneNodeError"]).toBe(false); expect(result.value.spanAttributes["details.test"]).toBe("something"); expect(result.value.endTime).toBeTypeOf("number"); - expect(result.value.spanAttributes["details.duration.quoteOrder"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.quoteOrder"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getPairMarketPrice"]).toBeGreaterThan( - 0, - ); - expect(result.value.spanAttributes["event.getPairMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.getEthMarketPrice"]).toBeGreaterThan( + expect(result.value.spanAttributes["events.duration.quoteOrder"]).toBeGreaterThan(0); + expect(result.value.spanEvents["quoteOrder"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getPairMarketPrice"]).toBeGreaterThan( 0, ); - expect(result.value.spanAttributes["event.getEthMarketPrice"]).toHaveLength(2); - expect(result.value.spanAttributes["details.duration.findBestTrade"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.findBestTrade"]).toHaveLength(2); + expect(result.value.spanEvents["getPairMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.getEthMarketPrice"]).toBeGreaterThan(0); + expect(result.value.spanEvents["getEthMarketPrice"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); + expect(result.value.spanAttributes["events.duration.findBestTrade"]).toBeGreaterThan(0); + expect(result.value.spanEvents["findBestTrade"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it("should proceed to processTransaction if all steps succeed (happy path)", async () => { diff --git a/src/core/process/order.ts b/src/core/process/order.ts index b2753bd1..766fe964 100644 --- a/src/core/process/order.ts +++ b/src/core/process/order.ts @@ -4,11 +4,13 @@ import { Result } from "../../common"; import { toNumber } from "../../math"; import { Token } from "sushi/currency"; import { errorSnapshot } from "../../error"; +import { SpanWithContext } from "../../logger"; import { formatUnits, parseUnits } from "viem"; import { Attributes } from "@opentelemetry/api"; import { RainSolverSigner } from "../../signer"; import { processTransaction } from "./transaction"; import { + OrderSpanEvents, ProcessOrderStatus, ProcessOrderSuccess, ProcessOrderFailure, @@ -21,6 +23,8 @@ export type ProcessOrderArgs = { orderDetails: Pair; signer: RainSolverSigner; blockNumber: bigint; + startTime: number; + roundSpanCtx?: SpanWithContext; }; /** @@ -32,7 +36,7 @@ export async function processOrder( this: RainSolver, args: ProcessOrderArgs, ): Promise<() => Promise>> { - const { orderDetails, signer, blockNumber: dataFetcherBlockNumber } = args; + const { orderDetails, signer, blockNumber: dataFetcherBlockNumber, startTime } = args; const fromToken = new Token({ chainId: this.state.chainConfig.id, decimals: orderDetails.sellTokenDecimals, @@ -46,6 +50,7 @@ export async function processOrder( symbol: orderDetails.buyTokenSymbol, }); const spanAttributes: Attributes = {}; + const spanEvents: OrderSpanEvents = {}; const tokenPair = `${orderDetails.buyTokenSymbol}/${orderDetails.sellTokenSymbol}`; const baseResult: ProcessOrderResultBase = { tokenPair, @@ -53,10 +58,12 @@ export async function processOrder( sellToken: orderDetails.sellToken, status: ProcessOrderStatus.NoOpportunity, // set default status to no opp spanAttributes, + spanEvents, }; spanAttributes["details.order"] = orderDetails.takeOrder.id; spanAttributes["details.pair"] = tokenPair; spanAttributes["details.orderbook"] = orderDetails.orderbook; + spanAttributes["details.owner"] = orderDetails.takeOrder.struct.order.owner.toLowerCase(); const quoteOrderTime = performance.now(); try { @@ -67,8 +74,8 @@ export async function processOrder( this.orderManager.removeFromPairMaps(orderDetails); const endTime = performance.now(); const quoteOrderDuration = endTime - quoteOrderTime; - spanAttributes["details.duration.quoteOrder"] = quoteOrderDuration; - spanAttributes["event.quoteOrder"] = [quoteOrderTime, quoteOrderDuration]; + spanAttributes["events.duration.quoteOrder"] = quoteOrderDuration; + spanEvents["quoteOrder"] = { startTime: quoteOrderTime, duration: quoteOrderDuration }; return async () => { return Result.ok({ ...baseResult, @@ -83,8 +90,8 @@ export async function processOrder( this.orderManager.removeFromPairMaps(orderDetails); const endTime = performance.now(); const quoteOrderDuration = endTime - quoteOrderTime; - spanAttributes["details.duration.quoteOrder"] = quoteOrderDuration; - spanAttributes["event.quoteOrder"] = [quoteOrderTime, quoteOrderDuration]; + spanAttributes["events.duration.quoteOrder"] = quoteOrderDuration; + spanEvents["quoteOrder"] = { startTime: quoteOrderTime, duration: quoteOrderDuration }; return async () => Result.err({ ...baseResult, @@ -96,8 +103,8 @@ export async function processOrder( // record order quote details in span attributes const quoteOrderDuration = performance.now() - quoteOrderTime; - spanAttributes["details.duration.quoteOrder"] = quoteOrderDuration; - spanAttributes["event.quoteOrder"] = [quoteOrderTime, quoteOrderDuration]; + spanAttributes["events.duration.quoteOrder"] = quoteOrderDuration; + spanEvents["quoteOrder"] = { startTime: quoteOrderTime, duration: quoteOrderDuration }; spanAttributes["details.quote"] = JSON.stringify({ maxOutput: formatUnits(orderDetails.takeOrder.quote!.maxOutput, 18), ratio: formatUnits(orderDetails.takeOrder.quote!.ratio, 18), @@ -118,11 +125,11 @@ export async function processOrder( ); } const getPairMarketPriceDuration = performance.now() - getPairMarketPriceTime; - spanAttributes["details.duration.getPairMarketPrice"] = getPairMarketPriceDuration; - spanAttributes["event.getPairMarketPrice"] = [ - getPairMarketPriceTime, - getPairMarketPriceDuration, - ]; + spanAttributes["events.duration.getPairMarketPrice"] = getPairMarketPriceDuration; + spanEvents["getPairMarketPrice"] = { + startTime: getPairMarketPriceTime, + duration: getPairMarketPriceDuration, + }; // get in/out tokens to eth price const getEthMarketPriceTime = performance.now(); @@ -175,8 +182,11 @@ export async function processOrder( } spanAttributes["gasPriceMultiplier"] = this.state.gasPriceMultiplier; const getEthMarketPriceDuration = performance.now() - getEthMarketPriceTime; - spanAttributes["details.duration.getEthMarketPrice"] = getEthMarketPriceDuration; - spanAttributes["event.getEthMarketPrice"] = [getEthMarketPriceTime, getEthMarketPriceDuration]; + spanAttributes["events.duration.getEthMarketPrice"] = getEthMarketPriceDuration; + spanEvents["getEthMarketPrice"] = { + startTime: getEthMarketPriceTime, + duration: getEthMarketPriceDuration, + }; const findBestTradeTime = performance.now(); const trade = await this.findBestTrade({ @@ -205,8 +215,11 @@ export async function processOrder( spanAttributes["details.noneNodeError"] = false; } const findBestTradeDuration = endTime - findBestTradeTime; - spanAttributes["details.duration.findBestTrade"] = findBestTradeDuration; - spanAttributes["event.findBestTrade"] = [findBestTradeTime, findBestTradeDuration]; + spanAttributes["events.duration.findBestTrade"] = findBestTradeDuration; + spanEvents["findBestTrade"] = { + startTime: findBestTradeTime, + duration: findBestTradeDuration, + }; return async () => Result.ok(result); } @@ -225,8 +238,8 @@ export async function processOrder( } } const findBestTradeDuration = endTime - findBestTradeTime; - spanAttributes["details.duration.findBestTrade"] = findBestTradeDuration; - spanAttributes["event.findBestTrade"] = [findBestTradeTime, findBestTradeDuration]; + spanAttributes["events.duration.findBestTrade"] = findBestTradeDuration; + spanEvents["findBestTrade"] = { startTime: findBestTradeTime, duration: findBestTradeDuration }; // get block number let blockNumber: number; @@ -244,14 +257,16 @@ export async function processOrder( } // process the found transaction opportunity - return processTransaction({ + return processTransaction.call(this, { rawtx, signer, toToken, fromToken, + startTime, baseResult, inputToEthPrice, outputToEthPrice, orderbook: orderDetails.orderbook as `0x${string}`, + roundSpanCtx: args.roundSpanCtx, }); } diff --git a/src/core/process/receipt.test.ts b/src/core/process/receipt.test.ts index 6d32de8b..fee75245 100644 --- a/src/core/process/receipt.test.ts +++ b/src/core/process/receipt.test.ts @@ -65,6 +65,7 @@ describe("Test processReceipt", () => { buyToken: "0xUSDC", sellToken: "0xETH", spanAttributes: {}, + spanEvents: {}, status: ProcessOrderStatus.FoundOpportunity, }, txUrl: "https://etherscan.io/tx/0xTxHash123", @@ -117,8 +118,11 @@ describe("Test processReceipt", () => { expect(result.value.spanAttributes["details.netProfit"]).toBeDefined(); expect(result.value.spanAttributes["details.netProfit"]).toBeTypeOf("number"); expect(result.value.spanAttributes["details.gasCostL1"]).toBeUndefined(); - expect(result.value.spanAttributes["details.duration.transaction"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.transaction"]).toHaveLength(2); + expect(result.value.spanAttributes["events.duration.transaction"]).toBeGreaterThan(0); + expect(result.value.spanEvents["transaction"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); it("should calculate gas cost correctly including L1 fee", async () => { @@ -150,8 +154,11 @@ describe("Test processReceipt", () => { expect(result.value.inputTokenIncome).toBeUndefined(); expect(result.value.outputTokenIncome).toBeUndefined(); expect(result.value.endTime).toBeTypeOf("number"); - expect(result.value.spanAttributes["details.duration.transaction"]).toBeGreaterThan(0); - expect(result.value.spanAttributes["event.transaction"]).toHaveLength(2); + expect(result.value.spanAttributes["events.duration.transaction"]).toBeGreaterThan(0); + expect(result.value.spanEvents["transaction"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); }); }); @@ -176,8 +183,11 @@ describe("Test processReceipt", () => { expect(result.error.error).toBe(mockSimulation); expect(result.error.txUrl).toBe(mockArgs.txUrl); expect(result.error.spanAttributes["txNoneNodeError"]).toBe(true); - expect(result.error.spanAttributes["details.duration.transaction"]).toBeGreaterThan(0); - expect(result.error.spanAttributes["event.transaction"]).toHaveLength(2); + expect(result.error.spanAttributes["events.duration.transaction"]).toBeGreaterThan(0); + expect(result.error.spanEvents["transaction"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); expect(result.error.endTime).toBeTypeOf("number"); }); @@ -213,8 +223,11 @@ describe("Test processReceipt", () => { assert(result.isErr()); expect(result.error.spanAttributes["txNoneNodeError"]).toBe(false); - expect(result.error.spanAttributes["details.duration.transaction"]).toBeGreaterThan(0); - expect(result.error.spanAttributes["event.transaction"]).toHaveLength(2); + expect(result.error.spanAttributes["events.duration.transaction"]).toBeGreaterThan(0); + expect(result.error.spanEvents["transaction"]).toEqual({ + startTime: expect.any(Number), + duration: expect.any(Number), + }); expect(result.error.endTime).toBeTypeOf("number"); }); diff --git a/src/core/process/receipt.ts b/src/core/process/receipt.ts index 78b080a1..0219f2a5 100644 --- a/src/core/process/receipt.ts +++ b/src/core/process/receipt.ts @@ -8,9 +8,9 @@ import { Result, sleep, RawTransaction } from "../../common"; import { getIncome, getTotalIncome, getActualClearAmount } from "./log"; import { ProcessOrderFailure, - ProcessOrderSuccess, ProcessOrderHaltReason, ProcessOrderResultBase, + ProcessTransactionSuccess, } from "../types"; /** Arguments for processing a transaction receipt */ @@ -45,14 +45,14 @@ export async function processReceipt({ baseResult, inputToEthPrice, outputToEthPrice, -}: ProcessReceiptArgs): Promise> { +}: ProcessReceiptArgs): Promise> { const l1Fee = getL1Fee(receipt); const gasCost = receipt.effectiveGasPrice * receipt.gasUsed + l1Fee; // record transaction time const txMineDuration = performance.now() - txSendTime; - baseResult.spanAttributes["details.duration.transaction"] = txMineDuration; - baseResult.spanAttributes["event.transaction"] = [txSendTime, txMineDuration]; + baseResult.spanAttributes["events.duration.transaction"] = txMineDuration; + baseResult.spanEvents["transaction"] = { startTime: txSendTime, duration: txMineDuration }; // keep track of gas consumption of the account and bounty token baseResult.gasCost = gasCost; @@ -99,7 +99,7 @@ export async function processReceipt({ ); } - const success: ProcessOrderSuccess = { + const success: ProcessTransactionSuccess = { ...baseResult, clearedAmount: clearActualAmount?.toString(), gasCost: gasCost, @@ -111,10 +111,7 @@ export async function processReceipt({ endTime: performance.now(), }; - return Result.ok({ - ...baseResult, - ...success, - }); + return Result.ok(success); } else { const simulation = await (async () => { const signerBalance = await signer.getSelfBalance(); diff --git a/src/core/process/round.test.ts b/src/core/process/round.test.ts index e01021a8..e62bfc7c 100644 --- a/src/core/process/round.test.ts +++ b/src/core/process/round.test.ts @@ -123,7 +123,7 @@ describe("Test initializeRound", () => { const settlement = result.settlements[0]; expect(settlement.pair).toBe("ETH/USDC"); - expect(settlement.owner).toBe("0xOwner123"); + expect(settlement.owner).toBe("0xowner123"); expect(settlement.orderHash).toBe("0xOrder123"); expect(settlement.startTime).toBeTypeOf("number"); expect(settlement.settle).toBe(mockSettleFn); @@ -133,7 +133,7 @@ describe("Test initializeRound", () => { expect(checkpointReport.name).toBe("checkpoint_ETH/USDC"); expect(checkpointReport.attributes["details.pair"]).toBe("ETH/USDC"); expect(checkpointReport.attributes["details.orderHash"]).toBe("0xOrder123"); - expect(checkpointReport.attributes["details.owner"]).toBe("0xOwner123"); + expect(checkpointReport.attributes["details.owner"]).toBe("0xowner123"); expect(checkpointReport.attributes["details.sender"]).toBe("0xSigner123"); expect(checkpointReport.endTime).toBeTypeOf("number"); }); @@ -279,6 +279,7 @@ describe("Test initializeRound", () => { orderDetails, signer: mockSigner, blockNumber: 123n, + startTime: expect.any(Number), }); }); }); @@ -309,7 +310,7 @@ describe("Test initializeRound", () => { expect(report.attributes["details.orderHash"]).toBe("0xOrderABC"); expect(report.attributes["details.orderbook"]).toBeTypeOf("string"); expect(report.attributes["details.sender"]).toBe("0xSignerDEF"); - expect(report.attributes["details.owner"]).toBe("0xOwnerXYZ"); + expect(report.attributes["details.owner"]).toBe("0xownerxyz"); }); it("should create one checkpoint report per order", async () => { @@ -522,7 +523,7 @@ describe("Test initializeRound", () => { // first settlement (zero balance) - should be skipped and have ZeroOutput status const zeroBalanceSettlement = result.settlements[0]; expect(zeroBalanceSettlement.pair).toBe("ETH/USDC"); - expect(zeroBalanceSettlement.owner).toBe("0xOwner1"); + expect(zeroBalanceSettlement.owner).toBe("0xowner1"); expect(zeroBalanceSettlement.orderHash).toBe("0xOrder1"); // test the settle function for zero balance order @@ -542,7 +543,7 @@ describe("Test initializeRound", () => { // second settlement (non-zero balance) - should be processed normally const normalSettlement = result.settlements[1]; expect(normalSettlement.pair).toBe("BTC/USDT"); - expect(normalSettlement.owner).toBe("0xOwner2"); + expect(normalSettlement.owner).toBe("0xowner2"); expect(normalSettlement.orderHash).toBe("0xOrder2"); expect(normalSettlement.settle).toBe(mockSettleFn); @@ -552,6 +553,7 @@ describe("Test initializeRound", () => { orderDetails: mockOrders[1], // second order with non-zero balance signer: mockSigner, blockNumber: 123n, + startTime: expect.any(Number), }); // verify getRandomSigner was called only once (for the non-zero balance order) @@ -563,7 +565,7 @@ describe("Test initializeRound", () => { expect(zeroBalanceReport.name).toBe("checkpoint_ETH/USDC"); expect(zeroBalanceReport.attributes["details.pair"]).toBe("ETH/USDC"); expect(zeroBalanceReport.attributes["details.orderHash"]).toBe("0xOrder1"); - expect(zeroBalanceReport.attributes["details.owner"]).toBe("0xOwner1"); + expect(zeroBalanceReport.attributes["details.owner"]).toBe("0xowner1"); expect(zeroBalanceReport.attributes["details.orderbook"]).toBe( "0x3333333333333333333333333333333333333333", ); @@ -575,7 +577,7 @@ describe("Test initializeRound", () => { expect(normalReport.name).toBe("checkpoint_BTC/USDT"); expect(normalReport.attributes["details.pair"]).toBe("BTC/USDT"); expect(normalReport.attributes["details.orderHash"]).toBe("0xOrder2"); - expect(normalReport.attributes["details.owner"]).toBe("0xOwner2"); + expect(normalReport.attributes["details.owner"]).toBe("0xowner2"); expect(normalReport.attributes["details.orderbook"]).toBe( "0x4444444444444444444444444444444444444444", ); @@ -617,7 +619,7 @@ describe("Test initializeRound", () => { // first settlement (zero balance) - should be skipped and have ZeroOutput status const zeroBalanceSettlement = result.settlements[0]; expect(zeroBalanceSettlement.pair).toBe("ETH/USDC"); - expect(zeroBalanceSettlement.owner).toBe("0xOwner1"); + expect(zeroBalanceSettlement.owner).toBe("0xowner1"); expect(zeroBalanceSettlement.orderHash).toBe("0xOrder1"); // test the settle function for missing trade addresses @@ -662,6 +664,7 @@ describe("Test finalizeRound", () => { status: ProcessOrderStatus.ZeroOutput, tokenPair: "ETH/USDC", spanAttributes: { "test.attr": "value" }, + spanEvents: {}, endTime: 789, }), ); @@ -689,6 +692,7 @@ describe("Test finalizeRound", () => { gasCost: 1000000n, spanAttributes: { "test.attr": "value" }, endTime: 789, + spanEvents: {}, }); // assert gas cost tracking @@ -700,7 +704,6 @@ describe("Test finalizeRound", () => { expect(report.name).toBe("order_ETH/USDC"); expect(report.startTime).toBe(123); expect(report.endTime).toBe(789); - expect(report.attributes["details.owner"]).toBe("0x123"); expect(report.attributes["test.attr"]).toBe("value"); expect(report.status?.code).toBe(SpanStatusCode.OK); expect(report.status?.message).toBe("zero max output"); @@ -712,6 +715,7 @@ describe("Test finalizeRound", () => { status: ProcessOrderStatus.NoOpportunity, spanAttributes: { liquidity: "low" }, message: "insufficient liquidity", + spanEvents: {}, endTime: 789, }), ); @@ -735,6 +739,7 @@ describe("Test finalizeRound", () => { spanAttributes: { liquidity: "low" }, message: "insufficient liquidity", endTime: 789, + spanEvents: {}, }); expect(result.reports[0].startTime).toBe(123); expect(result.reports[0].endTime).toBe(789); @@ -748,6 +753,7 @@ describe("Test finalizeRound", () => { Result.ok({ status: ProcessOrderStatus.NoOpportunity, spanAttributes: {}, + spanEvents: {}, endTime: 789, }), ); @@ -776,6 +782,7 @@ describe("Test finalizeRound", () => { status: ProcessOrderStatus.FoundOpportunity, profit: "0.05", spanAttributes: { "profit.eth": "0.05" }, + spanEvents: {}, endTime: 789, }), ); @@ -799,6 +806,7 @@ describe("Test finalizeRound", () => { profit: "0.05", spanAttributes: { "profit.eth": "0.05" }, endTime: 789, + spanEvents: {}, }); expect(result.reports[0].status?.code).toBe(SpanStatusCode.OK); expect(result.reports[0].status?.message).toBe("found opportunity"); @@ -812,6 +820,7 @@ describe("Test finalizeRound", () => { Result.ok({ status: "UNKNOWN_STATUS" as any, spanAttributes: { custom: "attr" }, + spanEvents: {}, endTime: 789, }), ); @@ -840,6 +849,7 @@ describe("Test finalizeRound", () => { Result.ok({ status: ProcessOrderStatus.FoundOpportunity, spanAttributes: {}, + spanEvents: {}, endTime: 789, // No gasCost provided }), @@ -865,9 +875,10 @@ describe("Test finalizeRound", () => { const mockSettle = vi.fn().mockResolvedValue( Result.ok({ status: ProcessOrderStatus.FoundOpportunity, - spanAttributes: { - "event.something": [1234, 456], - "event.another": [5678, 123], + spanAttributes: {}, + spanEvents: { + something: { startTime: 1234, duration: 456 }, + another: { startTime: 5678, duration: 123 }, }, endTime: 789, }), @@ -896,6 +907,7 @@ describe("Test finalizeRound", () => { status: ProcessOrderStatus.UndefinedTradeAddresses, tokenPair: "ETH/USDC", spanAttributes: { "test.attr": "value" }, + spanEvents: {}, endTime: 789, message: "undefined addresses", }), @@ -924,6 +936,7 @@ describe("Test finalizeRound", () => { spanAttributes: { "test.attr": "value" }, endTime: 789, message: "undefined addresses", + spanEvents: {}, }); // assert span creation and attributes @@ -931,7 +944,6 @@ describe("Test finalizeRound", () => { expect(report.name).toBe("order_ETH/USDC"); expect(report.startTime).toBe(123); expect(report.endTime).toBe(789); - expect(report.attributes["details.owner"]).toBe("0x123"); expect(report.attributes["test.attr"]).toBe("value"); expect(report.status?.code).toBe(SpanStatusCode.OK); expect(report.status?.message).toBe("undefined addresses"); @@ -944,6 +956,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.FailedToQuote, spanAttributes: { provider: "chainlink" }, + spanEvents: {}, status: "failed", endTime: 789, }), @@ -968,6 +981,7 @@ describe("Test finalizeRound", () => { reason: ProcessOrderHaltReason.FailedToQuote, spanAttributes: { provider: "chainlink" }, endTime: 789, + spanEvents: {}, }); expect(result.reports[0].status?.code).toBe(SpanStatusCode.OK); expect(result.reports[0].status?.message).toBe("failed to quote order: 0xabc"); @@ -981,6 +995,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.FailedToQuote, spanAttributes: { "retry.count": "3" }, + spanEvents: {}, status: "failed", error, endTime: 789, @@ -1016,6 +1031,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.FailedToGetPools, spanAttributes: { "pool.count": "0" }, + spanEvents: {}, status: "failed", error, endTime: 789, @@ -1054,6 +1070,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.FailedToGetEthPrice, spanAttributes: {}, + spanEvents: {}, status: "failed", error, endTime: 789, @@ -1086,6 +1103,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.FailedToUpdatePools, spanAttributes: { "test.attr": "value" }, + spanEvents: {}, status: "failed", error: new Error("update failed"), endTime: 789, @@ -1121,6 +1139,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxFailed, spanAttributes: { "tx.hash": "0x123" }, + spanEvents: {}, status: "failed", error: timeoutError, endTime: 789, @@ -1153,6 +1172,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxFailed, spanAttributes: {}, + spanEvents: {}, status: "failed", error, endTime: 789, @@ -1182,6 +1202,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxFailed, spanAttributes: { "test.attr": "value" }, + spanEvents: {}, status: "failed", endTime: 789, }), @@ -1211,6 +1232,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxReverted, spanAttributes: { "block.number": "12345" }, + spanEvents: {}, status: "reverted", error: { snapshot: "Transaction reverted: INSUFFICIENT_LIQUIDITY" }, endTime: 789, @@ -1243,6 +1265,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxReverted, spanAttributes: { "test.attr": "value" }, + spanEvents: {}, status: "failed", error: { err: new Error("INSUFFICIENT_LIQUIDITY") }, // This is typically a known error endTime: 789, @@ -1273,6 +1296,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxReverted, spanAttributes: { txNoneNodeError: true }, + spanEvents: {}, status: "reverted", error: { err: new Error("unknown revert") }, endTime: 789, @@ -1305,6 +1329,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxMineFailed, spanAttributes: { "test.attr": "value" }, + spanEvents: {}, status: "failed", error: timeoutError, endTime: 789, @@ -1336,6 +1361,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxMineFailed, spanAttributes: { "test.attr": "value" }, + spanEvents: {}, status: "failed", error: new Error("rpc error"), endTime: 789, @@ -1365,6 +1391,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: "unknown_reason", spanAttributes: { "test.attr": "value" }, + spanEvents: {}, status: "failed", error: new Error("unexpected"), endTime: 789, @@ -1399,9 +1426,10 @@ describe("Test finalizeRound", () => { const mockSettle = vi.fn().mockResolvedValue( Result.err({ reason: "unknown_reason", - spanAttributes: { - "event.something": [1234, 456], - "event.another": [5678, 123], + spanAttributes: {}, + spanEvents: { + something: { startTime: 1234, duration: 456 }, + another: { startTime: 5678, duration: 123 }, }, status: "failed", error: new Error("unexpected"), @@ -1435,6 +1463,7 @@ describe("Test finalizeRound", () => { status: ProcessOrderStatus.FoundOpportunity, txUrl: "url1", spanAttributes: { success: true }, + spanEvents: {}, endTime: 789, }), ); @@ -1443,6 +1472,7 @@ describe("Test finalizeRound", () => { Result.err({ reason: ProcessOrderHaltReason.TxFailed, spanAttributes: { failed: true }, + spanEvents: {}, status: "failed", txUrl: "url2", error: new Error("tx failed"), @@ -1507,6 +1537,7 @@ describe("Test finalizeRound", () => { Result.ok({ status: ProcessOrderStatus.FoundOpportunity, spanAttributes: {}, + spanEvents: {}, endTime: 789, }), ); @@ -1524,7 +1555,6 @@ describe("Test finalizeRound", () => { const result: finalizeRoundType = await finalizeRound.call(mockSolver, settlements); expect(result.reports[0].name).toBe("order_ETH/USDC"); - expect(result.reports[0].attributes["details.owner"]).toBe("0x123"); expect(result.reports[0].endTime).toBeTypeOf("number"); expect(result.reports[0].status?.code).toBe(SpanStatusCode.OK); expect(result.reports[0].startTime).toBe(123); @@ -1537,6 +1567,7 @@ describe("Test finalizeRound", () => { Result.ok({ status: ProcessOrderStatus.FoundOpportunity, spanAttributes, + spanEvents: {}, endTime: 789, }), ); @@ -1564,7 +1595,8 @@ describe("Test finalizeRound", () => { const mockSettle = vi.fn().mockResolvedValue( Result.ok({ status: ProcessOrderStatus.FoundOpportunity, - spanAttributes: { "event.something": 1234, "event.another": 5678 }, + spanAttributes: {}, + spanEvents: { something: 1234, another: 5678 }, endTime: 789, }), ); @@ -1599,7 +1631,8 @@ describe("Test finalizeRound", () => { const mockSettle = vi.fn().mockResolvedValue( Result.ok({ status: ProcessOrderStatus.FoundOpportunity, - spanAttributes: { "event.something": 1234, "event.another": 5678 }, + spanAttributes: {}, + spanEvents: { something: 1234, another: 5678 }, endTime: 789, }), ); @@ -1765,7 +1798,7 @@ describe("Test processOrderInit", () => { const settlement = result.settlement; expect(settlement.pair).toBe("ETH/USDC"); - expect(settlement.owner).toBe("0xOwner123"); + expect(settlement.owner).toBe("0xowner123"); expect(settlement.orderHash).toBe("0xOrder123"); expect(settlement.startTime).toBeTypeOf("number"); expect(settlement.settle).toBe(mockSettleFn); @@ -1775,7 +1808,7 @@ describe("Test processOrderInit", () => { expect(checkpointReport.name).toBe("checkpoint_ETH/USDC"); expect(checkpointReport.attributes["details.pair"]).toBe("ETH/USDC"); expect(checkpointReport.attributes["details.orderHash"]).toBe("0xOrder123"); - expect(checkpointReport.attributes["details.owner"]).toBe("0xOwner123"); + expect(checkpointReport.attributes["details.owner"]).toBe("0xowner123"); expect(checkpointReport.attributes["details.sender"]).toBe("0xSigner123"); expect(checkpointReport.endTime).toBeTypeOf("number"); }); @@ -1816,6 +1849,7 @@ describe("Test processOrderInit", () => { orderDetails, signer: mockSigner, blockNumber: 123n, + startTime: expect.any(Number), }); }); }); @@ -1844,7 +1878,7 @@ describe("Test processOrderInit", () => { expect(report.attributes["details.orderHash"]).toBe("0xOrderABC"); expect(report.attributes["details.orderbook"]).toBeTypeOf("string"); expect(report.attributes["details.sender"]).toBe("0xSignerDEF"); - expect(report.attributes["details.owner"]).toBe("0xOwnerXYZ"); + expect(report.attributes["details.owner"]).toBe("0xownerxyz"); }); it("should create one checkpoint report per order", async () => { diff --git a/src/core/process/round.ts b/src/core/process/round.ts index f88fbd43..e1ef6a11 100644 --- a/src/core/process/round.ts +++ b/src/core/process/round.ts @@ -145,7 +145,7 @@ export async function processOrderInit( "details.pair": pair, "details.orderHash": orderDetails.takeOrder.id, "details.orderbook": orderDetails.orderbook, - "details.owner": orderDetails.takeOrder.struct.order.owner, + "details.owner": owner, }); // get updated balance for the orderDetails from owner vaults map @@ -179,9 +179,9 @@ export async function processOrderInit( const endTime = performance.now(); const settlement: Settlement = { pair, + owner, startTime, orderHash: orderDetails.takeOrder.id, - owner: orderDetails.takeOrder.struct.order.owner, settle: async () => { return Result.ok({ endTime, @@ -193,6 +193,7 @@ export async function processOrderInit( "details.pair": pair, "details.orders": orderDetails.takeOrder.id, }, + spanEvents: {}, }); }, }; @@ -211,9 +212,9 @@ export async function processOrderInit( const endTime = performance.now(); const settlement: Settlement = { pair, + owner, startTime, orderHash: orderDetails.takeOrder.id, - owner: orderDetails.takeOrder.struct.order.owner, settle: async () => { return Result.ok({ endTime, @@ -226,6 +227,7 @@ export async function processOrderInit( "details.pair": pair, "details.orders": orderDetails.takeOrder.id, }, + spanEvents: {}, }); }, }; @@ -250,13 +252,15 @@ export async function processOrderInit( orderDetails, signer, blockNumber, + startTime, + roundSpanCtx, }); const settlement: Settlement = { settle, pair, + owner, startTime, orderHash: orderDetails.takeOrder.id, - owner: orderDetails.takeOrder.struct.order.owner, }; report.end(); @@ -282,10 +286,9 @@ export async function finalizeRound( }> { const results: Result[] = []; const reports: PreAssembledSpan[] = []; - for (const { settle, pair, owner, orderHash, startTime } of settlements) { + for (const { settle, pair, orderHash, startTime } of settlements) { // instantiate a span report for this pair const report = new PreAssembledSpan(`order_${pair}`, startTime); - report.setAttr("details.owner", owner); // settle the process results // this will return the report of the operation @@ -302,19 +305,8 @@ export async function finalizeRound( this.state.gasCosts.push(value.gasCost); } - // set the span attributes with the values gathered at processOrder() - for (const attrKey in value.spanAttributes) { - // record event attrs - if (attrKey.startsWith("event.")) { - const _event = value.spanAttributes[attrKey] as number[]; - report.addEvent( - attrKey.replace("event.", ""), - { duration: _event[1] }, - _event[0] as number, - ); - delete value.spanAttributes[attrKey]; - } - } + // record span events and attrs + report.recordOrderEvents(value.spanEvents); report.extendAttrs(value.spanAttributes); // set the otel span status based on report status @@ -348,19 +340,9 @@ export async function finalizeRound( } else { const err = result.error; endTime = err.endTime; - // set the span attributes with the values gathered at processOrder() - for (const attrKey in err.spanAttributes) { - // record event attrs - if (attrKey.startsWith("event.")) { - const _event = err.spanAttributes[attrKey] as number[]; - report.addEvent( - attrKey.replace("event.", ""), - { duration: _event[1] }, - _event[0] as number, - ); - delete err.spanAttributes[attrKey]; - } - } + + // record span events and attrs + report.recordOrderEvents(err.spanEvents); report.extendAttrs(err.spanAttributes); // Finalize the reports based on error type diff --git a/src/core/process/transaction.test.ts b/src/core/process/transaction.test.ts index 00a0f469..a5fe0260 100644 --- a/src/core/process/transaction.test.ts +++ b/src/core/process/transaction.test.ts @@ -1,21 +1,33 @@ +import { RainSolver } from ".."; import { BaseError } from "viem"; import { Token } from "sushi/currency"; import { processReceipt } from "./receipt"; -import { containsNodeError } from "../../error"; import { RainSolverSigner } from "../../signer"; -import { processTransaction, ProcessTransactionArgs } from "./transaction"; +import { PreAssembledSpan } from "../../logger"; +import { SpanStatusCode } from "@opentelemetry/api"; +import { containsNodeError, errorSnapshot, isTimeout } from "../../error"; import { RawTransaction, Result, withBigintSerializer } from "../../common"; import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest"; import { - ProcessOrderFailure, - ProcessOrderHaltReason, + processTransaction, + transactionSettlement, + ProcessTransactionArgs, + TransactionSettlementArgs, +} from "./transaction"; +import { ProcessOrderStatus, ProcessOrderSuccess, + ProcessOrderFailure, + ProcessOrderHaltReason, + ProcessTransactionSuccess, } from "../types"; // mock dependencies -vi.mock("../../error", () => ({ +vi.mock("../../error", async (importOriginal) => ({ + ...(await importOriginal()), containsNodeError: vi.fn(), + errorSnapshot: vi.fn(), + isTimeout: vi.fn(), })); vi.mock("./receipt", async (importOriginal) => ({ @@ -37,6 +49,7 @@ describe("Test processTransaction", () => { let mockRawTx: RawTransaction; let mockArgs: ProcessTransactionArgs; let mockWriteSigner: any; + let mockSolver: RainSolver; beforeEach(() => { vi.clearAllMocks(); @@ -52,6 +65,7 @@ describe("Test processTransaction", () => { address: "0xSignerAddress", }, state: { + gasCosts: [], client: { getTransactionReceipt: vi.fn(), }, @@ -84,11 +98,13 @@ describe("Test processTransaction", () => { orderbook: "0xOrderbookAddress", inputToEthPrice: "2000.0", outputToEthPrice: "1.0", + startTime: 123456, baseResult: { tokenPair: "ETH/USDC", buyToken: "0xUSDC", sellToken: "0xETH", - spanAttributes: {}, + spanAttributes: { baseAttr: "value" }, + spanEvents: { baseEvent: { startTime: 123, duration: 100 } }, status: ProcessOrderStatus.FoundOpportunity, }, toToken: { @@ -102,6 +118,9 @@ describe("Test processTransaction", () => { symbol: "ETH", } as any as Token, }; + mockSolver = { + logger: { exportPreAssembledSpan: vi.fn() }, + } as any; }); describe("successful transaction sending", () => { @@ -120,14 +139,15 @@ describe("Test processTransaction", () => { (mockSigner.waitForReceipt as Mock).mockResolvedValueOnce(mockReceipt); const mockHandleReceiptResult = Result.ok({ ...mockArgs.baseResult, - clearedAmount: "100", - gasCost: 420000000000000n, endTime: 123, }); (processReceipt as Mock).mockResolvedValueOnce(mockHandleReceiptResult); - const settlerFn = await processTransaction(mockArgs); + + const settlerFn = await processTransaction.call(mockSolver, mockArgs); const result = await settlerFn(); + assert(result.isOk()); + // verify transaction was sent with correct parameters expect(mockWriteSigner.sendTx).toHaveBeenCalledWith({ ...mockRawTx, @@ -154,9 +174,6 @@ describe("Test processTransaction", () => { fromToken: mockArgs.fromToken, txSendTime: expect.any(Number), }); - - // verify result is passed through from processReceipt - expect(result).toBe(mockHandleReceiptResult); }); }); @@ -165,7 +182,7 @@ describe("Test processTransaction", () => { const mockError = new Error("Persistent network error"); mockWriteSigner.sendTx.mockRejectedValue(mockError); (containsNodeError as Mock).mockResolvedValue(false); - const settlerFn = await processTransaction(mockArgs); + const settlerFn = await processTransaction.call(mockSolver, mockArgs); const result = await settlerFn(); // verify error result structure @@ -187,7 +204,7 @@ describe("Test processTransaction", () => { const mockNodeError = new BaseError("Node connection failed"); mockWriteSigner.sendTx.mockRejectedValue(mockNodeError); (containsNodeError as Mock).mockResolvedValue(true); - const settlerFn = await processTransaction(mockArgs); + const settlerFn = await processTransaction.call(mockSolver, mockArgs); const result = await settlerFn(); assert(result.isErr()); @@ -196,60 +213,179 @@ describe("Test processTransaction", () => { }); }); - describe("receipt processing failures", () => { - it("should return error result when receipt retrieval fails completely", async () => { - const mockTxHash = "0xFailedReceiptHash"; - const receiptError = new Error("Receipt retrieval failed"); - mockWriteSigner.sendTx.mockResolvedValueOnce({ - hash: mockTxHash, - wait: vi.fn().mockRejectedValueOnce(receiptError), + describe("Test transactionSettlement", () => { + let mockArgsSettlement: TransactionSettlementArgs; + + beforeEach(() => { + mockArgsSettlement = { + ...mockArgs, + txhash: "0xtxhash", + txUrl: "https://example/tx/hash", + txSendTime: 123, + }; + }); + + it("successful setllement", async () => { + const mockReceipt = { status: "success" }; + const mockProcessResult = Result.ok({ + clearedAmount: "100", + gasCost: 420000000000000n, + spanAttributes: { key: "value" }, + spanEvents: { event1: { duration: 100, startTime: 123 } }, + } as any); + + (mockSigner.waitForReceipt as Mock).mockResolvedValueOnce(mockReceipt); + (processReceipt as Mock).mockResolvedValueOnce(mockProcessResult); + const spanEventSpy = vi.spyOn(PreAssembledSpan.prototype, "addEvent"); + const spanSetStatusSpy = vi.spyOn(PreAssembledSpan.prototype, "setStatus"); + const spanExtendAttrsSpy = vi.spyOn(PreAssembledSpan.prototype, "extendAttrs"); + + const result = await transactionSettlement.call(mockSolver, mockArgsSettlement); + + assert(result.isOk()); + expect(result.value.clearedAmount).toBe("100"); + + // Verify receipt was fetched + expect(mockSigner.waitForReceipt).toHaveBeenCalledWith({ + hash: mockArgsSettlement.txhash, }); - (mockSigner.waitForReceipt as Mock).mockRejectedValueOnce(receiptError); - (mockSigner.state.client.getTransactionReceipt as Mock).mockRejectedValue(receiptError); - (containsNodeError as Mock).mockResolvedValue(true); - const settlerFn = await processTransaction(mockArgs); - const result = await settlerFn(); - assert(result.isErr()); - expect(result.error).toEqual({ - ...mockArgs.baseResult, - txUrl: "https://etherscan.io/tx/0xFailedReceiptHash", - reason: ProcessOrderHaltReason.TxMineFailed, - error: receiptError, - endTime: expect.any(Number), + // Verify processReceipt was called with correct parameters + expect(processReceipt).toHaveBeenCalledWith({ + receipt: mockReceipt, + signer: mockArgsSettlement.signer, + rawtx: mockArgsSettlement.rawtx, + orderbook: mockArgsSettlement.orderbook, + inputToEthPrice: mockArgsSettlement.inputToEthPrice, + outputToEthPrice: mockArgsSettlement.outputToEthPrice, + baseResult: mockArgsSettlement.baseResult, + txUrl: mockArgsSettlement.txUrl, + toToken: mockArgsSettlement.toToken, + fromToken: mockArgsSettlement.fromToken, + txSendTime: mockArgsSettlement.txSendTime, }); - expect(result.error.spanAttributes["details.rawTx"]).toBeDefined(); - expect(result.error.spanAttributes["txNoneNodeError"]).toBe(false); - }); - it("should return error result when processReceipt throws", async () => { - const mockTxHash = "0xHandleReceiptFailHash"; - const mockReceipt = { - status: "success", - transactionHash: mockTxHash, - gasUsed: 21000n, - effectiveGasPrice: 20000000000n, - }; - const handleReceiptError = new Error("Handle receipt failed"); - mockWriteSigner.sendTx.mockResolvedValueOnce({ - hash: mockTxHash, - wait: vi.fn().mockResolvedValue(mockReceipt), + // Verify gas cost was recorded + expect(mockSigner.state.gasCosts).toContain(420000000000000n); + + // Verify logger was called + expect(mockSolver.logger!.exportPreAssembledSpan).toHaveBeenCalledTimes(1); + + // Verify otel report calls + expect(spanEventSpy).toHaveBeenNthCalledWith(1, "event1", { duration: 100 }, 123); + expect(spanExtendAttrsSpy).toHaveBeenNthCalledWith(1, { key: "value" }); + expect(spanSetStatusSpy).toHaveBeenCalledWith({ + code: SpanStatusCode.OK, + message: "found opportunity", }); + + spanEventSpy.mockRestore(); + spanSetStatusSpy.mockRestore(); + spanExtendAttrsSpy.mockRestore(); + }); + + it("reverted setllement", async () => { + const mockReceipt = { status: "reverted" }; + const mockErrorResult = Result.err({ + error: { err: new Error("Transaction reverted") }, + spanAttributes: { errorKey: "errorValue" }, + spanEvents: { event1: { duration: 100, startTime: 123 } }, + } as any); + (mockSigner.waitForReceipt as Mock).mockResolvedValueOnce(mockReceipt); - (processReceipt as Mock).mockRejectedValueOnce(handleReceiptError); - (containsNodeError as Mock).mockResolvedValue(false); - const settlerFn = await processTransaction(mockArgs); - const result = await settlerFn(); + (processReceipt as Mock).mockResolvedValueOnce(mockErrorResult); + (errorSnapshot as Mock).mockResolvedValue("Transaction reverted onchain"); + const spanEventSpy = vi.spyOn(PreAssembledSpan.prototype, "addEvent"); + const spanSetStatusSpy = vi.spyOn(PreAssembledSpan.prototype, "setStatus"); + const spanExtendAttrsSpy = vi.spyOn(PreAssembledSpan.prototype, "extendAttrs"); + + const result = await transactionSettlement.call(mockSolver, mockArgsSettlement); assert(result.isErr()); - expect(result.error).toEqual({ - ...mockArgs.baseResult, - txUrl: "https://etherscan.io/tx/0xHandleReceiptFailHash", - reason: ProcessOrderHaltReason.TxMineFailed, - error: handleReceiptError, - endTime: expect.any(Number), + expect(result.error.error.err.message).toBe("Transaction reverted"); + + // Verify receipt was fetched + expect(mockSigner.waitForReceipt).toHaveBeenCalledWith({ + hash: mockArgsSettlement.txhash, + }); + + // Verify processReceipt was called + expect(processReceipt).toHaveBeenCalled(); + + // Verify error snapshot was created + expect(errorSnapshot).toHaveBeenCalledWith( + "transaction reverted onchain", + (mockErrorResult as any).error.error.err, + ); + + // Verify logger was called + expect(mockSolver.logger!.exportPreAssembledSpan).toHaveBeenCalled(); + + // Verify processReceipt was called with correct parameters + expect(processReceipt).toHaveBeenCalledWith({ + receipt: mockReceipt, + signer: mockArgsSettlement.signer, + rawtx: mockArgsSettlement.rawtx, + orderbook: mockArgsSettlement.orderbook, + inputToEthPrice: mockArgsSettlement.inputToEthPrice, + outputToEthPrice: mockArgsSettlement.outputToEthPrice, + baseResult: mockArgsSettlement.baseResult, + txUrl: mockArgsSettlement.txUrl, + toToken: mockArgsSettlement.toToken, + fromToken: mockArgsSettlement.fromToken, + txSendTime: mockArgsSettlement.txSendTime, + }); + + // Verify otel report calls + expect(spanEventSpy).toHaveBeenNthCalledWith(1, "event1", { duration: 100 }, 123); + expect(spanExtendAttrsSpy).toHaveBeenNthCalledWith(1, { errorKey: "errorValue" }); + expect(spanSetStatusSpy).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + message: expect.any(String), }); - expect(result.error.spanAttributes["txNoneNodeError"]).toBe(true); + + spanEventSpy.mockRestore(); + spanSetStatusSpy.mockRestore(); + spanExtendAttrsSpy.mockRestore(); + }); + + it("failed setllement", async () => { + const mockTimeoutError = new Error("Timeout error"); + (mockSigner.waitForReceipt as Mock).mockRejectedValueOnce(mockTimeoutError); + (isTimeout as Mock).mockReturnValueOnce(true); + (errorSnapshot as Mock).mockResolvedValue("Transaction timeout"); + const spanEventSpy = vi.spyOn(PreAssembledSpan.prototype, "addEvent"); + const spanSetStatusSpy = vi.spyOn(PreAssembledSpan.prototype, "setStatus"); + const spanExtendAttrsSpy = vi.spyOn(PreAssembledSpan.prototype, "extendAttrs"); + + const result = await transactionSettlement.call(mockSolver, mockArgsSettlement); + + assert(result.isErr()); + expect(result.error.reason).toBe(ProcessOrderHaltReason.TxMineFailed); + + // Verify timeout was detected + expect(isTimeout).toHaveBeenCalledWith(mockTimeoutError); + + // Verify error snapshot was created + expect(errorSnapshot).toHaveBeenCalledWith("transaction failed", mockTimeoutError); + + // Verify logger was called + expect(mockSolver.logger!.exportPreAssembledSpan).toHaveBeenCalled(); + + // Verify processReceipt was not called + expect(processReceipt).not.toHaveBeenCalled(); + + // Verify otel report calls + expect(spanEventSpy).toHaveBeenNthCalledWith(1, "baseEvent", { duration: 100 }, 123); + expect(spanExtendAttrsSpy).toHaveBeenNthCalledWith(1, { baseAttr: "value" }); + expect(spanSetStatusSpy).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + message: expect.any(String), + }); + + spanEventSpy.mockRestore(); + spanSetStatusSpy.mockRestore(); + spanExtendAttrsSpy.mockRestore(); }); }); }); diff --git a/src/core/process/transaction.ts b/src/core/process/transaction.ts index d279cb18..1e06dde2 100644 --- a/src/core/process/transaction.ts +++ b/src/core/process/transaction.ts @@ -1,15 +1,25 @@ +import { RainSolver } from ".."; import { BaseError } from "viem"; import { Result } from "../../common"; import { Token } from "sushi/currency"; import { processReceipt } from "./receipt"; import { RainSolverSigner } from "../../signer"; -import { containsNodeError } from "../../error"; +import { SpanStatusCode } from "@opentelemetry/api"; +import { PreAssembledSpan, SpanWithContext } from "../../logger"; import { withBigintSerializer, RawTransaction } from "../../common"; +import { + isTimeout, + KnownErrors, + ErrorSeverity, + errorSnapshot, + containsNodeError, +} from "../../error"; import { ProcessOrderSuccess, ProcessOrderFailure, ProcessOrderHaltReason, ProcessOrderResultBase, + ProcessTransactionSuccess, } from "../types"; /** Arguments for processing a transaction */ @@ -22,6 +32,15 @@ export type ProcessTransactionArgs = { baseResult: ProcessOrderResultBase; toToken: Token; fromToken: Token; + startTime: number; + roundSpanCtx?: SpanWithContext; +}; + +/** Arguments needed for processing the transaction settlement in background */ +export type TransactionSettlementArgs = ProcessTransactionArgs & { + txhash: `0x${string}`; + txUrl: string; + txSendTime: number; }; /** @@ -31,29 +50,58 @@ export type ProcessTransactionArgs = { * @param args - The arguments for processing the transaction * @returns A function that returns a promise resolving to the ProcessOrderResult */ -export async function processTransaction({ - rawtx, - signer, - toToken, - fromToken, - orderbook, - baseResult, - inputToEthPrice, - outputToEthPrice, -}: ProcessTransactionArgs): Promise< - () => Promise> -> { +export async function processTransaction( + this: RainSolver, + { + rawtx, + signer, + toToken, + startTime, + fromToken, + orderbook, + baseResult, + inputToEthPrice, + outputToEthPrice, + roundSpanCtx, + }: ProcessTransactionArgs, +): Promise<() => Promise>> { // submit the tx - let hash: `0x${string}`, txUrl: string, wait: Awaited>["wait"]; + let hash: `0x${string}`, txUrl: string; let txSendTime = 0; try { rawtx.type = "legacy"; - ({ hash, wait } = await signer.asWriteSigner().sendTx(rawtx as any)); + ({ hash } = await signer.asWriteSigner().sendTx(rawtx as any)); txUrl = signer.state.chainConfig.blockExplorers?.default.url + "/tx/" + hash; txSendTime = performance.now(); // eslint-disable-next-line no-console console.log("\x1b[33m%s\x1b[0m", txUrl, "\n"); baseResult.spanAttributes["details.txUrl"] = txUrl; + + // start getting tx receipt in background + const txSettlement = transactionSettlement.call(this, { + txhash: hash, + signer, + rawtx, + orderbook, + inputToEthPrice, + outputToEthPrice, + baseResult: structuredClone(baseResult), + txUrl, + toToken, + fromToken, + txSendTime, + startTime, + roundSpanCtx, + }); + + const endTime = performance.now(); + const res: ProcessOrderSuccess = { + ...baseResult, + txUrl, + endTime, + txSettlement, + }; + return async () => Result.ok(res); } catch (e) { // record rawtx in logs baseResult.spanAttributes["details.rawTx"] = JSON.stringify( @@ -73,42 +121,134 @@ export async function processTransaction({ endTime, }); } +} - // start getting tx receipt in background and return the settler fn - const receiptPromise = wait(); - - return async () => { - try { - const receipt = await receiptPromise; - return await processReceipt({ - receipt, - signer, - rawtx, - orderbook, - inputToEthPrice, - outputToEthPrice, - baseResult, - txUrl, - toToken, - fromToken, - txSendTime, - }); - } catch (e: any) { - baseResult.spanAttributes["details.rawTx"] = JSON.stringify( +/** + * Processes the transaction settlement by waiting for the receipt, this is + * meant to run in background while other orders keep on being processed. + */ +export async function transactionSettlement( + this: RainSolver, + { + txhash, + signer, + rawtx, + orderbook, + inputToEthPrice, + outputToEthPrice, + baseResult, + txUrl, + toToken, + fromToken, + txSendTime, + startTime, + roundSpanCtx, + }: TransactionSettlementArgs, +): Promise> { + const report = new PreAssembledSpan( + `tx_${baseResult.spanAttributes["details.pair"]}`, + startTime, + ); + try { + const receipt = await signer.waitForReceipt({ hash: txhash }); + const result = await processReceipt({ + receipt, + signer, + rawtx, + orderbook, + inputToEthPrice, + outputToEthPrice, + baseResult, + txUrl, + toToken, + fromToken, + txSendTime, + }); + if (result.isOk()) { + const value = result.value; + + // keep track of avg gas cost once transaction settles + if (value.gasCost) { + signer.state.gasCosts.push(value.gasCost); + } + + // record span events and attrs + report.recordOrderEvents(value.spanEvents); + report.extendAttrs(value.spanAttributes); + report.setStatus({ code: SpanStatusCode.OK, message: "found opportunity" }); + } else { + const err = result.error; + + // record span events and attrs + report.recordOrderEvents(err.spanEvents); + report.extendAttrs(err.spanAttributes); + + // Tx reverted onchain, this can happen for example + // because of mev front running or false positive opportunities, etc + let message = ""; + if (err.error) { + if ("snapshot" in err.error) { + message = err.error.snapshot; + } else { + message = await errorSnapshot("transaction reverted onchain", err.error.err); + } + report.setAttr("errorDetails", message); + } + if (KnownErrors.every((v) => !message.includes(v))) { + report.setAttr("severity", ErrorSeverity.HIGH); + } + if (err.spanAttributes["txNoneNodeError"]) { + report.setAttr("severity", ErrorSeverity.HIGH); + } + report.setStatus({ code: SpanStatusCode.ERROR, message }); + report.setAttr("unsuccessfulClear", true); + report.setAttr("txReverted", true); + } + + report.end(); + // export the report to logger if logger is available + this.logger?.exportPreAssembledSpan(report, roundSpanCtx?.context); + + return result; + } catch (err: any) { + report.setAttr( + "details.rawTx", + JSON.stringify( { ...rawtx, from: signer.account.address, }, withBigintSerializer, - ); - baseResult.spanAttributes["txNoneNodeError"] = !(await containsNodeError(e)); - return Result.err({ - ...baseResult, - txUrl, - reason: ProcessOrderHaltReason.TxMineFailed, - error: e, - endTime: performance.now(), - }); + ), + ); + report.setAttr("txNoneNodeError", !(await containsNodeError(err))); + + // record span events and attrs + report.recordOrderEvents(baseResult.spanEvents); + report.extendAttrs(baseResult.spanAttributes); + + // tx failed to get included onchain, this can happen as result of timeout, rpc dropping the tx, etc + const message = await errorSnapshot("transaction failed", err); + report.setAttr("errorDetails", message); + if (isTimeout(err)) { + report.setAttr("severity", ErrorSeverity.LOW); + } else { + report.setAttr("severity", ErrorSeverity.HIGH); } - }; + report.setStatus({ code: SpanStatusCode.ERROR, message }); + report.setAttr("unsuccessfulClear", true); + report.setAttr("txMineFailed", true); + + report.end(); + // export the report to logger if logger is available + this.logger?.exportPreAssembledSpan(report, roundSpanCtx?.context); + + return Result.err({ + ...baseResult, + txUrl, + reason: ProcessOrderHaltReason.TxMineFailed, + error: err, + endTime: performance.now(), + }); + } } diff --git a/src/core/types.ts b/src/core/types.ts index 85325b35..9e6cbf81 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -40,11 +40,20 @@ export type ProcessOrderResultBase = { buyToken: string; sellToken: string; spanAttributes: Attributes; + spanEvents: OrderSpanEvents; gasCost?: bigint; }; /** Successful process order result */ export type ProcessOrderSuccess = ProcessOrderResultBase & { + endTime: number; + message?: string; + txUrl?: string; + txSettlement?: Promise>; +}; + +/** Result of successful transaction processing */ +export type ProcessTransactionSuccess = ProcessOrderResultBase & { endTime: number; txUrl?: string; clearedAmount?: string; @@ -101,3 +110,12 @@ export type SimulationResult = Result; export type FindBestTradeSuccess = SuccessSimulation; export type FindBestTradeFailure = Pick; export type FindBestTradeResult = Result; + +/** Represents OTEL compatible event details paired with event name that occurs during order processing */ +export type OrderSpanEvents = Record< + string, + { + startTime: number; + duration: number; + } +>; diff --git a/src/logger/index.test.ts b/src/logger/index.test.ts index a62922f7..4e65f70a 100644 --- a/src/logger/index.test.ts +++ b/src/logger/index.test.ts @@ -155,6 +155,14 @@ describe("Test PreAssembledSpan", async function () { expected.events.push({ name: "event1", startTime: now + 25, attributes: { key: "value" } }); assert.deepEqual(span, expected); + span.recordOrderEvents({ orderEvents1: { startTime: 123, duration: 100 } }); + expected.events.push({ + name: "orderEvents1", + attributes: { duration: 100 }, + startTime: 123, + }); + assert.deepEqual(span, expected); + span.setAttr("test-attr", "test-value"); expected.attributes["test-attr"] = "test-value"; assert.deepEqual(span, expected); diff --git a/src/logger/index.ts b/src/logger/index.ts index f147a137..babbe1c1 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,4 +1,5 @@ import { sleep } from "../common"; +import { OrderSpanEvents } from "../core/types"; import { Resource } from "@opentelemetry/resources"; import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; @@ -301,4 +302,19 @@ export class PreAssembledSpan { this.exception = { time, exception }; return this; } + + /** + * Records the order processing events + * @param orderEvents - The order events k/v + */ + recordOrderEvents(orderEvents: OrderSpanEvents): this { + for (const eventName in orderEvents) { + this.addEvent( + eventName, + { duration: orderEvents[eventName].duration }, + orderEvents[eventName].startTime, + ); + } + return this; + } } diff --git a/test/e2e/data.js b/test/e2e/data.js index c35426fd..a9644e18 100644 --- a/test/e2e/data.js +++ b/test/e2e/data.js @@ -108,7 +108,7 @@ module.exports = [ [ ChainId.BASE, process?.env?.TEST_BASE_RPC, - 16418720, + 38187500, [ WNATIVE[ChainId.BASE], new Token({ @@ -131,10 +131,10 @@ module.exports = [ }), ], [ - "0x2B8804c2b652f05F7FDD8e0a02F01eE58F01667E", - "0xD6216fC19DB775Df9774a6E33526131dA7D19a2c", - "0x3ea31919Ef9b3e72Cc25657b604DB1ACDb1DdB4b", - "0xf6D07A291443F31B129Ca7e2b46C6F882f0FAa5b", + "0xb2cc224c1c9feE385f8ad6a55b4d94E92359DC59", + "0xe3715B2a3bB826cd9EC5429eE85B651f95879D34", + "0x4617C0F3e55930fdD72ec6EA92e79D384987C464", + "0x7731D522011b4ACE5D812C15539321F373d0E964", ], [LiquidityProviders.UniswapV3, LiquidityProviders.UniswapV2, LiquidityProviders.BaseSwap], ["1", "10000", "10000", "10000"], diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index ada15bb5..bb5228c9 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -388,10 +388,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -751,10 +758,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -1158,10 +1172,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -1546,10 +1567,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -1879,10 +1907,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -2179,10 +2214,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -2572,10 +2614,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -2972,10 +3021,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -3374,10 +3430,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -3730,10 +3793,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok( @@ -4053,10 +4123,17 @@ for (let i = 0; i < testData.length; i++) { }, // config, ); - const { results: reports } = await rainSolver.processNextRound( - undefined, - false, - ); + const { results } = await rainSolver.processNextRound(undefined, false); + + // wait for all tx settlements for testing the results + const reports = []; + for (const res of results) { + if (res.isOk() && typeof res.value.txSettlement !== "undefined") { + reports.push(await res.value.txSettlement); + } else { + reports.push(res); + } + } // should have cleared correct number of orders assert.ok(