@w3lib/solana-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 +113 -1
- package/dist/batchPlan.d.ts +52 -0
- package/dist/batchPlan.d.ts.map +1 -0
- package/dist/batchPlan.js +85 -0
- package/dist/batchPlan.js.map +1 -0
- package/dist/claim.d.ts +161 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +197 -0
- package/dist/claim.js.map +1 -0
- package/dist/decode.d.ts +22 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +187 -0
- package/dist/decode.js.map +1 -0
- package/dist/describe.d.ts +26 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +58 -0
- package/dist/describe.js.map +1 -0
- package/dist/history.d.ts +125 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +231 -0
- package/dist/history.js.map +1 -0
- package/dist/historyHelius.d.ts +52 -0
- package/dist/historyHelius.d.ts.map +1 -0
- package/dist/historyHelius.js +125 -0
- package/dist/historyHelius.js.map +1 -0
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -1
- package/dist/liquidity/orca.js +14 -3
- package/dist/liquidity/orca.js.map +1 -1
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
- package/dist/liquidity/raydiumClmm.js +12 -5
- package/dist/liquidity/raydiumClmm.js.map +1 -1
- package/dist/lp.d.ts +47 -0
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +91 -15
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +22 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +21 -0
- package/dist/lpMath.js.map +1 -1
- package/dist/programs.d.ts +19 -0
- package/dist/programs.d.ts.map +1 -0
- package/dist/programs.js +175 -0
- package/dist/programs.js.map +1 -0
- package/dist/reclaim.d.ts +190 -0
- package/dist/reclaim.d.ts.map +1 -0
- package/dist/reclaim.js +399 -0
- package/dist/reclaim.js.map +1 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +4 -2
- package/dist/rpc.js.map +1 -1
- package/dist/simulate.d.ts +106 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +418 -0
- package/dist/simulate.js.map +1 -0
- package/dist/subpaths/claim.d.ts +31 -0
- package/dist/subpaths/claim.d.ts.map +1 -0
- package/dist/subpaths/claim.js +31 -0
- package/dist/subpaths/claim.js.map +1 -0
- package/dist/subpaths/history.d.ts +21 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +22 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/lp.d.ts +2 -2
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +2 -2
- package/dist/subpaths/lp.js.map +1 -1
- package/dist/subpaths/reclaim.d.ts +34 -0
- package/dist/subpaths/reclaim.d.ts.map +1 -0
- package/dist/subpaths/reclaim.js +34 -0
- package/dist/subpaths/reclaim.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +22 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +23 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/write/instructions.d.ts +2 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +16 -0
- package/dist/write/instructions.js.map +1 -1
- package/package.json +21 -1
- package/src/batchPlan.ts +137 -0
- package/src/claim.ts +323 -0
- package/src/decode.ts +202 -0
- package/src/describe.ts +77 -0
- package/src/history.ts +336 -0
- package/src/historyHelius.ts +136 -0
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +140 -15
- package/src/lpMath.ts +38 -0
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +547 -0
- package/src/rpc.ts +4 -2
- package/src/simulate.ts +535 -0
- package/src/subpaths/claim.ts +53 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/reclaim.ts +57 -0
- package/src/subpaths/simulate.ts +37 -0
- package/src/write/instructions.ts +17 -0
package/src/lp.ts
CHANGED
|
@@ -5,7 +5,19 @@ import { decodeAddress, encodeAddress, findProgramAddress, isAddress } from './a
|
|
|
5
5
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
6
6
|
import { formatUnits, parseMint, parseTokenAccount, type AccountInfo } from './layout.js'
|
|
7
7
|
import { AllNodesFailedError, HttpError, RpcError } from './rpc.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getAmountsForLiquidity,
|
|
10
|
+
getFeeGrowthInside,
|
|
11
|
+
getFeesOwed,
|
|
12
|
+
getRewardGrowthInside,
|
|
13
|
+
orcaSqrtPriceAtTick,
|
|
14
|
+
raydiumSqrtPriceAtTick,
|
|
15
|
+
rewardGrowthGlobal,
|
|
16
|
+
sqrtPriceToPrice,
|
|
17
|
+
tickToPrice,
|
|
18
|
+
type RewardEmission,
|
|
19
|
+
type TickFeeData,
|
|
20
|
+
} from './lpMath.js'
|
|
9
21
|
import { defillamaPrices, type SolanaPriceSource } from './price.js'
|
|
10
22
|
|
|
11
23
|
export const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK'
|
|
@@ -97,6 +109,24 @@ export interface SolanaLpClmmPosition extends SolanaLpPositionBase {
|
|
|
97
109
|
fees1Formatted: string | null
|
|
98
110
|
/** 开启 prices 时:未领取手续费的美元价值(手续费为 null 或缺价格时为 null) */
|
|
99
111
|
feesUsd?: number | null
|
|
112
|
+
/** 开启 rewards 时:未领取的挖矿奖励(池子里已初始化的每个奖励一项,数量为 0 的也在) */
|
|
113
|
+
rewards?: SolanaLpReward[]
|
|
114
|
+
/** 开启 rewards 和 prices 时:未领取挖矿奖励的美元价值(有数量的奖励缺价格或读不到时为 null) */
|
|
115
|
+
rewardsUsd?: number | null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 集中流动性仓位未领取的挖矿奖励(Raydium CLMM reward_infos / Orca Whirlpool reward_infos) */
|
|
119
|
+
export interface SolanaLpReward {
|
|
120
|
+
/** 奖励在池子里的位置(0~2) */
|
|
121
|
+
index: number
|
|
122
|
+
mint: string
|
|
123
|
+
symbol: string | null
|
|
124
|
+
decimals: number | null
|
|
125
|
+
/** 未领取的数量(最小单位):已记账的 + 按池子最新状态结算的新增;读不到两端的 tick 数据时为 null */
|
|
126
|
+
amount: string | null
|
|
127
|
+
formatted: string | null
|
|
128
|
+
/** 开启 prices 时:美元价值(没有价格时为 null) */
|
|
129
|
+
usd?: number | null
|
|
100
130
|
}
|
|
101
131
|
|
|
102
132
|
export type SolanaLpPosition = SolanaLpAmmPosition | SolanaLpClmmPosition
|
|
@@ -116,6 +146,13 @@ export interface LpPositionsOptions extends RequestOptions {
|
|
|
116
146
|
pools?: readonly string[]
|
|
117
147
|
/** 返回已清空的仓位(流动性为 0、手续费也领完,NFT 还在)。默认 false */
|
|
118
148
|
includeClosed?: boolean
|
|
149
|
+
/**
|
|
150
|
+
* 集中流动性仓位同时算未领取的挖矿奖励(Raydium CLMM 农场奖励、Orca 的奖励 slot),放在 rewards 字段,照搬程序的结算:
|
|
151
|
+
* 池子的奖励增长按时间摊给当前流动性,再按仓位区间内的增长计算。默认 false。有奖励没领的仓位不算“已清空”
|
|
152
|
+
*/
|
|
153
|
+
rewards?: boolean
|
|
154
|
+
/** rewards 结算到哪个时刻(unix 秒),默认当前时间;链上用的是区块时间,差几秒影响很小 */
|
|
155
|
+
now?: number
|
|
119
156
|
/** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource(如 /price 的 combinePrices([defillamaPrices(), onchainPrices()]))。默认不查 */
|
|
120
157
|
prices?: boolean | SolanaPriceSource
|
|
121
158
|
/**
|
|
@@ -160,6 +197,7 @@ export class Bytes {
|
|
|
160
197
|
}
|
|
161
198
|
}
|
|
162
199
|
|
|
200
|
+
const ZERO_ADDRESS = '11111111111111111111111111111111'
|
|
163
201
|
const RAYDIUM_POSITION_SIZE = 281
|
|
164
202
|
const ORCA_POSITION_SIZE = 216
|
|
165
203
|
/** 池子账户至少要读到的长度(Raydium PoolState 实际 1544 字节,Orca Whirlpool 653 字节) */
|
|
@@ -172,7 +210,7 @@ const RAYDIUM_TICK_SIZE = 168
|
|
|
172
210
|
const ORCA_TICK_ARRAY_SIZE = 88
|
|
173
211
|
const ORCA_TICK_SIZE = 113
|
|
174
212
|
|
|
175
|
-
interface ClmmPositionState {
|
|
213
|
+
export interface ClmmPositionState {
|
|
176
214
|
protocol: 'raydium-clmm' | 'orca-whirlpool'
|
|
177
215
|
address: string
|
|
178
216
|
mint: string
|
|
@@ -184,8 +222,12 @@ interface ClmmPositionState {
|
|
|
184
222
|
growthLast1: bigint
|
|
185
223
|
owed0: bigint
|
|
186
224
|
owed1: bigint
|
|
225
|
+
/** 三个挖矿奖励:上次记录的区间内增长、已记账未领取的数量 */
|
|
226
|
+
rewards: Array<{ growthLast: bigint; owed: bigint }>
|
|
187
227
|
}
|
|
188
228
|
|
|
229
|
+
const positionRewards = (b: Bytes, offset: number) => [0, 1, 2].map((i) => ({ growthLast: b.u128(offset + i * 24), owed: b.u64(offset + i * 24 + 16) }))
|
|
230
|
+
|
|
189
231
|
function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
|
|
190
232
|
return {
|
|
191
233
|
protocol: 'raydium-clmm',
|
|
@@ -199,6 +241,7 @@ function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
|
|
|
199
241
|
growthLast1: b.u128(113),
|
|
200
242
|
owed0: b.u64(129),
|
|
201
243
|
owed1: b.u64(137),
|
|
244
|
+
rewards: positionRewards(b, 145),
|
|
202
245
|
}
|
|
203
246
|
}
|
|
204
247
|
|
|
@@ -215,9 +258,21 @@ function orcaPosition(address: string, b: Bytes): ClmmPositionState {
|
|
|
215
258
|
owed0: b.u64(112),
|
|
216
259
|
growthLast1: b.u128(120),
|
|
217
260
|
owed1: b.u64(136),
|
|
261
|
+
rewards: positionRewards(b, 144),
|
|
218
262
|
}
|
|
219
263
|
}
|
|
220
264
|
|
|
265
|
+
/** 按程序和账户大小解析仓位账户(Raydium PersonalPositionState 281 字节、Orca Position 216 字节);不是仓位账户时为 null */
|
|
266
|
+
export function parseClmmPosition(protocol: 'raydium-clmm' | 'orca-whirlpool', address: string, account: AccountInfo): ClmmPositionState | null {
|
|
267
|
+
if (protocol === 'raydium-clmm' && account.owner === RAYDIUM_CLMM_PROGRAM_ID && account.data.length === RAYDIUM_POSITION_SIZE) {
|
|
268
|
+
return raydiumPosition(address, new Bytes(account.data))
|
|
269
|
+
}
|
|
270
|
+
if (protocol === 'orca-whirlpool' && account.owner === ORCA_WHIRLPOOL_PROGRAM_ID && account.data.length === ORCA_POSITION_SIZE) {
|
|
271
|
+
return orcaPosition(address, new Bytes(account.data))
|
|
272
|
+
}
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
221
276
|
interface ClmmPoolState {
|
|
222
277
|
mint0: string
|
|
223
278
|
mint1: string
|
|
@@ -231,6 +286,41 @@ interface ClmmPoolState {
|
|
|
231
286
|
/** Orca:池子里的费率;Raydium:在 AmmConfig 里,ammConfig 为它的地址 */
|
|
232
287
|
fee: number | null
|
|
233
288
|
ammConfig: string | null
|
|
289
|
+
/** 当前区间内的流动性 */
|
|
290
|
+
liquidity: bigint
|
|
291
|
+
/** 已初始化的挖矿奖励(index 是在池子里的位置) */
|
|
292
|
+
rewards: Array<{ index: number; mint: string; emission: RewardEmission }>
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const U64_MAX_VALUE = (1n << 64n) - 1n
|
|
296
|
+
|
|
297
|
+
/** Raydium reward_infos(每个 169 字节,从 397 开始):open_time +1、end_time +9、last_update_time +17、emissions_per_second_x64 +25、mint +57、growth_global +153 */
|
|
298
|
+
function raydiumRewards(b: Bytes): ClmmPoolState['rewards'] {
|
|
299
|
+
if (b.data.length < 397 + 3 * 169) {
|
|
300
|
+
return []
|
|
301
|
+
}
|
|
302
|
+
return [0, 1, 2].flatMap((index) => {
|
|
303
|
+
const o = 397 + index * 169
|
|
304
|
+
const mint = b.key(o + 57)
|
|
305
|
+
return mint === ZERO_ADDRESS
|
|
306
|
+
? []
|
|
307
|
+
: [{ index, mint, emission: { initialized: true, openTime: b.u64(o + 1), endTime: b.u64(o + 9), lastUpdated: b.u64(o + 17), emissionsPerSecondX64: b.u128(o + 25), growthGlobalX64: b.u128(o + 153) } }]
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Orca reward_last_updated_timestamp(261)和 reward_infos(每个 128 字节,从 269 开始):mint +0、emissions_per_second_x64 +96、growth_global +112 */
|
|
312
|
+
function orcaRewards(b: Bytes): ClmmPoolState['rewards'] {
|
|
313
|
+
if (b.data.length < 269 + 3 * 128) {
|
|
314
|
+
return []
|
|
315
|
+
}
|
|
316
|
+
const lastUpdated = b.u64(261)
|
|
317
|
+
return [0, 1, 2].flatMap((index) => {
|
|
318
|
+
const o = 269 + index * 128
|
|
319
|
+
const mint = b.key(o)
|
|
320
|
+
return mint === ZERO_ADDRESS
|
|
321
|
+
? []
|
|
322
|
+
: [{ index, mint, emission: { initialized: true, openTime: 0n, endTime: U64_MAX_VALUE, lastUpdated, emissionsPerSecondX64: b.u128(o + 96), growthGlobalX64: b.u128(o + 112) } }]
|
|
323
|
+
})
|
|
234
324
|
}
|
|
235
325
|
|
|
236
326
|
function raydiumPool(b: Bytes): ClmmPoolState {
|
|
@@ -246,6 +336,8 @@ function raydiumPool(b: Bytes): ClmmPoolState {
|
|
|
246
336
|
global0: b.u128(277),
|
|
247
337
|
global1: b.u128(293),
|
|
248
338
|
fee: null,
|
|
339
|
+
liquidity: b.u128(237),
|
|
340
|
+
rewards: raydiumRewards(b),
|
|
249
341
|
}
|
|
250
342
|
}
|
|
251
343
|
|
|
@@ -262,6 +354,8 @@ function orcaPool(b: Bytes): ClmmPoolState {
|
|
|
262
354
|
global1: b.u128(245),
|
|
263
355
|
decimals0: null,
|
|
264
356
|
decimals1: null,
|
|
357
|
+
liquidity: b.u128(49),
|
|
358
|
+
rewards: orcaRewards(b),
|
|
265
359
|
}
|
|
266
360
|
}
|
|
267
361
|
|
|
@@ -346,7 +440,9 @@ export function orcaTickArray(pool: string, tick: number, spacing: number): { ad
|
|
|
346
440
|
return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), utf8(String(start))], ORCA_WHIRLPOOL_PROGRAM_ID), start }
|
|
347
441
|
}
|
|
348
442
|
|
|
349
|
-
|
|
443
|
+
const rewardOutside = (b: Bytes, offset: number) => [b.u128(offset), b.u128(offset + 16), b.u128(offset + 32)]
|
|
444
|
+
|
|
445
|
+
export function raydiumTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
350
446
|
raydiumDiscriminator ??= discriminator('TickArrayState')
|
|
351
447
|
const offset = 44 + ((tick - start) / spacing) * RAYDIUM_TICK_SIZE
|
|
352
448
|
// 先认判别符、查长度再读字段(PDA 上可能是别的账户,如被转过 SOL 的系统账户)
|
|
@@ -358,7 +454,7 @@ function raydiumTick(account: AccountInfo, start: number, tick: number, spacing:
|
|
|
358
454
|
return null
|
|
359
455
|
}
|
|
360
456
|
// Raydium 不区分是否初始化:没用过的 tick 各项都是 0,按程序的公式算出来就是对的
|
|
361
|
-
return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52) }
|
|
457
|
+
return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52), rewardGrowthsOutside: rewardOutside(b, offset + 68) }
|
|
362
458
|
}
|
|
363
459
|
|
|
364
460
|
/** Anchor 账户判别符:sha256("account:<名称>") 的前 8 字节 */
|
|
@@ -367,7 +463,7 @@ let orcaDiscriminators: { fixed: Uint8Array; dynamic: Uint8Array } | undefined
|
|
|
367
463
|
let raydiumDiscriminator: Uint8Array | undefined
|
|
368
464
|
const startsWith = (data: Uint8Array, prefix: Uint8Array) => data.length >= prefix.length && prefix.every((byte, i) => data[i] === byte)
|
|
369
465
|
|
|
370
|
-
function orcaTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
466
|
+
export function orcaTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
|
|
371
467
|
orcaDiscriminators ??= { fixed: discriminator('TickArray'), dynamic: discriminator('DynamicTickArray') }
|
|
372
468
|
if (account.data.length < 60) {
|
|
373
469
|
return null // 两种格式的头部(判别符 + start + …)都放不下
|
|
@@ -380,7 +476,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
|
|
|
380
476
|
if (b.i32(8) !== start || offset + ORCA_TICK_SIZE > account.data.length) {
|
|
381
477
|
return null
|
|
382
478
|
}
|
|
383
|
-
return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) }
|
|
479
|
+
return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49), rewardGrowthsOutside: rewardOutside(b, offset + 65) }
|
|
384
480
|
}
|
|
385
481
|
if (startsWith(account.data, orcaDiscriminators.dynamic)) {
|
|
386
482
|
// 可变大小:start i32 | whirlpool | tick_bitmap u128 | ticks(未初始化 1 字节 0;初始化 1 字节 1 + 112 字节数据)
|
|
@@ -403,7 +499,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
|
|
|
403
499
|
if (offset + 113 > account.data.length) {
|
|
404
500
|
return null
|
|
405
501
|
}
|
|
406
|
-
return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) }
|
|
502
|
+
return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49), rewardGrowthsOutside: rewardOutside(b, offset + 65) }
|
|
407
503
|
}
|
|
408
504
|
return null
|
|
409
505
|
}
|
|
@@ -426,6 +522,8 @@ interface RawClmm {
|
|
|
426
522
|
amount1: bigint
|
|
427
523
|
fees0: bigint | null
|
|
428
524
|
fees1: bigint | null
|
|
525
|
+
/** rewards 选项开启时:每个已初始化的奖励的未领取数量(读不到 tick 时为 null) */
|
|
526
|
+
rewards?: Array<{ index: number; mint: string; amount: bigint | null }>
|
|
429
527
|
}
|
|
430
528
|
|
|
431
529
|
interface RawAmm {
|
|
@@ -498,11 +596,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
498
596
|
if (!account) {
|
|
499
597
|
return
|
|
500
598
|
}
|
|
501
|
-
const
|
|
502
|
-
if (
|
|
503
|
-
positions.push(
|
|
504
|
-
} else if (c.protocol === 'orca-whirlpool' && account.owner === program && account.data.length === ORCA_POSITION_SIZE) {
|
|
505
|
-
positions.push(orcaPosition(c.address, new Bytes(account.data)))
|
|
599
|
+
const parsed = parseClmmPosition(c.protocol, c.address, account)
|
|
600
|
+
if (parsed) {
|
|
601
|
+
positions.push(parsed)
|
|
506
602
|
}
|
|
507
603
|
})
|
|
508
604
|
// LP mint 的权限是 AMM v4 / CPMM 的固定权限地址时才去找池子
|
|
@@ -605,10 +701,32 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
605
701
|
fees1 = null
|
|
606
702
|
}
|
|
607
703
|
}
|
|
608
|
-
|
|
704
|
+
// 未领取的挖矿奖励:没有流动性时只有已记账的;有流动性时先把池子的奖励增长结算到 now,再按区间内的增长算新增
|
|
705
|
+
let rewards: RawClmm['rewards']
|
|
706
|
+
if (options.rewards) {
|
|
707
|
+
const now = BigInt(Math.floor(options.now ?? Date.now() / 1000))
|
|
708
|
+
const ticks = (() => {
|
|
709
|
+
if (!arrays) return null
|
|
710
|
+
const read = position.protocol === 'raydium-clmm' ? raydiumTick : orcaTick
|
|
711
|
+
const lowerAccount = byAddress.get(arrays.lower.address)
|
|
712
|
+
const upperAccount = byAddress.get(arrays.upper.address)
|
|
713
|
+
const lower = lowerAccount ? read(lowerAccount, arrays.lower.start, position.tickLower, pool.tickSpacing) : null
|
|
714
|
+
const upper = upperAccount ? read(upperAccount, arrays.upper.start, position.tickUpper, pool.tickSpacing) : null
|
|
715
|
+
return lower && upper ? { lower, upper } : null
|
|
716
|
+
})()
|
|
717
|
+
rewards = pool.rewards.map(({ index, mint, emission }) => {
|
|
718
|
+
const state = position.rewards[index] as { growthLast: bigint; owed: bigint }
|
|
719
|
+
if (!arrays) return { index, mint, amount: state.owed }
|
|
720
|
+
if (!ticks) return { index, mint, amount: null }
|
|
721
|
+
const global = rewardGrowthGlobal(pool.liquidity, emission, now)
|
|
722
|
+
const inside = getRewardGrowthInside(pool.tick, position.tickLower, ticks.lower, position.tickUpper, ticks.upper, index, global)
|
|
723
|
+
return { index, mint, amount: getFeesOwed(state.owed, position.liquidity, inside, state.growthLast) }
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
if (!options.includeClosed && position.liquidity === 0n && fees0 === 0n && fees1 === 0n && !rewards?.some((x) => x.amount !== 0n)) {
|
|
609
727
|
continue
|
|
610
728
|
}
|
|
611
|
-
raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1 })
|
|
729
|
+
raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1, rewards })
|
|
612
730
|
}
|
|
613
731
|
for (const pool of ammHeld) {
|
|
614
732
|
const balance = held.get(pool.lpMint)?.amount ?? 0n
|
|
@@ -706,7 +824,7 @@ async function findAmmPool(client: SolanaClient, protocol: 'raydium-amm' | 'rayd
|
|
|
706
824
|
|
|
707
825
|
async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[], options: LpPositionsOptions): Promise<SolanaLpPosition[]> {
|
|
708
826
|
const { signal } = options
|
|
709
|
-
const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1]))]
|
|
827
|
+
const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1, ...(r.type === 'clmm' ? (r.rewards ?? []).map((x) => x.mint) : [])]))]
|
|
710
828
|
const priceSource = options.prices === true ? defillamaPrices() : options.prices || null
|
|
711
829
|
const fetchFn = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
712
830
|
// 价格源出错(服务不可用、被拦截、同步抛错等)时照样返回仓位,只是不带价格;取消查询时照常 reject
|
|
@@ -784,6 +902,11 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
784
902
|
const d1 = token1.decimals
|
|
785
903
|
const fees0Formatted = r.fees0 === null ? null : fmt(r.fees0, token0)
|
|
786
904
|
const fees1Formatted = r.fees1 === null ? null : fmt(r.fees1, token1)
|
|
905
|
+
const rewards = r.rewards?.map((x): SolanaLpReward => {
|
|
906
|
+
const t = token(x.mint, null)
|
|
907
|
+
const formatted = x.amount === null ? null : fmt(x.amount, t)
|
|
908
|
+
return { index: x.index, mint: x.mint, symbol: t.symbol, decimals: t.decimals, amount: x.amount === null ? null : x.amount.toString(), formatted, ...(priceSource ? { usd: x.amount === 0n ? 0 : usd(t, formatted) } : {}) }
|
|
909
|
+
})
|
|
787
910
|
return {
|
|
788
911
|
...base,
|
|
789
912
|
type: 'clmm',
|
|
@@ -804,6 +927,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
|
|
|
804
927
|
fees0Formatted,
|
|
805
928
|
fees1Formatted,
|
|
806
929
|
...(priceSource ? { feesUsd: r.fees0 === null || r.fees1 === null ? null : sum(usd(token0, fees0Formatted), usd(token1, fees1Formatted), r.fees0, r.fees1) } : {}),
|
|
930
|
+
...(rewards ? { rewards } : {}),
|
|
931
|
+
...(rewards && priceSource ? { rewardsUsd: rewards.some((x) => x.usd === null || x.usd === undefined) ? null : rewards.reduce((acc, x) => acc + (x.usd ?? 0), 0) } : {}),
|
|
807
932
|
}
|
|
808
933
|
})
|
|
809
934
|
if (priceSource) {
|
package/src/lpMath.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface TickFeeData {
|
|
|
153
153
|
initialized: boolean
|
|
154
154
|
feeGrowthOutside0: bigint
|
|
155
155
|
feeGrowthOutside1: bigint
|
|
156
|
+
/** 三个挖矿奖励的 tick 外增长(读 tick 时带上;只算手续费时可以不给) */
|
|
157
|
+
rewardGrowthsOutside?: readonly bigint[]
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
const wrap = (x: bigint) => ((x % (1n << 128n)) + (1n << 128n)) % (1n << 128n)
|
|
@@ -192,6 +194,42 @@ export function getFeesOwed(owed: bigint, liquidity: bigint, growthInside: bigin
|
|
|
192
194
|
return owed + (delta > U64_MAX ? 0n : delta)
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/** 池子里一个挖矿奖励的状态(两个程序的 reward_infos 里的同名字段) */
|
|
198
|
+
export interface RewardEmission {
|
|
199
|
+
/** 已初始化:奖励的 mint 不是默认地址 */
|
|
200
|
+
initialized: boolean
|
|
201
|
+
emissionsPerSecondX64: bigint
|
|
202
|
+
growthGlobalX64: bigint
|
|
203
|
+
/** 上次更新的时间(秒)。Raydium 每个奖励自己记;Orca 是池子的 reward_last_updated_timestamp(三个奖励相同) */
|
|
204
|
+
lastUpdated: bigint
|
|
205
|
+
/** 开始 / 结束时间(秒):Raydium 的 open_time / end_time;Orca 没有,传 0n / 2^64 − 1 */
|
|
206
|
+
openTime: bigint
|
|
207
|
+
endTime: bigint
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 池子在 now 时刻的奖励全局增长:程序每次操作池子(交易、增减流动性、领取)前先把排放按时间摊给当前的流动性,
|
|
212
|
+
* Q64.64:growth += Δt · emissions_per_second / liquidity(向下取整,u128 回绕)。
|
|
213
|
+
* Raydium:now ≤ open_time 不动,Δt 到 end_time 为止;Orca:池子流动性为 0 或 Δt 为 0 不动
|
|
214
|
+
*/
|
|
215
|
+
export function rewardGrowthGlobal(liquidity: bigint, reward: RewardEmission, now: bigint): bigint {
|
|
216
|
+
if (!reward.initialized || liquidity === 0n || now <= reward.openTime) {
|
|
217
|
+
return reward.growthGlobalX64
|
|
218
|
+
}
|
|
219
|
+
const latest = now < reward.endTime ? now : reward.endTime
|
|
220
|
+
const delta = latest > reward.lastUpdated ? latest - reward.lastUpdated : 0n
|
|
221
|
+
return wrap(reward.growthGlobalX64 + (delta * reward.emissionsPerSecondX64) / liquidity)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 区间内某个奖励的增长(u128 回绕),tick 未初始化的处理同 getFeeGrowthInside */
|
|
225
|
+
export function getRewardGrowthInside(tick: number, tickLower: number, lower: TickFeeData, tickUpper: number, upper: TickFeeData, index: number, global: bigint): bigint {
|
|
226
|
+
const lowerOutside = lower.rewardGrowthsOutside?.[index] ?? 0n
|
|
227
|
+
const upperOutside = upper.rewardGrowthsOutside?.[index] ?? 0n
|
|
228
|
+
const below = !lower.initialized ? global : tick < tickLower ? wrap(global - lowerOutside) : lowerOutside
|
|
229
|
+
const above = !upper.initialized ? 0n : tick < tickUpper ? upperOutside : wrap(global - upperOutside)
|
|
230
|
+
return wrap(global - below - above)
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
/** Q64.64 sqrt 价格 → token1 / token0 的价格(按精度换算,浮点数,只用于展示) */
|
|
196
234
|
export function sqrtPriceToPrice(sqrtPrice: bigint, decimals0: number, decimals1: number): number {
|
|
197
235
|
// 先取高位再转浮点,避免 2^128 级别的整数转换丢精度后再平方
|
package/src/programs.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 常见程序的名字、指令名(Anchor 程序按 sha256("global:<name>") 的前 8 字节识别)和自定义错误码,
|
|
3
|
+
* 给 /simulate 和 /history 说明交易在做什么。只收录认得准的,其他程序只显示地址
|
|
4
|
+
*/
|
|
5
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
9
|
+
SYSTEM_PROGRAM_ID,
|
|
10
|
+
TOKEN_2022_PROGRAM_ID,
|
|
11
|
+
TOKEN_PROGRAM_ID,
|
|
12
|
+
} from './constants.js'
|
|
13
|
+
|
|
14
|
+
// 与 lp.ts 里的同名常量相同;这里不 import 它,避免 /simulate、/history 把整个 LP 模块打进来
|
|
15
|
+
const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK'
|
|
16
|
+
const ORCA_WHIRLPOOL_PROGRAM_ID = 'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc'
|
|
17
|
+
const RAYDIUM_AMM_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'
|
|
18
|
+
const RAYDIUM_CPMM_PROGRAM_ID = 'CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C'
|
|
19
|
+
|
|
20
|
+
export const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111'
|
|
21
|
+
export const MEMO_V2 = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'
|
|
22
|
+
export const MEMO_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo'
|
|
23
|
+
export const BPF_LOADER_UPGRADEABLE = 'BPFLoaderUpgradeab1e11111111111111111111111'
|
|
24
|
+
|
|
25
|
+
/** 指令的大类:交易的类型由它们和余额变化一起决定 */
|
|
26
|
+
export type IxType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'burn' | 'close' | 'setup' | 'budget' | 'memo' | 'contract' | 'unknown'
|
|
27
|
+
|
|
28
|
+
export interface ProgramInfo {
|
|
29
|
+
name: string
|
|
30
|
+
/** 内置程序(系统、代币、ATA、计算预算、备忘录等):不算“不认识的程序” */
|
|
31
|
+
builtin?: boolean
|
|
32
|
+
/** 协议名(DEX / 借贷等),内置程序没有 */
|
|
33
|
+
protocol?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const p = (name: string, protocol = name): ProgramInfo => ({ name, protocol })
|
|
37
|
+
const b = (name: string): ProgramInfo => ({ name, builtin: true })
|
|
38
|
+
|
|
39
|
+
export const PROGRAMS: Readonly<Record<string, ProgramInfo>> = {
|
|
40
|
+
[SYSTEM_PROGRAM_ID]: b('System Program'),
|
|
41
|
+
[TOKEN_PROGRAM_ID]: b('SPL Token'),
|
|
42
|
+
[TOKEN_2022_PROGRAM_ID]: b('Token-2022'),
|
|
43
|
+
[ASSOCIATED_TOKEN_PROGRAM_ID]: b('Associated Token Account'),
|
|
44
|
+
[COMPUTE_BUDGET]: b('Compute Budget'),
|
|
45
|
+
[MEMO_V2]: b('Memo'),
|
|
46
|
+
[MEMO_V1]: b('Memo'),
|
|
47
|
+
[BPF_LOADER_UPGRADEABLE]: b('BPF Loader Upgradeable'),
|
|
48
|
+
AddressLookupTab1e1111111111111111111111111: b('Address Lookup Table'),
|
|
49
|
+
Stake11111111111111111111111111111111111111: b('Stake Program'),
|
|
50
|
+
Vote111111111111111111111111111111111111111: b('Vote Program'),
|
|
51
|
+
Ed25519SigVerify111111111111111111111111111: b('Ed25519 Program'),
|
|
52
|
+
KeccakSecp256k11111111111111111111111111111: b('Secp256k1 Program'),
|
|
53
|
+
metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s: b('Metaplex Token Metadata'),
|
|
54
|
+
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: p('Jupiter Aggregator v6', 'Jupiter'),
|
|
55
|
+
JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB: p('Jupiter Aggregator v4', 'Jupiter'),
|
|
56
|
+
DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M: p('Jupiter DCA', 'Jupiter'),
|
|
57
|
+
j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X: p('Jupiter Limit Order', 'Jupiter'),
|
|
58
|
+
jupoNjAxXgZ4rjzxzPMP4oxduvQsQtZzyknqvzYNrNu: p('Jupiter Limit Order v2', 'Jupiter'),
|
|
59
|
+
PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu: p('Jupiter Perpetuals', 'Jupiter'),
|
|
60
|
+
[RAYDIUM_AMM_PROGRAM_ID]: p('Raydium AMM v4', 'Raydium'),
|
|
61
|
+
[RAYDIUM_CLMM_PROGRAM_ID]: p('Raydium CLMM', 'Raydium'),
|
|
62
|
+
[RAYDIUM_CPMM_PROGRAM_ID]: p('Raydium CPMM', 'Raydium'),
|
|
63
|
+
routeUGWgWzqBWFcrCfv8tritsqukccJPu3q5GPP3xS: p('Raydium Route', 'Raydium'),
|
|
64
|
+
[ORCA_WHIRLPOOL_PROGRAM_ID]: p('Orca Whirlpool', 'Orca'),
|
|
65
|
+
'9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP': p('Orca v2', 'Orca'),
|
|
66
|
+
LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo: p('Meteora DLMM', 'Meteora'),
|
|
67
|
+
Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB: p('Meteora Pools', 'Meteora'),
|
|
68
|
+
cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG: p('Meteora DAMM v2', 'Meteora'),
|
|
69
|
+
dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN: p('Meteora Dynamic Bonding Curve', 'Meteora'),
|
|
70
|
+
'6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': p('Pump.fun', 'Pump.fun'),
|
|
71
|
+
pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA: p('PumpSwap', 'Pump.fun'),
|
|
72
|
+
PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY: p('Phoenix'),
|
|
73
|
+
srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX: p('OpenBook v1', 'OpenBook'),
|
|
74
|
+
opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb: p('OpenBook v2', 'OpenBook'),
|
|
75
|
+
LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj: p('Lifinity v2', 'Lifinity'),
|
|
76
|
+
MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD: p('Marinade'),
|
|
77
|
+
MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA: p('marginfi v2', 'marginfi'),
|
|
78
|
+
KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD: p('Kamino Lending', 'Kamino'),
|
|
79
|
+
dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH: p('Drift v2', 'Drift'),
|
|
80
|
+
So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo: p('Solend'),
|
|
81
|
+
SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf: p('Squads v4', 'Squads'),
|
|
82
|
+
wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb: p('Wormhole Token Bridge', 'Wormhole'),
|
|
83
|
+
CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR: p('Candy Machine v3', 'Metaplex'),
|
|
84
|
+
Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g: p('Candy Guard', 'Metaplex'),
|
|
85
|
+
CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d: p('Metaplex Core', 'Metaplex'),
|
|
86
|
+
TSWAPaqyCSx2KABk68Shruf4rp7CxcNi8hAsbdwmHbN: p('Tensor Swap', 'Tensor'),
|
|
87
|
+
M2mx93ekt1fmXSVkTrUL9xVFHkmME8HTUi5Cyc5aF7K: p('Magic Eden v2', 'Magic Eden'),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 按指令名(snake_case)识别的 Anchor 程序:指令名 → 大类 */
|
|
91
|
+
const JUPITER_METHODS: Record<string, IxType> = {
|
|
92
|
+
...Object.fromEntries(['route', 'route_with_token_ledger', 'shared_accounts_route', 'shared_accounts_route_with_token_ledger', 'exact_out_route', 'shared_accounts_exact_out_route'].map((n) => [n, 'swap'])),
|
|
93
|
+
set_token_ledger: 'setup',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ANCHOR: Record<string, Record<string, IxType>> = {
|
|
97
|
+
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: JUPITER_METHODS,
|
|
98
|
+
JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB: JUPITER_METHODS,
|
|
99
|
+
dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN: { swap: 'swap', swap2: 'swap' },
|
|
100
|
+
CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR: { mint: 'mint', mint_v2: 'mint' },
|
|
101
|
+
Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g: { mint: 'mint', mint_v2: 'mint' },
|
|
102
|
+
[ORCA_WHIRLPOOL_PROGRAM_ID]: {
|
|
103
|
+
...Object.fromEntries(['swap', 'swap_v2', 'two_hop_swap', 'two_hop_swap_v2'].map((n) => [n, 'swap'])),
|
|
104
|
+
...Object.fromEntries(
|
|
105
|
+
[
|
|
106
|
+
'increase_liquidity', 'increase_liquidity_v2', 'decrease_liquidity', 'decrease_liquidity_v2', 'open_position', 'open_position_with_metadata',
|
|
107
|
+
'open_position_with_token_extensions', 'close_position', 'close_position_with_token_extensions', 'collect_fees', 'collect_fees_v2', 'collect_reward',
|
|
108
|
+
'collect_reward_v2', 'open_bundled_position', 'close_bundled_position', 'increase_liquidity_by_token_amounts_v2',
|
|
109
|
+
].map((n) => [n, 'liquidity']),
|
|
110
|
+
),
|
|
111
|
+
initialize_tick_array: 'setup',
|
|
112
|
+
initialize_dynamic_tick_array: 'setup',
|
|
113
|
+
},
|
|
114
|
+
[RAYDIUM_CLMM_PROGRAM_ID]: {
|
|
115
|
+
...Object.fromEntries(['swap', 'swap_v2', 'swap_router_base_in'].map((n) => [n, 'swap'])),
|
|
116
|
+
...Object.fromEntries(
|
|
117
|
+
['open_position', 'open_position_v2', 'open_position_with_token22_nft', 'increase_liquidity', 'increase_liquidity_v2', 'decrease_liquidity', 'decrease_liquidity_v2', 'close_position', 'collect_fund_fee'].map((n) => [n, 'liquidity']),
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
[RAYDIUM_CPMM_PROGRAM_ID]: { swap_base_input: 'swap', swap_base_output: 'swap', deposit: 'liquidity', withdraw: 'liquidity' },
|
|
121
|
+
LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo: {
|
|
122
|
+
...Object.fromEntries(['swap', 'swap_exact_out', 'swap_with_price_impact'].map((n) => [n, 'swap'])),
|
|
123
|
+
...Object.fromEntries(
|
|
124
|
+
[
|
|
125
|
+
'add_liquidity', 'add_liquidity_by_weight', 'add_liquidity_by_strategy', 'add_liquidity_by_strategy_one_side', 'add_liquidity_one_side', 'remove_liquidity',
|
|
126
|
+
'remove_liquidity_by_range', 'remove_all_liquidity', 'claim_fee', 'claim_reward', 'close_position', 'initialize_position',
|
|
127
|
+
].map((n) => [n, 'liquidity']),
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB: {
|
|
131
|
+
swap: 'swap',
|
|
132
|
+
add_balance_liquidity: 'liquidity',
|
|
133
|
+
add_imbalance_liquidity: 'liquidity',
|
|
134
|
+
remove_balance_liquidity: 'liquidity',
|
|
135
|
+
remove_liquidity_single_side: 'liquidity',
|
|
136
|
+
},
|
|
137
|
+
cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG: {
|
|
138
|
+
swap: 'swap',
|
|
139
|
+
swap2: 'swap',
|
|
140
|
+
add_liquidity: 'liquidity',
|
|
141
|
+
remove_liquidity: 'liquidity',
|
|
142
|
+
remove_all_liquidity: 'liquidity',
|
|
143
|
+
claim_position_fee: 'liquidity',
|
|
144
|
+
create_position: 'liquidity',
|
|
145
|
+
close_position: 'liquidity',
|
|
146
|
+
},
|
|
147
|
+
'6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': { buy: 'swap', sell: 'swap', create: 'mint', create_v2: 'mint' },
|
|
148
|
+
pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA: { buy: 'swap', sell: 'swap', deposit: 'liquidity', withdraw: 'liquidity', create_pool: 'liquidity' },
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Raydium AMM v4:第一个字节是指令编号 */
|
|
152
|
+
const AMM_V4: Record<number, [string, IxType]> = {
|
|
153
|
+
3: ['deposit', 'liquidity'],
|
|
154
|
+
4: ['withdraw', 'liquidity'],
|
|
155
|
+
9: ['swapBaseIn', 'swap'],
|
|
156
|
+
11: ['swapBaseOut', 'swap'],
|
|
157
|
+
16: ['swapBaseInV2', 'swap'],
|
|
158
|
+
17: ['swapBaseOutV2', 'swap'],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let discriminators: Map<string, Map<string, [string, IxType]>> | undefined
|
|
162
|
+
|
|
163
|
+
const hex = (bytes: Uint8Array) => Array.from(bytes, (x) => x.toString(16).padStart(2, '0')).join('')
|
|
164
|
+
|
|
165
|
+
/** 认得的程序指令:返回指令名和大类;不认识的指令(程序认得)返回 null */
|
|
166
|
+
export function knownMethod(programId: string, data: Uint8Array): [method: string, type: IxType] | null {
|
|
167
|
+
if (programId === RAYDIUM_AMM_PROGRAM_ID) return AMM_V4[data[0] as number] ?? null
|
|
168
|
+
const methods = ANCHOR[programId]
|
|
169
|
+
if (!methods || data.length < 8) return null
|
|
170
|
+
if (!discriminators) {
|
|
171
|
+
discriminators = new Map(
|
|
172
|
+
Object.entries(ANCHOR).map(([program, list]) => [
|
|
173
|
+
program,
|
|
174
|
+
new Map(Object.entries(list).map(([name, type]) => [hex(sha256(new TextEncoder().encode(`global:${name}`)).slice(0, 8)), [name, type] as [string, IxType]])),
|
|
175
|
+
]),
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
return discriminators.get(programId)?.get(hex(data.slice(0, 8))) ?? null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// 自定义错误码
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
const list = (names: string): Record<number, string> => Object.fromEntries(names.split(',').map((n, i) => [i, n]))
|
|
186
|
+
|
|
187
|
+
const SYSTEM_ERRORS = list('AccountAlreadyInUse,ResultWithNegativeLamports,InvalidProgramId,InvalidAccountDataLength,MaxSeedLengthExceeded,AddressWithSeedMismatch,NonceNoRecentBlockhashes,NonceBlockhashNotExpired,NonceUnexpectedBlockhashValue')
|
|
188
|
+
const TOKEN_ERRORS = list(
|
|
189
|
+
'NotRentExempt,InsufficientFunds,InvalidMint,MintMismatch,OwnerMismatch,FixedSupply,AlreadyInUse,InvalidNumberOfProvidedSigners,InvalidNumberOfRequiredSigners,UninitializedState,NativeNotSupported,NonNativeHasBalance,InvalidInstruction,InvalidState,Overflow,AuthorityTypeNotSupported,MintCannotFreeze,AccountFrozen,MintDecimalsMismatch,NonNativeNotSupported',
|
|
190
|
+
)
|
|
191
|
+
const JUPITER_ERRORS: Record<number, string> = Object.fromEntries(
|
|
192
|
+
'EmptyRoute,SlippageToleranceExceeded,InvalidCalculation,MissingPlatformFeeAccount,InvalidSlippage,NotEnoughPercent,InvalidInputIndex,InvalidOutputIndex,NotEnoughAccountKeys,NonZeroMinimumOutAmountNotSupported,InvalidRoutePlan,InvalidReferralAuthority,LedgerTokenAccountDoesNotMatch,InvalidTokenLedger,IncorrectTokenProgramID,TokenProgramNotProvided,SwapNotSupported,ExactOutAmountNotMatched,SourceAndDestinationMintCannotBeTheSame'
|
|
193
|
+
.split(',')
|
|
194
|
+
.map((n, i) => [6000 + i, n]),
|
|
195
|
+
)
|
|
196
|
+
const RAYDIUM_AMM_ERRORS: Record<number, string> = { 22: 'InvalidStatus', 23: 'InvalidInstruction', 30: 'ExceededSlippage', 40: 'InsufficientFunds' }
|
|
197
|
+
/** Anchor 框架自带的错误码(2000 起是账户约束) */
|
|
198
|
+
const ANCHOR_ERRORS: Record<number, string> = {
|
|
199
|
+
100: 'InstructionMissing', 101: 'InstructionFallbackNotFound', 102: 'InstructionDidNotDeserialize', 2000: 'ConstraintMut', 2001: 'ConstraintHasOne',
|
|
200
|
+
2002: 'ConstraintSigner', 2003: 'ConstraintRaw', 2004: 'ConstraintOwner', 2006: 'ConstraintSeeds', 2012: 'ConstraintAddress', 2014: 'ConstraintTokenMint',
|
|
201
|
+
2015: 'ConstraintTokenOwner', 3001: 'AccountDiscriminatorNotFound', 3002: 'AccountDiscriminatorMismatch', 3003: 'AccountDidNotDeserialize',
|
|
202
|
+
3007: 'AccountOwnedByWrongProgram', 3010: 'AccountNotSigner', 3012: 'AccountNotInitialized',
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const PROGRAM_ERRORS: Record<string, Record<number, string>> = {
|
|
206
|
+
[SYSTEM_PROGRAM_ID]: SYSTEM_ERRORS,
|
|
207
|
+
[TOKEN_PROGRAM_ID]: TOKEN_ERRORS,
|
|
208
|
+
[TOKEN_2022_PROGRAM_ID]: TOKEN_ERRORS,
|
|
209
|
+
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: JUPITER_ERRORS,
|
|
210
|
+
[RAYDIUM_AMM_PROGRAM_ID]: RAYDIUM_AMM_ERRORS,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 自定义错误码的名字:先看这个程序自己的表,再看 Anchor 框架的;认不出时返回 undefined */
|
|
214
|
+
export function programErrorName(programId: string | undefined, code: number): string | undefined {
|
|
215
|
+
return (programId ? PROGRAM_ERRORS[programId]?.[code] : undefined) ?? ANCHOR_ERRORS[code]
|
|
216
|
+
}
|