@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +113 -1
  3. package/dist/batchPlan.d.ts +52 -0
  4. package/dist/batchPlan.d.ts.map +1 -0
  5. package/dist/batchPlan.js +85 -0
  6. package/dist/batchPlan.js.map +1 -0
  7. package/dist/claim.d.ts +161 -0
  8. package/dist/claim.d.ts.map +1 -0
  9. package/dist/claim.js +197 -0
  10. package/dist/claim.js.map +1 -0
  11. package/dist/decode.d.ts +22 -0
  12. package/dist/decode.d.ts.map +1 -0
  13. package/dist/decode.js +187 -0
  14. package/dist/decode.js.map +1 -0
  15. package/dist/describe.d.ts +26 -0
  16. package/dist/describe.d.ts.map +1 -0
  17. package/dist/describe.js +58 -0
  18. package/dist/describe.js.map +1 -0
  19. package/dist/history.d.ts +125 -0
  20. package/dist/history.d.ts.map +1 -0
  21. package/dist/history.js +231 -0
  22. package/dist/history.js.map +1 -0
  23. package/dist/historyHelius.d.ts +52 -0
  24. package/dist/historyHelius.d.ts.map +1 -0
  25. package/dist/historyHelius.js +125 -0
  26. package/dist/historyHelius.js.map +1 -0
  27. package/dist/liquidity/orca.d.ts +6 -0
  28. package/dist/liquidity/orca.d.ts.map +1 -1
  29. package/dist/liquidity/orca.js +14 -3
  30. package/dist/liquidity/orca.js.map +1 -1
  31. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  32. package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
  33. package/dist/liquidity/raydiumClmm.js +12 -5
  34. package/dist/liquidity/raydiumClmm.js.map +1 -1
  35. package/dist/lp.d.ts +47 -0
  36. package/dist/lp.d.ts.map +1 -1
  37. package/dist/lp.js +91 -15
  38. package/dist/lp.js.map +1 -1
  39. package/dist/lpMath.d.ts +22 -0
  40. package/dist/lpMath.d.ts.map +1 -1
  41. package/dist/lpMath.js +21 -0
  42. package/dist/lpMath.js.map +1 -1
  43. package/dist/programs.d.ts +19 -0
  44. package/dist/programs.d.ts.map +1 -0
  45. package/dist/programs.js +175 -0
  46. package/dist/programs.js.map +1 -0
  47. package/dist/reclaim.d.ts +190 -0
  48. package/dist/reclaim.d.ts.map +1 -0
  49. package/dist/reclaim.js +399 -0
  50. package/dist/reclaim.js.map +1 -0
  51. package/dist/rpc.d.ts.map +1 -1
  52. package/dist/rpc.js +4 -2
  53. package/dist/rpc.js.map +1 -1
  54. package/dist/simulate.d.ts +106 -0
  55. package/dist/simulate.d.ts.map +1 -0
  56. package/dist/simulate.js +418 -0
  57. package/dist/simulate.js.map +1 -0
  58. package/dist/subpaths/claim.d.ts +31 -0
  59. package/dist/subpaths/claim.d.ts.map +1 -0
  60. package/dist/subpaths/claim.js +31 -0
  61. package/dist/subpaths/claim.js.map +1 -0
  62. package/dist/subpaths/history.d.ts +21 -0
  63. package/dist/subpaths/history.d.ts.map +1 -0
  64. package/dist/subpaths/history.js +22 -0
  65. package/dist/subpaths/history.js.map +1 -0
  66. package/dist/subpaths/lp.d.ts +2 -2
  67. package/dist/subpaths/lp.d.ts.map +1 -1
  68. package/dist/subpaths/lp.js +2 -2
  69. package/dist/subpaths/lp.js.map +1 -1
  70. package/dist/subpaths/reclaim.d.ts +34 -0
  71. package/dist/subpaths/reclaim.d.ts.map +1 -0
  72. package/dist/subpaths/reclaim.js +34 -0
  73. package/dist/subpaths/reclaim.js.map +1 -0
  74. package/dist/subpaths/simulate.d.ts +22 -0
  75. package/dist/subpaths/simulate.d.ts.map +1 -0
  76. package/dist/subpaths/simulate.js +23 -0
  77. package/dist/subpaths/simulate.js.map +1 -0
  78. package/dist/write/instructions.d.ts +2 -0
  79. package/dist/write/instructions.d.ts.map +1 -1
  80. package/dist/write/instructions.js +16 -0
  81. package/dist/write/instructions.js.map +1 -1
  82. package/package.json +21 -1
  83. package/src/batchPlan.ts +137 -0
  84. package/src/claim.ts +323 -0
  85. package/src/decode.ts +202 -0
  86. package/src/describe.ts +77 -0
  87. package/src/history.ts +336 -0
  88. package/src/historyHelius.ts +136 -0
  89. package/src/liquidity/orca.ts +15 -5
  90. package/src/liquidity/raydiumClmm.ts +13 -5
  91. package/src/lp.ts +140 -15
  92. package/src/lpMath.ts +38 -0
  93. package/src/programs.ts +216 -0
  94. package/src/reclaim.ts +547 -0
  95. package/src/rpc.ts +4 -2
  96. package/src/simulate.ts +535 -0
  97. package/src/subpaths/claim.ts +53 -0
  98. package/src/subpaths/history.ts +38 -0
  99. package/src/subpaths/lp.ts +5 -1
  100. package/src/subpaths/reclaim.ts +57 -0
  101. package/src/subpaths/simulate.ts +37 -0
  102. 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 { getAmountsForLiquidity, getFeeGrowthInside, getFeesOwed, orcaSqrtPriceAtTick, raydiumSqrtPriceAtTick, sqrtPriceToPrice, tickToPrice, type TickFeeData } from './lpMath.js'
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
- function raydiumTick(account: AccountInfo, start: number, tick: number, spacing: number): TickFeeData | null {
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 program = SOLANA_LP_PROTOCOLS[c.protocol].programId
502
- if (c.protocol === 'raydium-clmm' && account.owner === program && account.data.length === RAYDIUM_POSITION_SIZE) {
503
- positions.push(raydiumPosition(c.address, new Bytes(account.data)))
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
- if (!options.includeClosed && position.liquidity === 0n && fees0 === 0n && fees1 === 0n) {
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 级别的整数转换丢精度后再平方
@@ -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
+ }