Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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