@w3lib/solana-batch-call 0.3.0 → 0.4.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +74 -1
  3. package/dist/address.d.ts +2 -0
  4. package/dist/address.d.ts.map +1 -1
  5. package/dist/address.js +8 -0
  6. package/dist/address.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/liquidity/amm.d.ts +7 -0
  12. package/dist/liquidity/amm.d.ts.map +1 -0
  13. package/dist/liquidity/amm.js +307 -0
  14. package/dist/liquidity/amm.js.map +1 -0
  15. package/dist/liquidity/clmm.d.ts +18 -0
  16. package/dist/liquidity/clmm.d.ts.map +1 -0
  17. package/dist/liquidity/clmm.js +85 -0
  18. package/dist/liquidity/clmm.js.map +1 -0
  19. package/dist/liquidity/common.d.ts +94 -0
  20. package/dist/liquidity/common.d.ts.map +1 -0
  21. package/dist/liquidity/common.js +228 -0
  22. package/dist/liquidity/common.js.map +1 -0
  23. package/dist/liquidity/index.d.ts +34 -0
  24. package/dist/liquidity/index.d.ts.map +1 -0
  25. package/dist/liquidity/index.js +157 -0
  26. package/dist/liquidity/index.js.map +1 -0
  27. package/dist/liquidity/orca.d.ts +6 -0
  28. package/dist/liquidity/orca.d.ts.map +1 -0
  29. package/dist/liquidity/orca.js +303 -0
  30. package/dist/liquidity/orca.js.map +1 -0
  31. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  32. package/dist/liquidity/raydiumClmm.d.ts.map +1 -0
  33. package/dist/liquidity/raydiumClmm.js +307 -0
  34. package/dist/liquidity/raydiumClmm.js.map +1 -0
  35. package/dist/liquidity/types.d.ts +150 -0
  36. package/dist/liquidity/types.d.ts.map +1 -0
  37. package/dist/liquidity/types.js +2 -0
  38. package/dist/liquidity/types.js.map +1 -0
  39. package/dist/lp.d.ts +45 -0
  40. package/dist/lp.d.ts.map +1 -1
  41. package/dist/lp.js +8 -7
  42. package/dist/lp.js.map +1 -1
  43. package/dist/lpMath.d.ts +45 -0
  44. package/dist/lpMath.d.ts.map +1 -1
  45. package/dist/lpMath.js +189 -0
  46. package/dist/lpMath.js.map +1 -1
  47. package/dist/subpaths/liquidity.d.ts +23 -0
  48. package/dist/subpaths/liquidity.d.ts.map +1 -0
  49. package/dist/subpaths/liquidity.js +19 -0
  50. package/dist/subpaths/liquidity.js.map +1 -0
  51. package/dist/subpaths/lp.d.ts +1 -1
  52. package/dist/subpaths/lp.d.ts.map +1 -1
  53. package/dist/subpaths/lp.js +1 -1
  54. package/dist/subpaths/lp.js.map +1 -1
  55. package/package.json +17 -10
  56. package/src/address.ts +9 -0
  57. package/src/index.ts +1 -1
  58. package/src/liquidity/amm.ts +353 -0
  59. package/src/liquidity/clmm.ts +108 -0
  60. package/src/liquidity/common.ts +285 -0
  61. package/src/liquidity/index.ts +177 -0
  62. package/src/liquidity/orca.ts +344 -0
  63. package/src/liquidity/raydiumClmm.ts +348 -0
  64. package/src/liquidity/types.ts +158 -0
  65. package/src/lp.ts +9 -8
  66. package/src/lpMath.ts +215 -0
  67. package/src/subpaths/liquidity.ts +45 -0
  68. package/src/subpaths/lp.ts +9 -0
package/src/index.ts CHANGED
@@ -64,7 +64,7 @@ export {
64
64
  clusterOfGenesis,
65
65
  type Cluster,
66
66
  } from './constants.js'
67
- export { decodeAddress, encodeAddress, findProgramAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress, isOnCurve } from './address.js'
67
+ export { createProgramAddress, decodeAddress, encodeAddress, findProgramAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress, isOnCurve } from './address.js'
68
68
  export {
69
69
  formatAmount,
70
70
  formatUnits,
@@ -0,0 +1,353 @@
1
+ import { createProgramAddress, decodeAddress, getAssociatedTokenAddress } from '../address.js'
2
+ import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
3
+ import { parseTokenAccount, type AccountInfo } from '../layout.js'
4
+ import {
5
+ RAYDIUM_AMM_AUTHORITY,
6
+ RAYDIUM_AMM_PROGRAM_ID,
7
+ RAYDIUM_CPMM_AUTHORITY,
8
+ RAYDIUM_CPMM_PROGRAM_ID,
9
+ SOLANA_LP_PROTOCOLS,
10
+ parseAmmPool,
11
+ type AmmPoolState,
12
+ type SolanaLpToken,
13
+ } from '../lp.js'
14
+ import { ceilDiv } from '../lpMath.js'
15
+ import type { Instruction } from '../write/transaction.js'
16
+ import {
17
+ Bytes,
18
+ MEMO_PROGRAM_ID,
19
+ Writer,
20
+ anchorIx,
21
+ makePlan,
22
+ minus,
23
+ mintMeta,
24
+ mustAccounts,
25
+ nativeSide,
26
+ parseAmount,
27
+ plus,
28
+ prepareTokenAccounts,
29
+ r,
30
+ refPool,
31
+ removeAmount,
32
+ tokenInfo,
33
+ w,
34
+ type Ctx,
35
+ type MintMeta,
36
+ } from './common.js'
37
+ import type { SolanaIncreaseLiquidityParams, SolanaLiquidityPlan, SolanaNewPositionParams, SolanaRemoveLiquidityParams } from './types.js'
38
+
39
+ type AmmProtocol = 'raydium-amm' | 'raydium-cpmm'
40
+
41
+ interface Pool {
42
+ state: AmmPoolState
43
+ /** 池子账户的原始数据(AMM v4 还要读 OpenBook 相关字段) */
44
+ data: Uint8Array
45
+ mints: [MintMeta, MintMeta]
46
+ /** 属于 LP 的储备:金库余额 − 不属于 LP 的部分(与程序计算撤出 / 存入数量时一致) */
47
+ reserve0: bigint
48
+ reserve1: bigint
49
+ }
50
+
51
+ async function readPool(ctx: Ctx, protocol: AmmProtocol, address: string): Promise<Pool> {
52
+ const [account] = await mustAccounts(ctx, [address], () => 'Pool')
53
+ const state = parseAmmPool(account as AccountInfo)
54
+ if (!state || state.protocol !== protocol) {
55
+ throw new Error(`Not a ${SOLANA_LP_PROTOCOLS[protocol].name} pool: ${address}`)
56
+ }
57
+ const [m0, m1, v0, v1] = await mustAccounts(ctx, [state.mint0, state.mint1, state.vault0, state.vault1], (i) => (i < 2 ? 'Token mint' : 'Pool vault'))
58
+ const sub = (a: bigint, b: bigint) => (a > b ? a - b : 0n)
59
+ return {
60
+ state,
61
+ data: (account as AccountInfo).data,
62
+ mints: [mintMeta(state.mint0, m0 as AccountInfo), mintMeta(state.mint1, m1 as AccountInfo)],
63
+ reserve0: sub(parseTokenAccount((v0 as AccountInfo).data).amount, state.reserved0),
64
+ reserve1: sub(parseTokenAccount((v1 as AccountInfo).data).amount, state.reserved1),
65
+ }
66
+ }
67
+
68
+ /** AMM v4 存入:按固定的一边(baseSide 0 = token0)收取,另一边 = 固定数量 × 储备比例(向上取整),LP = 固定数量 × 总量 / 储备 */
69
+ function ammDeposit(pool: Pool, given0: bigint | undefined, given1: bigint | undefined) {
70
+ const { reserve0, reserve1 } = pool
71
+ const supply = pool.state.lpSupply
72
+ const by0 = (a0: bigint) => ({ side: 0, amount0: a0, amount1: ceilDiv(a0 * reserve1, reserve0), lp: (a0 * supply) / reserve0 })
73
+ const by1 = (a1: bigint) => ({ side: 1, amount0: ceilDiv(a1 * reserve0, reserve1), amount1: a1, lp: (a1 * supply) / reserve1 })
74
+ if (given0 !== undefined && given1 !== undefined) {
75
+ const a = by0(given0)
76
+ // 两边都给:按不超出另一边的那一边固定
77
+ return a.amount1 <= given1 ? a : by1(given1)
78
+ }
79
+ return given0 !== undefined ? by0(given0) : by1(given1 as bigint)
80
+ }
81
+
82
+ /** CPMM 存入:按要拿到的 LP 收取两边(向上取整);给一边时 LP = 数量 × 总量 / 储备(向下取整,用掉的不超过给的) */
83
+ function cpmmDeposit(pool: Pool, given0: bigint | undefined, given1: bigint | undefined) {
84
+ const { reserve0, reserve1 } = pool
85
+ const supply = pool.state.lpSupply
86
+ const lp0 = given0 === undefined ? null : (given0 * supply) / reserve0
87
+ const lp1 = given1 === undefined ? null : (given1 * supply) / reserve1
88
+ // side:决定 LP 数量的那一边(两边都给时是较少的一边)
89
+ const side = lp0 === null ? 1 : lp1 === null ? 0 : lp0 <= lp1 ? 0 : 1
90
+ const lp = side === 0 ? (lp0 as bigint) : (lp1 as bigint)
91
+ return { side, lp, amount0: ceilDiv(lp * reserve0, supply), amount1: ceilDiv(lp * reserve1, supply) }
92
+ }
93
+
94
+ function info(pool: Pool, token0: SolanaLpToken, token1: SolanaLpToken, protocol: AmmProtocol) {
95
+ return {
96
+ protocol,
97
+ protocolName: SOLANA_LP_PROTOCOLS[protocol].name,
98
+ type: 'amm' as const,
99
+ pool: pool.state.address,
100
+ token0,
101
+ token1,
102
+ positionMint: null,
103
+ position: null,
104
+ lpMint: pool.state.lpMint,
105
+ tickLower: null,
106
+ tickUpper: null,
107
+ tick: null,
108
+ price: null,
109
+ priceLower: null,
110
+ priceUpper: null,
111
+ }
112
+ }
113
+
114
+ /** AMM v4 池子里 OpenBook 相关的账户:open_orders 496、market 528、market_program 560、target_orders 592 */
115
+ function ammKeys(pool: Pool) {
116
+ const b = new Bytes(pool.data)
117
+ return { openOrders: b.key(496), market: b.key(528), marketProgram: b.key(560), targetOrders: b.key(592) }
118
+ }
119
+
120
+ /** OpenBook 市场账户(MarketStateV3,含 5 字节前缀):vault_signer_nonce 45、base_vault 117、quote_vault 165、event_queue 253、bids 285、asks 317 */
121
+ async function readMarket(ctx: Ctx, market: string, program: string) {
122
+ const [account] = await mustAccounts(ctx, [market], () => 'OpenBook market of the AMM v4 pool')
123
+ const data = (account as AccountInfo).data
124
+ if (data.length < 388) {
125
+ throw new Error(`Unexpected OpenBook market account: ${market}`)
126
+ }
127
+ const b = new Bytes(data)
128
+ const nonce = new Uint8Array(8)
129
+ new DataView(nonce.buffer).setBigUint64(0, b.u64(45), true)
130
+ return {
131
+ baseVault: b.key(117),
132
+ quoteVault: b.key(165),
133
+ eventQueue: b.key(253),
134
+ bids: b.key(285),
135
+ asks: b.key(317),
136
+ authority: createProgramAddress([decodeAddress(market), nonce], program),
137
+ }
138
+ }
139
+
140
+ export async function ammAdd(ctx: Ctx, protocol: AmmProtocol, params: SolanaIncreaseLiquidityParams | SolanaNewPositionParams): Promise<SolanaLiquidityPlan> {
141
+ const address = 'position' in params ? refPool(params.position) : params.pool
142
+ if (!('position' in params) && (params.fullRange || params.priceLower !== undefined || params.priceUpper !== undefined || params.tickLower !== undefined || params.tickUpper !== undefined)) {
143
+ throw new Error(`${SOLANA_LP_PROTOCOLS[protocol].name} has no price range: pass only the amounts`)
144
+ }
145
+ const pool = await readPool(ctx, protocol, address)
146
+ const { state, mints } = pool
147
+ if (state.lpSupply === 0n || pool.reserve0 === 0n || pool.reserve1 === 0n) {
148
+ throw new Error(`Pool ${address} is empty`)
149
+ }
150
+ const [token0, token1] = (await tokenInfo(ctx, mints)) as [SolanaLpToken, SolanaLpToken]
151
+ const given0 = parseAmount(params.amount0, params.amount0Formatted, mints[0].decimals, 'amount0')
152
+ const given1 = parseAmount(params.amount1, params.amount1Formatted, mints[1].decimals, 'amount1')
153
+ if (given0 === undefined && given1 === undefined) {
154
+ throw new Error('Pass the amount of at least one token')
155
+ }
156
+ const lpMeta = { mint: state.lpMint, program: TOKEN_PROGRAM_ID, decimals: 0 }
157
+ let lp: bigint
158
+ let amount0: bigint
159
+ let amount1: bigint
160
+ let max0: bigint
161
+ let max1: bigint
162
+ /** 决定数量的那一边(AMM v4 是固定收取的一边) */
163
+ let side = 0
164
+ let ix: Instruction
165
+ if (protocol === 'raydium-cpmm') {
166
+ ;({ lp, amount0, amount1, side } = cpmmDeposit(pool, given0, given1))
167
+ // 价格(储备比例)变动时每份 LP 对应的数量会变:两边都按滑点给出上限
168
+ ;[max0, max1] = [plus(amount0, ctx.slippage), plus(amount1, ctx.slippage)]
169
+ } else {
170
+ const d = ammDeposit(pool, given0, given1)
171
+ ;({ lp, amount0, amount1, side } = d)
172
+ // 固定的一边按给的数量收取;另一边按滑点给出上限
173
+ ;[max0, max1] = side === 0 ? [amount0, plus(amount1, ctx.slippage)] : [plus(amount0, ctx.slippage), amount1]
174
+ }
175
+ // 两边都给时,另一边的上限不超过给的数量(决定数量的那一边保留滑点余量,否则价格稍有不利就失败)
176
+ if (side === 0 && given1 !== undefined && max1 > given1) max1 = given1
177
+ if (side === 1 && given0 !== undefined && max0 > given0) max0 = given0
178
+ if (lp === 0n || amount0 === 0n || amount1 === 0n) {
179
+ throw new Error('Amount too small: no LP tokens would be minted')
180
+ }
181
+ const tokens = await prepareTokenAccounts(ctx, [...mints, lpMeta], [max0, max1, null])
182
+ const [user0, user1, userLp] = tokens.accounts as [string, string, string]
183
+ if (protocol === 'raydium-cpmm') {
184
+ ix = {
185
+ programId: RAYDIUM_CPMM_PROGRAM_ID,
186
+ keys: [
187
+ r(ctx.owner, true),
188
+ r(RAYDIUM_CPMM_AUTHORITY),
189
+ w(state.address),
190
+ w(userLp),
191
+ w(user0),
192
+ w(user1),
193
+ w(state.vault0),
194
+ w(state.vault1),
195
+ r(TOKEN_PROGRAM_ID),
196
+ r(TOKEN_2022_PROGRAM_ID),
197
+ r(state.mint0),
198
+ r(state.mint1),
199
+ w(state.lpMint),
200
+ ],
201
+ data: new Writer().bytes(anchorIx('deposit')).u64(lp).u64(max0).u64(max1).build(),
202
+ }
203
+ } else {
204
+ const keys = ammKeys(pool)
205
+ const market = await readMarket(ctx, keys.market, keys.marketProgram)
206
+ ix = {
207
+ programId: RAYDIUM_AMM_PROGRAM_ID,
208
+ keys: [
209
+ r(TOKEN_PROGRAM_ID),
210
+ w(state.address),
211
+ r(RAYDIUM_AMM_AUTHORITY),
212
+ r(keys.openOrders),
213
+ w(keys.targetOrders),
214
+ w(state.lpMint),
215
+ w(state.vault0),
216
+ w(state.vault1),
217
+ r(keys.market),
218
+ w(user0),
219
+ w(user1),
220
+ w(userLp),
221
+ r(ctx.owner, true),
222
+ r(market.eventQueue),
223
+ ],
224
+ // deposit:max_coin、max_pc、base_side(0 = 固定 coin)、other_amount_min(另一边至少收取多少)
225
+ data: new Writer()
226
+ .u8(3)
227
+ .u64(max0)
228
+ .u64(max1)
229
+ .u64(BigInt(side))
230
+ .u64(side === 0 ? minus(amount1, ctx.slippage) : minus(amount0, ctx.slippage))
231
+ .build(),
232
+ }
233
+ }
234
+ return makePlan(ctx, {
235
+ action: 'add',
236
+ ...info(pool, token0, token1, protocol),
237
+ liquidity: lp.toString(),
238
+ amount0: amount0.toString(),
239
+ amount1: amount1.toString(),
240
+ amount0Max: max0.toString(),
241
+ amount1Max: max1.toString(),
242
+ amount0Min: null,
243
+ amount1Min: null,
244
+ fees0: '0',
245
+ fees1: '0',
246
+ rewardMints: [],
247
+ native: nativeSide(tokens.nativeIndex),
248
+ steps: [
249
+ { kind: 'setup', instructions: tokens.setup },
250
+ { kind: 'add', instructions: [...tokens.wrap, ix, ...tokens.cleanup] },
251
+ ],
252
+ signers: [],
253
+ })
254
+ }
255
+
256
+ export async function ammRemove(ctx: Ctx, protocol: AmmProtocol, params: SolanaRemoveLiquidityParams): Promise<SolanaLiquidityPlan> {
257
+ const address = refPool(params.position)
258
+ const pool = await readPool(ctx, protocol, address)
259
+ const { state, mints } = pool
260
+ const lpAccount = getAssociatedTokenAddress(ctx.owner, state.lpMint, TOKEN_PROGRAM_ID)
261
+ const [held] = await ctx.client.accounts([lpAccount], { signal: ctx.signal })
262
+ const balance = held ? parseTokenAccount(held.data).amount : 0n
263
+ if (balance === 0n) {
264
+ throw new Error(`${ctx.owner} holds no LP tokens of ${address} (in its associated token account)`)
265
+ }
266
+ const lp = removeAmount(balance, params.percent, params.liquidity)
267
+ const [token0, token1] = (await tokenInfo(ctx, mints)) as [SolanaLpToken, SolanaLpToken]
268
+ // 与程序一致:份额 × 储备,向下取整
269
+ const amount0 = (lp * pool.reserve0) / state.lpSupply
270
+ const amount1 = (lp * pool.reserve1) / state.lpSupply
271
+ if (amount0 === 0n && amount1 === 0n) {
272
+ throw new Error('Amount too small: nothing would be withdrawn')
273
+ }
274
+ const min0 = minus(amount0, ctx.slippage)
275
+ const min1 = minus(amount1, ctx.slippage)
276
+ const tokens = await prepareTokenAccounts(ctx, mints, [null, null])
277
+ const [user0, user1] = tokens.accounts as [string, string]
278
+ let ix: Instruction
279
+ if (protocol === 'raydium-cpmm') {
280
+ ix = {
281
+ programId: RAYDIUM_CPMM_PROGRAM_ID,
282
+ keys: [
283
+ r(ctx.owner, true),
284
+ r(RAYDIUM_CPMM_AUTHORITY),
285
+ w(state.address),
286
+ w(lpAccount),
287
+ w(user0),
288
+ w(user1),
289
+ w(state.vault0),
290
+ w(state.vault1),
291
+ r(TOKEN_PROGRAM_ID),
292
+ r(TOKEN_2022_PROGRAM_ID),
293
+ r(state.mint0),
294
+ r(state.mint1),
295
+ w(state.lpMint),
296
+ r(MEMO_PROGRAM_ID),
297
+ ],
298
+ data: new Writer().bytes(anchorIx('withdraw')).u64(lp).u64(min0).u64(min1).build(),
299
+ }
300
+ } else {
301
+ const keys = ammKeys(pool)
302
+ const market = await readMarket(ctx, keys.market, keys.marketProgram)
303
+ ix = {
304
+ programId: RAYDIUM_AMM_PROGRAM_ID,
305
+ keys: [
306
+ r(TOKEN_PROGRAM_ID),
307
+ w(state.address),
308
+ r(RAYDIUM_AMM_AUTHORITY),
309
+ w(keys.openOrders),
310
+ w(keys.targetOrders),
311
+ w(state.lpMint),
312
+ w(state.vault0),
313
+ w(state.vault1),
314
+ // 原来的 withdraw_queue / temp_lp 账户,程序已不再使用,按官方 SDK 传池子地址占位
315
+ w(state.address),
316
+ w(state.address),
317
+ r(keys.marketProgram),
318
+ w(keys.market),
319
+ w(market.baseVault),
320
+ w(market.quoteVault),
321
+ r(market.authority),
322
+ w(lpAccount),
323
+ w(user0),
324
+ w(user1),
325
+ r(ctx.owner, true),
326
+ w(market.eventQueue),
327
+ w(market.bids),
328
+ w(market.asks),
329
+ ],
330
+ data: new Writer().u8(4).u64(lp).u64(min0).u64(min1).build(),
331
+ }
332
+ }
333
+ return makePlan(ctx, {
334
+ action: 'remove',
335
+ ...info(pool, token0, token1, protocol),
336
+ liquidity: lp.toString(),
337
+ amount0: amount0.toString(),
338
+ amount1: amount1.toString(),
339
+ amount0Max: null,
340
+ amount1Max: null,
341
+ amount0Min: min0.toString(),
342
+ amount1Min: min1.toString(),
343
+ fees0: '0',
344
+ fees1: '0',
345
+ rewardMints: [],
346
+ native: nativeSide(tokens.nativeIndex),
347
+ steps: [
348
+ { kind: 'setup', instructions: tokens.setup },
349
+ { kind: 'remove', instructions: [...tokens.wrap, ix, ...tokens.cleanup] },
350
+ ],
351
+ signers: [],
352
+ })
353
+ }
@@ -0,0 +1,108 @@
1
+ import { lpPositions, type SolanaLpClmmPosition, type SolanaLpToken } from '../lp.js'
2
+ import {
3
+ MAX_TICK,
4
+ MIN_TICK,
5
+ getLiquidityForAmounts,
6
+ nearestUsableTick,
7
+ priceToTick,
8
+ sqrtPriceAtTick,
9
+ sqrtPriceToPrice,
10
+ tickToPrice,
11
+ type ClmmProgram,
12
+ } from '../lpMath.js'
13
+ import type { Ctx } from './common.js'
14
+ import type { SolanaNewPositionParams } from './types.js'
15
+
16
+ /** 新建仓位的 tick 区间:fullRange / tickLower + tickUpper / priceLower + priceUpper(token1 / token0)三选一 */
17
+ export function resolveRange(program: ClmmProgram, params: SolanaNewPositionParams, token0: SolanaLpToken, token1: SolanaLpToken, tickSpacing: number): [number, number] {
18
+ const byTicks = params.tickLower !== undefined || params.tickUpper !== undefined
19
+ const byPrices = params.priceLower !== undefined || params.priceUpper !== undefined
20
+ if (Number(!!params.fullRange) + Number(byTicks) + Number(byPrices) !== 1) {
21
+ throw new Error('Pass exactly one of fullRange, tickLower + tickUpper, or priceLower + priceUpper')
22
+ }
23
+ let lower: number
24
+ let upper: number
25
+ if (params.fullRange) {
26
+ lower = Math.ceil(MIN_TICK / tickSpacing) * tickSpacing
27
+ upper = Math.floor(MAX_TICK / tickSpacing) * tickSpacing
28
+ } else if (byTicks) {
29
+ if (params.tickLower === undefined || params.tickUpper === undefined) {
30
+ throw new Error('Pass both tickLower and tickUpper')
31
+ }
32
+ lower = params.tickLower
33
+ upper = params.tickUpper
34
+ for (const tick of [lower, upper]) {
35
+ if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK || tick % tickSpacing !== 0) {
36
+ throw new RangeError(`tick ${tick} must be an integer multiple of tickSpacing ${tickSpacing} within [${MIN_TICK}, ${MAX_TICK}]`)
37
+ }
38
+ }
39
+ } else {
40
+ if (params.priceLower === undefined || params.priceUpper === undefined) {
41
+ throw new Error('Pass both priceLower and priceUpper')
42
+ }
43
+ if (token0.decimals === null || token1.decimals === null) {
44
+ throw new Error('Cannot read token decimals to convert prices; pass ticks instead')
45
+ }
46
+ const ticks = [params.priceLower, params.priceUpper].map((price) => {
47
+ return nearestUsableTick(priceToTick(program, price, token0.decimals as number, token1.decimals as number), tickSpacing)
48
+ }) as [number, number]
49
+ ;[lower, upper] = ticks[0] <= ticks[1] ? ticks : [ticks[1], ticks[0]]
50
+ }
51
+ if (lower >= upper) {
52
+ throw new RangeError(`The price range is too narrow: tickLower ${lower} must be below tickUpper ${upper} (tickSpacing ${tickSpacing})`)
53
+ }
54
+ return [lower, upper]
55
+ }
56
+
57
+ /** 按输入的数量算添加的流动性;价格在区间外时只需要一种代币,给错了另一种会报错 */
58
+ export function liquidityFor(
59
+ program: ClmmProgram,
60
+ tick: number,
61
+ sqrtPrice: bigint,
62
+ tickLower: number,
63
+ tickUpper: number,
64
+ amount0: bigint | undefined,
65
+ amount1: bigint | undefined,
66
+ token0: SolanaLpToken,
67
+ token1: SolanaLpToken,
68
+ ): bigint {
69
+ const name = (t: SolanaLpToken) => t.symbol ?? t.mint
70
+ if (amount0 === undefined && amount1 === undefined) {
71
+ throw new Error('Pass the amount of at least one token')
72
+ }
73
+ if (amount1 === undefined && (tick >= tickUpper || sqrtPrice >= sqrtPriceAtTick(program, tickUpper))) {
74
+ throw new Error(`The current price is above the range: the position takes only ${name(token1)} (token1); pass amount1 instead`)
75
+ }
76
+ if (amount0 === undefined && (tick < tickLower || sqrtPrice <= sqrtPriceAtTick(program, tickLower))) {
77
+ throw new Error(`The current price is below the range: the position takes only ${name(token0)} (token0); pass amount0 instead`)
78
+ }
79
+ const liquidity = getLiquidityForAmounts(tick, sqrtPrice, sqrtPriceAtTick(program, tickLower), sqrtPriceAtTick(program, tickUpper), amount0, amount1, tickLower, tickUpper)
80
+ if (liquidity === 0n) {
81
+ throw new Error('Amount too small: the liquidity rounds to 0')
82
+ }
83
+ if (liquidity >= 1n << 128n) {
84
+ throw new RangeError('Amount too large')
85
+ }
86
+ return liquidity
87
+ }
88
+
89
+ /** 计划里的价格字段(token1 / token0,按精度换算,只用于展示) */
90
+ export function priceInfo(tick: number, sqrtPrice: bigint, token0: SolanaLpToken, token1: SolanaLpToken, tickLower: number, tickUpper: number) {
91
+ const known = token0.decimals !== null && token1.decimals !== null
92
+ return {
93
+ tick,
94
+ price: known ? sqrtPriceToPrice(sqrtPrice, token0.decimals as number, token1.decimals as number) : null,
95
+ priceLower: known ? tickToPrice(tickLower, token0.decimals as number, token1.decimals as number) : null,
96
+ priceUpper: known ? tickToPrice(tickUpper, token0.decimals as number, token1.decimals as number) : null,
97
+ }
98
+ }
99
+
100
+ /** 已有的仓位:用查询拿到数量、手续费、价格(要求 owner 持有仓位 NFT 的关联代币账户) */
101
+ export async function queryPosition(ctx: Ctx, protocol: 'raydium-clmm' | 'orca-whirlpool', positionMint: string): Promise<SolanaLpClmmPosition> {
102
+ const list = await lpPositions(ctx.client, ctx.owner, { protocols: [protocol], mints: [positionMint], includeClosed: true, signal: ctx.signal })
103
+ const found = list.find((p): p is SolanaLpClmmPosition => p.type === 'clmm' && p.positionMint === positionMint)
104
+ if (!found) {
105
+ throw new Error(`${ctx.owner} does not hold the ${protocol} position ${positionMint} (in its associated token account)`)
106
+ }
107
+ return found
108
+ }