Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions src/core/modes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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({
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = <T>(ms: number, value: T): Promise<T> =>
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 () => {
Expand Down
53 changes: 29 additions & 24 deletions src/core/modes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -99,36 +101,39 @@ 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<SimulationResult>[];

// 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<SimulationResult | undefined>((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;
} else {
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
Expand Down
Loading