@w3lib/evm-batch-call 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +79 -2
- package/dist/liquidity/common.d.ts +72 -0
- package/dist/liquidity/common.d.ts.map +1 -0
- package/dist/liquidity/common.js +271 -0
- package/dist/liquidity/common.js.map +1 -0
- package/dist/liquidity/concentrated.d.ts +19 -0
- package/dist/liquidity/concentrated.d.ts.map +1 -0
- package/dist/liquidity/concentrated.js +106 -0
- package/dist/liquidity/concentrated.js.map +1 -0
- package/dist/liquidity/index.d.ts +42 -0
- package/dist/liquidity/index.d.ts.map +1 -0
- package/dist/liquidity/index.js +143 -0
- package/dist/liquidity/index.js.map +1 -0
- package/dist/liquidity/types.d.ts +182 -0
- package/dist/liquidity/types.d.ts.map +1 -0
- package/dist/liquidity/types.js +2 -0
- package/dist/liquidity/types.js.map +1 -0
- package/dist/liquidity/v2.d.ts +5 -0
- package/dist/liquidity/v2.d.ts.map +1 -0
- package/dist/liquidity/v2.js +342 -0
- package/dist/liquidity/v2.js.map +1 -0
- package/dist/liquidity/v3.d.ts +6 -0
- package/dist/liquidity/v3.d.ts.map +1 -0
- package/dist/liquidity/v3.js +310 -0
- package/dist/liquidity/v3.js.map +1 -0
- package/dist/liquidity/v4.d.ts +5 -0
- package/dist/liquidity/v4.d.ts.map +1 -0
- package/dist/liquidity/v4.js +279 -0
- package/dist/liquidity/v4.js.map +1 -0
- package/dist/lp.d.ts +8 -1
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +29 -18
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +55 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +209 -0
- package/dist/lpMath.js.map +1 -1
- package/dist/subpaths/liquidity.d.ts +24 -0
- package/dist/subpaths/liquidity.d.ts.map +1 -0
- package/dist/subpaths/liquidity.js +21 -0
- package/dist/subpaths/liquidity.js.map +1 -0
- package/dist/subpaths/lp.d.ts +1 -1
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +1 -1
- package/dist/subpaths/lp.js.map +1 -1
- package/dist/write/tron.d.ts +2 -0
- package/dist/write/tron.d.ts.map +1 -1
- package/dist/write/tron.js +4 -0
- package/dist/write/tron.js.map +1 -1
- package/package.json +20 -12
- package/src/liquidity/common.ts +314 -0
- package/src/liquidity/concentrated.ts +121 -0
- package/src/liquidity/index.ts +171 -0
- package/src/liquidity/types.ts +194 -0
- package/src/liquidity/v2.ts +371 -0
- package/src/liquidity/v3.ts +382 -0
- package/src/liquidity/v4.ts +348 -0
- package/src/lp.ts +36 -18
- package/src/lpMath.ts +223 -0
- package/src/subpaths/liquidity.ts +52 -0
- package/src/subpaths/lp.ts +20 -1
- package/src/write/tron.ts +5 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { AbiCoder, Interface, hexlify } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import type { LpPosition, LpProtocol, LpToken } from '../lp.js'
|
|
4
|
+
import { getAmountsForLiquidity, getFeesEarned, getMaxAmounts, getMinAmounts, getMintAmounts, getSqrtRatioAtTick, getTickAtSqrtRatio } from '../lpMath.js'
|
|
5
|
+
import { decodePositionInfo, poolIdOf } from '../lpV4.js'
|
|
6
|
+
import { liquidityFor, resolveRange, sqrtPriceOf } from './concentrated.js'
|
|
7
|
+
import {
|
|
8
|
+
ZERO_ADDRESS,
|
|
9
|
+
concentratedProtocol,
|
|
10
|
+
evm,
|
|
11
|
+
isNative,
|
|
12
|
+
makePlan,
|
|
13
|
+
parseAmount,
|
|
14
|
+
permit2Approvals,
|
|
15
|
+
priceFields,
|
|
16
|
+
removeAmount,
|
|
17
|
+
tokenIdOf,
|
|
18
|
+
tokenInfo,
|
|
19
|
+
type Ctx,
|
|
20
|
+
} from './common.js'
|
|
21
|
+
import type { IncreaseLiquidityParams, LiquidityPlan, NewPositionParams, PositionRef, RemoveLiquidityParams } from './types.js'
|
|
22
|
+
|
|
23
|
+
const pmInterface = new Interface([
|
|
24
|
+
'function ownerOf(uint256 tokenId) view returns (address)',
|
|
25
|
+
'function getApproved(uint256 tokenId) view returns (address)',
|
|
26
|
+
'function isApprovedForAll(address owner, address operator) view returns (bool)',
|
|
27
|
+
'function getPoolAndPositionInfo(uint256 tokenId) view returns ((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, uint256 info)',
|
|
28
|
+
'function getPositionLiquidity(uint256 tokenId) view returns (uint128)',
|
|
29
|
+
'function modifyLiquidities(bytes unlockData, uint256 deadline) payable',
|
|
30
|
+
'function initializePool((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, uint160 sqrtPriceX96) payable returns (int24)',
|
|
31
|
+
'function multicall(bytes[] data) payable returns (bytes[] results)',
|
|
32
|
+
])
|
|
33
|
+
const stateViewInterface = new Interface([
|
|
34
|
+
'function getSlot0(bytes32 poolId) view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)',
|
|
35
|
+
'function getFeeGrowthInside(bytes32 poolId, int24 tickLower, int24 tickUpper) view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128)',
|
|
36
|
+
'function getPositionInfo(bytes32 poolId, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) view returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/** v4-periphery 的 Actions */
|
|
40
|
+
const Actions = {
|
|
41
|
+
INCREASE_LIQUIDITY: 0x00,
|
|
42
|
+
DECREASE_LIQUIDITY: 0x01,
|
|
43
|
+
MINT_POSITION: 0x02,
|
|
44
|
+
BURN_POSITION: 0x03,
|
|
45
|
+
SETTLE_PAIR: 0x0d,
|
|
46
|
+
TAKE_PAIR: 0x11,
|
|
47
|
+
CLOSE_CURRENCY: 0x12,
|
|
48
|
+
SWEEP: 0x14,
|
|
49
|
+
} as const
|
|
50
|
+
|
|
51
|
+
/** 标准手续费档位对应的 tick 间距(与 Uniswap 网页一致) */
|
|
52
|
+
const DEFAULT_TICK_SPACING: Readonly<Record<number, number>> = { 100: 1, 500: 10, 3000: 60, 10000: 200 }
|
|
53
|
+
|
|
54
|
+
const coder = AbiCoder.defaultAbiCoder()
|
|
55
|
+
const POOL_KEY = 'tuple(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks)'
|
|
56
|
+
|
|
57
|
+
interface PoolKey {
|
|
58
|
+
currency0: string
|
|
59
|
+
currency1: string
|
|
60
|
+
fee: number
|
|
61
|
+
tickSpacing: number
|
|
62
|
+
hooks: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const keyTuple = (k: PoolKey) => [k.currency0, k.currency1, k.fee, k.tickSpacing, k.hooks]
|
|
66
|
+
|
|
67
|
+
/** 依次执行的 action 列表 → modifyLiquidities 的 unlockData */
|
|
68
|
+
class Planner {
|
|
69
|
+
readonly actions: number[] = []
|
|
70
|
+
readonly params: string[] = []
|
|
71
|
+
|
|
72
|
+
add(action: number, types: string[], values: unknown[]): this {
|
|
73
|
+
this.actions.push(action)
|
|
74
|
+
this.params.push(coder.encode(types, values))
|
|
75
|
+
return this
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
encode(): string {
|
|
79
|
+
return coder.encode(['bytes', 'bytes[]'], [hexlify(new Uint8Array(this.actions)), this.params])
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface V4Pool {
|
|
84
|
+
key: PoolKey
|
|
85
|
+
poolId: string
|
|
86
|
+
sqrtPriceX96: bigint
|
|
87
|
+
tick: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface V4Position {
|
|
91
|
+
protocol: LpProtocol
|
|
92
|
+
pm: string
|
|
93
|
+
stateView: string
|
|
94
|
+
tokenId: bigint
|
|
95
|
+
owner: string
|
|
96
|
+
/** 发送者能不能管理这个仓位(所有者或被授权) */
|
|
97
|
+
canManage: boolean
|
|
98
|
+
pool: V4Pool
|
|
99
|
+
tickLower: number
|
|
100
|
+
tickUpper: number
|
|
101
|
+
liquidity: bigint
|
|
102
|
+
fees0: bigint
|
|
103
|
+
fees1: bigint
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function addresses(ctx: Ctx, protocol: LpProtocol): { pm: string; stateView: string } {
|
|
107
|
+
if (ctx.tron) {
|
|
108
|
+
throw new Error('Uniswap V4 is not supported on Tron')
|
|
109
|
+
}
|
|
110
|
+
if (!protocol.positionManager || !protocol.stateView) {
|
|
111
|
+
throw new Error(`Protocol ${protocol.id} needs positionManager and stateView`)
|
|
112
|
+
}
|
|
113
|
+
return { pm: evm(protocol.positionManager), stateView: evm(protocol.stateView) }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function readPosition(ctx: Ctx, ref: PositionRef | LpPosition): Promise<V4Position> {
|
|
117
|
+
const protocol = concentratedProtocol(ctx, ref)
|
|
118
|
+
const { pm, stateView } = addresses(ctx, protocol)
|
|
119
|
+
const tokenId = tokenIdOf(ref)
|
|
120
|
+
const c = ctx.provider.contract(pm, pmInterface)
|
|
121
|
+
const [owner, info, liquidity] = (await ctx.provider.all([c.ownerOf(tokenId), c.getPoolAndPositionInfo(tokenId), c.getPositionLiquidity(tokenId)])) as [
|
|
122
|
+
string,
|
|
123
|
+
{ 0: { currency0: string; currency1: string; fee: bigint; tickSpacing: bigint; hooks: string }; 1: bigint },
|
|
124
|
+
bigint,
|
|
125
|
+
]
|
|
126
|
+
const raw = info[0]
|
|
127
|
+
const key: PoolKey = { currency0: evm(raw.currency0), currency1: evm(raw.currency1), fee: Number(raw.fee), tickSpacing: Number(raw.tickSpacing), hooks: evm(raw.hooks) }
|
|
128
|
+
const poolId = poolIdOf(key)
|
|
129
|
+
const { tickLower, tickUpper } = decodePositionInfo(info[1])
|
|
130
|
+
const sv = ctx.provider.contract(stateView, stateViewInterface)
|
|
131
|
+
const salt = `0x${tokenId.toString(16).padStart(64, '0')}`
|
|
132
|
+
const [slot0, inside, last, approved, approvedForAll] = (await ctx.provider.all([
|
|
133
|
+
sv.getSlot0(poolId),
|
|
134
|
+
sv.getFeeGrowthInside(poolId, tickLower, tickUpper),
|
|
135
|
+
sv.getPositionInfo(poolId, pm, tickLower, tickUpper, salt),
|
|
136
|
+
c.getApproved(tokenId),
|
|
137
|
+
c.isApprovedForAll(owner, ctx.owner),
|
|
138
|
+
])) as [{ 0: bigint; 1: bigint }, { 0: bigint; 1: bigint }, { 1: bigint; 2: bigint }, string, boolean]
|
|
139
|
+
return {
|
|
140
|
+
protocol,
|
|
141
|
+
pm,
|
|
142
|
+
stateView,
|
|
143
|
+
tokenId,
|
|
144
|
+
owner: evm(owner),
|
|
145
|
+
// 与 PositionManager 的 onlyIfApproved 一致:所有者、单个仓位的 approve、setApprovalForAll 都可以
|
|
146
|
+
canManage: evm(owner) === ctx.owner || evm(approved) === ctx.owner || approvedForAll,
|
|
147
|
+
pool: { key, poolId, sqrtPriceX96: slot0[0], tick: Number(slot0[1]) },
|
|
148
|
+
tickLower,
|
|
149
|
+
tickUpper,
|
|
150
|
+
liquidity,
|
|
151
|
+
fees0: getFeesEarned(liquidity, inside[0], last[1]),
|
|
152
|
+
fees1: getFeesEarned(liquidity, inside[1], last[2]),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertOwner(ctx: Ctx, position: V4Position): void {
|
|
157
|
+
if (!position.canManage) {
|
|
158
|
+
throw new Error(`${ctx.owner} is not the owner of position ${position.tokenId} (owner: ${position.owner})`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const hookData = (ctx: Ctx) => (ctx.options.hookData ? hexlify(ctx.options.hookData) : '0x')
|
|
163
|
+
|
|
164
|
+
export async function v4Add(ctx: Ctx, params: IncreaseLiquidityParams | NewPositionParams): Promise<LiquidityPlan> {
|
|
165
|
+
let protocol: LpProtocol
|
|
166
|
+
let pm: string
|
|
167
|
+
let pool: V4Pool
|
|
168
|
+
let tickLower: number
|
|
169
|
+
let tickUpper: number
|
|
170
|
+
let tokenId: bigint | null = null
|
|
171
|
+
let createPrice: bigint | null = null
|
|
172
|
+
let fees: [bigint, bigint] = [0n, 0n]
|
|
173
|
+
let t0: LpToken
|
|
174
|
+
let t1: LpToken
|
|
175
|
+
let given0: bigint | undefined
|
|
176
|
+
let given1: bigint | undefined
|
|
177
|
+
|
|
178
|
+
if ('position' in params) {
|
|
179
|
+
const position = await readPosition(ctx, params.position)
|
|
180
|
+
assertOwner(ctx, position)
|
|
181
|
+
;({ protocol, pm, pool, tickLower, tickUpper } = position)
|
|
182
|
+
tokenId = position.tokenId
|
|
183
|
+
fees = [position.fees0, position.fees1]
|
|
184
|
+
;[t0, t1] = (await tokenInfo(ctx, [pool.key.currency0, pool.key.currency1])) as [LpToken, LpToken]
|
|
185
|
+
given0 = parseAmount(params.amount0, params.amount0Formatted, t0, 'amount0')
|
|
186
|
+
given1 = parseAmount(params.amount1, params.amount1Formatted, t1, 'amount1')
|
|
187
|
+
} else {
|
|
188
|
+
protocol = params.protocol as LpProtocol
|
|
189
|
+
const { pm: positionManager, stateView } = addresses(ctx, protocol)
|
|
190
|
+
pm = positionManager
|
|
191
|
+
if (params.fee === undefined) {
|
|
192
|
+
throw new Error('fee is required for V4 (e.g. 500, 3000, 10000)')
|
|
193
|
+
}
|
|
194
|
+
const tickSpacing = params.tickSpacing ?? DEFAULT_TICK_SPACING[params.fee]
|
|
195
|
+
if (tickSpacing === undefined) {
|
|
196
|
+
throw new Error(`Pass tickSpacing: there is no default for fee ${params.fee}`)
|
|
197
|
+
}
|
|
198
|
+
// 主币是零地址,排在前面
|
|
199
|
+
const a = isNative(ctx, params.tokenA) ? ZERO_ADDRESS : evm(params.tokenA)
|
|
200
|
+
const b = isNative(ctx, params.tokenB) ? ZERO_ADDRESS : evm(params.tokenB)
|
|
201
|
+
if (a === b) {
|
|
202
|
+
throw new Error('tokenA and tokenB must be different')
|
|
203
|
+
}
|
|
204
|
+
const aIs0 = BigInt(a) < BigInt(b)
|
|
205
|
+
const key: PoolKey = { currency0: aIs0 ? a : b, currency1: aIs0 ? b : a, fee: params.fee, tickSpacing, hooks: params.hooks ? evm(params.hooks) : ZERO_ADDRESS }
|
|
206
|
+
const poolId = poolIdOf(key)
|
|
207
|
+
const slot0 = (await ctx.provider.call(ctx.provider.contract(stateView, stateViewInterface).getSlot0(poolId))) as { 0: bigint; 1: bigint }
|
|
208
|
+
pool = { key, poolId, sqrtPriceX96: slot0[0], tick: Number(slot0[1]) }
|
|
209
|
+
;[t0, t1] = (await tokenInfo(ctx, [key.currency0, key.currency1])) as [LpToken, LpToken]
|
|
210
|
+
if (pool.sqrtPriceX96 === 0n) {
|
|
211
|
+
if (params.initialPrice === undefined) {
|
|
212
|
+
throw new Error('This V4 pool (currencies, fee, tickSpacing, hooks) is not initialized yet; pass initialPrice to create it')
|
|
213
|
+
}
|
|
214
|
+
createPrice = sqrtPriceOf(params.initialPrice, aIs0, t0, t1, 'initialPrice')
|
|
215
|
+
pool = { ...pool, sqrtPriceX96: createPrice, tick: getTickAtSqrtRatio(createPrice) }
|
|
216
|
+
}
|
|
217
|
+
;[tickLower, tickUpper] = resolveRange(params, aIs0, t0, t1, tickSpacing)
|
|
218
|
+
const amountA = parseAmount(params.amountA, params.amountAFormatted, aIs0 ? t0 : t1, 'amountA')
|
|
219
|
+
const amountB = parseAmount(params.amountB, params.amountBFormatted, aIs0 ? t1 : t0, 'amountB')
|
|
220
|
+
;[given0, given1] = aIs0 ? [amountA, amountB] : [amountB, amountA]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { key } = pool
|
|
224
|
+
const sqrtA = getSqrtRatioAtTick(tickLower)
|
|
225
|
+
const sqrtB = getSqrtRatioAtTick(tickUpper)
|
|
226
|
+
const liquidity = liquidityFor(pool.sqrtPriceX96, tickLower, tickUpper, given0, given1, t0, t1)
|
|
227
|
+
const [amount0, amount1] = getMintAmounts(pool.sqrtPriceX96, sqrtA, sqrtB, liquidity)
|
|
228
|
+
// V4 直接指定流动性,价格变动时需要的数量随之变化:按滑点给出上限
|
|
229
|
+
const [max0, max1] = getMaxAmounts(pool.sqrtPriceX96, sqrtA, sqrtB, liquidity, ctx.slippage)
|
|
230
|
+
const native = key.currency0 === ZERO_ADDRESS
|
|
231
|
+
const planner = new Planner()
|
|
232
|
+
if (tokenId === null) {
|
|
233
|
+
planner.add(
|
|
234
|
+
Actions.MINT_POSITION,
|
|
235
|
+
[POOL_KEY, 'int24', 'int24', 'uint256', 'uint128', 'uint128', 'address', 'bytes'],
|
|
236
|
+
[keyTuple(key), tickLower, tickUpper, liquidity, max0, max1, ctx.recipient, hookData(ctx)],
|
|
237
|
+
)
|
|
238
|
+
planner.add(Actions.SETTLE_PAIR, ['address', 'address'], [key.currency0, key.currency1])
|
|
239
|
+
} else {
|
|
240
|
+
planner.add(Actions.INCREASE_LIQUIDITY, ['uint256', 'uint256', 'uint128', 'uint128', 'bytes'], [tokenId, liquidity, max0, max1, hookData(ctx)])
|
|
241
|
+
// 追加时会自动领取手续费:手续费可能比要存入的多(差额为正),CLOSE_CURRENCY 按差额的正负自动付款或收款
|
|
242
|
+
planner.add(Actions.CLOSE_CURRENCY, ['address'], [key.currency0])
|
|
243
|
+
planner.add(Actions.CLOSE_CURRENCY, ['address'], [key.currency1])
|
|
244
|
+
}
|
|
245
|
+
if (native) {
|
|
246
|
+
// 附带的主币按上限给,多余的退回
|
|
247
|
+
planner.add(Actions.SWEEP, ['address', 'address'], [ZERO_ADDRESS, ctx.owner])
|
|
248
|
+
}
|
|
249
|
+
const modify = pmInterface.encodeFunctionData('modifyLiquidities', [planner.encode(), ctx.deadline])
|
|
250
|
+
const data = createPrice === null ? modify : pmInterface.encodeFunctionData('multicall', [[pmInterface.encodeFunctionData('initializePool', [keyTuple(key), createPrice]), modify]])
|
|
251
|
+
const value = native ? max0 : 0n
|
|
252
|
+
const approve = await permit2Approvals(
|
|
253
|
+
ctx,
|
|
254
|
+
pm,
|
|
255
|
+
[
|
|
256
|
+
{ token: key.currency0, amount: max0 },
|
|
257
|
+
{ token: key.currency1, amount: max1 },
|
|
258
|
+
].filter((i) => i.token !== ZERO_ADDRESS),
|
|
259
|
+
)
|
|
260
|
+
return makePlan(ctx, {
|
|
261
|
+
action: 'add',
|
|
262
|
+
version: 'v4',
|
|
263
|
+
protocol: protocol.id,
|
|
264
|
+
protocolName: protocol.name,
|
|
265
|
+
pool: pool.poolId,
|
|
266
|
+
createPool: createPrice !== null,
|
|
267
|
+
token0: t0,
|
|
268
|
+
token1: t1,
|
|
269
|
+
tokenId: tokenId === null ? null : tokenId.toString(),
|
|
270
|
+
tickLower,
|
|
271
|
+
tickUpper,
|
|
272
|
+
tick: pool.tick,
|
|
273
|
+
...priceFields(t0, t1, pool.sqrtPriceX96, tickLower, tickUpper),
|
|
274
|
+
liquidity: liquidity.toString(),
|
|
275
|
+
amount0: amount0.toString(),
|
|
276
|
+
amount1: amount1.toString(),
|
|
277
|
+
amount0Max: max0.toString(),
|
|
278
|
+
amount1Max: max1.toString(),
|
|
279
|
+
amount0Min: null,
|
|
280
|
+
amount1Min: null,
|
|
281
|
+
fees0: fees[0].toString(),
|
|
282
|
+
fees1: fees[1].toString(),
|
|
283
|
+
native: native ? 'token0' : null,
|
|
284
|
+
value: value.toString(),
|
|
285
|
+
transactions: [...approve, { kind: 'add', to: pm, data, value: value.toString() }],
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function v4Remove(ctx: Ctx, params: RemoveLiquidityParams, collectOnly = false): Promise<LiquidityPlan> {
|
|
290
|
+
const position = await readPosition(ctx, params.position)
|
|
291
|
+
assertOwner(ctx, position)
|
|
292
|
+
const { pm, pool, tickLower, tickUpper, tokenId } = position
|
|
293
|
+
const { key } = pool
|
|
294
|
+
let liquidity = 0n
|
|
295
|
+
if (!collectOnly) {
|
|
296
|
+
if (position.liquidity === 0n) {
|
|
297
|
+
throw new Error(`Position ${tokenId} has no liquidity`)
|
|
298
|
+
}
|
|
299
|
+
liquidity = removeAmount(position.liquidity, params.percent, params.liquidity)
|
|
300
|
+
} else if (position.fees0 === 0n && position.fees1 === 0n) {
|
|
301
|
+
throw new Error(`Position ${tokenId} has no fees to collect`)
|
|
302
|
+
}
|
|
303
|
+
const burn = !collectOnly && !!params.burn
|
|
304
|
+
if (burn && liquidity !== position.liquidity) {
|
|
305
|
+
throw new Error('burn: true needs the whole position to be removed')
|
|
306
|
+
}
|
|
307
|
+
const [t0, t1] = (await tokenInfo(ctx, [key.currency0, key.currency1])) as [LpToken, LpToken]
|
|
308
|
+
const sqrtA = getSqrtRatioAtTick(tickLower)
|
|
309
|
+
const sqrtB = getSqrtRatioAtTick(tickUpper)
|
|
310
|
+
const [amount0, amount1] = getAmountsForLiquidity(pool.sqrtPriceX96, sqrtA, sqrtB, liquidity)
|
|
311
|
+
const [min0, min1] = getMinAmounts(pool.sqrtPriceX96, sqrtA, sqrtB, liquidity, ctx.slippage)
|
|
312
|
+
const planner = new Planner()
|
|
313
|
+
if (burn) {
|
|
314
|
+
planner.add(Actions.BURN_POSITION, ['uint256', 'uint128', 'uint128', 'bytes'], [tokenId, min0, min1, hookData(ctx)])
|
|
315
|
+
} else {
|
|
316
|
+
// 流动性为 0 时只领取手续费
|
|
317
|
+
planner.add(Actions.DECREASE_LIQUIDITY, ['uint256', 'uint256', 'uint128', 'uint128', 'bytes'], [tokenId, liquidity, min0, min1, hookData(ctx)])
|
|
318
|
+
}
|
|
319
|
+
planner.add(Actions.TAKE_PAIR, ['address', 'address', 'address'], [key.currency0, key.currency1, ctx.recipient])
|
|
320
|
+
const data = pmInterface.encodeFunctionData('modifyLiquidities', [planner.encode(), ctx.deadline])
|
|
321
|
+
return makePlan(ctx, {
|
|
322
|
+
action: collectOnly ? 'collect' : 'remove',
|
|
323
|
+
version: 'v4',
|
|
324
|
+
protocol: position.protocol.id,
|
|
325
|
+
protocolName: position.protocol.name,
|
|
326
|
+
pool: pool.poolId,
|
|
327
|
+
createPool: false,
|
|
328
|
+
token0: t0,
|
|
329
|
+
token1: t1,
|
|
330
|
+
tokenId: tokenId.toString(),
|
|
331
|
+
tickLower,
|
|
332
|
+
tickUpper,
|
|
333
|
+
tick: pool.tick,
|
|
334
|
+
...priceFields(t0, t1, pool.sqrtPriceX96, tickLower, tickUpper),
|
|
335
|
+
liquidity: liquidity.toString(),
|
|
336
|
+
amount0: amount0.toString(),
|
|
337
|
+
amount1: amount1.toString(),
|
|
338
|
+
amount0Max: null,
|
|
339
|
+
amount1Max: null,
|
|
340
|
+
amount0Min: min0.toString(),
|
|
341
|
+
amount1Min: min1.toString(),
|
|
342
|
+
fees0: position.fees0.toString(),
|
|
343
|
+
fees1: position.fees1.toString(),
|
|
344
|
+
native: key.currency0 === ZERO_ADDRESS ? 'token0' : null,
|
|
345
|
+
value: '0',
|
|
346
|
+
transactions: [{ kind: collectOnly ? 'collect' : 'remove', to: pm, data, value: '0' }],
|
|
347
|
+
})
|
|
348
|
+
}
|
package/src/lp.ts
CHANGED
|
@@ -24,6 +24,13 @@ export interface LpProtocol {
|
|
|
24
24
|
chainId: number
|
|
25
25
|
/** V2:工厂合约(getPair、识别 LP 代币属于哪个协议);V3:池子的工厂合约(getPool) */
|
|
26
26
|
factory?: string
|
|
27
|
+
/** V2:Router 合约(添加 / 移除流动性用,见子路径 `/liquidity`) */
|
|
28
|
+
router?: string
|
|
29
|
+
/**
|
|
30
|
+
* V2:协议手续费开启(factory.feeTo 不为零)时,每次添加 / 移除前给 feeTo 增发的 LP 占 √k 增长的比例:
|
|
31
|
+
* Uniswap 及大多数分叉是 '1/6',PancakeSwap 是 '8/25'。不填时按 '8/25'(增发得多,算出的移除数量偏少,最小值更宽松)
|
|
32
|
+
*/
|
|
33
|
+
protocolFee?: '1/6' | '8/25'
|
|
27
34
|
/** V3 / V4:仓位 NFT 合约(NonfungiblePositionManager / PositionManager) */
|
|
28
35
|
positionManager?: string
|
|
29
36
|
/** V4:StateView(读池子和仓位的状态) */
|
|
@@ -32,7 +39,15 @@ export interface LpProtocol {
|
|
|
32
39
|
deployBlock?: number
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
const v2 = (id: string, name: string, chainId: number, factory: string
|
|
42
|
+
const v2 = (id: string, name: string, chainId: number, factory: string, router: string, protocolFee: '1/6' | '8/25'): LpProtocol => ({
|
|
43
|
+
id,
|
|
44
|
+
name,
|
|
45
|
+
version: 'v2',
|
|
46
|
+
chainId,
|
|
47
|
+
factory,
|
|
48
|
+
router,
|
|
49
|
+
protocolFee,
|
|
50
|
+
})
|
|
36
51
|
const v3 = (id: string, name: string, chainId: number, positionManager: string, factory: string): LpProtocol => ({ id, name, version: 'v3', chainId, positionManager, factory })
|
|
37
52
|
const v4 = (chainId: number, positionManager: string, stateView: string, deployBlock?: number): LpProtocol => ({
|
|
38
53
|
id: 'uniswap-v4',
|
|
@@ -49,41 +64,44 @@ const UNI_V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984'
|
|
|
49
64
|
const CAKE_V3_NPM = '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364'
|
|
50
65
|
const CAKE_V3_FACTORY = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865'
|
|
51
66
|
const SUSHI_V2_FACTORY = '0xc35DADB65012eC5796536bD9864eD8773aBc74C4'
|
|
67
|
+
const SUSHI_V2_ROUTER = '0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506'
|
|
68
|
+
const UNI_V2_ROUTER_L2 = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24'
|
|
69
|
+
const CAKE_V2_ROUTER_L2 = '0x8cFe327CEc66d1C090Dd72bd0FF11d690C33a2Eb'
|
|
52
70
|
|
|
53
71
|
/**
|
|
54
|
-
* 内置的 DEX 部署(2026-10 在链上逐个核对:V3 的 name / factory、V2 的 allPairsLength)。
|
|
72
|
+
* 内置的 DEX 部署(2026-10 在链上逐个核对:V3 的 name / factory、V2 的 allPairsLength、V2 Router 的 factory / WETH)。
|
|
55
73
|
* 不在表里的 DEX / 链可以通过 `protocols` 选项传入自己的 LpProtocol。
|
|
56
74
|
*/
|
|
57
75
|
export const LP_PROTOCOLS: readonly LpProtocol[] = [
|
|
58
76
|
// Ethereum
|
|
59
|
-
v2('uniswap-v2', 'Uniswap V2', 1, '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'),
|
|
60
|
-
v2('sushiswap-v2', 'SushiSwap', 1, '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac'),
|
|
61
|
-
v2('pancakeswap-v2', 'PancakeSwap V2', 1, '0x1097053Fd2ea711dad45caCcc45EfF7548fCB362'),
|
|
77
|
+
v2('uniswap-v2', 'Uniswap V2', 1, '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', '1/6'),
|
|
78
|
+
v2('sushiswap-v2', 'SushiSwap', 1, '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac', '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F', '1/6'),
|
|
79
|
+
v2('pancakeswap-v2', 'PancakeSwap V2', 1, '0x1097053Fd2ea711dad45caCcc45EfF7548fCB362', '0xEfF92A263d31888d860bD50809A8D171709b7b1c', '8/25'),
|
|
62
80
|
v3('uniswap-v3', 'Uniswap V3', 1, UNI_V3_NPM, UNI_V3_FACTORY),
|
|
63
81
|
v3('pancakeswap-v3', 'PancakeSwap V3', 1, CAKE_V3_NPM, CAKE_V3_FACTORY),
|
|
64
82
|
// BSC
|
|
65
|
-
v2('pancakeswap-v2', 'PancakeSwap V2', 56, '0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73'),
|
|
66
|
-
v2('uniswap-v2', 'Uniswap V2', 56, '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6'),
|
|
67
|
-
v2('sushiswap-v2', 'SushiSwap', 56, SUSHI_V2_FACTORY),
|
|
83
|
+
v2('pancakeswap-v2', 'PancakeSwap V2', 56, '0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73', '0x10ED43C718714eb63d5aA57B78B54704E256024E', '8/25'),
|
|
84
|
+
v2('uniswap-v2', 'Uniswap V2', 56, '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6', UNI_V2_ROUTER_L2, '1/6'),
|
|
85
|
+
v2('sushiswap-v2', 'SushiSwap', 56, SUSHI_V2_FACTORY, SUSHI_V2_ROUTER, '1/6'),
|
|
68
86
|
v3('pancakeswap-v3', 'PancakeSwap V3', 56, CAKE_V3_NPM, CAKE_V3_FACTORY),
|
|
69
87
|
v3('uniswap-v3', 'Uniswap V3', 56, '0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613', '0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7'),
|
|
70
88
|
// Base
|
|
71
|
-
v2('uniswap-v2', 'Uniswap V2', 8453, '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6'),
|
|
72
|
-
v2('pancakeswap-v2', 'PancakeSwap V2', 8453, '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E'),
|
|
89
|
+
v2('uniswap-v2', 'Uniswap V2', 8453, '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6', UNI_V2_ROUTER_L2, '1/6'),
|
|
90
|
+
v2('pancakeswap-v2', 'PancakeSwap V2', 8453, '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E', CAKE_V2_ROUTER_L2, '8/25'),
|
|
73
91
|
v3('uniswap-v3', 'Uniswap V3', 8453, '0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1', '0x33128a8fC17869897dcE68Ed026d694621f6FDfD'),
|
|
74
92
|
v3('pancakeswap-v3', 'PancakeSwap V3', 8453, CAKE_V3_NPM, CAKE_V3_FACTORY),
|
|
75
93
|
// Arbitrum
|
|
76
|
-
v2('uniswap-v2', 'Uniswap V2', 42161, '0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9'),
|
|
77
|
-
v2('sushiswap-v2', 'SushiSwap', 42161, SUSHI_V2_FACTORY),
|
|
78
|
-
v2('pancakeswap-v2', 'PancakeSwap V2', 42161, '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E'),
|
|
94
|
+
v2('uniswap-v2', 'Uniswap V2', 42161, '0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9', UNI_V2_ROUTER_L2, '1/6'),
|
|
95
|
+
v2('sushiswap-v2', 'SushiSwap', 42161, SUSHI_V2_FACTORY, SUSHI_V2_ROUTER, '1/6'),
|
|
96
|
+
v2('pancakeswap-v2', 'PancakeSwap V2', 42161, '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E', CAKE_V2_ROUTER_L2, '8/25'),
|
|
79
97
|
v3('uniswap-v3', 'Uniswap V3', 42161, UNI_V3_NPM, UNI_V3_FACTORY),
|
|
80
98
|
v3('pancakeswap-v3', 'PancakeSwap V3', 42161, CAKE_V3_NPM, CAKE_V3_FACTORY),
|
|
81
99
|
// Optimism
|
|
82
|
-
v2('uniswap-v2', 'Uniswap V2', 10, '0x0c3c1c532F1e39EdF36BE9Fe0bE1410313E074Bf'),
|
|
100
|
+
v2('uniswap-v2', 'Uniswap V2', 10, '0x0c3c1c532F1e39EdF36BE9Fe0bE1410313E074Bf', '0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2', '1/6'),
|
|
83
101
|
v3('uniswap-v3', 'Uniswap V3', 10, UNI_V3_NPM, UNI_V3_FACTORY),
|
|
84
102
|
// Polygon
|
|
85
|
-
v2('uniswap-v2', 'Uniswap V2', 137, '0x9e5A52f57b3038F1B8EeE45F28b3C1967e22799C'),
|
|
86
|
-
v2('sushiswap-v2', 'SushiSwap', 137, SUSHI_V2_FACTORY),
|
|
103
|
+
v2('uniswap-v2', 'Uniswap V2', 137, '0x9e5A52f57b3038F1B8EeE45F28b3C1967e22799C', '0xedf6066a2b290C185783862C7F4776A2C8077AD1', '1/6'),
|
|
104
|
+
v2('sushiswap-v2', 'SushiSwap', 137, SUSHI_V2_FACTORY, SUSHI_V2_ROUTER, '1/6'),
|
|
87
105
|
v3('uniswap-v3', 'Uniswap V3', 137, UNI_V3_NPM, UNI_V3_FACTORY),
|
|
88
106
|
// Avalanche
|
|
89
107
|
v3('uniswap-v3', 'Uniswap V3', 43114, '0x655C406EBFa14EE2006250925e54ec43AD184f8B', '0x740b1c1de25031C31FF4fC9A62f554A55cdC1baD'),
|
|
@@ -94,8 +112,8 @@ export const LP_PROTOCOLS: readonly LpProtocol[] = [
|
|
|
94
112
|
v4(42161, '0xd88f38f930b7952f2db2432cb002e7abbf3dd869', '0x76fd297e2d437cd7f76d50f01afe6160f86e9990', 297842893),
|
|
95
113
|
v4(10, '0x3c3ea4b57a46241e54610e5f022e5c45859a1017', '0xc18a3169788f4f75a170290584eca6395c75ecdb', 130947681),
|
|
96
114
|
v4(137, '0x1ec2ebf4f37e7363fdfe3551602425af0b3ceef9', '0x5ea1bd7974c8a611cbab0bdcafcb1d9cc9b3ba5a', 66980394),
|
|
97
|
-
// Tron:SunSwap
|
|
98
|
-
v2('sunswap-v2', 'SunSwap V2', TRON_CHAIN_ID.mainnet, 'TKWJdrQkqHisa1X8HUdHEfREvTzw4pMAaY'),
|
|
115
|
+
// Tron:SunSwap(协议手续费与 Uniswap 相同,1/6,用主网数据核对过;V2 工厂的 allPairsLength、V3 仓位合约的 name / factory 在链上核对过)
|
|
116
|
+
v2('sunswap-v2', 'SunSwap V2', TRON_CHAIN_ID.mainnet, 'TKWJdrQkqHisa1X8HUdHEfREvTzw4pMAaY', 'TNJVzGqKBWkJxJB5XYSqGAwUTV15U24pPq', '1/6'),
|
|
99
117
|
v3('sunswap-v3', 'SunSwap V3', TRON_CHAIN_ID.mainnet, 'TLSWrv7eC1AZCXkRjpqMZUmvgd99cj7pPF', 'TThJt8zaJzJMhCEScH7zWKnp5buVZqys9x'),
|
|
100
118
|
]
|
|
101
119
|
|
package/src/lpMath.ts
CHANGED
|
@@ -111,3 +111,226 @@ export function sqrtPriceToPrice(sqrtPriceX96: bigint, decimals0: number, decima
|
|
|
111
111
|
export function tickToPrice(tick: number, decimals0: number, decimals1: number): number {
|
|
112
112
|
return 1.0001 ** tick * 10 ** (decimals0 - decimals1)
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// 添加 / 移除流动性用到的计算
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** getSqrtRatioAtTick(MIN_TICK) / getSqrtRatioAtTick(MAX_TICK):池子价格的上下限(价格必须在 [MIN, MAX) 内) */
|
|
120
|
+
export const MIN_SQRT_RATIO = 4295128739n
|
|
121
|
+
export const MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342n
|
|
122
|
+
export const MAX_UINT128 = 2n ** 128n - 1n
|
|
123
|
+
|
|
124
|
+
const divUp = (a: bigint, b: bigint) => (a + b - 1n) / b
|
|
125
|
+
|
|
126
|
+
/** SqrtPriceMath.getAmount0Delta:区间 [a, b] 内 L 对应的 token0,roundUp 时向上取整(添加流动性时池子收取的数量) */
|
|
127
|
+
export function getAmount0Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint, roundUp: boolean): bigint {
|
|
128
|
+
if (sqrtA > sqrtB) {
|
|
129
|
+
;[sqrtA, sqrtB] = [sqrtB, sqrtA]
|
|
130
|
+
}
|
|
131
|
+
const n1 = liquidity << 96n
|
|
132
|
+
const n2 = sqrtB - sqrtA
|
|
133
|
+
return roundUp ? divUp(divUp(n1 * n2, sqrtB), sqrtA) : (n1 * n2) / sqrtB / sqrtA
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** SqrtPriceMath.getAmount1Delta:区间 [a, b] 内 L 对应的 token1,roundUp 时向上取整 */
|
|
137
|
+
export function getAmount1Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint, roundUp: boolean): bigint {
|
|
138
|
+
if (sqrtA > sqrtB) {
|
|
139
|
+
;[sqrtA, sqrtB] = [sqrtB, sqrtA]
|
|
140
|
+
}
|
|
141
|
+
return roundUp ? divUp(liquidity * (sqrtB - sqrtA), Q96) : (liquidity * (sqrtB - sqrtA)) / Q96
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 添加流动性 L 时池子收取的两种代币数量(向上取整,与池子的 modifyPosition 一致) */
|
|
145
|
+
export function getMintAmounts(sqrtPrice: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint): [bigint, bigint] {
|
|
146
|
+
if (sqrtA > sqrtB) {
|
|
147
|
+
;[sqrtA, sqrtB] = [sqrtB, sqrtA]
|
|
148
|
+
}
|
|
149
|
+
if (sqrtPrice <= sqrtA) {
|
|
150
|
+
return [getAmount0Delta(sqrtA, sqrtB, liquidity, true), 0n]
|
|
151
|
+
}
|
|
152
|
+
if (sqrtPrice < sqrtB) {
|
|
153
|
+
return [getAmount0Delta(sqrtPrice, sqrtB, liquidity, true), getAmount1Delta(sqrtA, sqrtPrice, liquidity, true)]
|
|
154
|
+
}
|
|
155
|
+
return [0n, getAmount1Delta(sqrtA, sqrtB, liquidity, true)]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** LiquidityAmounts.getLiquidityForAmount0:区间 [a, b] 内 amount0 个 token0 对应的流动性(向下取整) */
|
|
159
|
+
export function getLiquidityForAmount0(sqrtA: bigint, sqrtB: bigint, amount0: bigint): bigint {
|
|
160
|
+
if (sqrtA > sqrtB) {
|
|
161
|
+
;[sqrtA, sqrtB] = [sqrtB, sqrtA]
|
|
162
|
+
}
|
|
163
|
+
return (amount0 * ((sqrtA * sqrtB) / Q96)) / (sqrtB - sqrtA)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** LiquidityAmounts.getLiquidityForAmount1:区间 [a, b] 内 amount1 个 token1 对应的流动性(向下取整) */
|
|
167
|
+
export function getLiquidityForAmount1(sqrtA: bigint, sqrtB: bigint, amount1: bigint): bigint {
|
|
168
|
+
if (sqrtA > sqrtB) {
|
|
169
|
+
;[sqrtA, sqrtB] = [sqrtB, sqrtA]
|
|
170
|
+
}
|
|
171
|
+
return (amount1 * Q96) / (sqrtB - sqrtA)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* LiquidityAmounts.getLiquidityForAmounts:最多用 amount0 / amount1 时能添加的流动性(取两边较小的,与 V3 仓位合约一致)。
|
|
176
|
+
* 价格在区间下方只用 token0,上方只用 token1
|
|
177
|
+
*/
|
|
178
|
+
export function getLiquidityForAmounts(sqrtPrice: bigint, sqrtA: bigint, sqrtB: bigint, amount0: bigint, amount1: bigint): bigint {
|
|
179
|
+
if (sqrtA > sqrtB) {
|
|
180
|
+
;[sqrtA, sqrtB] = [sqrtB, sqrtA]
|
|
181
|
+
}
|
|
182
|
+
if (sqrtPrice <= sqrtA) {
|
|
183
|
+
return getLiquidityForAmount0(sqrtA, sqrtB, amount0)
|
|
184
|
+
}
|
|
185
|
+
if (sqrtPrice < sqrtB) {
|
|
186
|
+
const l0 = getLiquidityForAmount0(sqrtPrice, sqrtB, amount0)
|
|
187
|
+
const l1 = getLiquidityForAmount1(sqrtA, sqrtPrice, amount1)
|
|
188
|
+
return l0 < l1 ? l0 : l1
|
|
189
|
+
}
|
|
190
|
+
return getLiquidityForAmount1(sqrtA, sqrtB, amount1)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 流动性是 uint128:超出时报错(合约里同样会 revert) */
|
|
194
|
+
export function toUint128(liquidity: bigint): bigint {
|
|
195
|
+
if (liquidity < 0n || liquidity > MAX_UINT128) {
|
|
196
|
+
throw new RangeError(`liquidity out of uint128 range: ${liquidity}`)
|
|
197
|
+
}
|
|
198
|
+
return liquidity
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** TickMath.getTickAtSqrtRatio:满足 getSqrtRatioAtTick(tick) <= sqrtPrice 的最大 tick(精确,二分查找) */
|
|
202
|
+
export function getTickAtSqrtRatio(sqrtPriceX96: bigint): number {
|
|
203
|
+
if (sqrtPriceX96 < MIN_SQRT_RATIO || sqrtPriceX96 >= MAX_SQRT_RATIO) {
|
|
204
|
+
throw new RangeError(`sqrtPriceX96 out of range: ${sqrtPriceX96}`)
|
|
205
|
+
}
|
|
206
|
+
let lo = MIN_TICK
|
|
207
|
+
let hi = MAX_TICK
|
|
208
|
+
while (lo < hi) {
|
|
209
|
+
const mid = Math.floor((lo + hi + 1) / 2)
|
|
210
|
+
if (getSqrtRatioAtTick(mid) <= sqrtPriceX96) {
|
|
211
|
+
lo = mid
|
|
212
|
+
} else {
|
|
213
|
+
hi = mid - 1
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return lo
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 整数平方根(向下取整) */
|
|
220
|
+
export function sqrtBigInt(value: bigint): bigint {
|
|
221
|
+
if (value < 0n) {
|
|
222
|
+
throw new RangeError('square root of a negative number')
|
|
223
|
+
}
|
|
224
|
+
if (value < 2n) {
|
|
225
|
+
return value
|
|
226
|
+
}
|
|
227
|
+
// 牛顿迭代:从不小于平方根的 2^ceil(位数 / 2) 开始单调递减,收敛到向下取整的平方根
|
|
228
|
+
let x = 1n << BigInt((value.toString(2).length + 1) >> 1)
|
|
229
|
+
for (;;) {
|
|
230
|
+
const y = (x + value / x) >> 1n
|
|
231
|
+
if (y >= x) {
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
x = y
|
|
235
|
+
}
|
|
236
|
+
return x
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 十进制数('1.5'、'2e-7'、1.5)→ 分子 / 分母,精确,不经过浮点 */
|
|
240
|
+
export function parseDecimal(value: string | number): { num: bigint; den: bigint } {
|
|
241
|
+
const text = typeof value === 'number' ? (Number.isFinite(value) ? String(value) : '') : value.trim()
|
|
242
|
+
const m = /^\+?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text)
|
|
243
|
+
if (!m || (!m[1] && !m[2])) {
|
|
244
|
+
throw new Error(`Invalid number: ${String(value)}`)
|
|
245
|
+
}
|
|
246
|
+
const frac = m[2] ?? ''
|
|
247
|
+
const exp = Number(m[3] ?? 0) - frac.length
|
|
248
|
+
if (Math.abs(exp) > 400) {
|
|
249
|
+
throw new RangeError(`Number out of range: ${String(value)}`)
|
|
250
|
+
}
|
|
251
|
+
const digits = BigInt(`${m[1] ?? ''}${frac}` || '0')
|
|
252
|
+
return exp >= 0 ? { num: digits * 10n ** BigInt(exp), den: 1n } : { num: digits, den: 10n ** BigInt(-exp) }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 价格(token1 / token0,按精度换算后的人类可读数字,如 600 表示 1 个 token0 值 600 个 token1)→ sqrtPriceX96。
|
|
257
|
+
* 精确计算(字符串不经过浮点),结果向下取整;价格必须大于 0
|
|
258
|
+
*/
|
|
259
|
+
export function priceToSqrtPriceX96(price: string | number, decimals0: number, decimals1: number): bigint {
|
|
260
|
+
const { num, den } = parseDecimal(price)
|
|
261
|
+
return ratioToSqrtPriceX96(num, den, decimals0, decimals1)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 价格(分数 num / den,token1 / token0,按精度换算)→ sqrtPriceX96,向下取整;价格必须大于 0 */
|
|
265
|
+
export function ratioToSqrtPriceX96(num: bigint, den: bigint, decimals0: number, decimals1: number): bigint {
|
|
266
|
+
if (num <= 0n || den <= 0n) {
|
|
267
|
+
throw new RangeError('price must be greater than 0')
|
|
268
|
+
}
|
|
269
|
+
// raw = price × 10^(d1 − d0);sqrtPriceX96 = sqrt(raw × 2^192)
|
|
270
|
+
const scale = decimals1 - decimals0
|
|
271
|
+
const n = num * (scale > 0 ? 10n ** BigInt(scale) : 1n) * 2n ** 192n
|
|
272
|
+
const d = den * (scale < 0 ? 10n ** BigInt(-scale) : 1n)
|
|
273
|
+
return sqrtBigInt(n / d)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** sqrtPriceX96 → tick(getTickAtSqrtRatio),超出池子价格范围时取 MIN_TICK / MAX_TICK */
|
|
277
|
+
export function clampedTickAtSqrtRatio(sqrtPrice: bigint): number {
|
|
278
|
+
if (sqrtPrice < MIN_SQRT_RATIO) {
|
|
279
|
+
return MIN_TICK
|
|
280
|
+
}
|
|
281
|
+
if (sqrtPrice >= MAX_SQRT_RATIO) {
|
|
282
|
+
return MAX_TICK
|
|
283
|
+
}
|
|
284
|
+
return getTickAtSqrtRatio(sqrtPrice)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** 价格(token1 / token0,按精度换算)→ tick:满足 tickToPrice(tick) <= price 的最大 tick,限制在 [MIN_TICK, MAX_TICK] 内 */
|
|
288
|
+
export function priceToTick(price: string | number, decimals0: number, decimals1: number): number {
|
|
289
|
+
return clampedTickAtSqrtRatio(priceToSqrtPriceX96(price, decimals0, decimals1))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 最近的可用 tick(tickSpacing 的整数倍),限制在 [MIN_TICK, MAX_TICK] 内 */
|
|
293
|
+
export function nearestUsableTick(tick: number, tickSpacing: number): number {
|
|
294
|
+
if (!Number.isInteger(tickSpacing) || tickSpacing <= 0) {
|
|
295
|
+
throw new RangeError(`invalid tickSpacing: ${tickSpacing}`)
|
|
296
|
+
}
|
|
297
|
+
const rounded = Math.round(tick / tickSpacing) * tickSpacing
|
|
298
|
+
if (rounded < MIN_TICK) {
|
|
299
|
+
return rounded + tickSpacing
|
|
300
|
+
}
|
|
301
|
+
if (rounded > MAX_TICK) {
|
|
302
|
+
return rounded - tickSpacing
|
|
303
|
+
}
|
|
304
|
+
return rounded
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 价格按 slippage 上下浮动后的 sqrtPrice:[sqrtPrice × sqrt(1 − s), sqrtPrice × sqrt(1 + s)],限制在池子允许的价格范围内。
|
|
309
|
+
* slippage 是比例(0.005 = 0.5%),精确到百万分之一
|
|
310
|
+
*/
|
|
311
|
+
export function sqrtPriceBounds(sqrtPriceX96: bigint, slippage: number): [bigint, bigint] {
|
|
312
|
+
if (!(slippage >= 0 && slippage < 1)) {
|
|
313
|
+
throw new RangeError(`slippage must be in [0, 1): ${slippage}`)
|
|
314
|
+
}
|
|
315
|
+
const ppm = BigInt(Math.round(slippage * 1e6))
|
|
316
|
+
const square = sqrtPriceX96 * sqrtPriceX96
|
|
317
|
+
const lower = sqrtBigInt((square * (1_000_000n - ppm)) / 1_000_000n)
|
|
318
|
+
const upper = sqrtBigInt((square * (1_000_000n + ppm)) / 1_000_000n)
|
|
319
|
+
const clamp = (x: bigint) => (x <= MIN_SQRT_RATIO ? MIN_SQRT_RATIO + 1n : x >= MAX_SQRT_RATIO ? MAX_SQRT_RATIO - 1n : x)
|
|
320
|
+
return [clamp(lower), clamp(upper)]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 价格在 slippage 范围内变动时,流动性 L 至少对应多少代币(移除流动性 / V3 添加时的最小数量,向下取整)。
|
|
325
|
+
* token0 随价格上涨而减少,取上限价格时的数量;token1 取下限价格时的
|
|
326
|
+
*/
|
|
327
|
+
export function getMinAmounts(sqrtPriceX96: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint, slippage: number): [bigint, bigint] {
|
|
328
|
+
const [lower, upper] = sqrtPriceBounds(sqrtPriceX96, slippage)
|
|
329
|
+
return [getAmountsForLiquidity(upper, sqrtA, sqrtB, liquidity)[0], getAmountsForLiquidity(lower, sqrtA, sqrtB, liquidity)[1]]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 价格在 slippage 范围内变动时,添加流动性 L 最多需要多少代币(V4 的 amountMax,向上取整) */
|
|
333
|
+
export function getMaxAmounts(sqrtPriceX96: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint, slippage: number): [bigint, bigint] {
|
|
334
|
+
const [lower, upper] = sqrtPriceBounds(sqrtPriceX96, slippage)
|
|
335
|
+
return [getMintAmounts(lower, sqrtA, sqrtB, liquidity)[0], getMintAmounts(upper, sqrtA, sqrtB, liquidity)[1]]
|
|
336
|
+
}
|