diff --git a/README.md b/README.md index c70f9e6..8989079 100644 --- a/README.md +++ b/README.md @@ -125,14 +125,40 @@ const poolKey = await uniDevKit.getPoolKeyFromPoolId({ ``` ### `buildSwapCallData` -Construct calldata and value for a Universal Router swap. +Construct calldata for a Universal Router swap. ```ts +// Basic swap const { calldata, value } = await uniDevKit.buildSwapCallData({ - tokenIn, - tokenOut, - amountIn: "1000000000000000000", - recipient, - slippageBips: 50 + tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + amountIn: parseUnits("100", 6), // 100 USDC + pool: pool, + slippageTolerance: 50, // 0.5% + recipient: "0x..." +}); + +// Swap with permit2 +const permitData = await uniDevKit.preparePermit2Data({ + token: tokenIn, + spender: uniDevKit.getContractAddress('universalRouter'), + owner: userAddress +}); + +const signature = await signer._signTypedData(permitData.toSign); +const permitWithSignature = permitData.buildPermit2DataWithSignature(signature); + +const { calldata: calldataWithPermit, value: valueWithPermit } = await uniDevKit.buildSwapCallData({ + tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + amountIn: parseUnits("100", 6), + pool: pool, + slippageTolerance: 50, + recipient: "0x...", + permit2Signature: permitWithSignature +}); + +const tx = await sendTransaction({ + to: uniDevKit.getContractAddress('universalRouter'), + data: calldata, + value }); ``` @@ -208,4 +234,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. - Releases are automated with [semantic-release](https://semantic-release.gitbook.io/semantic-release/). ## License -MIT +MIT \ No newline at end of file diff --git a/package.json b/package.json index 2683061..3cbf2d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniswap-dev-kit", - "version": "1.0.9", + "version": "1.0.10", "description": "A modern TypeScript library for integrating Uniswap into your dapp.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index cbc55e1..7ada051 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -16,7 +16,9 @@ import type { GetTokensParams } from "@/types/utils/getTokens"; import type { PreparePermit2BatchDataParams, PreparePermit2BatchDataResult, -} from "@/types/utils/preparePermit2BatchData"; + PreparePermit2DataParams, + PreparePermit2DataResult, +} from "@/types/utils/permit2"; import { buildAddLiquidityCallData } from "@/utils/buildAddLiquidityCallData"; import { buildSwapCallData } from "@/utils/buildSwapCallData"; import { getPool } from "@/utils/getPool"; @@ -25,9 +27,10 @@ import { getPosition } from "@/utils/getPosition"; import { getQuote } from "@/utils/getQuote"; import { getTokens } from "@/utils/getTokens"; import { preparePermit2BatchData } from "@/utils/preparePermit2BatchData"; +import { preparePermit2Data } from "@/utils/preparePermit2Data"; import type { Currency } from "@uniswap/sdk-core"; import type { Pool, PoolKey } from "@uniswap/v4-sdk"; -import type { Abi, Address, Hex, PublicClient } from "viem"; +import type { Abi, Address, PublicClient } from "viem"; import { http, createPublicClient } from "viem"; /** @@ -208,7 +211,7 @@ export class UniDevKitV4 { * @returns Promise resolving to swap call data including calldata and value * @throws Error if SDK instance is not found or if swap call data is invalid */ - async buildSwapCallData(params: BuildSwapCallDataParams): Promise { + async buildSwapCallData(params: BuildSwapCallDataParams) { return buildSwapCallData(params, this.instance); } @@ -236,4 +239,17 @@ export class UniDevKitV4 { ): Promise { return preparePermit2BatchData(params, this.instance); } + + /** + * Prepares the permit2 simple data for a single token. (Used to swap) + * Use toSign.values to sign the permit2 simple data. + * @param params @type {PreparePermit2DataParams} + * @returns Promise resolving to permit2 simple data + * @throws Error if SDK instance is not found or if permit2 simple data is invalid + */ + async preparePermit2Data( + params: PreparePermit2DataParams, + ): Promise { + return preparePermit2Data(params, this.instance); + } } diff --git a/src/test/utils/buildSwapCallData.test.ts b/src/test/utils/buildSwapCallData.test.ts index 377b049..d0bc74c 100644 --- a/src/test/utils/buildSwapCallData.test.ts +++ b/src/test/utils/buildSwapCallData.test.ts @@ -44,6 +44,7 @@ describe("buildSwapCallData", () => { amountIn: BigInt(1000000), // 1 USDC slippageTolerance: 50, pool: mockPool, + recipient: zeroAddress, }; const calldata = await buildSwapCallData(params, sdkInstance); @@ -57,6 +58,7 @@ describe("buildSwapCallData", () => { amountIn: BigInt(1000000000000000000), // 1 WETH slippageTolerance: 50, pool: mockPool, + recipient: zeroAddress, }; const calldata = await buildSwapCallData(params, sdkInstance); @@ -70,6 +72,7 @@ describe("buildSwapCallData", () => { amountIn: BigInt(1000000), slippageTolerance: 50, pool: mockPool, + recipient: zeroAddress, }; const calldata = await buildSwapCallData(params, sdkInstance); @@ -83,6 +86,7 @@ describe("buildSwapCallData", () => { amountIn: BigInt(1000000), slippageTolerance: 50, pool: mockPool, + recipient: zeroAddress, }; const calldata = await buildSwapCallData(params, sdkInstance); @@ -96,6 +100,7 @@ describe("buildSwapCallData", () => { amountIn: BigInt(1000000), slippageTolerance: 50, pool: mockPool, + recipient: zeroAddress, }; const calldata = await buildSwapCallData(params, sdkInstance); diff --git a/src/types/utils/buildSwapCallData.ts b/src/types/utils/buildSwapCallData.ts index 9f85762..7756792 100644 --- a/src/types/utils/buildSwapCallData.ts +++ b/src/types/utils/buildSwapCallData.ts @@ -1,15 +1,17 @@ +import type { PermitSingle } from "@uniswap/permit2-sdk"; import type { Pool } from "@uniswap/v4-sdk"; -import type { Address } from "viem"; +import type { Address, Hex } from "viem"; /** * Command codes for Universal Router operations * @see https://docs.uniswap.org/contracts/universal-router/technical-reference */ export const COMMANDS = { + PERMIT2_PERMIT: 0x0a, SWAP_EXACT_IN_SINGLE: 0x06, SETTLE_ALL: 0x0c, TAKE_ALL: 0x0f, - V4_ROUTER_EXECUTE: 0x10, + V4_SWAP: 0x10, } as const; /** @@ -24,4 +26,12 @@ export type BuildSwapCallDataParams = { pool: Pool; /** Slippage tolerance in basis points (e.g., 50 = 0.5%). Defaults to 50 (0.5%) */ slippageTolerance?: number; + /** Recipient address */ + recipient: Address; + /** Permit2 signature */ + permit2Signature?: { + signature: Hex; + owner: Address; + permit: PermitSingle; + }; }; diff --git a/src/types/utils/permit2.ts b/src/types/utils/permit2.ts new file mode 100644 index 0000000..8876fa2 --- /dev/null +++ b/src/types/utils/permit2.ts @@ -0,0 +1,84 @@ +import type { PermitBatch, PermitSingle } from "@uniswap/permit2-sdk"; +import type { BatchPermitOptions } from "@uniswap/v4-sdk"; +import type { TypedDataField } from "ethers"; +import type { Address, Hex } from "viem"; + +/** + * Base interface for Permit2 data + */ +interface BasePermit2Data { + /** Address that will be allowed to spend the tokens */ + spender: Address | string; + /** User's wallet address */ + owner: Address | string; + /** Signature deadline in seconds */ + sigDeadline?: number; +} + +/** + * Interface for the parameters required to generate a Permit2 batch signature + */ +export interface PreparePermit2BatchDataParams extends BasePermit2Data { + /** Array of token addresses to permit */ + tokens: (Address | string)[]; +} + +/** + * Interface for the parameters required to generate a single Permit2 signature + */ +export interface PreparePermit2DataParams extends BasePermit2Data { + /** Token address to permit */ + token: Address | string; +} + +/** + * Base interface for Permit2 data result + */ +interface BasePermit2DataResult { + /** User's wallet address */ + owner: Address | string; + /** Data needed to sign the permit2 data */ + toSign: { + /** Domain of the permit2 data */ + domain: { + name: string; + version: string; + chainId: number; + verifyingContract: `0x${string}`; + }; + /** Types of the permit2 data */ + types: Record; + /** Values of the permit2 data */ + values: PermitBatch | PermitSingle; + /** Primary type of the permit2 data */ + primaryType: "PermitBatch" | "PermitSingle"; + /** Message of the permit2 data */ + message: Record; + }; +} + +/** + * Interface for the return value of the batch permit function + */ +export interface PreparePermit2BatchDataResult extends BasePermit2DataResult { + /** Function to build the permit2 batch data with a signature */ + buildPermit2BatchDataWithSignature: ( + signature: string | Hex, + ) => BatchPermitOptions; + /** Permit2 batch data */ + permitBatch: PermitBatch; +} + +/** + * Interface for the return value of the single permit function + */ +export interface PreparePermit2DataResult extends BasePermit2DataResult { + /** Function to build the permit2 data with a signature */ + buildPermit2DataWithSignature: (signature: Hex) => { + owner: Address; + permit: PermitSingle; + signature: Hex; + }; + /** Permit2 data */ + permit: PermitSingle; +} diff --git a/src/types/utils/preparePermit2BatchData.ts b/src/types/utils/preparePermit2BatchData.ts deleted file mode 100644 index 20a67ea..0000000 --- a/src/types/utils/preparePermit2BatchData.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { PermitBatch } from "@uniswap/permit2-sdk"; -import type { BatchPermitOptions } from "@uniswap/v4-sdk"; -import type { TypedDataDomain, TypedDataField } from "ethers"; -import type { Address, Hex } from "viem"; - -/** - * Interface for the parameters required to generate a Permit2 batch signature - */ -export interface PreparePermit2BatchDataParams { - /** Array of token addresses to permit */ - tokens: (Address | string)[]; - /** Address that will be allowed to spend the tokens */ - spender: Address | string; - /** User's wallet address */ - owner: Address | string; - /** Signature deadline in seconds */ - sigDeadline?: number; -} - -/** - * Interface for the return value of the function - */ -export interface PreparePermit2BatchDataResult { - /** Function to build the permit2 batch data with a signature */ - buildPermit2BatchDataWithSignature: ( - signature: string | Hex, - ) => BatchPermitOptions; - /** User's wallet address */ - owner: Address | string; - /** Permit2 batch data */ - permitBatch: PermitBatch; - /** Data needed to sign the permit2 batch data */ - toSign: { - /** Domain of the permit2 batch data */ - domain: TypedDataDomain; - /** Types of the permit2 batch data */ - types: Record; - /** Values of the permit2 batch data */ - values: PermitBatch; - }; -} diff --git a/src/utils/__tests__/preparePermit2Data.test.ts b/src/utils/__tests__/preparePermit2Data.test.ts new file mode 100644 index 0000000..0d88eeb --- /dev/null +++ b/src/utils/__tests__/preparePermit2Data.test.ts @@ -0,0 +1,104 @@ +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; +import { PERMIT2_ADDRESS } from "@uniswap/permit2-sdk"; +import { type Block, zeroAddress } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { preparePermit2Data } from "../preparePermit2Data"; + +describe("preparePermit2Data", () => { + const mockInstance = createMockSdkInstance(); + const mockBlockTimestamp = 1234567890n; + + const mockParams = { + token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + spender: "0x1234567890123456789012345678901234567890", + owner: "0x0987654321098765432109876543210987654321", + }; + + beforeEach(() => { + vi.spyOn(mockInstance.client, "readContract").mockImplementation( + async () => ({ + amount: "0", + expiration: "0", + nonce: "0", + }), + ); + vi.spyOn(mockInstance.client, "getBlock").mockResolvedValue({ + timestamp: mockBlockTimestamp, + } as Block); + }); + + it("should throw error for native token", async () => { + await expect( + preparePermit2Data( + { + ...mockParams, + token: zeroAddress, + }, + mockInstance, + ), + ).rejects.toThrow("Native tokens are not supported for permit2"); + }); + + it("should prepare permit2 data with default sigDeadline", async () => { + const result = await preparePermit2Data(mockParams, mockInstance); + + expect(result.owner).toBe(mockParams.owner); + expect(result.permit.details.token).toBe(mockParams.token); + expect(result.permit.spender).toBe(mockParams.spender); + expect(result.permit.sigDeadline).toBe(Number(mockBlockTimestamp) + 3600); // block timestamp + 1 hour + expect(result.toSign.domain).toBeDefined(); + expect(result.toSign.types).toBeDefined(); + expect(result.toSign.values).toBeDefined(); + expect(result.buildPermit2DataWithSignature).toBeDefined(); + }); + + it("should prepare permit2 data with custom sigDeadline", async () => { + const customDeadline = Number(mockBlockTimestamp) + 7200; // 2 hours from now + const result = await preparePermit2Data( + { + ...mockParams, + sigDeadline: customDeadline, + }, + mockInstance, + ); + + expect(result.permit.sigDeadline).toBe(customDeadline); + }); + + it("should build permit2 data with signature", async () => { + const result = await preparePermit2Data(mockParams, mockInstance); + const signature = + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + const permitWithSignature = result.buildPermit2DataWithSignature(signature); + + expect(permitWithSignature).toEqual({ + owner: mockParams.owner, + permit: result.permit, + signature, + }); + }); + + it("should fetch current allowance details", async () => { + const mockAllowance = { + amount: "1000000", + expiration: "1234567890", + nonce: "42", + }; + + vi.spyOn(mockInstance.client, "readContract").mockImplementationOnce( + async () => mockAllowance, + ); + + const result = await preparePermit2Data(mockParams, mockInstance); + + expect(mockInstance.client.readContract).toHaveBeenCalledWith({ + address: PERMIT2_ADDRESS, + abi: expect.any(Array), + functionName: "allowance", + args: [mockParams.owner, mockParams.token, mockParams.spender], + }); + + expect(result.permit.details.expiration).toBe(mockAllowance.expiration); + expect(result.permit.details.nonce).toBe(mockAllowance.nonce); + }); +}); diff --git a/src/utils/buildSwapCallData.ts b/src/utils/buildSwapCallData.ts index a13930d..4f670f9 100644 --- a/src/utils/buildSwapCallData.ts +++ b/src/utils/buildSwapCallData.ts @@ -1,11 +1,26 @@ import { calculateMinimumOutput } from "@/helpers/swap"; -import type { BuildSwapCallDataParams } from "@/types"; -import { COMMANDS } from "@/types"; +import { type BuildSwapCallDataParams, COMMANDS } from "@/types"; import type { UniDevKitV4Instance } from "@/types/core"; import { getQuote } from "@/utils/getQuote"; +import type { PermitSingle } from "@uniswap/permit2-sdk"; +import { Actions, V4Planner } from "@uniswap/v4-sdk"; import { ethers } from "ethers"; import type { Hex } from "viem"; +const buildPermit2StructInput = (permit: PermitSingle, signature: Hex) => { + return ethers.utils.defaultAbiCoder.encode( + [ + "tuple(" + + "tuple(address token,uint160 amount,uint48 expiration,uint48 nonce) details," + + "address spender," + + "uint256 sigDeadline" + + ")", + "bytes", + ], + [permit, signature], + ); +}; + /** * Builds calldata for a Uniswap V4 swap * @@ -21,6 +36,7 @@ import type { Hex } from "viem"; * * @example * ```typescript + * // Basic swap * const swapParams = { * tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC * amountIn: parseUnits("100", 6), // 100 USDC @@ -28,7 +44,24 @@ import type { Hex } from "viem"; * slippageTolerance: 50, // 0.5% * }; * - * const calldata = await buildSwapCallData(swapParams); + * const calldata = await buildSwapCallData(swapParams, instance); + * + * // Swap with permit2 + * const permitData = await preparePermit2Data({ + * token: tokenIn, + * spender: universalRouterAddress, + * owner: userAddress + * }, instance); + * + * const signature = await signer._signTypedData(permitData.toSign); + * const permitWithSignature = permitData.buildPermit2DataWithSignature(signature); + * + * const swapParamsWithPermit = { + * ...swapParams, + * permit2Signature: permitWithSignature + * }; + * + * const calldataWithPermit = await buildSwapCallData(swapParamsWithPermit, instance); * * // Send transaction * const tx = await sendTransaction({ @@ -43,7 +76,14 @@ export async function buildSwapCallData( instance: UniDevKitV4Instance, ): Promise { // Extract and set default parameters - const { tokenIn, amountIn, pool, slippageTolerance = 50 } = params; + const { + tokenIn, + amountIn, + pool, + slippageTolerance = 50, + permit2Signature, + recipient, + } = params; const zeroForOne = tokenIn.toLowerCase() === pool.poolKey.currency0.toLowerCase(); @@ -59,66 +99,66 @@ export async function buildSwapCallData( ); // Calculate minimum output amount based on slippage - const amountOutMin = calculateMinimumOutput( + const amountOutMinimum = calculateMinimumOutput( quote.amountOut, slippageTolerance, ); - // Encode Universal Router commands - const commands = ethers.utils.solidityPack( - ["uint8"], - [COMMANDS.V4_ROUTER_EXECUTE], - ); + const planner = new V4Planner(); - // Encode swap actions sequence - const actions = ethers.utils.solidityPack( - ["uint8", "uint8", "uint8"], - [COMMANDS.SWAP_EXACT_IN_SINGLE, COMMANDS.SETTLE_ALL, COMMANDS.TAKE_ALL], - ); + planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [ + { + poolKey: pool.poolKey, + zeroForOne, + amountIn: amountIn.toString(), + amountOutMinimum: amountOutMinimum.toString(), + hookData: "0x", + }, + ]); - // Encode swap parameters - const exactInputSingleParams = ethers.utils.defaultAbiCoder.encode( - [ - "tuple(tuple(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bool zeroForOne, uint128 amountIn, uint128 amountOutMinimum, bytes hookData)", - ], - [ - { - poolKey: pool.poolKey, - zeroForOne, - amountIn: ethers.BigNumber.from(amountIn.toString()), - amountOutMinimum: amountOutMin, - hookData: "0x", - }, - ], - ); + const currencyIn = zeroForOne ? pool.currency0 : pool.currency1; + const currencyOut = zeroForOne ? pool.currency1 : pool.currency0; - // Encode token amounts for settlement - const swapParams = [ - exactInputSingleParams, - ethers.utils.defaultAbiCoder.encode( - ["address", "uint128"], - zeroForOne - ? [pool.poolKey.currency0, amountIn] - : [pool.poolKey.currency1, amountIn], - ), - ethers.utils.defaultAbiCoder.encode( - ["address", "uint128"], - zeroForOne ? [pool.poolKey.currency1, 0] : [pool.poolKey.currency0, 0], - ), - ]; + // Agrega la acción de settle + planner.addSettle(currencyIn, true); + + // Agrega la acción de take + planner.addTake(currencyOut, recipient); - // Encode final inputs - const inputs = [ + let commands = ethers.utils.solidityPack(["uint8"], [COMMANDS.V4_SWAP]); + + if (permit2Signature) { + commands = ethers.utils.solidityPack( + ["uint8", "uint8"], + [COMMANDS.PERMIT2_PERMIT, COMMANDS.V4_SWAP], + ); + } + + // Combine actions and params into a single bytes array to match with V4_SWAP command input + let inputs = [ + // V4_SWAP input ethers.utils.defaultAbiCoder.encode( ["bytes", "bytes[]"], - [actions, swapParams], + [planner.actions, planner.params], ), ]; - // Set 5-minute deadline - const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 5); + // If permit2Signature is provided, add the permit2 struct input to the inputs array in the first position + if (permit2Signature) { + inputs = [ + buildPermit2StructInput( + permit2Signature.permit, + permit2Signature.signature, + ), + ethers.utils.defaultAbiCoder.encode( + ["bytes", "bytes[]"], + [planner.actions, planner.params], + ), + ]; + } + + const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 5); // 5 minutes - // Create Universal Router interface const universalRouterInterface = new ethers.utils.Interface([ "function execute(bytes commands, bytes[] inputs, uint256 deadline)", ]); diff --git a/src/utils/preparePermit2BatchData.ts b/src/utils/preparePermit2BatchData.ts index 01023c6..5c58586 100644 --- a/src/utils/preparePermit2BatchData.ts +++ b/src/utils/preparePermit2BatchData.ts @@ -2,7 +2,7 @@ import type { UniDevKitV4Instance } from "@/types"; import type { PreparePermit2BatchDataParams, PreparePermit2BatchDataResult, -} from "@/types/utils/preparePermit2BatchData"; +} from "@/types/utils/permit2"; import { AllowanceTransfer, MaxUint160, @@ -10,16 +10,14 @@ import { type PermitBatch, } from "@uniswap/permit2-sdk"; import type { BatchPermitOptions } from "@uniswap/v4-sdk"; -import type { TypedDataDomain, TypedDataField } from "ethers"; -import { zeroAddress } from "viem"; -import type { Hex } from "viem/_types/types/misc"; - -/** +import type { TypedDataField } from "ethers"; +import type { Hex } from "viem"; +import { zeroAddress } from "viem"; /** * Prepares the permit2 batch data for multiple tokens * * This function creates a batch permit that allows a spender to use multiple tokens * on behalf of the user. It fetches current allowance details for each token and - * prepares the data needed for signing. + * prepares the data needed for signing. You can use with viem or ethers. * * The complete flow to use this function is: * 1. Prepare the permit data: @@ -33,10 +31,14 @@ import type { Hex } from "viem/_types/types/misc"; * * 2. Sign the permit data using your signer: * ```typescript + * // viem + * const signature = await signer._signTypedData(permitData.toSign) + * + * // ethers * const signature = await signer.signTypedData( * permitData.toSign.domain, * permitData.toSign.types, - * permitData.toSign.values + * permitData.toSign.values, * ) * ``` * @@ -135,7 +137,7 @@ export async function preparePermit2BatchData( PERMIT2_ADDRESS, chainId, ) as { - domain: TypedDataDomain; + domain: PreparePermit2BatchDataResult["toSign"]["domain"]; types: Record; values: PermitBatch; }; @@ -158,6 +160,8 @@ export async function preparePermit2BatchData( domain, types, values, + primaryType: "PermitBatch", + message: values as unknown as Record, }, }; } diff --git a/src/utils/preparePermit2Data.ts b/src/utils/preparePermit2Data.ts new file mode 100644 index 0000000..0295116 --- /dev/null +++ b/src/utils/preparePermit2Data.ts @@ -0,0 +1,163 @@ +import type { UniDevKitV4Instance } from "@/types"; +import type { + PreparePermit2DataParams, + PreparePermit2DataResult, +} from "@/types/utils/permit2"; +import { + AllowanceTransfer, + MaxUint160, + PERMIT2_ADDRESS, + type PermitSingle, +} from "@uniswap/permit2-sdk"; +import type { TypedDataField } from "ethers"; +import type { Address, Hex } from "viem"; +import { zeroAddress } from "viem"; + +/** + * Prepares the permit2 data for a single token + * + * This function creates a permit that allows a spender to use a single token + * on behalf of the user. It fetches current allowance details for the token and + * prepares the data needed for signing. + * + * The complete flow to use this function is: + * 1. Prepare the permit data: + * ```typescript + * const permitData = await preparePermit2Data({ + * token: token0, + * spender: positionManagerAddress, + * owner: userAddress + * }, instance) + * ``` + * + * 2. Sign the permit data using your signer (viem): + * ```typescript + * const signature = await signer._signTypedData(permitData.toSign) + * + * // ethers + * const signature = await signer.signTypedData( + * permitData.toSign.domain, + * permitData.toSign.types, + * permitData.toSign.values, + * ) + * ``` + * + * 3. Build the final permit data with signature: + * ```typescript + * const permitWithSignature = permitData.buildPermit2DataWithSignature(signature) + * ``` + * + * 4. Use the permit in your transaction (e.g. with buildAddLiquidityCallData): + * ```typescript + * const { calldata } = await buildAddLiquidityCallData({ + * permit2Signature: permitWithSignature, + * // ... other params + * }, instance) + * ``` + * + * @param params - Parameters for preparing the permit2 batch data + * @returns Promise resolving to the permit2 batch data and helper functions + * @throws Error if any required dependencies are missing + */ +export async function preparePermit2Data( + params: PreparePermit2DataParams, + instance: UniDevKitV4Instance, +): Promise { + const { token, spender, owner, sigDeadline: sigDeadlineParam } = params; + + const chainId = instance.chain.id; + + // calculate sigDeadline if not provided + let sigDeadline = sigDeadlineParam; + if (!sigDeadline) { + const blockTimestamp = await instance.client + .getBlock() + .then((block) => block.timestamp); + + sigDeadline = Number(blockTimestamp + 60n * 60n); // 30 minutes from current block timestamp + } + + if (token.toLowerCase() === zeroAddress.toLowerCase()) { + throw new Error("Native tokens are not supported for permit2"); + } + + // Fetch allowance details for each token + const details = await instance.client.readContract({ + address: PERMIT2_ADDRESS as `0x${string}`, + abi: [ + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "token", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [ + { + components: [ + { name: "amount", type: "uint160" }, + { name: "expiration", type: "uint48" }, + { name: "nonce", type: "uint48" }, + ], + name: "details", + type: "tuple", + }, + ], + }, + ] as const, + functionName: "allowance", + args: [ + owner as `0x${string}`, + token as `0x${string}`, + spender as `0x${string}`, + ], + }); + + const permit: PermitSingle = { + details: { + token, + amount: MaxUint160.toString(), + expiration: details.expiration.toString(), + nonce: details.nonce.toString(), + }, + spender, + sigDeadline, + }; + + // Create the permit object + // Get the data needed for signing + const { domain, types, values } = AllowanceTransfer.getPermitData( + permit, + PERMIT2_ADDRESS, + chainId, + ) as { + domain: PreparePermit2DataResult["toSign"]["domain"]; + types: Record; + values: PermitSingle; + }; + + const buildPermit2DataWithSignature = ( + signature: Hex, + ): ReturnType => { + return { + owner: owner as Address, + permit, + signature, + }; + }; + + return { + buildPermit2DataWithSignature, + owner, + permit, + toSign: { + domain, + types, + values, + primaryType: "PermitSingle", + message: values as unknown as Record, + }, + }; +}