diff --git a/src/core/modes/index.test.ts b/src/core/modes/index.test.ts index 72ee1fe5..eecd2e31 100644 --- a/src/core/modes/index.test.ts +++ b/src/core/modes/index.test.ts @@ -62,7 +62,7 @@ describe("Test findBestTrade", () => { }; }); - it("should return highest profit result when all modes succeed", async () => { + it("should return the first resolved success when all modes succeed", async () => { const rpResult = Result.ok({ type: "routeProcessor", spanAttributes: { foundOpp: true }, @@ -72,7 +72,7 @@ describe("Test findBestTrade", () => { const intraResult = Result.ok({ type: "intraOrderbook", spanAttributes: { foundOpp: true }, - estimatedProfit: 200n, // highest profit + estimatedProfit: 200n, // highest profit, but not the first to resolve oppBlockNumber: 123, }); const interResult = Result.ok({ @@ -96,10 +96,12 @@ describe("Test findBestTrade", () => { const result = await findBestTrade.call(mockRainSolver, args); assert(result.isOk()); - expect(result.value.estimatedProfit).toBe(200n); // highest profit - expect(result.value.type).toBe("intraOrderbook"); + // all resolve instantly, so the race is won by the first one in + // order, the estimated profit does not decide the winner anymore + expect(result.value.estimatedProfit).toBe(100n); + expect(result.value.type).toBe("routeProcessor"); expect(result.value.spanAttributes.foundOpp).toBe(true); - expect(result.value.spanAttributes.tradeType).toBe("intraOrderbook"); + expect(result.value.spanAttributes.tradeType).toBe("routeProcessor"); }); it("should return success result when only some modes succeed", async () => { @@ -263,8 +265,9 @@ describe("Test findBestTrade", () => { const result = await findBestTrade.call(mocksolver, args); assert(result.isOk()); - expect(result.value.estimatedProfit).toBe(150n); // highest profit - expect(result.value.type).toBe("intraOrderbook"); + // all resolve instantly, so the first one in order wins the race + expect(result.value.estimatedProfit).toBe(100n); + expect(result.value.type).toBe("routeProcessor"); expect(findBestRouterTrade).toHaveBeenCalledWith( args.orderDetails, args.signer, @@ -331,8 +334,9 @@ describe("Test findBestTrade", () => { const result = await findBestTrade.call(mockRainSolver, args); assert(result.isOk()); - expect(result.value.estimatedProfit).toBe(150n); // highest profit - expect(result.value.type).toBe("intraOrderbook"); + // all resolve instantly, so the first one in order wins the race + expect(result.value.estimatedProfit).toBe(100n); + expect(result.value.type).toBe("routeProcessor"); expect(findBestRouterTrade).toHaveBeenCalledWith( args.orderDetails, args.signer, @@ -365,42 +369,45 @@ describe("Test findBestTrade", () => { ); }); - it("should sort results by estimated profit in descending order", async () => { + it("should pick the winner by resolution time and not by estimated profit", async () => { + const delayed = (ms: number, value: T): Promise => + new Promise((resolve) => setTimeout(() => resolve(value), ms)); const rpResult = Result.ok({ type: "routeProcessor", spanAttributes: { foundOpp: true }, - estimatedProfit: 300n, // highest + estimatedProfit: 300n, // highest profit but slowest to resolve oppBlockNumber: 123, }); const intraResult = Result.ok({ type: "intraOrderbook", spanAttributes: { foundOpp: true }, - estimatedProfit: 100n, // lowest + estimatedProfit: 100n, // lowest profit but fastest success oppBlockNumber: 123, }); - const interResult = Result.ok({ + const interResult = Result.err({ type: "interOrderbook", - spanAttributes: { foundOpp: true }, - estimatedProfit: 200n, // middle - oppBlockNumber: 123, + spanAttributes: { error: "no counterparty" }, + noneNodeError: "inter orderbook failed", }); - const raindexResult = Result.ok({ + const raindexResult = Result.err({ type: "raindex", - spanAttributes: { foundOpp: true }, - estimatedProfit: 250n, // middle - oppBlockNumber: 123, + spanAttributes: { error: "no route" }, + noneNodeError: "raindex router failed", }); - (findBestRouterTrade as Mock).mockResolvedValue(rpResult); - (findBestIntraOrderbookTrade as Mock).mockResolvedValue(intraResult); + (findBestRouterTrade as Mock).mockReturnValue(delayed(50, rpResult)); + (findBestIntraOrderbookTrade as Mock).mockReturnValue(delayed(10, intraResult)); (findBestInterOrderbookTrade as Mock).mockResolvedValue(interResult); (findBestRaindexRouterTrade as Mock).mockResolvedValue(raindexResult); const result = await findBestTrade.call(mockRainSolver, args); assert(result.isOk()); - expect(result.value.estimatedProfit).toBe(300n); // should return the highest profit - expect(result.value.type).toBe("routeProcessor"); + // the fast failures do not end the race and the slow high + // profit success loses to the faster low profit success + expect(result.value.estimatedProfit).toBe(100n); + expect(result.value.type).toBe("intraOrderbook"); + expect(result.value.spanAttributes.tradeType).toBe("intraOrderbook"); }); it("should handle mixed success and error results", async () => { diff --git a/src/core/modes/index.ts b/src/core/modes/index.ts index 5ea20b5b..e2b783bb 100644 --- a/src/core/modes/index.ts +++ b/src/core/modes/index.ts @@ -31,12 +31,14 @@ export type FindBestTradeArgs = { }; /** - * Finds and returns the most profitable trade transaction and other relevant information for the given order + * Finds and returns a trade transaction and other relevant information for the given order * to be broadcasted onchain. * - * This function concurrently evaluates multiple trade strategies, including route processor, intra-orderbook, - * and inter-orderbook trades. It selects the trade with the highest estimated profit among all successful - * results. If all strategies fail, it aggregates error information and returns a comprehensive error result. + * This function concurrently evaluates multiple trade strategies, including route processor, + * intra-orderbook and inter-orderbook trades. It resolves with the first strategy that + * simulates successfully, so the found opportunity is acted on with the lowest latency + * instead of waiting for all strategies to settle. If all strategies fail, it aggregates + * error information and returns a comprehensive error result. * * @param this - The instance of `RainSolver` * @param args - The arguments required to find the best trade @@ -99,29 +101,28 @@ export async function findBestTrade( blockNumber, ), ]; - const results = (await Promise.all(promises)).filter( - (v) => v !== undefined, - ) as SimulationResult[]; + const trades = promises.filter((v) => v !== undefined) as Promise[]; - // if at least one result is ok, we can proceed to pick the best one - if (results.some((v) => v.isOk())) { - // sort results descending by estimated profit, - // so those that are errors will be at the end - // and the first one will be the one with highest estimated profit - // as we know at least one result is ok, so we can safely access it - const pick = results.sort((a, b) => { - if (a.isErr() && b.isErr()) return 0; - if (a.isErr()) return 1; - if (b.isErr()) return -1; - return a.value.estimatedProfit < b.value.estimatedProfit - ? 1 - : a.value.estimatedProfit > b.value.estimatedProfit - ? -1 - : 0; - })[0]; + // resolve with the first trade sim that succeeds instead of waiting for all + // of them to settle, so the found opportunity is acted on with the lowest + // latency, the sims that lose the race keep running in the background but + // their outcome is discarded, resolves undefined when all sims fail + const pick = await new Promise((resolve, reject) => { + if (!trades.length) return resolve(undefined); + let remaining = trades.length; + const settle = (result: SimulationResult) => { + if (result.isOk()) { + resolve(result); // first success wins the race + } else if (--remaining === 0) { + resolve(undefined); // all sims failed + } + }; + trades.forEach((trade) => trade.then(settle, reject)); + }); + if (pick) { // set the picked trade type in attrs - assert(pick.isOk()); // just for type check as we know at least one result is ok + assert(pick.isOk()); // just for type check as we know the picked result is ok pick.value.spanAttributes["tradeType"] = pick.value.type; return pick; @@ -129,6 +130,10 @@ export async function findBestTrade( const spanAttributes: Attributes = {}; let noneNodeError: string | undefined = undefined; + // all sims have already settled with error at this point, so this + // resolves instantly while keeping the original trade type order + const results = await Promise.all(trades); + // extend span attributes with the result error attrs and trade type header for (const result of results) { assert(result.isErr()); // just for type check as we know all results are errors