Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/core/uniDevKitV4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,6 @@ export class UniDevKitV4 {
}

async buildSwapCallData(params: BuildSwapCallDataParams): Promise<Hex> {
return buildSwapCallData(params);
return buildSwapCallData(params, this.instance);
}
}
File renamed without changes.
33 changes: 22 additions & 11 deletions src/test/utils/buildSwapCallData.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { createMockSdkInstance } from "@/test/helpers/sdkInstance";
import { buildSwapCallData } from "@/utils/buildSwapCallData";
import * as getQuoteModule from "@/utils/getQuote";
import { Token } from "@uniswap/sdk-core";
import { Pool } from "@uniswap/v4-sdk";
import { type Address, zeroAddress } from "viem";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";

const sdkInstance = createMockSdkInstance();

// Mock getQuote to return a fixed amount
vi.spyOn(getQuoteModule, "getQuote").mockImplementation(async () => ({
amountOut: BigInt(1000000000000000000), // 1 WETH
estimatedGasUsed: BigInt(100000),
timestamp: Date.now(),
}));

describe("buildSwapCallData", () => {
// USDC and WETH on Mainnet
Expand Down Expand Up @@ -31,11 +42,11 @@ describe("buildSwapCallData", () => {
const params = {
tokenIn: mockTokens[0],
amountIn: BigInt(1000000), // 1 USDC
amountOutMin: BigInt(0),
slippageTolerance: 50,
pool: mockPool,
};

const calldata = await buildSwapCallData(params);
const calldata = await buildSwapCallData(params, sdkInstance);
expect(calldata).toBeDefined();
expect(calldata).toMatch(/^0x/); // Should be a hex string
});
Expand All @@ -44,11 +55,11 @@ describe("buildSwapCallData", () => {
const params = {
tokenIn: mockTokens[1],
amountIn: BigInt(1000000000000000000), // 1 WETH
amountOutMin: BigInt(0),
slippageTolerance: 50,
pool: mockPool,
};

const calldata = await buildSwapCallData(params);
const calldata = await buildSwapCallData(params, sdkInstance);
expect(calldata).toBeDefined();
expect(calldata).toMatch(/^0x/);
});
Expand All @@ -57,11 +68,11 @@ describe("buildSwapCallData", () => {
const params = {
tokenIn: mockTokens[0],
amountIn: BigInt(1000000),
amountOutMin: BigInt(500000), // 0.5 WETH minimum
slippageTolerance: 50,
pool: mockPool,
};

const calldata = await buildSwapCallData(params);
const calldata = await buildSwapCallData(params, sdkInstance);
expect(calldata).toBeDefined();
expect(calldata).toMatch(/^0x/);
});
Expand All @@ -70,11 +81,11 @@ describe("buildSwapCallData", () => {
const params = {
tokenIn: mockTokens[0],
amountIn: BigInt(1000000),
amountOutMin: BigInt(0),
slippageTolerance: 50,
pool: mockPool,
};

const calldata = await buildSwapCallData(params);
const calldata = await buildSwapCallData(params, sdkInstance);
expect(calldata).toBeDefined();
expect(calldata).toMatch(/^0x/);
});
Expand All @@ -83,11 +94,11 @@ describe("buildSwapCallData", () => {
const params = {
tokenIn: mockTokens[0],
amountIn: BigInt(1000000),
amountOutMin: BigInt(0),
slippageTolerance: 50,
pool: mockPool,
};

const calldata = await buildSwapCallData(params);
const calldata = await buildSwapCallData(params, sdkInstance);
expect(calldata).toBeDefined();
expect(calldata).toMatch(/^0x/);
});
Expand Down
21 changes: 15 additions & 6 deletions src/test/utils/getQuote.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { createMockSdkInstance } from "@/test/helpers/sdkInstance";
import { getQuote } from "@/utils/getQuote";
import type { Pool } from "@uniswap/v4-sdk";
import type { Abi } from "viem";
import type { SimulateContractReturnType } from "viem/actions";
import { describe, expect, it, vi } from "vitest";

const mockPool: Pool = {
poolKey: {
currency0: "0x123",
currency1: "0x456",
fee: 3000,
tickSpacing: 10,
hooks: "0x",
},
} as Pool;

describe("getQuote", () => {
it("should throw error if SDK instance not found", async () => {
const mockDeps = createMockSdkInstance();
Expand All @@ -14,10 +25,9 @@ describe("getQuote", () => {
await expect(
getQuote(
{
tokens: ["0x123", "0x456"],
zeroForOne: true,
pool: mockPool,
amountIn: BigInt(1000000),
feeTier: 3000,
zeroForOne: true,
},
mockDeps,
),
Expand All @@ -32,10 +42,9 @@ describe("getQuote", () => {

const result = await getQuote(
{
tokens: ["0x123", "0x456"],
zeroForOne: true,
pool: mockPool,
amountIn: BigInt(1000000),
feeTier: 3000,
zeroForOne: true,
},
mockDeps,
);
Expand Down
4 changes: 2 additions & 2 deletions src/types/utils/buildSwapCallData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export type BuildSwapCallDataParams = {
tokenIn: Address;
/** Amount of input tokens to swap (in token's smallest unit) */
amountIn: bigint;
/** Minimum amount of output tokens to receive (in token's smallest unit) */
amountOutMin?: bigint;
/** Pool */
pool: Pool;
/** Slippage tolerance in basis points (e.g., 50 = 0.5%). Defaults to 50 (0.5%) */
slippageTolerance?: number;
};
23 changes: 4 additions & 19 deletions src/types/utils/getQuote.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import type { FeeTier } from "@/types/utils/getPool";
import type { Address, Hex } from "viem";
import type { Pool } from "@uniswap/v4-sdk";
import type { Hex } from "viem";

/**
* Parameters required for fetching a quote using the V4 Quoter contract.
*/
export interface QuoteParams {
/**
* Array of two token addresses representing the pair. The order will be handled internally.
* The pool instance to quote from
*/
tokens: [Address, Address];

/**
* The fee tier of the pool (e.g., FeeTier.MEDIUM).
*/
feeTier: FeeTier;

/**
* The tick spacing for the pool. If not provided, it will be derived from the fee tier.
*/
tickSpacing?: number;
pool: Pool;

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

/**
* Address of the hooks contract, if any. Defaults to zero address if not provided.
*/
hooks?: Address;

/**
* Optional additional data for the hooks, if any.
*/
Expand Down
26 changes: 23 additions & 3 deletions src/utils/buildSwapCallData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { calculateMinimumOutput } from "@/helpers/swap";
import type { BuildSwapCallDataParams } from "@/types";
import { COMMANDS } from "@/types";
import type { UniDevKitV4Instance } from "@/types/core";
import { getQuote } from "@/utils/getQuote";
import { ethers } from "ethers";
import type { Hex } from "viem";

Expand All @@ -21,8 +24,8 @@ import type { Hex } from "viem";
* const swapParams = {
* tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
* amountIn: parseUnits("100", 6), // 100 USDC
* amountOutMin: parseUnits("0.05", 18), // Min 0.05 WETH
* pool: pool,
* slippageTolerance: 50, // 0.5%
* };
*
* const calldata = await buildSwapCallData(swapParams);
Expand All @@ -37,13 +40,30 @@ import type { Hex } from "viem";
*/
export async function buildSwapCallData(
params: BuildSwapCallDataParams,
instance: UniDevKitV4Instance,
): Promise<Hex> {
// Extract and set default parameters
const { tokenIn, amountIn, amountOutMin = 0n, pool } = params;
const { tokenIn, amountIn, pool, slippageTolerance = 50 } = params;

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

// Get quote and calculate minimum output amount
const quote = await getQuote(
{
pool,
amountIn,
zeroForOne,
},
instance,
);

// Calculate minimum output amount based on slippage
const amountOutMin = calculateMinimumOutput(
quote.amountOut,
slippageTolerance,
);

// Encode Universal Router commands
const commands = ethers.utils.solidityPack(
["uint8"],
Expand All @@ -66,7 +86,7 @@ export async function buildSwapCallData(
poolKey: pool.poolKey,
zeroForOne,
amountIn: ethers.BigNumber.from(amountIn.toString()),
amountOutMinimum: ethers.BigNumber.from(amountOutMin.toString()),
amountOutMinimum: amountOutMin,
hookData: "0x",
},
],
Expand Down
41 changes: 13 additions & 28 deletions src/utils/getQuote.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import V4QuoterAbi from "@/constants/abis/V4Quoter";
import { sortTokens } from "@/helpers/tokens";
import type { UniDevKitV4Instance } from "@/types/core";
import { FeeTier, TICK_SPACING_BY_FEE } from "@/types/utils/getPool";
import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote";
import { zeroAddress } from "viem";

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

try {
// Sort tokens to ensure consistent pool key ordering
const [currency0, currency1] = sortTokens(
params.tokens[0],
params.tokens[1],
);

// Use provided tick spacing or derive from fee tier
const fee = (params.feeTier ?? FeeTier.MEDIUM) as FeeTier;
const tickSpacing = params.tickSpacing ?? TICK_SPACING_BY_FEE[fee];

// Construct the poolKey
const poolKey = {
currency0,
currency1,
fee,
tickSpacing,
hooks: params.hooks || zeroAddress,
};

// Build the parameters for quoteExactInputSingle
const quoteParams = {
poolKey,
poolKey: {
currency0: poolKey.currency0 as `0x${string}`,
currency1: poolKey.currency1 as `0x${string}`,
fee: poolKey.fee,
tickSpacing: poolKey.tickSpacing,
hooks: poolKey.hooks as `0x${string}`,
},
zeroForOne: params.zeroForOne,
exactAmount: params.amountIn,
hookData: params.hookData || "0x",
Expand Down