@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.
- package/CHANGELOG.md +12 -0
- package/README.md +74 -1
- package/dist/address.d.ts +2 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +8 -0
- package/dist/address.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/liquidity/amm.d.ts +7 -0
- package/dist/liquidity/amm.d.ts.map +1 -0
- package/dist/liquidity/amm.js +307 -0
- package/dist/liquidity/amm.js.map +1 -0
- package/dist/liquidity/clmm.d.ts +18 -0
- package/dist/liquidity/clmm.d.ts.map +1 -0
- package/dist/liquidity/clmm.js +85 -0
- package/dist/liquidity/clmm.js.map +1 -0
- package/dist/liquidity/common.d.ts +94 -0
- package/dist/liquidity/common.d.ts.map +1 -0
- package/dist/liquidity/common.js +228 -0
- package/dist/liquidity/common.js.map +1 -0
- package/dist/liquidity/index.d.ts +34 -0
- package/dist/liquidity/index.d.ts.map +1 -0
- package/dist/liquidity/index.js +157 -0
- package/dist/liquidity/index.js.map +1 -0
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -0
- package/dist/liquidity/orca.js +303 -0
- package/dist/liquidity/orca.js.map +1 -0
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -0
- package/dist/liquidity/raydiumClmm.js +307 -0
- package/dist/liquidity/raydiumClmm.js.map +1 -0
- package/dist/liquidity/types.d.ts +150 -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/lp.d.ts +45 -0
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +8 -7
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +45 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +189 -0
- package/dist/lpMath.js.map +1 -1
- package/dist/subpaths/liquidity.d.ts +23 -0
- package/dist/subpaths/liquidity.d.ts.map +1 -0
- package/dist/subpaths/liquidity.js +19 -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/package.json +17 -10
- package/src/address.ts +9 -0
- package/src/index.ts +1 -1
- package/src/liquidity/amm.ts +353 -0
- package/src/liquidity/clmm.ts +108 -0
- package/src/liquidity/common.ts +285 -0
- package/src/liquidity/index.ts +177 -0
- package/src/liquidity/orca.ts +344 -0
- package/src/liquidity/raydiumClmm.ts +348 -0
- package/src/liquidity/types.ts +158 -0
- package/src/lp.ts +9 -8
- package/src/lpMath.ts +215 -0
- package/src/subpaths/liquidity.ts +45 -0
- package/src/subpaths/lp.ts +9 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { decodeAddress, findProgramAddress, getAssociatedTokenAddress } from '../address.js'
|
|
2
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
3
|
+
import type { AccountInfo } from '../layout.js'
|
|
4
|
+
import { ORCA_WHIRLPOOL_PROGRAM_ID, SOLANA_LP_PROTOCOLS, orcaTickArray, utf8, type SolanaLpToken } from '../lp.js'
|
|
5
|
+
import { getAmountsForLiquidity, getMaxAmounts, getMinAmounts, getMintAmounts, sqrtPriceAtTick } from '../lpMath.js'
|
|
6
|
+
import type { Instruction } from '../write/transaction.js'
|
|
7
|
+
import { liquidityFor, priceInfo, queryPosition, resolveRange } from './clmm.js'
|
|
8
|
+
import {
|
|
9
|
+
Bytes,
|
|
10
|
+
DEFAULT_ADDRESS,
|
|
11
|
+
MEMO_PROGRAM_ID,
|
|
12
|
+
Writer,
|
|
13
|
+
anchorIx,
|
|
14
|
+
makePlan,
|
|
15
|
+
mintMeta,
|
|
16
|
+
mustAccounts,
|
|
17
|
+
nativeSide,
|
|
18
|
+
newMintKeypair,
|
|
19
|
+
parseAmount,
|
|
20
|
+
prepareTokenAccounts,
|
|
21
|
+
r,
|
|
22
|
+
refMint,
|
|
23
|
+
removeAmount,
|
|
24
|
+
tokenInfo,
|
|
25
|
+
w,
|
|
26
|
+
type Ctx,
|
|
27
|
+
type MintMeta,
|
|
28
|
+
} from './common.js'
|
|
29
|
+
import type { SolanaIncreaseLiquidityParams, SolanaLiquidityPlan, SolanaNewPositionParams, SolanaRemoveLiquidityParams } from './types.js'
|
|
30
|
+
|
|
31
|
+
const PROGRAM = ORCA_WHIRLPOOL_PROGRAM_ID
|
|
32
|
+
/** 带 Token-2022 元数据扩展的仓位 NFT 的元数据更新权限(Orca 固定地址) */
|
|
33
|
+
const METADATA_UPDATE_AUTH = '3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr'
|
|
34
|
+
|
|
35
|
+
interface Whirlpool {
|
|
36
|
+
address: string
|
|
37
|
+
tickSpacing: number
|
|
38
|
+
sqrtPrice: bigint
|
|
39
|
+
tick: number
|
|
40
|
+
mint0: string
|
|
41
|
+
vault0: string
|
|
42
|
+
mint1: string
|
|
43
|
+
vault1: string
|
|
44
|
+
rewards: Array<{ index: number; mint: string; vault: string }>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whirlpool:tick_spacing 41、sqrt_price 65、tick 81、mint_a 101、vault_a 133、mint_b 181、vault_b 213、reward_infos 269(3 个,每个 128 字节:mint +0、vault +32) */
|
|
48
|
+
function parsePool(address: string, data: Uint8Array): Whirlpool {
|
|
49
|
+
if (data.length < 653) {
|
|
50
|
+
throw new Error(`Not an Orca Whirlpool: ${address}`)
|
|
51
|
+
}
|
|
52
|
+
const b = new Bytes(data)
|
|
53
|
+
const rewards: Whirlpool['rewards'] = []
|
|
54
|
+
for (let i = 0; i < 3; i++) {
|
|
55
|
+
const mint = b.key(269 + i * 128)
|
|
56
|
+
if (mint !== DEFAULT_ADDRESS) {
|
|
57
|
+
rewards.push({ index: i, mint, vault: b.key(269 + i * 128 + 32) })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { address, tickSpacing: b.u16(41), sqrtPrice: b.u128(65), tick: b.i32(81), mint0: b.key(101), vault0: b.key(133), mint1: b.key(181), vault1: b.key(213), rewards }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 读池子和两种代币的 mint;已知 mint 时(已有仓位,查询结果里有)与池子一次读完 */
|
|
64
|
+
async function readPool(ctx: Ctx, address: string, knownMints?: readonly [string, string], extra: readonly string[] = []): Promise<{ extra: AccountInfo[]; pool: Whirlpool; mints: [MintMeta, MintMeta] }> {
|
|
65
|
+
// 已知 mint 时与池子、额外的账户(如仓位 NFT 的 mint)一次读完
|
|
66
|
+
const first = await mustAccounts(ctx, [address, ...(knownMints ?? []), ...extra], (i) => (i > (knownMints?.length ?? 0) ? 'Account' : i ? 'Token mint' : 'Orca Whirlpool'))
|
|
67
|
+
if (first[0]?.owner !== PROGRAM) {
|
|
68
|
+
throw new Error(`Not an Orca Whirlpool: ${address}`)
|
|
69
|
+
}
|
|
70
|
+
const pool = parsePool(address, (first[0] as AccountInfo).data)
|
|
71
|
+
const [m0, m1] = knownMints && knownMints[0] === pool.mint0 && knownMints[1] === pool.mint1 ? first.slice(1, 3) : await mustAccounts(ctx, [pool.mint0, pool.mint1], () => 'Token mint')
|
|
72
|
+
return { pool, mints: [mintMeta(pool.mint0, m0 as AccountInfo), mintMeta(pool.mint1, m1 as AccountInfo)], extra: first.slice(1 + (knownMints?.length ?? 0)) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const positionAddress = (mint: string) => findProgramAddress([utf8('position'), decodeAddress(mint)], PROGRAM)
|
|
76
|
+
|
|
77
|
+
function tickArrays(pool: Whirlpool, tickLower: number, tickUpper: number) {
|
|
78
|
+
const lower = orcaTickArray(pool.address, tickLower, pool.tickSpacing)
|
|
79
|
+
const upper = orcaTickArray(pool.address, tickUpper, pool.tickSpacing)
|
|
80
|
+
return { lowerStart: lower.start, upperStart: upper.start, lower: lower.address, upper: upper.address }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** increase_liquidity_v2 / decrease_liquidity_v2 共用的账户 */
|
|
84
|
+
function liquidityKeys(ctx: Ctx, pool: Whirlpool, mints: readonly MintMeta[], position: string, nftAccount: string, owner: readonly string[], lower: string, upper: string) {
|
|
85
|
+
return [
|
|
86
|
+
w(pool.address),
|
|
87
|
+
r((mints[0] as MintMeta).program),
|
|
88
|
+
r((mints[1] as MintMeta).program),
|
|
89
|
+
r(MEMO_PROGRAM_ID),
|
|
90
|
+
r(ctx.owner, true),
|
|
91
|
+
w(position),
|
|
92
|
+
r(nftAccount),
|
|
93
|
+
r(pool.mint0),
|
|
94
|
+
r(pool.mint1),
|
|
95
|
+
w(owner[0] as string),
|
|
96
|
+
w(owner[1] as string),
|
|
97
|
+
w(pool.vault0),
|
|
98
|
+
w(pool.vault1),
|
|
99
|
+
w(lower),
|
|
100
|
+
w(upper),
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function orcaAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams | SolanaNewPositionParams): Promise<SolanaLiquidityPlan> {
|
|
105
|
+
let pool: Whirlpool
|
|
106
|
+
let mints: [MintMeta, MintMeta]
|
|
107
|
+
let tickLower: number
|
|
108
|
+
let tickUpper: number
|
|
109
|
+
let existing: { mint: string; program: string } | null = null
|
|
110
|
+
/** 已有仓位:代币信息用查询结果里的,不再读一次 */
|
|
111
|
+
let known: [SolanaLpToken, SolanaLpToken] | null = null
|
|
112
|
+
if ('position' in params) {
|
|
113
|
+
const mint = refMint(params.position)
|
|
114
|
+
const position = await queryPosition(ctx, 'orca-whirlpool', mint)
|
|
115
|
+
known = [position.token0, position.token1]
|
|
116
|
+
let nft: AccountInfo[]
|
|
117
|
+
;({ pool, mints, extra: nft } = await readPool(ctx, position.pool, [position.token0.mint, position.token1.mint], [mint]))
|
|
118
|
+
;[tickLower, tickUpper] = [position.tickLower, position.tickUpper]
|
|
119
|
+
existing = { mint, program: (nft[0] as AccountInfo).owner }
|
|
120
|
+
} else {
|
|
121
|
+
if (ctx.recipient !== ctx.owner) {
|
|
122
|
+
// 新建后要由仓位 NFT 的持有者签名才能加流动性:同一笔交易里只能是 owner 自己
|
|
123
|
+
throw new Error('Orca Whirlpool: recipient must be the owner when opening a position (transfer the NFT afterwards)')
|
|
124
|
+
}
|
|
125
|
+
;({ pool, mints } = await readPool(ctx, params.pool))
|
|
126
|
+
tickLower = 0
|
|
127
|
+
tickUpper = 0
|
|
128
|
+
}
|
|
129
|
+
const [token0, token1] = known ?? ((await tokenInfo(ctx, mints)) as [SolanaLpToken, SolanaLpToken])
|
|
130
|
+
if (!existing) {
|
|
131
|
+
;[tickLower, tickUpper] = resolveRange('orca', params as SolanaNewPositionParams, token0, token1, pool.tickSpacing)
|
|
132
|
+
}
|
|
133
|
+
const amount0 = parseAmount(params.amount0, params.amount0Formatted, mints[0].decimals, 'amount0')
|
|
134
|
+
const amount1 = parseAmount(params.amount1, params.amount1Formatted, mints[1].decimals, 'amount1')
|
|
135
|
+
const liquidity = liquidityFor('orca', pool.tick, pool.sqrtPrice, tickLower, tickUpper, amount0, amount1, token0, token1)
|
|
136
|
+
const [expected0, expected1] = getMintAmounts(pool.tick, pool.sqrtPrice, sqrtPriceAtTick('orca', tickLower), sqrtPriceAtTick('orca', tickUpper), liquidity, tickLower, tickUpper)
|
|
137
|
+
const [max0, max1] = getMaxAmounts('orca', pool.sqrtPrice, tickLower, tickUpper, liquidity, ctx.slippage)
|
|
138
|
+
|
|
139
|
+
const tokens = await prepareTokenAccounts(ctx, mints, [max0, max1])
|
|
140
|
+
const ticks = tickArrays(pool, tickLower, tickUpper)
|
|
141
|
+
const setup = [...tokens.setup]
|
|
142
|
+
const main: Instruction[] = []
|
|
143
|
+
const signers: SolanaLiquidityPlan['signers'] = []
|
|
144
|
+
let positionMint: string
|
|
145
|
+
let nftAccount: string
|
|
146
|
+
if (existing) {
|
|
147
|
+
positionMint = existing.mint
|
|
148
|
+
nftAccount = getAssociatedTokenAddress(ctx.owner, existing.mint, existing.program)
|
|
149
|
+
} else {
|
|
150
|
+
// 区间两端的 tick 数组不存在时先初始化(idempotent:别人同时初始化了也不失败)
|
|
151
|
+
const arrays = await ctx.client.accounts([ticks.lower, ticks.upper], { signal: ctx.signal })
|
|
152
|
+
// 地址上可能只是别人转过 SOL 的系统账户:按是否归 Whirlpool 程序判断
|
|
153
|
+
const starts = [...new Set([ticks.lowerStart, ticks.upperStart])]
|
|
154
|
+
for (const start of starts) {
|
|
155
|
+
const address = start === ticks.lowerStart ? ticks.lower : ticks.upper
|
|
156
|
+
if (arrays[start === ticks.lowerStart ? 0 : 1]?.owner !== PROGRAM) {
|
|
157
|
+
setup.push({
|
|
158
|
+
programId: PROGRAM,
|
|
159
|
+
keys: [r(pool.address), w(ctx.owner, true), w(address), r(SYSTEM_PROGRAM_ID)],
|
|
160
|
+
data: new Writer().bytes(anchorIx('initialize_dynamic_tick_array')).i32(start).bool(true).build(),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const nft = await newMintKeypair()
|
|
165
|
+
signers.push(nft)
|
|
166
|
+
positionMint = nft.address
|
|
167
|
+
nftAccount = getAssociatedTokenAddress(ctx.owner, nft.address, TOKEN_2022_PROGRAM_ID)
|
|
168
|
+
main.push({
|
|
169
|
+
programId: PROGRAM,
|
|
170
|
+
keys: [
|
|
171
|
+
w(ctx.owner, true),
|
|
172
|
+
r(ctx.owner),
|
|
173
|
+
w(positionAddress(nft.address)),
|
|
174
|
+
w(nft.address, true),
|
|
175
|
+
w(nftAccount),
|
|
176
|
+
r(pool.address),
|
|
177
|
+
r(TOKEN_2022_PROGRAM_ID),
|
|
178
|
+
r(SYSTEM_PROGRAM_ID),
|
|
179
|
+
r(ASSOCIATED_TOKEN_PROGRAM_ID),
|
|
180
|
+
r(METADATA_UPDATE_AUTH),
|
|
181
|
+
],
|
|
182
|
+
// 仓位 NFT 用 Token-2022 并带元数据扩展(钱包里能显示,与 Orca 网页一致)
|
|
183
|
+
data: new Writer().bytes(anchorIx('open_position_with_token_extensions')).i32(tickLower).i32(tickUpper).bool(true).build(),
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
main.push({
|
|
187
|
+
programId: PROGRAM,
|
|
188
|
+
keys: liquidityKeys(ctx, pool, mints, positionAddress(positionMint), nftAccount, tokens.accounts, ticks.lower, ticks.upper),
|
|
189
|
+
// remaining_accounts_info: None
|
|
190
|
+
data: new Writer().bytes(anchorIx('increase_liquidity_v2')).u128(liquidity).u64(max0).u64(max1).u8(0).build(),
|
|
191
|
+
})
|
|
192
|
+
return makePlan(ctx, {
|
|
193
|
+
action: 'add',
|
|
194
|
+
protocol: 'orca-whirlpool',
|
|
195
|
+
protocolName: SOLANA_LP_PROTOCOLS['orca-whirlpool'].name,
|
|
196
|
+
type: 'clmm',
|
|
197
|
+
pool: pool.address,
|
|
198
|
+
token0,
|
|
199
|
+
token1,
|
|
200
|
+
positionMint,
|
|
201
|
+
position: positionAddress(positionMint),
|
|
202
|
+
lpMint: null,
|
|
203
|
+
tickLower,
|
|
204
|
+
tickUpper,
|
|
205
|
+
...priceInfo(pool.tick, pool.sqrtPrice, token0, token1, tickLower, tickUpper),
|
|
206
|
+
liquidity: liquidity.toString(),
|
|
207
|
+
amount0: expected0.toString(),
|
|
208
|
+
amount1: expected1.toString(),
|
|
209
|
+
amount0Max: max0.toString(),
|
|
210
|
+
amount1Max: max1.toString(),
|
|
211
|
+
amount0Min: null,
|
|
212
|
+
amount1Min: null,
|
|
213
|
+
// 追加时 Orca 只更新手续费记账,不转出
|
|
214
|
+
fees0: '0',
|
|
215
|
+
fees1: '0',
|
|
216
|
+
rewardMints: [],
|
|
217
|
+
native: nativeSide(tokens.nativeIndex),
|
|
218
|
+
steps: [
|
|
219
|
+
{ kind: 'setup', instructions: setup },
|
|
220
|
+
{ kind: 'add', instructions: [...tokens.wrap, ...main, ...tokens.cleanup] },
|
|
221
|
+
],
|
|
222
|
+
signers,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 移除(decrease + 领取手续费和奖励)/ 只领取(collectOnly) */
|
|
227
|
+
export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
|
|
228
|
+
const mint = refMint(params.position)
|
|
229
|
+
const position = await queryPosition(ctx, 'orca-whirlpool', mint)
|
|
230
|
+
const { pool, mints, extra } = await readPool(ctx, position.pool, [position.token0.mint, position.token1.mint], [mint])
|
|
231
|
+
const total = BigInt(position.liquidity)
|
|
232
|
+
let liquidity = 0n
|
|
233
|
+
if (!collectOnly) {
|
|
234
|
+
if (total === 0n) {
|
|
235
|
+
throw new Error(`Position ${mint} has no liquidity; use collectFees to collect what is owed`)
|
|
236
|
+
}
|
|
237
|
+
liquidity = removeAmount(total, params.percent, params.liquidity)
|
|
238
|
+
}
|
|
239
|
+
const burn = !collectOnly && !!params.burn
|
|
240
|
+
if (burn && liquidity !== total) {
|
|
241
|
+
throw new Error('burn: true needs the whole position to be removed')
|
|
242
|
+
}
|
|
243
|
+
const [token0, token1] = [position.token0, position.token1]
|
|
244
|
+
const { tickLower, tickUpper } = position
|
|
245
|
+
const [amount0, amount1] = getAmountsForLiquidity(pool.tick, pool.sqrtPrice, sqrtPriceAtTick('orca', tickLower), sqrtPriceAtTick('orca', tickUpper), liquidity, tickLower, tickUpper)
|
|
246
|
+
const [min0, min1] = liquidity === 0n ? [0n, 0n] : getMinAmounts('orca', pool.sqrtPrice, tickLower, tickUpper, liquidity, ctx.slippage)
|
|
247
|
+
|
|
248
|
+
const nftMintAccount = extra[0] as AccountInfo
|
|
249
|
+
const rewardMintAccounts = pool.rewards.length ? await mustAccounts(ctx, pool.rewards.map((x) => x.mint), () => 'Reward mint') : []
|
|
250
|
+
const nftProgram = (nftMintAccount as AccountInfo).owner
|
|
251
|
+
const rewardMints = pool.rewards.map((x, i) => mintMeta(x.mint, rewardMintAccounts[i] as AccountInfo, false))
|
|
252
|
+
// 两种代币和奖励的代币都收到 owner 的关联代币账户:没有时创建;wSOL(池子的一边或奖励)原来没有账户时用完关闭、换回 SOL
|
|
253
|
+
const rewardOnly = rewardMints.filter((m) => !mints.some((x) => x.mint === m.mint))
|
|
254
|
+
const tokens = await prepareTokenAccounts(ctx, [...mints, ...rewardOnly], [null, null, ...rewardOnly.map(() => null)])
|
|
255
|
+
const ticks = tickArrays(pool, tickLower, tickUpper)
|
|
256
|
+
const positionAccount = positionAddress(mint)
|
|
257
|
+
const nftAccount = getAssociatedTokenAddress(ctx.owner, mint, nftProgram)
|
|
258
|
+
const main: Instruction[] = []
|
|
259
|
+
if (liquidity > 0n) {
|
|
260
|
+
main.push({
|
|
261
|
+
programId: PROGRAM,
|
|
262
|
+
keys: liquidityKeys(ctx, pool, mints, positionAccount, nftAccount, tokens.accounts, ticks.lower, ticks.upper),
|
|
263
|
+
data: new Writer().bytes(anchorIx('decrease_liquidity_v2')).u128(liquidity).u64(min0).u64(min1).u8(0).build(),
|
|
264
|
+
})
|
|
265
|
+
} else if (total > 0n) {
|
|
266
|
+
// 只领取:先按最新的价格更新手续费和奖励的记账(流动性为 0 的仓位不需要)
|
|
267
|
+
main.push({ programId: PROGRAM, keys: [w(pool.address), w(positionAccount), r(ticks.lower), r(ticks.upper)], data: anchorIx('update_fees_and_rewards') })
|
|
268
|
+
}
|
|
269
|
+
main.push({
|
|
270
|
+
programId: PROGRAM,
|
|
271
|
+
keys: [
|
|
272
|
+
r(pool.address),
|
|
273
|
+
r(ctx.owner, true),
|
|
274
|
+
w(positionAccount),
|
|
275
|
+
r(nftAccount),
|
|
276
|
+
r(pool.mint0),
|
|
277
|
+
r(pool.mint1),
|
|
278
|
+
w(tokens.accounts[0] as string),
|
|
279
|
+
w(pool.vault0),
|
|
280
|
+
w(tokens.accounts[1] as string),
|
|
281
|
+
w(pool.vault1),
|
|
282
|
+
r((mints[0] as MintMeta).program),
|
|
283
|
+
r((mints[1] as MintMeta).program),
|
|
284
|
+
r(MEMO_PROGRAM_ID),
|
|
285
|
+
],
|
|
286
|
+
data: new Writer().bytes(anchorIx('collect_fees_v2')).u8(0).build(),
|
|
287
|
+
})
|
|
288
|
+
pool.rewards.forEach((reward, i) => {
|
|
289
|
+
const m = rewardMints[i] as MintMeta
|
|
290
|
+
main.push({
|
|
291
|
+
programId: PROGRAM,
|
|
292
|
+
keys: [
|
|
293
|
+
r(pool.address),
|
|
294
|
+
r(ctx.owner, true),
|
|
295
|
+
w(positionAccount),
|
|
296
|
+
r(nftAccount),
|
|
297
|
+
w(getAssociatedTokenAddress(ctx.owner, m.mint, m.program)),
|
|
298
|
+
r(m.mint),
|
|
299
|
+
w(reward.vault),
|
|
300
|
+
r(m.program),
|
|
301
|
+
r(MEMO_PROGRAM_ID),
|
|
302
|
+
],
|
|
303
|
+
data: new Writer().bytes(anchorIx('collect_reward_v2')).u8(reward.index).u8(0).build(),
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
if (burn) {
|
|
307
|
+
main.push(
|
|
308
|
+
nftProgram === TOKEN_2022_PROGRAM_ID
|
|
309
|
+
? { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_2022_PROGRAM_ID)], data: anchorIx('close_position_with_token_extensions') }
|
|
310
|
+
: { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_PROGRAM_ID)], data: anchorIx('close_position') },
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
return makePlan(ctx, {
|
|
314
|
+
action: collectOnly ? 'collect' : 'remove',
|
|
315
|
+
protocol: 'orca-whirlpool',
|
|
316
|
+
protocolName: SOLANA_LP_PROTOCOLS['orca-whirlpool'].name,
|
|
317
|
+
type: 'clmm',
|
|
318
|
+
pool: pool.address,
|
|
319
|
+
token0,
|
|
320
|
+
token1,
|
|
321
|
+
positionMint: mint,
|
|
322
|
+
position: positionAccount,
|
|
323
|
+
lpMint: null,
|
|
324
|
+
tickLower,
|
|
325
|
+
tickUpper,
|
|
326
|
+
...priceInfo(pool.tick, pool.sqrtPrice, token0, token1, tickLower, tickUpper),
|
|
327
|
+
liquidity: liquidity.toString(),
|
|
328
|
+
amount0: amount0.toString(),
|
|
329
|
+
amount1: amount1.toString(),
|
|
330
|
+
amount0Max: null,
|
|
331
|
+
amount1Max: null,
|
|
332
|
+
amount0Min: min0.toString(),
|
|
333
|
+
amount1Min: min1.toString(),
|
|
334
|
+
fees0: position.fees0 ?? '0',
|
|
335
|
+
fees1: position.fees1 ?? '0',
|
|
336
|
+
rewardMints: rewardMints.map((m) => m.mint),
|
|
337
|
+
native: nativeSide(tokens.nativeIndex),
|
|
338
|
+
steps: [
|
|
339
|
+
{ kind: 'setup', instructions: tokens.setup },
|
|
340
|
+
{ kind: collectOnly ? 'collect' : 'remove', instructions: [...tokens.wrap, ...main, ...tokens.cleanup] },
|
|
341
|
+
],
|
|
342
|
+
signers: [],
|
|
343
|
+
})
|
|
344
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { decodeAddress, findProgramAddress, getAssociatedTokenAddress } from '../address.js'
|
|
2
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
3
|
+
import type { AccountInfo } from '../layout.js'
|
|
4
|
+
import { RAYDIUM_CLMM_PROGRAM_ID, SOLANA_LP_PROTOCOLS, raydiumTickArray, utf8, type SolanaLpToken } from '../lp.js'
|
|
5
|
+
import { getAmountsForLiquidity, getMaxAmounts, getMinAmounts, getMintAmounts, sqrtPriceAtTick } from '../lpMath.js'
|
|
6
|
+
import type { Instruction } from '../write/transaction.js'
|
|
7
|
+
import { liquidityFor, priceInfo, queryPosition, resolveRange } from './clmm.js'
|
|
8
|
+
import {
|
|
9
|
+
Bytes,
|
|
10
|
+
DEFAULT_ADDRESS,
|
|
11
|
+
MEMO_PROGRAM_ID,
|
|
12
|
+
RENT_SYSVAR_ID,
|
|
13
|
+
Writer,
|
|
14
|
+
anchorIx,
|
|
15
|
+
makePlan,
|
|
16
|
+
mintMeta,
|
|
17
|
+
mustAccounts,
|
|
18
|
+
nativeSide,
|
|
19
|
+
newMintKeypair,
|
|
20
|
+
parseAmount,
|
|
21
|
+
prepareTokenAccounts,
|
|
22
|
+
r,
|
|
23
|
+
refMint,
|
|
24
|
+
removeAmount,
|
|
25
|
+
tokenInfo,
|
|
26
|
+
w,
|
|
27
|
+
type Ctx,
|
|
28
|
+
type MintMeta,
|
|
29
|
+
} from './common.js'
|
|
30
|
+
import type { SolanaIncreaseLiquidityParams, SolanaLiquidityPlan, SolanaNewPositionParams, SolanaRemoveLiquidityParams } from './types.js'
|
|
31
|
+
|
|
32
|
+
const PROGRAM = RAYDIUM_CLMM_PROGRAM_ID
|
|
33
|
+
const TICK_ARRAY_SIZE = 60
|
|
34
|
+
/** 池子账户里的 tick 数组位图覆盖的范围:± tickSpacing × 60 × 512,超出时要带上位图扩展账户 */
|
|
35
|
+
const BITMAP_SIZE = 512
|
|
36
|
+
|
|
37
|
+
interface RaydiumPool {
|
|
38
|
+
address: string
|
|
39
|
+
mint0: string
|
|
40
|
+
mint1: string
|
|
41
|
+
vault0: string
|
|
42
|
+
vault1: string
|
|
43
|
+
tickSpacing: number
|
|
44
|
+
sqrtPrice: bigint
|
|
45
|
+
tick: number
|
|
46
|
+
/** 已初始化的奖励:mint 和池子的奖励金库 */
|
|
47
|
+
rewards: Array<{ mint: string; vault: string }>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** PoolState:mint 73 / 105、vault 137 / 169、tick_spacing 235、sqrt_price 253、tick 269、reward_infos 397(3 个,每个 169 字节:mint +57、vault +89) */
|
|
51
|
+
function parsePool(address: string, data: Uint8Array): RaydiumPool {
|
|
52
|
+
if (data.length < 904) {
|
|
53
|
+
throw new Error(`Not a Raydium CLMM pool: ${address}`)
|
|
54
|
+
}
|
|
55
|
+
const b = new Bytes(data)
|
|
56
|
+
const rewards: RaydiumPool['rewards'] = []
|
|
57
|
+
for (let i = 0; i < 3; i++) {
|
|
58
|
+
const offset = 397 + i * 169
|
|
59
|
+
const mint = b.key(offset + 57)
|
|
60
|
+
// 与程序一致:奖励的 mint 不为默认地址就算已初始化
|
|
61
|
+
if (mint !== DEFAULT_ADDRESS) {
|
|
62
|
+
rewards.push({ mint, vault: b.key(offset + 89) })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { address, mint0: b.key(73), mint1: b.key(105), vault0: b.key(137), vault1: b.key(169), tickSpacing: b.u16(235), sqrtPrice: b.u128(253), tick: b.i32(269), rewards }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 读池子和两种代币的 mint;已知 mint 时(已有仓位,查询结果里有)与池子一次读完 */
|
|
69
|
+
async function readPool(ctx: Ctx, address: string, knownMints?: readonly [string, string], extra: readonly string[] = []): Promise<{ extra: AccountInfo[]; pool: RaydiumPool; mints: [MintMeta, MintMeta] }> {
|
|
70
|
+
// 已知 mint 时与池子、额外的账户(如仓位 NFT 的 mint)一次读完
|
|
71
|
+
const first = await mustAccounts(ctx, [address, ...(knownMints ?? []), ...extra], (i) => (i > (knownMints?.length ?? 0) ? 'Account' : i ? 'Token mint' : 'Raydium CLMM pool'))
|
|
72
|
+
if (first[0]?.owner !== PROGRAM) {
|
|
73
|
+
throw new Error(`Not a Raydium CLMM pool: ${address}`)
|
|
74
|
+
}
|
|
75
|
+
const pool = parsePool(address, (first[0] as AccountInfo).data)
|
|
76
|
+
const [m0, m1] = knownMints && knownMints[0] === pool.mint0 && knownMints[1] === pool.mint1 ? first.slice(1, 3) : await mustAccounts(ctx, [pool.mint0, pool.mint1], () => 'Token mint')
|
|
77
|
+
return { pool, mints: [mintMeta(pool.mint0, m0 as AccountInfo), mintMeta(pool.mint1, m1 as AccountInfo)], extra: first.slice(1 + (knownMints?.length ?? 0)) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const be32 = (n: number) => {
|
|
81
|
+
const b = new Uint8Array(4)
|
|
82
|
+
new DataView(b.buffer).setInt32(0, n, false)
|
|
83
|
+
return b
|
|
84
|
+
}
|
|
85
|
+
const personalPositionAddress = (nftMint: string) => findProgramAddress([utf8('position'), decodeAddress(nftMint)], PROGRAM)
|
|
86
|
+
/**
|
|
87
|
+
* ProtocolPositionState:程序的种子是 tick 的大端字节(to_be_bytes)。当前版本的程序已不再校验这个账户(官方 SDK 传的是小端推导出的地址,
|
|
88
|
+
* 主网照样成功),按程序的种子推导,以后恢复校验也对
|
|
89
|
+
*/
|
|
90
|
+
const protocolPositionAddress = (pool: string, lower: number, upper: number) => findProgramAddress([utf8('position'), decodeAddress(pool), be32(lower), be32(upper)], PROGRAM)
|
|
91
|
+
|
|
92
|
+
/** 区间两端的 tick 数组;超出池子位图覆盖的范围时还要位图扩展账户 */
|
|
93
|
+
function tickAccounts(pool: RaydiumPool, tickLower: number, tickUpper: number) {
|
|
94
|
+
const lower = raydiumTickArray(pool.address, tickLower, pool.tickSpacing)
|
|
95
|
+
const upper = raydiumTickArray(pool.address, tickUpper, pool.tickSpacing)
|
|
96
|
+
const [lowerStart, upperStart] = [lower.start, upper.start]
|
|
97
|
+
const max = pool.tickSpacing * TICK_ARRAY_SIZE * BITMAP_SIZE
|
|
98
|
+
const overflow = [lowerStart, upperStart].some((s) => s >= max || s < -max)
|
|
99
|
+
return {
|
|
100
|
+
lowerStart,
|
|
101
|
+
upperStart,
|
|
102
|
+
lower: lower.address,
|
|
103
|
+
upper: upper.address,
|
|
104
|
+
bitmap: overflow ? [w(findProgramAddress([utf8('pool_tick_array_bitmap_extension'), decodeAddress(pool.address)], PROGRAM))] : [],
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function raydiumAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams | SolanaNewPositionParams): Promise<SolanaLiquidityPlan> {
|
|
109
|
+
let pool: RaydiumPool
|
|
110
|
+
let mints: [MintMeta, MintMeta]
|
|
111
|
+
let tickLower: number
|
|
112
|
+
let tickUpper: number
|
|
113
|
+
/** 已有仓位:NFT mint 和它的代币程序 */
|
|
114
|
+
let existing: { mint: string; program: string } | null = null
|
|
115
|
+
/** 已有仓位:代币信息用查询结果里的,不再读一次 */
|
|
116
|
+
let known: [SolanaLpToken, SolanaLpToken] | null = null
|
|
117
|
+
if ('position' in params) {
|
|
118
|
+
const mint = refMint(params.position)
|
|
119
|
+
const position = await queryPosition(ctx, 'raydium-clmm', mint)
|
|
120
|
+
known = [position.token0, position.token1]
|
|
121
|
+
let nft: AccountInfo[]
|
|
122
|
+
;({ pool, mints, extra: nft } = await readPool(ctx, position.pool, [position.token0.mint, position.token1.mint], [mint]))
|
|
123
|
+
;[tickLower, tickUpper] = [position.tickLower, position.tickUpper]
|
|
124
|
+
existing = { mint, program: (nft[0] as AccountInfo).owner }
|
|
125
|
+
} else {
|
|
126
|
+
;({ pool, mints } = await readPool(ctx, params.pool))
|
|
127
|
+
tickLower = 0
|
|
128
|
+
tickUpper = 0
|
|
129
|
+
}
|
|
130
|
+
const [token0, token1] = known ?? ((await tokenInfo(ctx, mints)) as [SolanaLpToken, SolanaLpToken])
|
|
131
|
+
if (!existing) {
|
|
132
|
+
;[tickLower, tickUpper] = resolveRange('raydium', params as SolanaNewPositionParams, token0, token1, pool.tickSpacing)
|
|
133
|
+
}
|
|
134
|
+
const amount0 = parseAmount(params.amount0, params.amount0Formatted, mints[0].decimals, 'amount0')
|
|
135
|
+
const amount1 = parseAmount(params.amount1, params.amount1Formatted, mints[1].decimals, 'amount1')
|
|
136
|
+
const liquidity = liquidityFor('raydium', pool.tick, pool.sqrtPrice, tickLower, tickUpper, amount0, amount1, token0, token1)
|
|
137
|
+
const sqrtA = sqrtPriceAtTick('raydium', tickLower)
|
|
138
|
+
const sqrtB = sqrtPriceAtTick('raydium', tickUpper)
|
|
139
|
+
const [expected0, expected1] = getMintAmounts(pool.tick, pool.sqrtPrice, sqrtA, sqrtB, liquidity, tickLower, tickUpper)
|
|
140
|
+
const [max0, max1] = getMaxAmounts('raydium', pool.sqrtPrice, tickLower, tickUpper, liquidity, ctx.slippage)
|
|
141
|
+
|
|
142
|
+
const tokens = await prepareTokenAccounts(ctx, mints, [max0, max1])
|
|
143
|
+
const ticks = tickAccounts(pool, tickLower, tickUpper)
|
|
144
|
+
const protocolPosition = protocolPositionAddress(pool.address, tickLower, tickUpper)
|
|
145
|
+
const signers: SolanaLiquidityPlan['signers'] = []
|
|
146
|
+
let positionMint: string
|
|
147
|
+
let ix: Instruction
|
|
148
|
+
if (existing) {
|
|
149
|
+
positionMint = existing.mint
|
|
150
|
+
ix = {
|
|
151
|
+
programId: PROGRAM,
|
|
152
|
+
keys: [
|
|
153
|
+
r(ctx.owner, true),
|
|
154
|
+
r(getAssociatedTokenAddress(ctx.owner, existing.mint, existing.program)),
|
|
155
|
+
w(pool.address),
|
|
156
|
+
r(protocolPosition),
|
|
157
|
+
w(personalPositionAddress(existing.mint)),
|
|
158
|
+
w(ticks.lower),
|
|
159
|
+
w(ticks.upper),
|
|
160
|
+
w(tokens.accounts[0] as string),
|
|
161
|
+
w(tokens.accounts[1] as string),
|
|
162
|
+
w(pool.vault0),
|
|
163
|
+
w(pool.vault1),
|
|
164
|
+
r(TOKEN_PROGRAM_ID),
|
|
165
|
+
r(TOKEN_2022_PROGRAM_ID),
|
|
166
|
+
r(pool.mint0),
|
|
167
|
+
r(pool.mint1),
|
|
168
|
+
...ticks.bitmap,
|
|
169
|
+
],
|
|
170
|
+
// base_flag: None(按给定的流动性)
|
|
171
|
+
data: new Writer().bytes(anchorIx('increase_liquidity_v2')).u128(liquidity).u64(max0).u64(max1).u8(0).bool(false).build(),
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
const nft = await newMintKeypair()
|
|
175
|
+
signers.push(nft)
|
|
176
|
+
positionMint = nft.address
|
|
177
|
+
ix = {
|
|
178
|
+
programId: PROGRAM,
|
|
179
|
+
keys: [
|
|
180
|
+
w(ctx.owner, true),
|
|
181
|
+
r(ctx.recipient),
|
|
182
|
+
w(nft.address, true),
|
|
183
|
+
w(getAssociatedTokenAddress(ctx.recipient, nft.address, TOKEN_2022_PROGRAM_ID)),
|
|
184
|
+
w(pool.address),
|
|
185
|
+
r(protocolPosition),
|
|
186
|
+
w(ticks.lower),
|
|
187
|
+
w(ticks.upper),
|
|
188
|
+
w(personalPositionAddress(nft.address)),
|
|
189
|
+
w(tokens.accounts[0] as string),
|
|
190
|
+
w(tokens.accounts[1] as string),
|
|
191
|
+
w(pool.vault0),
|
|
192
|
+
w(pool.vault1),
|
|
193
|
+
r(RENT_SYSVAR_ID),
|
|
194
|
+
r(SYSTEM_PROGRAM_ID),
|
|
195
|
+
r(TOKEN_PROGRAM_ID),
|
|
196
|
+
r(ASSOCIATED_TOKEN_PROGRAM_ID),
|
|
197
|
+
r(TOKEN_2022_PROGRAM_ID),
|
|
198
|
+
r(pool.mint0),
|
|
199
|
+
r(pool.mint1),
|
|
200
|
+
...ticks.bitmap,
|
|
201
|
+
],
|
|
202
|
+
// 仓位 NFT 用 Token-2022(不建 Metaplex 元数据,租金更少);tick 数组不存在时程序自动创建
|
|
203
|
+
data: new Writer()
|
|
204
|
+
.bytes(anchorIx('open_position_with_token22_nft'))
|
|
205
|
+
.i32(tickLower)
|
|
206
|
+
.i32(tickUpper)
|
|
207
|
+
.i32(ticks.lowerStart)
|
|
208
|
+
.i32(ticks.upperStart)
|
|
209
|
+
.u128(liquidity)
|
|
210
|
+
.u64(max0)
|
|
211
|
+
.u64(max1)
|
|
212
|
+
.bool(false)
|
|
213
|
+
.u8(0)
|
|
214
|
+
.bool(false)
|
|
215
|
+
.build(),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return makePlan(ctx, {
|
|
219
|
+
action: 'add',
|
|
220
|
+
protocol: 'raydium-clmm',
|
|
221
|
+
protocolName: SOLANA_LP_PROTOCOLS['raydium-clmm'].name,
|
|
222
|
+
type: 'clmm',
|
|
223
|
+
pool: pool.address,
|
|
224
|
+
token0,
|
|
225
|
+
token1,
|
|
226
|
+
positionMint,
|
|
227
|
+
position: personalPositionAddress(positionMint),
|
|
228
|
+
lpMint: null,
|
|
229
|
+
tickLower,
|
|
230
|
+
tickUpper,
|
|
231
|
+
...priceInfo(pool.tick, pool.sqrtPrice, token0, token1, tickLower, tickUpper),
|
|
232
|
+
liquidity: liquidity.toString(),
|
|
233
|
+
amount0: expected0.toString(),
|
|
234
|
+
amount1: expected1.toString(),
|
|
235
|
+
amount0Max: max0.toString(),
|
|
236
|
+
amount1Max: max1.toString(),
|
|
237
|
+
amount0Min: null,
|
|
238
|
+
amount1Min: null,
|
|
239
|
+
// 追加流动性时 Raydium 不领取手续费(留在仓位里)
|
|
240
|
+
fees0: '0',
|
|
241
|
+
fees1: '0',
|
|
242
|
+
rewardMints: [],
|
|
243
|
+
native: nativeSide(tokens.nativeIndex),
|
|
244
|
+
steps: [
|
|
245
|
+
{ kind: 'setup', instructions: tokens.setup },
|
|
246
|
+
{ kind: 'add', instructions: [...tokens.wrap, ix, ...tokens.cleanup] },
|
|
247
|
+
],
|
|
248
|
+
signers,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 移除(liquidity > 0)/ 只领取手续费和奖励(collectOnly:流动性为 0 的 decrease) */
|
|
253
|
+
export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
|
|
254
|
+
const mint = refMint(params.position)
|
|
255
|
+
const position = await queryPosition(ctx, 'raydium-clmm', mint)
|
|
256
|
+
const { pool, mints, extra } = await readPool(ctx, position.pool, [position.token0.mint, position.token1.mint], [mint])
|
|
257
|
+
const total = BigInt(position.liquidity)
|
|
258
|
+
let liquidity = 0n
|
|
259
|
+
if (!collectOnly) {
|
|
260
|
+
if (total === 0n) {
|
|
261
|
+
throw new Error(`Position ${mint} has no liquidity; use collectFees to collect what is owed`)
|
|
262
|
+
}
|
|
263
|
+
liquidity = removeAmount(total, params.percent, params.liquidity)
|
|
264
|
+
}
|
|
265
|
+
const burn = !collectOnly && !!params.burn
|
|
266
|
+
if (burn && liquidity !== total) {
|
|
267
|
+
throw new Error('burn: true needs the whole position to be removed')
|
|
268
|
+
}
|
|
269
|
+
const [token0, token1] = [position.token0, position.token1]
|
|
270
|
+
const { tickLower, tickUpper } = position
|
|
271
|
+
const [amount0, amount1] = getAmountsForLiquidity(pool.tick, pool.sqrtPrice, sqrtPriceAtTick('raydium', tickLower), sqrtPriceAtTick('raydium', tickUpper), liquidity, tickLower, tickUpper)
|
|
272
|
+
const [min0, min1] = liquidity === 0n ? [0n, 0n] : getMinAmounts('raydium', pool.sqrtPrice, tickLower, tickUpper, liquidity, ctx.slippage)
|
|
273
|
+
|
|
274
|
+
const nftAccount = extra[0] as AccountInfo
|
|
275
|
+
const rewardMintAccounts = pool.rewards.length ? await mustAccounts(ctx, pool.rewards.map((x) => x.mint), () => 'Reward mint') : []
|
|
276
|
+
const nftProgram = (nftAccount as AccountInfo).owner
|
|
277
|
+
const rewardMints = pool.rewards.map((x, i) => mintMeta(x.mint, rewardMintAccounts[i] as AccountInfo, false))
|
|
278
|
+
// 两种代币和奖励的代币都收到 owner 的关联代币账户:没有时创建;wSOL(池子的一边或奖励)原来没有账户时用完关闭、换回 SOL
|
|
279
|
+
const rewardOnly = rewardMints.filter((m) => !mints.some((x) => x.mint === m.mint))
|
|
280
|
+
const tokens = await prepareTokenAccounts(ctx, [...mints, ...rewardOnly], [null, null, ...rewardOnly.map(() => null)])
|
|
281
|
+
const rewardAccounts = rewardMints.map((m) => getAssociatedTokenAddress(ctx.owner, m.mint, m.program))
|
|
282
|
+
const ticks = tickAccounts(pool, tickLower, tickUpper)
|
|
283
|
+
const personal = personalPositionAddress(mint)
|
|
284
|
+
const nftAta = getAssociatedTokenAddress(ctx.owner, mint, nftProgram)
|
|
285
|
+
const decrease: Instruction = {
|
|
286
|
+
programId: PROGRAM,
|
|
287
|
+
keys: [
|
|
288
|
+
r(ctx.owner, true),
|
|
289
|
+
r(nftAta),
|
|
290
|
+
w(personal),
|
|
291
|
+
w(pool.address),
|
|
292
|
+
r(protocolPositionAddress(pool.address, tickLower, tickUpper)),
|
|
293
|
+
w(pool.vault0),
|
|
294
|
+
w(pool.vault1),
|
|
295
|
+
w(ticks.lower),
|
|
296
|
+
w(ticks.upper),
|
|
297
|
+
w(tokens.accounts[0] as string),
|
|
298
|
+
w(tokens.accounts[1] as string),
|
|
299
|
+
r(TOKEN_PROGRAM_ID),
|
|
300
|
+
r(TOKEN_2022_PROGRAM_ID),
|
|
301
|
+
r(MEMO_PROGRAM_ID),
|
|
302
|
+
r(pool.mint0),
|
|
303
|
+
r(pool.mint1),
|
|
304
|
+
...ticks.bitmap,
|
|
305
|
+
...pool.rewards.flatMap((x, i) => [w(x.vault), w(rewardAccounts[i] as string), r(x.mint)]),
|
|
306
|
+
],
|
|
307
|
+
data: new Writer().bytes(anchorIx('decrease_liquidity_v2')).u128(liquidity).u64(min0).u64(min1).build(),
|
|
308
|
+
}
|
|
309
|
+
const main: Instruction[] = [decrease]
|
|
310
|
+
if (burn) {
|
|
311
|
+
main.push({
|
|
312
|
+
programId: PROGRAM,
|
|
313
|
+
keys: [w(ctx.owner, true), w(mint), w(nftAta), w(personal), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool.address)],
|
|
314
|
+
data: anchorIx('close_position'),
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
return makePlan(ctx, {
|
|
318
|
+
action: collectOnly ? 'collect' : 'remove',
|
|
319
|
+
protocol: 'raydium-clmm',
|
|
320
|
+
protocolName: SOLANA_LP_PROTOCOLS['raydium-clmm'].name,
|
|
321
|
+
type: 'clmm',
|
|
322
|
+
pool: pool.address,
|
|
323
|
+
token0,
|
|
324
|
+
token1,
|
|
325
|
+
positionMint: mint,
|
|
326
|
+
position: personal,
|
|
327
|
+
lpMint: null,
|
|
328
|
+
tickLower,
|
|
329
|
+
tickUpper,
|
|
330
|
+
...priceInfo(pool.tick, pool.sqrtPrice, token0, token1, tickLower, tickUpper),
|
|
331
|
+
liquidity: liquidity.toString(),
|
|
332
|
+
amount0: amount0.toString(),
|
|
333
|
+
amount1: amount1.toString(),
|
|
334
|
+
amount0Max: null,
|
|
335
|
+
amount1Max: null,
|
|
336
|
+
amount0Min: min0.toString(),
|
|
337
|
+
amount1Min: min1.toString(),
|
|
338
|
+
fees0: position.fees0 ?? '0',
|
|
339
|
+
fees1: position.fees1 ?? '0',
|
|
340
|
+
rewardMints: rewardMints.map((m) => m.mint),
|
|
341
|
+
native: nativeSide(tokens.nativeIndex),
|
|
342
|
+
steps: [
|
|
343
|
+
{ kind: 'setup', instructions: tokens.setup },
|
|
344
|
+
{ kind: collectOnly ? 'collect' : 'remove', instructions: [...tokens.wrap, ...main, ...tokens.cleanup] },
|
|
345
|
+
],
|
|
346
|
+
signers: [],
|
|
347
|
+
})
|
|
348
|
+
}
|