Skip to content

Commit f09a982

Browse files
authored
Merge pull request #15 from BootNodeDev/feat/removeLiquidity
Feat/remove liquidity
2 parents 32c33f9 + 9716913 commit f09a982

File tree

8 files changed

+204
-34
lines changed

8 files changed

+204
-34
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Full API documentation with TypeDoc: [https://bootnodedev.github.io/uni-dev-kit]
7575
- [`buildSwapCallData`](#buildswapcalldata)
7676
- [`buildAddLiquidityCallData`](#buildaddliquiditycalldata)
7777
- [`preparePermit2BatchCallData`](#preparepermit2batchcalldata)
78+
- [`buildRemoveLiquidityCallData`](#buildremoveliquiditycalldata)
7879
- [Basis Points Reference](#basis-points-reference)
7980
- [Useful Links](#useful-links)
8081
- [Development](#development)
@@ -214,6 +215,22 @@ const permitData = await uniDevKit.preparePermit2BatchCallData({
214215
});
215216
```
216217

218+
### `buildRemoveLiquidityCallData`
219+
Build calldata to remove liquidity from a pool.
220+
```ts
221+
const { calldata, value } = await uniDevKit.buildRemoveLiquidityCallData({
222+
liquidityPercentage: 10_000, // 100%
223+
tokenId: '123',
224+
slippageTolerance: 50, // 0.5%
225+
});
226+
227+
const tx = await sendTransaction({
228+
to: uniDevKit.getContractAddress('positionManager'),
229+
data: calldata,
230+
value
231+
});
232+
```
233+
217234
#### Basis Points Reference
218235

219236
Throughout the library, percentages are represented in basis points (bps). For example, when setting a slippage tolerance of 0.5%, you would use `50` bps. Here's a quick reference:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "uniswap-dev-kit",
3-
"version": "1.0.11",
3+
"version": "1.0.12",
44
"description": "A modern TypeScript library for integrating Uniswap into your dapp.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/constants/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const DEFAULT_DEADLINE = 60 * 10 // 10 minutes
2+
export const DEFAULT_SLIPPAGE_TOLERANCE = 50

src/core/uniDevKitV4.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import type {
1717
PreparePermit2DataResult,
1818
} from '@/types/utils/permit2'
1919
import { buildAddLiquidityCallData } from '@/utils/buildAddLiquidityCallData'
20+
import {
21+
type BuildRemoveLiquidityCallDataParams,
22+
buildRemoveLiquidityCallData,
23+
} from '@/utils/buildRemoveLiquidityCallData'
2024
import { buildSwapCallData } from '@/utils/buildSwapCallData'
2125
import { getPool } from '@/utils/getPool'
2226
import { getPoolKeyFromPoolId } from '@/utils/getPoolKeyFromPoolId'
@@ -234,4 +238,14 @@ export class UniDevKitV4 {
234238
async preparePermit2Data(params: PreparePermit2DataParams): Promise<PreparePermit2DataResult> {
235239
return preparePermit2Data(params, this.instance)
236240
}
241+
242+
/**
243+
* Builds a remove liquidity call data for a given remove liquidity parameters.
244+
* @param params @type {BuildRemoveLiquidityCallDataParams}
245+
* @returns Promise resolving to remove liquidity call data including calldata and value
246+
* @throws Error if SDK instance is not found or if remove liquidity call data is invalid
247+
*/
248+
async buildRemoveLiquidityCallData(params: BuildRemoveLiquidityCallDataParams) {
249+
return buildRemoveLiquidityCallData(params, this.instance)
250+
}
237251
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { createMockSdkInstance } from '@/test/helpers/sdkInstance'
2+
import { getPosition } from '@/utils/getPosition'
3+
import { Token } from '@uniswap/sdk-core'
4+
import { Pool, Position, V4PositionManager } from '@uniswap/v4-sdk'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const instance = createMockSdkInstance()
8+
9+
vi.mock('@/utils/getPosition', () => ({
10+
getPosition: vi.fn(),
11+
}))
12+
13+
const token0 = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD Coin')
14+
const token1 = new Token(
15+
1,
16+
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
17+
18,
18+
'WETH',
19+
'Wrapped Ether',
20+
)
21+
const pool = new Pool(
22+
token0,
23+
token1,
24+
3000,
25+
60,
26+
'0x1111111111111111111111111111111111111111',
27+
'79228162514264337593543950336',
28+
'1000000', // liquidity as string
29+
0,
30+
)
31+
const position = new Position({ pool, liquidity: '1000000', tickLower: -60, tickUpper: 60 }) // liquidity as string
32+
33+
const mockPosition = {
34+
position,
35+
pool,
36+
token0,
37+
token1,
38+
poolId: '0x1111111111111111111111111111111111111111' as `0x${string}`,
39+
tokenId: '1',
40+
}
41+
42+
describe('buildRemoveLiquidityCallData', () => {
43+
beforeEach(() => {
44+
vi.resetAllMocks()
45+
})
46+
47+
it('should build calldata for removing 100% liquidity', async () => {
48+
vi.mock('@/utils/getDefaultDeadline', () => ({
49+
getDefaultDeadline: vi.fn().mockResolvedValue('1234567890'),
50+
}))
51+
vi.mocked(getPosition).mockReturnValueOnce(Promise.resolve(mockPosition))
52+
vi.spyOn(V4PositionManager, 'removeCallParameters').mockReturnValueOnce({
53+
calldata: '0x123',
54+
value: '0',
55+
})
56+
const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData')
57+
const result = await buildRemoveLiquidityCallData(
58+
{
59+
liquidityPercentage: 10_000,
60+
tokenId: '1',
61+
deadline: '123',
62+
},
63+
instance,
64+
)
65+
expect(result.calldata).toBe('0x123')
66+
expect(result.value).toBe('0')
67+
})
68+
69+
it('should use custom slippageTolerance', async () => {
70+
vi.mock('@/utils/getDefaultDeadline', () => ({
71+
getDefaultDeadline: vi.fn().mockResolvedValue('1234567890'),
72+
}))
73+
vi.mocked(getPosition).mockReturnValueOnce(Promise.resolve(mockPosition))
74+
const spy = vi.spyOn(V4PositionManager, 'removeCallParameters').mockReturnValueOnce({
75+
calldata: '0xabc',
76+
value: '1',
77+
})
78+
const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData')
79+
await buildRemoveLiquidityCallData(
80+
{
81+
liquidityPercentage: 5000,
82+
tokenId: '1',
83+
slippageTolerance: 123,
84+
deadline: '123',
85+
},
86+
instance,
87+
)
88+
expect(spy).toHaveBeenCalledWith(
89+
mockPosition.position,
90+
expect.objectContaining({ slippageTolerance: expect.any(Object) }),
91+
)
92+
})
93+
94+
it('should throw if position not found', async () => {
95+
vi.mock('@/utils/getDefaultDeadline', () => ({
96+
getDefaultDeadline: vi.fn().mockResolvedValue('1234567890'),
97+
}))
98+
vi.mocked(getPosition).mockReturnValueOnce(
99+
Promise.resolve(undefined as unknown as ReturnType<typeof getPosition>),
100+
)
101+
const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData')
102+
await expect(
103+
buildRemoveLiquidityCallData({ liquidityPercentage: 10_000, tokenId: '404' }, instance),
104+
).rejects.toThrow('Position not found')
105+
})
106+
107+
it('should throw if V4PositionManager throws', async () => {
108+
vi.mock('@/utils/getDefaultDeadline', () => ({
109+
getDefaultDeadline: vi.fn().mockResolvedValue('1234567890'),
110+
}))
111+
vi.mocked(getPosition).mockReturnValueOnce(Promise.resolve(mockPosition))
112+
vi.spyOn(V4PositionManager, 'removeCallParameters').mockImplementationOnce(() => {
113+
throw new Error('fail')
114+
})
115+
const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData')
116+
await expect(
117+
buildRemoveLiquidityCallData(
118+
{ liquidityPercentage: 10_000, tokenId: '1', deadline: '123' },
119+
instance,
120+
),
121+
).rejects.toThrow('fail')
122+
})
123+
})

src/utils/buildAddLiquidityCallData.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1+
import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common'
2+
import { percentFromBips } from '@/helpers/percent'
13
import type { UniDevKitV4Instance } from '@/types'
24
import type {
35
BuildAddLiquidityCallDataResult,
46
BuildAddLiquidityParams,
57
} from '@/types/utils/buildAddLiquidityCallData'
6-
import { Percent } from '@uniswap/sdk-core'
8+
import { getDefaultDeadline } from '@/utils/getDefaultDeadline'
79
import { TickMath, encodeSqrtRatioX96, nearestUsableTick } from '@uniswap/v3-sdk'
810
import { Position, V4PositionManager } from '@uniswap/v4-sdk'
911

10-
const DEFAULT_DEADLINE = 1800n // 30 minutes
11-
const DEFAULT_SLIPPAGE_TOLERANCE = 50
12-
1312
/**
1413
* Builds the calldata and native value required to add liquidity to a Uniswap V4 pool.
1514
*
@@ -80,14 +79,10 @@ export async function buildAddLiquidityCallData(
8079
permit2BatchSignature,
8180
} = params
8281

83-
console.log('params', params)
84-
8582
try {
86-
const deadline =
87-
deadlineParam ??
88-
(await instance.client.getBlock().then((b) => b.timestamp + DEFAULT_DEADLINE)).toString()
83+
const deadline = deadlineParam ?? (await getDefaultDeadline(instance)).toString()
8984

90-
const slippagePercent = new Percent(slippageTolerance, 10_000)
85+
const slippagePercent = percentFromBips(slippageTolerance)
9186
const createPool = pool.liquidity.toString() === '0'
9287

9388
const tickLower = tickLowerParam ?? nearestUsableTick(TickMath.MIN_TICK, pool.tickSpacing)

src/utils/buildRemoveLiquidityCallData.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,33 @@
1-
import { Percent } from '@uniswap/sdk-core'
2-
import { type Position, V4PositionManager } from '@uniswap/v4-sdk'
1+
import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common'
2+
import { percentFromBips } from '@/helpers/percent'
3+
import type { UniDevKitV4Instance } from '@/types'
4+
import { getDefaultDeadline } from '@/utils/getDefaultDeadline'
5+
import { getPosition } from '@/utils/getPosition'
6+
import { V4PositionManager } from '@uniswap/v4-sdk'
37

48
/**
59
* Parameters required to build the calldata for removing liquidity from a Uniswap v4 position.
610
*/
711
export interface BuildRemoveLiquidityCallDataParams {
8-
/**
9-
* The position object representing the liquidity position to modify.
10-
*/
11-
position: Position
12-
1312
/**
1413
* The percentage of liquidity to remove from the position.
1514
*/
1615
liquidityPercentage: number
1716

1817
/**
19-
* The deadline for the transaction.
18+
* The tokenId of the position to remove liquidity from.
2019
*/
21-
deadline: string
20+
tokenId: string
2221

2322
/**
2423
* The slippage tolerance for the transaction.
2524
*/
26-
slippageTolerance: number
25+
slippageTolerance?: number
2726

2827
/**
29-
* The tokenId of the position to remove liquidity from.
28+
* The deadline for the transaction. (default: 5 minutes from now)
3029
*/
31-
tokenId: string
30+
deadline?: string
3231
}
3332

3433
/**
@@ -41,7 +40,7 @@ export interface BuildRemoveLiquidityCallDataParams {
4140
* ```typescript
4241
* const { calldata, value } = buildRemoveLiquidityCallData({
4342
* position,
44-
* liquidityPercentage: new Percent(1, 1), // 100%
43+
* liquidityPercentage: 10_000, // 100%
4544
* });
4645
*
4746
* const tx = await sendTransaction({
@@ -51,18 +50,29 @@ export interface BuildRemoveLiquidityCallDataParams {
5150
* });
5251
* ```
5352
*/
54-
export function buildRemoveLiquidityCallData({
55-
position,
56-
liquidityPercentage,
57-
deadline,
58-
slippageTolerance,
59-
tokenId,
60-
}: BuildRemoveLiquidityCallDataParams) {
53+
export async function buildRemoveLiquidityCallData(
54+
{
55+
liquidityPercentage,
56+
deadline: deadlineParam,
57+
slippageTolerance,
58+
tokenId,
59+
}: BuildRemoveLiquidityCallDataParams,
60+
instance: UniDevKitV4Instance,
61+
) {
62+
// Get position data
63+
const positionData = await getPosition({ tokenId }, instance)
64+
if (!positionData) {
65+
throw new Error('Position not found')
66+
}
67+
68+
const deadline = deadlineParam ?? (await getDefaultDeadline(instance)).toString()
69+
70+
// Build remove liquidity call data
6171
try {
62-
const { calldata, value } = V4PositionManager.removeCallParameters(position, {
63-
slippageTolerance: new Percent(slippageTolerance, 100),
72+
const { calldata, value } = V4PositionManager.removeCallParameters(positionData.position, {
73+
slippageTolerance: percentFromBips(slippageTolerance ?? DEFAULT_SLIPPAGE_TOLERANCE),
6474
deadline: deadline,
65-
liquidityPercentage: new Percent(liquidityPercentage, 100),
75+
liquidityPercentage: percentFromBips(liquidityPercentage),
6676
tokenId: tokenId,
6777
})
6878

src/utils/getDefaultDeadline.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { DEFAULT_DEADLINE } from '@/constants/common'
2+
import type { UniDevKitV4Instance } from '@/types'
3+
4+
export async function getDefaultDeadline(
5+
instance: UniDevKitV4Instance,
6+
timeFromNow: number = DEFAULT_DEADLINE,
7+
): Promise<bigint> {
8+
return (await instance.client.getBlock()).timestamp + BigInt(timeFromNow)
9+
}

0 commit comments

Comments
 (0)