diff --git a/src/router/router.test.ts b/src/router/router.test.ts index 1f83037a..bee40908 100644 --- a/src/router/router.test.ts +++ b/src/router/router.test.ts @@ -929,6 +929,7 @@ describe("RainSolverRouter", () => { mockSwapAmount, mockGasPrice, "single", + false, ); sushiSpy.mockRestore(); diff --git a/src/router/router.ts b/src/router/router.ts index 02bb575f..0a0a6095 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -290,6 +290,7 @@ export class RainSolverRouter extends RainSolverRouterBase { maximumInputFixed: bigint, gasPriceBI: bigint, routeType: "single" | "multi" = "single", + absolute = false, ): bigint | undefined { return this.sushi?.findLargestTradeSize( orderDetails, @@ -298,6 +299,7 @@ export class RainSolverRouter extends RainSolverRouterBase { maximumInputFixed, gasPriceBI, routeType, + absolute, ); } diff --git a/src/router/sushi/index.ts b/src/router/sushi/index.ts index 0c745e3b..52651e08 100644 --- a/src/router/sushi/index.ts +++ b/src/router/sushi/index.ts @@ -482,6 +482,7 @@ export class SushiRouter extends RainSolverRouterBase { maximumInputFixed: bigint, gasPriceBI: bigint, routeType: "single" | "multi" = "single", + absolute = false, ): bigint | undefined { const result: bigint[] = []; const gasPrice = Number(gasPriceBI); @@ -506,6 +507,9 @@ export class SushiRouter extends RainSolverRouterBase { if (route.status == "NoWay") { maximumInput = maximumInput - initAmount / 2n ** i; + } else if (absolute) { + result.unshift(maxInput18); + maximumInput = maximumInput + initAmount / 2n ** i; } else { const price = calculatePrice18( maximumInput, diff --git a/src/state/index.test.ts b/src/state/index.test.ts index 00667080..635d6377 100644 --- a/src/state/index.test.ts +++ b/src/state/index.test.ts @@ -259,6 +259,7 @@ describe("Test SharedState", () => { initL1GasPrice: 0n, router: { getMarketPrice: vi.fn(), + findLargestTradeSize: vi.fn(), }, appOptions: { route: "multi" }, gasManager: { @@ -335,20 +336,49 @@ describe("Test SharedState", () => { }); describe("Test getMarketPrice method", () => { - it("should call getMarketPrice with correct params", () => { - const token1 = new Token({ - chainId: 1, - address: `0x${"1".repeat(40)}`, - symbol: "TKN1", - decimals: 18, - }); - const token2 = new Token({ - chainId: 1, - address: `0x${"2".repeat(40)}`, - symbol: "TKN2", - decimals: 18, + const token1 = new Token({ + chainId: 1, + address: `0x${"1".repeat(40)}`, + symbol: "TKN1", + decimals: 18, + }); + const token2 = new Token({ + chainId: 1, + address: `0x${"2".repeat(40)}`, + symbol: "TKN2", + decimals: 18, + }); + + it("should call getMarketPrice with correct params for 1 unit size happy", async () => { + (sharedState.router.getMarketPrice as Mock).mockResolvedValueOnce( + Result.ok({ price: 1n }), + ); + const result = await sharedState.getMarketPrice(token1, token2, 12345n); + + assert(result.isOk()); + expect(result.value).toEqual({ price: 1n }); + expect(sharedState.router.getMarketPrice).toHaveBeenCalledTimes(1); + expect(sharedState.router.findLargestTradeSize).not.toHaveBeenCalled(); + expect(sharedState.router.getMarketPrice).toHaveBeenCalledWith({ + fromToken: token1, + toToken: token2, + blockNumber: 12345n, + skipFetch: false, + gasPrice: sharedState.gasPrice, + amountIn: 1000000000000000000n, + sushiRouteType: sharedState.appOptions.route, }); - sharedState.getMarketPrice(token1, token2, 12345n); + }); + + it("should call getMarketPrice with correct params for partial size unhappy", async () => { + (sharedState.router.getMarketPrice as Mock).mockResolvedValueOnce(Result.err("error")); + (sharedState.router.findLargestTradeSize as Mock).mockReturnValueOnce(undefined); + const result = await sharedState.getMarketPrice(token1, token2, 12345n); + + assert(result.isErr()); + expect(result.error).toBe("error"); + expect(sharedState.router.getMarketPrice).toHaveBeenCalledTimes(1); + expect(sharedState.router.findLargestTradeSize).toHaveBeenCalledTimes(1); expect(sharedState.router.getMarketPrice).toHaveBeenCalledWith({ fromToken: token1, toToken: token2, @@ -358,6 +388,99 @@ describe("Test SharedState", () => { amountIn: 1000000000000000000n, sushiRouteType: sharedState.appOptions.route, }); + expect(sharedState.router.findLargestTradeSize).toHaveBeenCalledWith( + { takeOrder: { quote: { ratio: 0n } } } as any, + token2, + token1, + 1000000000000000000n, + sharedState.gasPrice, + sharedState.appOptions.route, + true, + ); + }); + + it("should call getMarketPrice with correct params for partial size happy", async () => { + (sharedState.router.getMarketPrice as Mock) + .mockResolvedValueOnce(Result.err("error")) + .mockResolvedValueOnce(Result.ok({ price: 1n })); + (sharedState.router.findLargestTradeSize as Mock).mockReturnValueOnce( + 500000000000000000n, + ); + const result = await sharedState.getMarketPrice(token1, token2, 12345n); + + assert(result.isOk()); + expect(result.value).toEqual({ price: 1n }); + expect(sharedState.router.getMarketPrice).toHaveBeenCalledTimes(2); + expect(sharedState.router.findLargestTradeSize).toHaveBeenCalledTimes(1); + expect(sharedState.router.getMarketPrice).toHaveBeenNthCalledWith(1, { + fromToken: token1, + toToken: token2, + blockNumber: 12345n, + skipFetch: false, + gasPrice: sharedState.gasPrice, + amountIn: 1000000000000000000n, + sushiRouteType: sharedState.appOptions.route, + }); + expect(sharedState.router.getMarketPrice).toHaveBeenNthCalledWith(2, { + fromToken: token1, + toToken: token2, + blockNumber: 12345n, + skipFetch: false, + gasPrice: sharedState.gasPrice, + amountIn: 500000000000000000n, + sushiRouteType: sharedState.appOptions.route, + }); + expect(sharedState.router.findLargestTradeSize).toHaveBeenCalledWith( + { takeOrder: { quote: { ratio: 0n } } } as any, + token2, + token1, + 1000000000000000000n, + sharedState.gasPrice, + sharedState.appOptions.route, + true, + ); + }); + + it("should call getMarketPrice with correct params for partial size unhappy all", async () => { + (sharedState.router.getMarketPrice as Mock) + .mockResolvedValueOnce(Result.err("error1")) + .mockResolvedValueOnce(Result.err("error2")); + (sharedState.router.findLargestTradeSize as Mock).mockReturnValueOnce( + 500000000000000000n, + ); + const result = await sharedState.getMarketPrice(token1, token2, 12345n); + + assert(result.isErr()); + expect(result.error).toBe("error1"); + expect(sharedState.router.getMarketPrice).toHaveBeenCalledTimes(2); + expect(sharedState.router.findLargestTradeSize).toHaveBeenCalledTimes(1); + expect(sharedState.router.getMarketPrice).toHaveBeenNthCalledWith(1, { + fromToken: token1, + toToken: token2, + blockNumber: 12345n, + skipFetch: false, + gasPrice: sharedState.gasPrice, + amountIn: 1000000000000000000n, + sushiRouteType: sharedState.appOptions.route, + }); + expect(sharedState.router.getMarketPrice).toHaveBeenNthCalledWith(2, { + fromToken: token1, + toToken: token2, + blockNumber: 12345n, + skipFetch: false, + gasPrice: sharedState.gasPrice, + amountIn: 500000000000000000n, + sushiRouteType: sharedState.appOptions.route, + }); + expect(sharedState.router.findLargestTradeSize).toHaveBeenCalledWith( + { takeOrder: { quote: { ratio: 0n } } } as any, + token2, + token1, + 1000000000000000000n, + sharedState.gasPrice, + sharedState.appOptions.route, + true, + ); }); }); }); diff --git a/src/state/index.ts b/src/state/index.ts index 349f27b8..3b792553 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -310,15 +310,49 @@ export class SharedState { * @param skipFetch - (optional) Skips a fresh onchain call to fetch pools * @returns The market price for the token pair or undefined if no route were found */ - getMarketPrice(fromToken: Token, toToken: Token, blockNumber?: bigint, skipFetch?: boolean) { - return this.router.getMarketPrice({ + async getMarketPrice( + fromToken: Token, + toToken: Token, + blockNumber?: bigint, + skipFetch?: boolean, + ) { + const amountIn = parseUnits("1", fromToken.decimals); + const result = await this.router.getMarketPrice({ + fromToken, + toToken, + blockNumber, + gasPrice: this.gasPrice, + amountIn, + sushiRouteType: this.appOptions.route, + skipFetch: !!skipFetch, + }); + if (result.isOk()) { + return result; + } + const partialAmountIn = this.router.findLargestTradeSize( + { takeOrder: { quote: { ratio: 0n } } } as any, // ratio unused when absolute + toToken, + fromToken, + amountIn, + this.gasPrice, + this.appOptions.route, + true, // absolute + ); + if (typeof partialAmountIn !== "bigint") { + return result; + } + const partialResult = await this.router.getMarketPrice({ fromToken, toToken, blockNumber, gasPrice: this.gasPrice, - amountIn: parseUnits("1", fromToken.decimals), + amountIn: partialAmountIn, sushiRouteType: this.appOptions.route, skipFetch: !!skipFetch, }); + if (partialResult.isOk()) { + return partialResult; + } + return result; } }