UNPKG

@uniswap/v4-sdk

Version:

⚒️ An SDK for building applications on top of Uniswap V4

237 lines 11.4 kB
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