@uniswap/v4-sdk
Version:
⚒️ An SDK for building applications on top of Uniswap V4
237 lines • 11.4 kB
JavaScript
import { validateAndParseAddress } from '@uniswap/sdk-core';
import JSBI from 'jsbi';
import { Position } from './entities/position';
import { toHex } from './utils/calldata';
import { MSG_SENDER } from './actionConstants';
import { Interface } from '@ethersproject/abi';
import { Multicall } from './multicall';
import invariant from 'tiny-invariant';
import { EMPTY_BYTES, CANNOT_BURN, NATIVE_NOT_SET, NO_SQRT_PRICE, ONE, OPEN_DELTA, PositionFunctions, ZERO, ZERO_LIQUIDITY, } from './internalConstants';
import { V4PositionPlanner } from './utils';
import { positionManagerAbi } from './utils/positionManagerAbi';
const NFT_PERMIT_TYPES = {
Permit: [
{ name: 'spender', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
// type guard
function isMint(options) {
return Object.keys(options).some((k) => k === 'recipient');
}
function shouldCreatePool(options) {
if (options.createPool) {
invariant(options.sqrtPriceX96 !== undefined, NO_SQRT_PRICE);
return true;
}
return false;
}
export class V4PositionManager {
/**
* Cannot be constructed.
*/
constructor() { }
/**
* Public methods to encode method parameters for different actions on the PositionManager contract
*/
static createCallParameters(poolKey, sqrtPriceX96) {
return {
calldata: this.encodeInitializePool(poolKey, sqrtPriceX96),
value: toHex(0),
};
}
static addCallParameters(position, options) {
/**
* Cases:
* - if pool does not exist yet, encode initializePool
* then,
* - if is mint, encode MINT_POSITION. If migrating, encode a SETTLE and SWEEP for both currencies. Else, encode a SETTLE_PAIR. If on a NATIVE pool, encode a SWEEP.
* - else, encode INCREASE_LIQUIDITY and CLOSE_CURRENCY for each token (handles accrued fees). If on a NATIVE pool, encode a SWEEP.
*/
invariant(JSBI.greaterThan(position.liquidity, ZERO), ZERO_LIQUIDITY);
const calldataList = [];
const planner = new V4PositionPlanner();
// Encode initialize pool.
if (isMint(options) && shouldCreatePool(options)) {
// No planner used here because initializePool is not supported as an Action
calldataList.push(V4PositionManager.encodeInitializePool(position.pool.poolKey, options.sqrtPriceX96));
}
// position.pool.currency0 is native if and only if options.useNative is set
invariant(position.pool.currency0 === options.useNative ||
(!position.pool.currency0.isNative && options.useNative === undefined), NATIVE_NOT_SET);
// adjust for slippage
const maximumAmounts = position.mintAmountsWithSlippage(options.slippageTolerance);
const amount0Max = toHex(maximumAmounts.amount0);
const amount1Max = toHex(maximumAmounts.amount1);
// We use permit2 to approve tokens to the position manager
if (options.batchPermit) {
calldataList.push(V4PositionManager.encodePermitBatch(options.batchPermit.owner, options.batchPermit.permitBatch, options.batchPermit.signature));
}
// mint
if (isMint(options)) {
const recipient = validateAndParseAddress(options.recipient);
planner.addMint(position.pool, position.tickLower, position.tickUpper, position.liquidity, amount0Max, amount1Max, recipient, options.hookData);
}
else {
// increase
planner.addIncrease(options.tokenId, position.liquidity, amount0Max, amount1Max, options.hookData);
}
let value = toHex(0);
// If migrating, we need to settle and sweep both currencies individually
if (isMint(options) && options.migrate) {
if (options.useNative) {
// unwrap the exact amount needed to send to the pool manager
planner.addUnwrap(OPEN_DELTA);
}
// payer is v4 position manager
planner.addSettle(position.pool.currency0, false);
planner.addSettle(position.pool.currency1, false);
// sweep any leftover wrapped native that was not unwrapped
// recipient will be same as the v4 lp token recipient
planner.addSweep(options.useNative ? position.pool.currency0.wrapped : position.pool.currency0, options.recipient);
planner.addSweep(position.pool.currency1, options.recipient);
}
else {
if (isMint(options)) {
// Mint: the user can never be owed a token when minting (delta is always >= 0), so SETTLE_PAIR is safe
planner.addSettlePair(position.pool.currency0, position.pool.currency1);
}
else {
// Increase: use CLOSE_CURRENCY instead of SETTLE_PAIR because accrued fees on
// existing positions can flip the delta positive on one side (e.g. single-sided
// positions with fees), causing SETTLE_PAIR to revert. CLOSE_CURRENCY handles
// both directions — settles if the user owes, takes if the user is owed.
planner.addCloseCurrency(position.pool.currency0);
planner.addCloseCurrency(position.pool.currency1);
}
if (options.useNative) {
// Any sweeping must happen after the settling.
// native currency will always be currency0 in v4
value = toHex(amount0Max);
planner.addSweep(position.pool.currency0, MSG_SENDER);
}
}
calldataList.push(V4PositionManager.encodeModifyLiquidities(planner.finalize(), options.deadline));
return {
calldata: Multicall.encodeMulticall(calldataList),
value,
};
}
/**
* Produces the calldata for completely or partially exiting a position
* @param position The position to exit
* @param options Additional information necessary for generating the calldata
* @returns The call parameters
*/
static removeCallParameters(position, options) {
var _a;
/**
* cases:
* - if liquidityPercentage is 100%, encode BURN_POSITION and then TAKE_PAIR
* - else, encode DECREASE_LIQUIDITY and then TAKE_PAIR
*/
const calldataList = [];
const planner = new V4PositionPlanner();
const tokenId = toHex(options.tokenId);
if (options.burnToken) {
// if burnToken is true, the specified liquidity percentage must be 100%
invariant(options.liquidityPercentage.equalTo(ONE), CANNOT_BURN);
// if there is a permit, encode the ERC721Permit permit call
if (options.permit) {
calldataList.push(V4PositionManager.encodeERC721Permit(options.permit.spender, options.permit.tokenId, options.permit.deadline, options.permit.nonce, options.permit.signature));
}
// slippage-adjusted amounts derived from current position liquidity
const { amount0: amount0Min, amount1: amount1Min } = position.burnAmountsWithSlippage(options.slippageTolerance);
planner.addBurn(tokenId, amount0Min, amount1Min, options.hookData);
}
else {
// construct a partial position with a percentage of liquidity
const partialPosition = new Position({
pool: position.pool,
liquidity: options.liquidityPercentage.multiply(position.liquidity).quotient,
tickLower: position.tickLower,
tickUpper: position.tickUpper,
});
// If the partial position has liquidity=0, this is a collect call and collectCallParameters should be used
invariant(JSBI.greaterThan(partialPosition.liquidity, ZERO), ZERO_LIQUIDITY);
// slippage-adjusted underlying amounts
const { amount0: amount0Min, amount1: amount1Min } = partialPosition.burnAmountsWithSlippage(options.slippageTolerance);
planner.addDecrease(tokenId, partialPosition.liquidity.toString(), amount0Min.toString(), amount1Min.toString(), (_a = options.hookData) !== null && _a !== void 0 ? _a : EMPTY_BYTES);
}
planner.addTakePair(position.pool.currency0, position.pool.currency1, MSG_SENDER);
calldataList.push(V4PositionManager.encodeModifyLiquidities(planner.finalize(), options.deadline));
return {
calldata: Multicall.encodeMulticall(calldataList),
value: toHex(0),
};
}
/**
* Produces the calldata for collecting fees from a position
* @param position The position to collect fees from
* @param options Additional information necessary for generating the calldata
* @returns The call parameters
*/
static collectCallParameters(position, options) {
const calldataList = [];
const planner = new V4PositionPlanner();
const tokenId = toHex(options.tokenId);
const recipient = validateAndParseAddress(options.recipient);
/**
* To collect fees in V4, we need to:
* - encode a decrease liquidity by 0
* - and encode a TAKE_PAIR
*/
planner.addDecrease(tokenId, '0', '0', '0', options.hookData);
planner.addTakePair(position.pool.currency0, position.pool.currency1, recipient);
calldataList.push(V4PositionManager.encodeModifyLiquidities(planner.finalize(), options.deadline));
return {
calldata: Multicall.encodeMulticall(calldataList),
value: toHex(0),
};
}
// Initialize a pool
static encodeInitializePool(poolKey, sqrtPriceX96) {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.INITIALIZE_POOL, [
poolKey,
sqrtPriceX96.toString(),
]);
}
// Encode a modify liquidities call
static encodeModifyLiquidities(unlockData, deadline) {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.MODIFY_LIQUIDITIES, [unlockData, deadline]);
}
// Encode a permit batch call
static encodePermitBatch(owner, permitBatch, signature) {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.PERMIT_BATCH, [
owner,
permitBatch,
signature,
]);
}
// Encode a ERC721Permit permit call
static encodeERC721Permit(spender, tokenId, deadline, nonce, signature) {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.ERC721PERMIT_PERMIT, [
spender,
tokenId,
deadline,
nonce,
signature,
]);
}
// Prepare the params for an EIP712 signTypedData request
static getPermitData(permit, positionManagerAddress, chainId) {
return {
domain: {
name: 'Uniswap V4 Positions NFT',
chainId,
verifyingContract: positionManagerAddress,
},
types: NFT_PERMIT_TYPES,
values: permit,
};
}
}
V4PositionManager.INTERFACE = new Interface(positionManagerAbi);
//# sourceMappingURL=PositionManager.js.map