Skip to content

Commit 304b2b2

Browse files
authored
Merge pull request #10 from BootNodeDev/fix/getQuote
fix: getQuote & add SlippageTolerance in buildSwapCallData
2 parents 8486ca4 + 7a96d40 commit 304b2b2

8 files changed

Lines changed: 80 additions & 70 deletions

File tree

src/core/uniDevKitV4.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,6 @@ export class UniDevKitV4 {
188188
}
189189

190190
async buildSwapCallData(params: BuildSwapCallDataParams): Promise<Hex> {
191-
return buildSwapCallData(params);
191+
return buildSwapCallData(params, this.instance);
192192
}
193193
}

src/test/utils/buildSwapCallData.test.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { createMockSdkInstance } from "@/test/helpers/sdkInstance";
12
import { buildSwapCallData } from "@/utils/buildSwapCallData";
3+
import * as getQuoteModule from "@/utils/getQuote";
24
import { Token } from "@uniswap/sdk-core";
35
import { Pool } from "@uniswap/v4-sdk";
46
import { type Address, zeroAddress } from "viem";
5-
import { describe, expect, it } from "vitest";
7+
import { describe, expect, it, vi } from "vitest";
8+
9+
const sdkInstance = createMockSdkInstance();
10+
11+
// Mock getQuote to return a fixed amount
12+
vi.spyOn(getQuoteModule, "getQuote").mockImplementation(async () => ({
13+
amountOut: BigInt(1000000000000000000), // 1 WETH
14+
estimatedGasUsed: BigInt(100000),
15+
timestamp: Date.now(),
16+
}));
617

718
describe("buildSwapCallData", () => {
819
// USDC and WETH on Mainnet
@@ -31,11 +42,11 @@ describe("buildSwapCallData", () => {
3142
const params = {
3243
tokenIn: mockTokens[0],
3344
amountIn: BigInt(1000000), // 1 USDC
34-
amountOutMin: BigInt(0),
45+
slippageTolerance: 50,
3546
pool: mockPool,
3647
};
3748

38-
const calldata = await buildSwapCallData(params);
49+
const calldata = await buildSwapCallData(params, sdkInstance);
3950
expect(calldata).toBeDefined();
4051
expect(calldata).toMatch(/^0x/); // Should be a hex string
4152
});
@@ -44,11 +55,11 @@ describe("buildSwapCallData", () => {
4455
const params = {
4556
tokenIn: mockTokens[1],
4657
amountIn: BigInt(1000000000000000000), // 1 WETH
47-
amountOutMin: BigInt(0),
58+
slippageTolerance: 50,
4859
pool: mockPool,
4960
};
5061

51-
const calldata = await buildSwapCallData(params);
62+
const calldata = await buildSwapCallData(params, sdkInstance);
5263
expect(calldata).toBeDefined();
5364
expect(calldata).toMatch(/^0x/);
5465
});
@@ -57,11 +68,11 @@ describe("buildSwapCallData", () => {
5768
const params = {
5869
tokenIn: mockTokens[0],
5970
amountIn: BigInt(1000000),
60-
amountOutMin: BigInt(500000), // 0.5 WETH minimum
71+
slippageTolerance: 50,
6172
pool: mockPool,
6273
};
6374

64-
const calldata = await buildSwapCallData(params);
75+
const calldata = await buildSwapCallData(params, sdkInstance);
6576
expect(calldata).toBeDefined();
6677
expect(calldata).toMatch(/^0x/);
6778
});
@@ -70,11 +81,11 @@ describe("buildSwapCallData", () => {
7081
const params = {
7182
tokenIn: mockTokens[0],
7283
amountIn: BigInt(1000000),
73-
amountOutMin: BigInt(0),
84+
slippageTolerance: 50,
7485
pool: mockPool,
7586
};
7687

77-
const calldata = await buildSwapCallData(params);
88+
const calldata = await buildSwapCallData(params, sdkInstance);
7889
expect(calldata).toBeDefined();
7990
expect(calldata).toMatch(/^0x/);
8091
});
@@ -83,11 +94,11 @@ describe("buildSwapCallData", () => {
8394
const params = {
8495
tokenIn: mockTokens[0],
8596
amountIn: BigInt(1000000),
86-
amountOutMin: BigInt(0),
97+
slippageTolerance: 50,
8798
pool: mockPool,
8899
};
89100

90-
const calldata = await buildSwapCallData(params);
101+
const calldata = await buildSwapCallData(params, sdkInstance);
91102
expect(calldata).toBeDefined();
92103
expect(calldata).toMatch(/^0x/);
93104
});

src/test/utils/getQuote.test.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import { createMockSdkInstance } from "@/test/helpers/sdkInstance";
22
import { getQuote } from "@/utils/getQuote";
3+
import type { Pool } from "@uniswap/v4-sdk";
34
import type { Abi } from "viem";
45
import type { SimulateContractReturnType } from "viem/actions";
56
import { describe, expect, it, vi } from "vitest";
67

8+
const mockPool: Pool = {
9+
poolKey: {
10+
currency0: "0x123",
11+
currency1: "0x456",
12+
fee: 3000,
13+
tickSpacing: 10,
14+
hooks: "0x",
15+
},
16+
} as Pool;
17+
718
describe("getQuote", () => {
819
it("should throw error if SDK instance not found", async () => {
920
const mockDeps = createMockSdkInstance();
@@ -14,10 +25,9 @@ describe("getQuote", () => {
1425
await expect(
1526
getQuote(
1627
{
17-
tokens: ["0x123", "0x456"],
18-
zeroForOne: true,
28+
pool: mockPool,
1929
amountIn: BigInt(1000000),
20-
feeTier: 3000,
30+
zeroForOne: true,
2131
},
2232
mockDeps,
2333
),
@@ -32,10 +42,9 @@ describe("getQuote", () => {
3242

3343
const result = await getQuote(
3444
{
35-
tokens: ["0x123", "0x456"],
36-
zeroForOne: true,
45+
pool: mockPool,
3746
amountIn: BigInt(1000000),
38-
feeTier: 3000,
47+
zeroForOne: true,
3948
},
4049
mockDeps,
4150
);

src/types/utils/buildSwapCallData.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export type BuildSwapCallDataParams = {
2020
tokenIn: Address;
2121
/** Amount of input tokens to swap (in token's smallest unit) */
2222
amountIn: bigint;
23-
/** Minimum amount of output tokens to receive (in token's smallest unit) */
24-
amountOutMin?: bigint;
2523
/** Pool */
2624
pool: Pool;
25+
/** Slippage tolerance in basis points (e.g., 50 = 0.5%). Defaults to 50 (0.5%) */
26+
slippageTolerance?: number;
2727
};

src/types/utils/getQuote.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
1-
import type { FeeTier } from "@/types/utils/getPool";
2-
import type { Address, Hex } from "viem";
1+
import type { Pool } from "@uniswap/v4-sdk";
2+
import type { Hex } from "viem";
33

44
/**
55
* Parameters required for fetching a quote using the V4 Quoter contract.
66
*/
77
export interface QuoteParams {
88
/**
9-
* Array of two token addresses representing the pair. The order will be handled internally.
9+
* The pool instance to quote from
1010
*/
11-
tokens: [Address, Address];
12-
13-
/**
14-
* The fee tier of the pool (e.g., FeeTier.MEDIUM).
15-
*/
16-
feeTier: FeeTier;
17-
18-
/**
19-
* The tick spacing for the pool. If not provided, it will be derived from the fee tier.
20-
*/
21-
tickSpacing?: number;
11+
pool: Pool;
2212

2313
/**
2414
* The amount of tokens being swapped, expressed as a bigint.
@@ -30,11 +20,6 @@ export interface QuoteParams {
3020
*/
3121
zeroForOne: boolean;
3222

33-
/**
34-
* Address of the hooks contract, if any. Defaults to zero address if not provided.
35-
*/
36-
hooks?: Address;
37-
3823
/**
3924
* Optional additional data for the hooks, if any.
4025
*/

src/utils/buildSwapCallData.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { calculateMinimumOutput } from "@/helpers/swap";
12
import type { BuildSwapCallDataParams } from "@/types";
23
import { COMMANDS } from "@/types";
4+
import type { UniDevKitV4Instance } from "@/types/core";
5+
import { getQuote } from "@/utils/getQuote";
36
import { ethers } from "ethers";
47
import type { Hex } from "viem";
58

@@ -21,8 +24,8 @@ import type { Hex } from "viem";
2124
* const swapParams = {
2225
* tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
2326
* amountIn: parseUnits("100", 6), // 100 USDC
24-
* amountOutMin: parseUnits("0.05", 18), // Min 0.05 WETH
2527
* pool: pool,
28+
* slippageTolerance: 50, // 0.5%
2629
* };
2730
*
2831
* const calldata = await buildSwapCallData(swapParams);
@@ -37,13 +40,30 @@ import type { Hex } from "viem";
3740
*/
3841
export async function buildSwapCallData(
3942
params: BuildSwapCallDataParams,
43+
instance: UniDevKitV4Instance,
4044
): Promise<Hex> {
4145
// Extract and set default parameters
42-
const { tokenIn, amountIn, amountOutMin = 0n, pool } = params;
46+
const { tokenIn, amountIn, pool, slippageTolerance = 50 } = params;
4347

4448
const zeroForOne =
4549
tokenIn.toLowerCase() === pool.poolKey.currency0.toLowerCase();
4650

51+
// Get quote and calculate minimum output amount
52+
const quote = await getQuote(
53+
{
54+
pool,
55+
amountIn,
56+
zeroForOne,
57+
},
58+
instance,
59+
);
60+
61+
// Calculate minimum output amount based on slippage
62+
const amountOutMin = calculateMinimumOutput(
63+
quote.amountOut,
64+
slippageTolerance,
65+
);
66+
4767
// Encode Universal Router commands
4868
const commands = ethers.utils.solidityPack(
4969
["uint8"],
@@ -66,7 +86,7 @@ export async function buildSwapCallData(
6686
poolKey: pool.poolKey,
6787
zeroForOne,
6888
amountIn: ethers.BigNumber.from(amountIn.toString()),
69-
amountOutMinimum: ethers.BigNumber.from(amountOutMin.toString()),
89+
amountOutMinimum: amountOutMin,
7090
hookData: "0x",
7191
},
7292
],

src/utils/getQuote.ts

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import V4QuoterAbi from "@/constants/abis/V4Quoter";
2-
import { sortTokens } from "@/helpers/tokens";
32
import type { UniDevKitV4Instance } from "@/types/core";
4-
import { FeeTier, TICK_SPACING_BY_FEE } from "@/types/utils/getPool";
53
import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote";
6-
import { zeroAddress } from "viem";
74

85
/**
96
* Fetches a quote for a token swap using the V4 Quoter contract.
10-
* This function constructs the pool key from the given tokens and parameters,
11-
* and then simulates the quote to estimate the output amount.
7+
* This function uses the provided pool instance to simulate the quote.
128
*
13-
* @param params - The parameters required for the quote, including tokens, fee tier, tick spacing, and amount.
14-
* @param chainId - (Optional) The chain ID to use. If only one instance is registered, this is not required.
9+
* @param params - The parameters required for the quote, including pool and amount.
10+
* @param instance - UniDevKitV4 instance for contract interaction
1511
* @returns A Promise that resolves to the quote result, including the amount out and gas estimate.
1612
* @throws Will throw an error if:
17-
* - SDK instance is not found
1813
* - Simulation fails (e.g., insufficient liquidity, invalid parameters)
1914
* - Contract call reverts
2015
*/
@@ -24,30 +19,20 @@ export async function getQuote(
2419
): Promise<QuoteResponse> {
2520
const { client, contracts } = instance;
2621
const { quoter } = contracts;
22+
const {
23+
pool: { poolKey },
24+
} = params;
2725

2826
try {
29-
// Sort tokens to ensure consistent pool key ordering
30-
const [currency0, currency1] = sortTokens(
31-
params.tokens[0],
32-
params.tokens[1],
33-
);
34-
35-
// Use provided tick spacing or derive from fee tier
36-
const fee = (params.feeTier ?? FeeTier.MEDIUM) as FeeTier;
37-
const tickSpacing = params.tickSpacing ?? TICK_SPACING_BY_FEE[fee];
38-
39-
// Construct the poolKey
40-
const poolKey = {
41-
currency0,
42-
currency1,
43-
fee,
44-
tickSpacing,
45-
hooks: params.hooks || zeroAddress,
46-
};
47-
4827
// Build the parameters for quoteExactInputSingle
4928
const quoteParams = {
50-
poolKey,
29+
poolKey: {
30+
currency0: poolKey.currency0 as `0x${string}`,
31+
currency1: poolKey.currency1 as `0x${string}`,
32+
fee: poolKey.fee,
33+
tickSpacing: poolKey.tickSpacing,
34+
hooks: poolKey.hooks as `0x${string}`,
35+
},
5136
zeroForOne: params.zeroForOne,
5237
exactAmount: params.amountIn,
5338
hookData: params.hookData || "0x",

0 commit comments

Comments
 (0)