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
165 changes: 154 additions & 11 deletions src/core/modes/router/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { SimulationResult, TradeType } from "../../types";
import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest";

// Mocks
vi.mock("../../../common", async (importOriginal) => ({
...(await importOriginal()),
extendObjectWithHeader: vi.fn(),
}));
// extendObjectWithHeader is wrapped with its real implementation so call
// assertions work while span attributes still get merged for assertions
vi.mock("../../../common", async (importOriginal) => {
const original = await importOriginal<typeof import("../../../common")>();
return {
...original,
extendObjectWithHeader: vi.fn(original.extendObjectWithHeader),
};
});

vi.mock("sushi/currency", async (importOriginal) => {
return {
Expand Down Expand Up @@ -99,7 +104,7 @@ describe("Test findBestRouterTrade", () => {
);

assert(result.isOk());
expect(result.value.spanAttributes.foundOpp).toBe(true);
expect(result.value.spanAttributes).toEqual({ foundOpp: true });
expect(result.value.estimatedProfit).toBe(100n);
expect(result.value.oppBlockNumber).toBe(123);
expect(result.value.type).toBe("balancer");
Expand Down Expand Up @@ -139,6 +144,10 @@ describe("Test findBestRouterTrade", () => {
assert(result.isErr());
expect(result.error.noneNodeError).toBe("no route available");
expect(result.error.type).toBe("router");
expect(result.error.spanAttributes).toEqual({
"full.route": "no-way",
"partial.error": "no viable partial trade size found",
});
expect(extendObjectWithHeader).toHaveBeenCalledWith(
expect.any(Object),
{ route: "no-way" },
Expand Down Expand Up @@ -175,7 +184,7 @@ describe("Test findBestRouterTrade", () => {
);

assert(result.isOk());
expect(result.value.spanAttributes.foundOpp).toBe(true);
expect(result.value.spanAttributes).toEqual({ foundOpp: true });
expect(result.value.estimatedProfit).toBe(50n);
expect(result.value.type).toBe("routeProcessor");
expect(mockRainSolver.state.router.findLargestTradeSize).toHaveBeenCalledWith(
Expand All @@ -185,6 +194,8 @@ describe("Test findBestRouterTrade", () => {
1000n,
100n,
undefined,
false,
undefined,
);
expect(trySimulateTradeSpy).toHaveBeenCalledTimes(2);
expect(simulatorWithArgsSpy).toHaveBeenLastCalledWith({
Expand Down Expand Up @@ -230,7 +241,7 @@ describe("Test findBestRouterTrade", () => {
);

assert(result.isOk());
expect(result.value.spanAttributes.foundOpp).toBe(true);
expect(result.value.spanAttributes).toEqual({ foundOpp: true });
expect(result.value.estimatedProfit).toBe(50n);
expect(result.value.type).toBe("routeProcessor");
expect(mockRainSolver.state.router.findLargestTradeSize).toHaveBeenCalledWith(
Expand All @@ -240,6 +251,8 @@ describe("Test findBestRouterTrade", () => {
1000n,
100n,
undefined,
false,
undefined,
);
expect(trySimulateTradeSpy).toHaveBeenCalledTimes(2);
expect(simulatorWithArgsSpy).toHaveBeenLastCalledWith({
Expand Down Expand Up @@ -280,9 +293,10 @@ describe("Test findBestRouterTrade", () => {
assert(result.isErr());
expect(result.error.noneNodeError).toBe("order ratio issue");
expect(result.error.type).toBe("router");
expect(result.error.spanAttributes["partial.error"]).toBe(
"no viable partial trade size found",
);
expect(result.error.spanAttributes).toEqual({
"full.error": "ratio too high",
"partial.error": "no viable partial trade size found",
});
expect(extendObjectWithHeader).toHaveBeenCalledWith(
expect.any(Object),
{ error: "ratio too high" },
Expand Down Expand Up @@ -322,6 +336,10 @@ describe("Test findBestRouterTrade", () => {
assert(result.isErr());
expect(result.error.noneNodeError).toBe("order ratio issue"); // from full trade error
expect(result.error.type).toBe("balancer");
expect(result.error.spanAttributes).toEqual({
"full.error": "ratio too high",
"partial.error": "no opportunity",
});
expect(extendObjectWithHeader).toHaveBeenCalledWith(
expect.any(Object),
{ error: "ratio too high" },
Expand Down Expand Up @@ -363,7 +381,7 @@ describe("Test findBestRouterTrade", () => {
);

assert(result.isOk());
expect(result.value.spanAttributes.foundOpp).toBe(true);
expect(result.value.spanAttributes).toEqual({ foundOpp: true });
expect(result.value.estimatedProfit).toBe(75n);
expect(result.value.oppBlockNumber).toBe(123);
expect(result.value.type).toBe("routeProcessor");
Expand All @@ -374,6 +392,8 @@ describe("Test findBestRouterTrade", () => {
1000n,
100n,
undefined,
false,
undefined,
);
expect(trySimulateTradeSpy).toHaveBeenCalledTimes(2);
expect(simulatorWithArgsSpy).toHaveBeenLastCalledWith({
Expand All @@ -395,6 +415,129 @@ describe("Test findBestRouterTrade", () => {
);
});

it("should retry with the failing route dexes excluded when full trade dryrun fails", async () => {
const sushiQuote = {
route: {
pcMap: new Map([["pool1", { liquidityProvider: "Hydrex" }]]),
route: { legs: [{ uniqueId: "pool1" }] },
},
} as any;
const mockFullTradeError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: { error: "dryrun failed" },
});
const mockRetrySuccess = Result.ok({
type: TradeType.RouteProcessor,
spanAttributes: { foundOpp: true },
estimatedProfit: 50n,
oppBlockNumber: 123,
});
(simulatorWithArgsSpy as Mock)
.mockReturnValueOnce({
quote: sushiQuote,
trySimulateTrade: vi.fn().mockResolvedValue(mockFullTradeError),
})
.mockReturnValueOnce({
quote: sushiQuote,
trySimulateTrade: vi.fn().mockResolvedValue(mockRetrySuccess),
});

const result: SimulationResult = await findBestRouterTrade.call(
mockRainSolver,
orderDetails,
signer,
ethPrice,
toToken,
fromToken,
blockNumber,
);

assert(result.isOk());
expect(result.value.spanAttributes).toEqual({ foundOpp: true });
expect(result.value.estimatedProfit).toBe(50n);
expect(simulatorWithArgsSpy).toHaveBeenCalledTimes(2);
expect(simulatorWithArgsSpy).toHaveBeenLastCalledWith({
type: TradeType.Router,
solver: mockRainSolver,
orderDetails,
fromToken,
toToken,
signer,
maximumInputFixed: 1000n,
ethPrice,
isPartial: false,
blockNumber: 123n,
excludeDexes: new Set(["Hydrex"]),
});
expect(mockRainSolver.state.router.findLargestTradeSize).not.toHaveBeenCalled();
});

it("should return error when retry attempt also fails", async () => {
const sushiQuote = {
route: {
pcMap: new Map([["pool1", { liquidityProvider: "Hydrex" }]]),
route: { legs: [{ uniqueId: "pool1" }] },
},
} as any;
const mockFullTradeError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: { error: "dryrun failed" },
noneNodeError: "full failed",
});
const mockRetryError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: { error: "retry dryrun failed" },
noneNodeError: "retry failed",
});
(simulatorWithArgsSpy as Mock)
.mockReturnValueOnce({
quote: sushiQuote,
trySimulateTrade: vi.fn().mockResolvedValue(mockFullTradeError),
})
.mockReturnValueOnce({
quote: undefined,
trySimulateTrade: vi.fn().mockResolvedValue(mockRetryError),
});

const result: SimulationResult = await findBestRouterTrade.call(
mockRainSolver,
orderDetails,
signer,
ethPrice,
toToken,
fromToken,
blockNumber,
);

assert(result.isErr());
expect(result.error.noneNodeError).toBe("full failed");
expect(result.error.type).toBe(TradeType.RouteProcessor);
expect(result.error.spanAttributes).toEqual({
"full.error": "dryrun failed",
"secondary.full.error": "retry dryrun failed",
});
expect(simulatorWithArgsSpy).toHaveBeenCalledTimes(2);
expect(extendObjectWithHeader).toHaveBeenCalledWith(
expect.any(Object),
{ error: "dryrun failed" },
"full",
);
expect(extendObjectWithHeader).toHaveBeenCalledWith(
expect.any(Object),
{ error: "retry dryrun failed" },
"full",
);
expect(extendObjectWithHeader).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
"secondary",
);
expect(mockRainSolver.state.router.findLargestTradeSize).not.toHaveBeenCalled();
});

it("should return early if ethPrice is unknown", async () => {
const result: SimulationResult = await findBestRouterTrade.call(
mockRainSolver,
Expand Down
Loading
Loading