@w3lib/solana-batch-call 0.4.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 (223) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +343 -3
  3. package/dist/approvals.d.ts +129 -0
  4. package/dist/approvals.d.ts.map +1 -0
  5. package/dist/approvals.js +221 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/batchPlan.d.ts +52 -0
  8. package/dist/batchPlan.d.ts.map +1 -0
  9. package/dist/batchPlan.js +85 -0
  10. package/dist/batchPlan.js.map +1 -0
  11. package/dist/claim.d.ts +161 -0
  12. package/dist/claim.d.ts.map +1 -0
  13. package/dist/claim.js +197 -0
  14. package/dist/claim.js.map +1 -0
  15. package/dist/decode.d.ts +22 -0
  16. package/dist/decode.d.ts.map +1 -0
  17. package/dist/decode.js +187 -0
  18. package/dist/decode.js.map +1 -0
  19. package/dist/describe.d.ts +26 -0
  20. package/dist/describe.d.ts.map +1 -0
  21. package/dist/describe.js +58 -0
  22. package/dist/describe.js.map +1 -0
  23. package/dist/fees.d.ts +89 -0
  24. package/dist/fees.d.ts.map +1 -0
  25. package/dist/fees.js +166 -0
  26. package/dist/fees.js.map +1 -0
  27. package/dist/history.d.ts +125 -0
  28. package/dist/history.d.ts.map +1 -0
  29. package/dist/history.js +231 -0
  30. package/dist/history.js.map +1 -0
  31. package/dist/historyHelius.d.ts +52 -0
  32. package/dist/historyHelius.d.ts.map +1 -0
  33. package/dist/historyHelius.js +125 -0
  34. package/dist/historyHelius.js.map +1 -0
  35. package/dist/layout.d.ts +2 -0
  36. package/dist/layout.d.ts.map +1 -1
  37. package/dist/layout.js +2 -2
  38. package/dist/layout.js.map +1 -1
  39. package/dist/liquidity/orca.d.ts +6 -0
  40. package/dist/liquidity/orca.d.ts.map +1 -1
  41. package/dist/liquidity/orca.js +14 -3
  42. package/dist/liquidity/orca.js.map +1 -1
  43. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  44. package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
  45. package/dist/liquidity/raydiumClmm.js +12 -5
  46. package/dist/liquidity/raydiumClmm.js.map +1 -1
  47. package/dist/lp.d.ts +55 -15
  48. package/dist/lp.d.ts.map +1 -1
  49. package/dist/lp.js +98 -46
  50. package/dist/lp.js.map +1 -1
  51. package/dist/lpMath.d.ts +22 -0
  52. package/dist/lpMath.d.ts.map +1 -1
  53. package/dist/lpMath.js +21 -0
  54. package/dist/lpMath.js.map +1 -1
  55. package/dist/names.d.ts +68 -0
  56. package/dist/names.d.ts.map +1 -0
  57. package/dist/names.js +257 -0
  58. package/dist/names.js.map +1 -0
  59. package/dist/pools.d.ts +9 -0
  60. package/dist/pools.d.ts.map +1 -0
  61. package/dist/pools.js +45 -0
  62. package/dist/pools.js.map +1 -0
  63. package/dist/portfolio.d.ts +64 -0
  64. package/dist/portfolio.d.ts.map +1 -0
  65. package/dist/portfolio.js +57 -0
  66. package/dist/portfolio.js.map +1 -0
  67. package/dist/price.d.ts +90 -0
  68. package/dist/price.d.ts.map +1 -0
  69. package/dist/price.js +287 -0
  70. package/dist/price.js.map +1 -0
  71. package/dist/programs.d.ts +19 -0
  72. package/dist/programs.d.ts.map +1 -0
  73. package/dist/programs.js +175 -0
  74. package/dist/programs.js.map +1 -0
  75. package/dist/reclaim.d.ts +190 -0
  76. package/dist/reclaim.d.ts.map +1 -0
  77. package/dist/reclaim.js +399 -0
  78. package/dist/reclaim.js.map +1 -0
  79. package/dist/risk.d.ts +84 -0
  80. package/dist/risk.d.ts.map +1 -0
  81. package/dist/risk.js +211 -0
  82. package/dist/risk.js.map +1 -0
  83. package/dist/rpc.d.ts +2 -0
  84. package/dist/rpc.d.ts.map +1 -1
  85. package/dist/rpc.js +36 -10
  86. package/dist/rpc.js.map +1 -1
  87. package/dist/simulate.d.ts +106 -0
  88. package/dist/simulate.d.ts.map +1 -0
  89. package/dist/simulate.js +418 -0
  90. package/dist/simulate.js.map +1 -0
  91. package/dist/subpaths/approvals.d.ts +29 -0
  92. package/dist/subpaths/approvals.d.ts.map +1 -0
  93. package/dist/subpaths/approvals.js +30 -0
  94. package/dist/subpaths/approvals.js.map +1 -0
  95. package/dist/subpaths/claim.d.ts +31 -0
  96. package/dist/subpaths/claim.d.ts.map +1 -0
  97. package/dist/subpaths/claim.js +31 -0
  98. package/dist/subpaths/claim.js.map +1 -0
  99. package/dist/subpaths/fees.d.ts +23 -0
  100. package/dist/subpaths/fees.d.ts.map +1 -0
  101. package/dist/subpaths/fees.js +25 -0
  102. package/dist/subpaths/fees.js.map +1 -0
  103. package/dist/subpaths/history.d.ts +21 -0
  104. package/dist/subpaths/history.d.ts.map +1 -0
  105. package/dist/subpaths/history.js +22 -0
  106. package/dist/subpaths/history.js.map +1 -0
  107. package/dist/subpaths/lp.d.ts +2 -2
  108. package/dist/subpaths/lp.d.ts.map +1 -1
  109. package/dist/subpaths/lp.js +2 -2
  110. package/dist/subpaths/lp.js.map +1 -1
  111. package/dist/subpaths/names.d.ts +37 -0
  112. package/dist/subpaths/names.d.ts.map +1 -0
  113. package/dist/subpaths/names.js +27 -0
  114. package/dist/subpaths/names.js.map +1 -0
  115. package/dist/subpaths/price.d.ts +40 -0
  116. package/dist/subpaths/price.d.ts.map +1 -0
  117. package/dist/subpaths/price.js +43 -0
  118. package/dist/subpaths/price.js.map +1 -0
  119. package/dist/subpaths/reclaim.d.ts +34 -0
  120. package/dist/subpaths/reclaim.d.ts.map +1 -0
  121. package/dist/subpaths/reclaim.js +34 -0
  122. package/dist/subpaths/reclaim.js.map +1 -0
  123. package/dist/subpaths/risk.d.ts +28 -0
  124. package/dist/subpaths/risk.d.ts.map +1 -0
  125. package/dist/subpaths/risk.js +30 -0
  126. package/dist/subpaths/risk.js.map +1 -0
  127. package/dist/subpaths/simulate.d.ts +22 -0
  128. package/dist/subpaths/simulate.d.ts.map +1 -0
  129. package/dist/subpaths/simulate.js +23 -0
  130. package/dist/subpaths/simulate.js.map +1 -0
  131. package/dist/subpaths/swap.d.ts +22 -0
  132. package/dist/subpaths/swap.d.ts.map +1 -0
  133. package/dist/subpaths/swap.js +21 -0
  134. package/dist/subpaths/swap.js.map +1 -0
  135. package/dist/subpaths/write.d.ts +1 -1
  136. package/dist/subpaths/write.d.ts.map +1 -1
  137. package/dist/subpaths/write.js +1 -1
  138. package/dist/subpaths/write.js.map +1 -1
  139. package/dist/swap/amm.d.ts +20 -0
  140. package/dist/swap/amm.d.ts.map +1 -0
  141. package/dist/swap/amm.js +77 -0
  142. package/dist/swap/amm.js.map +1 -0
  143. package/dist/swap/clmmMath.d.ts +24 -0
  144. package/dist/swap/clmmMath.d.ts.map +1 -0
  145. package/dist/swap/clmmMath.js +82 -0
  146. package/dist/swap/clmmMath.js.map +1 -0
  147. package/dist/swap/common.d.ts +68 -0
  148. package/dist/swap/common.d.ts.map +1 -0
  149. package/dist/swap/common.js +65 -0
  150. package/dist/swap/common.js.map +1 -0
  151. package/dist/swap/cpmm.d.ts +28 -0
  152. package/dist/swap/cpmm.d.ts.map +1 -0
  153. package/dist/swap/cpmm.js +113 -0
  154. package/dist/swap/cpmm.js.map +1 -0
  155. package/dist/swap/index.d.ts +145 -0
  156. package/dist/swap/index.d.ts.map +1 -0
  157. package/dist/swap/index.js +349 -0
  158. package/dist/swap/index.js.map +1 -0
  159. package/dist/swap/orca.d.ts +50 -0
  160. package/dist/swap/orca.d.ts.map +1 -0
  161. package/dist/swap/orca.js +382 -0
  162. package/dist/swap/orca.js.map +1 -0
  163. package/dist/swap/raydiumClmm.d.ts +62 -0
  164. package/dist/swap/raydiumClmm.d.ts.map +1 -0
  165. package/dist/swap/raydiumClmm.js +447 -0
  166. package/dist/swap/raydiumClmm.js.map +1 -0
  167. package/dist/tradeCheck.d.ts +45 -0
  168. package/dist/tradeCheck.d.ts.map +1 -0
  169. package/dist/tradeCheck.js +131 -0
  170. package/dist/tradeCheck.js.map +1 -0
  171. package/dist/write/instructions.d.ts +9 -0
  172. package/dist/write/instructions.d.ts.map +1 -1
  173. package/dist/write/instructions.js +40 -1
  174. package/dist/write/instructions.js.map +1 -1
  175. package/dist/write/transaction.d.ts +23 -0
  176. package/dist/write/transaction.d.ts.map +1 -1
  177. package/dist/write/transaction.js +50 -0
  178. package/dist/write/transaction.js.map +1 -1
  179. package/package.json +66 -14
  180. package/src/approvals.ts +358 -0
  181. package/src/batchPlan.ts +137 -0
  182. package/src/claim.ts +323 -0
  183. package/src/decode.ts +202 -0
  184. package/src/describe.ts +77 -0
  185. package/src/fees.ts +259 -0
  186. package/src/history.ts +336 -0
  187. package/src/historyHelius.ts +136 -0
  188. package/src/layout.ts +2 -2
  189. package/src/liquidity/orca.ts +15 -5
  190. package/src/liquidity/raydiumClmm.ts +13 -5
  191. package/src/lp.ts +148 -59
  192. package/src/lpMath.ts +38 -0
  193. package/src/names.ts +329 -0
  194. package/src/pools.ts +43 -0
  195. package/src/portfolio.ts +124 -0
  196. package/src/price.ts +366 -0
  197. package/src/programs.ts +216 -0
  198. package/src/reclaim.ts +547 -0
  199. package/src/risk.ts +243 -0
  200. package/src/rpc.ts +39 -10
  201. package/src/simulate.ts +535 -0
  202. package/src/subpaths/approvals.ts +47 -0
  203. package/src/subpaths/claim.ts +53 -0
  204. package/src/subpaths/fees.ts +41 -0
  205. package/src/subpaths/history.ts +38 -0
  206. package/src/subpaths/lp.ts +5 -1
  207. package/src/subpaths/names.ts +45 -0
  208. package/src/subpaths/price.ts +62 -0
  209. package/src/subpaths/reclaim.ts +57 -0
  210. package/src/subpaths/risk.ts +36 -0
  211. package/src/subpaths/simulate.ts +37 -0
  212. package/src/subpaths/swap.ts +41 -0
  213. package/src/subpaths/write.ts +2 -0
  214. package/src/swap/amm.ts +88 -0
  215. package/src/swap/clmmMath.ts +96 -0
  216. package/src/swap/common.ts +113 -0
  217. package/src/swap/cpmm.ts +132 -0
  218. package/src/swap/index.ts +467 -0
  219. package/src/swap/orca.ts +397 -0
  220. package/src/swap/raydiumClmm.ts +464 -0
  221. package/src/tradeCheck.ts +166 -0
  222. package/src/write/instructions.ts +43 -1
  223. package/src/write/transaction.ts +62 -0
@@ -0,0 +1,136 @@
1
+ /** Helius 增强交易历史数据源(需要应用自己的 API key):把 `/v0/addresses/{address}/transactions` 的结果整理成和 rpcHistory 一样的 Activity */
2
+ import { NATIVE_MINT, SOL_DECIMALS } from './constants.js'
3
+ import { formatUnits } from './layout.js'
4
+ import type { Activity, ActivityChange, ActivityType, HistorySource } from './history.js'
5
+
6
+ export interface HeliusHistoryOptions {
7
+ apiKey: string
8
+ /** 默认 https://api.helius.xyz */
9
+ baseUrl?: string
10
+ }
11
+
12
+ interface HeliusItem {
13
+ signature: string
14
+ timestamp?: number
15
+ slot?: number
16
+ fee?: number
17
+ feePayer?: string
18
+ type?: string
19
+ source?: string
20
+ description?: string
21
+ transactionError?: unknown
22
+ nativeTransfers?: Array<{ fromUserAccount: string; toUserAccount: string; amount: number }>
23
+ tokenTransfers?: Array<{ fromUserAccount: string | null; toUserAccount: string | null; tokenAmount: number; mint: string }>
24
+ accountData?: Array<{
25
+ account: string
26
+ nativeBalanceChange?: number
27
+ tokenBalanceChanges?: Array<{ userAccount: string; mint: string; rawTokenAmount: { tokenAmount: string; decimals: number } }>
28
+ }>
29
+ }
30
+
31
+ const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
32
+
33
+ /** Helius 的交易类型 → Activity 类型(认不出的按资产变化判断) */
34
+ function typeOf(item: HeliusItem, changes: readonly ActivityChange[]): ActivityType {
35
+ const t = item.type ?? ''
36
+ if (/SWAP/.test(t)) return 'swap'
37
+ if (t === 'TRANSFER') return 'transfer'
38
+ if (/LIQUIDITY|POSITION|POOL/.test(t)) return 'liquidity'
39
+ if (/MINT/.test(t)) return 'mint'
40
+ if (/APPROVE|REVOKE|DELEGATE/.test(t)) return 'approve'
41
+ const out = changes.filter((c) => c.delta.startsWith('-'))
42
+ if (out.length && changes.some((c) => !c.delta.startsWith('-') && out.some((o) => o.asset !== c.asset))) return 'swap'
43
+ return t && t !== 'UNKNOWN' ? 'contract' : 'other'
44
+ }
45
+
46
+ export function activityFromHelius(address: string, item: HeliusItem): Activity {
47
+ const me = item.accountData?.find((a) => a.account === address)
48
+ const isPayer = item.feePayer === address
49
+ const fee = BigInt(item.fee ?? 0)
50
+ let native = BigInt(me?.nativeBalanceChange ?? 0) + (isPayer ? fee : 0n)
51
+ const tokens = new Map<string, { delta: bigint; decimals: number }>()
52
+ for (const entry of item.accountData ?? []) {
53
+ for (const t of entry.tokenBalanceChanges ?? []) {
54
+ if (t.userAccount !== address) continue
55
+ const prev = tokens.get(t.mint)
56
+ tokens.set(t.mint, { delta: (prev?.delta ?? 0n) + BigInt(t.rawTokenAmount.tokenAmount), decimals: t.rawTokenAmount.decimals })
57
+ }
58
+ }
59
+ const wsol = tokens.get(NATIVE_MINT)
60
+ if (wsol) {
61
+ native += wsol.delta
62
+ tokens.delete(NATIVE_MINT)
63
+ }
64
+ const failed = item.transactionError !== null && item.transactionError !== undefined
65
+ const changes: ActivityChange[] = []
66
+ if (!failed) {
67
+ if (native !== 0n) changes.push({ asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
68
+ for (const [asset, { delta, decimals }] of tokens) {
69
+ if (delta !== 0n) changes.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
70
+ }
71
+ }
72
+ const type = typeOf(item, changes)
73
+ let counterparty: string | undefined
74
+ for (const t of [...(item.nativeTransfers ?? []), ...(item.tokenTransfers ?? []).map((x) => ({ fromUserAccount: x.fromUserAccount ?? '', toUserAccount: x.toUserAccount ?? '' }))]) {
75
+ if (t.fromUserAccount === address && t.toUserAccount) counterparty = t.toUserAccount
76
+ else if (t.toUserAccount === address && t.fromUserAccount) counterparty = t.fromUserAccount
77
+ if (counterparty) break
78
+ }
79
+ const negative = changes.filter((c) => c.delta.startsWith('-')).length
80
+ const protocol = item.source && !/^(SYSTEM_PROGRAM|UNKNOWN)$/.test(item.source) ? item.source[0] + item.source.slice(1).toLowerCase().replace(/_/g, ' ') : undefined
81
+ return {
82
+ hash: item.signature,
83
+ time: item.timestamp ?? 0,
84
+ block: item.slot,
85
+ status: failed ? 'failed' : 'success',
86
+ type,
87
+ direction: (type === 'transfer' || type === 'other') && changes.length ? (negative === 0 ? 'in' : negative === changes.length ? 'out' : undefined) : undefined,
88
+ counterparty,
89
+ label: item.description || undefined,
90
+ changes,
91
+ ...(isPayer ? { fee: fee.toString() } : {}),
92
+ protocol,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Helius 增强交易历史(`GET /v0/addresses/{address}/transactions`,按 before 签名翻页)。apiKey 由应用传入,库里不带。
98
+ * 按交易类型过滤时翻页最多再取 3 页
99
+ *
100
+ * ```ts
101
+ * await getHistory(address, { source: [heliusHistory({ apiKey }), rpcHistory()] }) // Helius 失败时用节点
102
+ * ```
103
+ */
104
+ export function heliusHistory(options: HeliusHistoryOptions): HistorySource {
105
+ if (!options.apiKey) throw new Error('heliusHistory needs an apiKey')
106
+ const base = (options.baseUrl ?? 'https://api.helius.xyz').replace(/\/+$/, '')
107
+ return {
108
+ name: 'helius',
109
+ async getHistory(ctx) {
110
+ const items: Activity[] = []
111
+ let cursor = ctx.cursor
112
+ let more = true
113
+ for (let page = 0; page < (ctx.types ? 4 : 1) && items.length < ctx.limit && more; page++) {
114
+ const url = `${base}/v0/addresses/${ctx.address}/transactions?api-key=${encodeURIComponent(options.apiKey)}&limit=${Math.min(100, ctx.limit)}${cursor ? `&before=${encodeURIComponent(cursor)}` : ''}`
115
+ const res = await ctx.fetch(url, { signal: ctx.signal })
116
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
117
+ const raw = (await res.json()) as HeliusItem[]
118
+ if (!Array.isArray(raw)) throw new Error('Unexpected response')
119
+ more = raw.length >= Math.min(100, ctx.limit)
120
+ for (const item of raw) {
121
+ cursor = item.signature
122
+ const t = item.timestamp ?? 0
123
+ if (ctx.before !== undefined && t >= ctx.before) continue
124
+ if (ctx.after !== undefined && t < ctx.after) {
125
+ more = false
126
+ break
127
+ }
128
+ const activity = activityFromHelius(ctx.address, item)
129
+ if (!ctx.types || ctx.types.includes(activity.type)) items.push(activity)
130
+ if (items.length >= ctx.limit) break
131
+ }
132
+ }
133
+ return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
134
+ },
135
+ }
136
+ }
package/src/layout.ts CHANGED
@@ -134,8 +134,8 @@ export function parseMint(data: Uint8Array): MintInfo {
134
134
  }
135
135
  }
136
136
 
137
- /** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null */
138
- function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
137
+ /** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null(包内使用,不从主入口导出) */
138
+ export function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
139
139
  if (data.length <= EXTENSIONS_OFFSET) {
140
140
  return null
141
141
  }
@@ -223,6 +223,20 @@ export async function orcaAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams |
223
223
  })
224
224
  }
225
225
 
226
+ /**
227
+ * 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭仓位账户和 NFT 的代币账户(nftAccount,默认 owner 的 ATA),租金退给 receiver。
228
+ * NFT 是 Token-2022(带元数据扩展)时连 mint 一起关闭,否则(Metaplex 时代的 SPL Token NFT)mint 留着
229
+ */
230
+ export function orcaClosePosition(owner: string, receiver: string, positionMint: string, nftProgram: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
231
+ const position = positionAddress(positionMint)
232
+ const token2022 = nftProgram === TOKEN_2022_PROGRAM_ID
233
+ return {
234
+ programId: PROGRAM,
235
+ keys: [r(owner, true), w(receiver), w(position), w(positionMint), w(nftAccount), r(nftProgram)],
236
+ data: anchorIx(token2022 ? 'close_position_with_token_extensions' : 'close_position'),
237
+ }
238
+ }
239
+
226
240
  /** 移除(decrease + 领取手续费和奖励)/ 只领取(collectOnly) */
227
241
  export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
228
242
  const mint = refMint(params.position)
@@ -304,11 +318,7 @@ export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams,
304
318
  })
305
319
  })
306
320
  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
- )
321
+ main.push(orcaClosePosition(ctx.owner, ctx.owner, mint, nftProgram))
312
322
  }
313
323
  return makePlan(ctx, {
314
324
  action: collectOnly ? 'collect' : 'remove',
@@ -249,6 +249,18 @@ export async function raydiumAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams
249
249
  })
250
250
  }
251
251
 
252
+ /**
253
+ * 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭 NFT 的代币账户和仓位账户,租金退给 owner(程序固定,不能指定别人)。
254
+ * pool 是仓位所在的池子(官方 SDK 也带着它)
255
+ */
256
+ export function raydiumClosePosition(owner: string, positionMint: string, nftProgram: string, pool: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
257
+ return {
258
+ programId: PROGRAM,
259
+ keys: [w(owner, true), w(positionMint), w(nftAccount), w(personalPositionAddress(positionMint)), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool)],
260
+ data: anchorIx('close_position'),
261
+ }
262
+ }
263
+
252
264
  /** 移除(liquidity > 0)/ 只领取手续费和奖励(collectOnly:流动性为 0 的 decrease) */
253
265
  export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
254
266
  const mint = refMint(params.position)
@@ -308,11 +320,7 @@ export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParam
308
320
  }
309
321
  const main: Instruction[] = [decrease]
310
322
  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
- })
323
+ main.push(raydiumClosePosition(ctx.owner, mint, nftProgram, pool.address))
316
324
  }
317
325
  return makePlan(ctx, {
318
326
  action: collectOnly ? 'collect' : 'remove',
package/src/lp.ts CHANGED
@@ -5,7 +5,20 @@ 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'
21
+ import { defillamaPrices, type SolanaPriceSource } from './price.js'
9
22
 
10
23
  export const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK'
11
24
  export const ORCA_WHIRLPOOL_PROGRAM_ID = 'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc'
@@ -25,45 +38,8 @@ export const SOLANA_LP_PROTOCOLS: Readonly<Record<SolanaLpProtocol, { name: stri
25
38
  'raydium-cpmm': { name: 'Raydium CPMM', type: 'amm', programId: RAYDIUM_CPMM_PROGRAM_ID },
26
39
  }
27
40
 
28
- // ---------------------------------------------------------------------------
29
- // 价格源
30
- // ---------------------------------------------------------------------------
31
-
32
- export interface SolanaPriceSource {
33
- readonly name: string
34
- /** 返回 mint → 美元价格;没有价格的不返回 */
35
- prices(ctx: { mints: readonly string[]; fetch: typeof fetch; signal?: AbortSignal }): Promise<Map<string, number>>
36
- }
37
-
38
- /** DefiLlama 价格(免费免 Key,按 100 个一批查询);confidence 低于 minConfidence(默认 0.9)的价格不用 */
39
- export function defillamaPrices(options: { minConfidence?: number } = {}): SolanaPriceSource {
40
- const minConfidence = options.minConfidence ?? 0.9
41
- return {
42
- name: 'defillama',
43
- async prices({ mints, fetch: fetchFn, signal }) {
44
- const result = new Map<string, number>()
45
- const batches: string[][] = []
46
- for (let i = 0; i < mints.length; i += 100) {
47
- batches.push(mints.slice(i, i + 100))
48
- }
49
- await Promise.all(
50
- batches.map(async (batch) => {
51
- const res = await fetchFn(`https://coins.llama.fi/prices/current/${batch.map((m) => `solana:${m}`).join(',')}`, { signal })
52
- if (!res.ok) {
53
- throw new Error(`DefiLlama prices: HTTP ${res.status}`)
54
- }
55
- const body = (await res.json()) as { coins?: Record<string, { price?: number; confidence?: number }> }
56
- for (const [key, coin] of Object.entries(body.coins ?? {})) {
57
- if (typeof coin.price === 'number' && Number.isFinite(coin.price) && (coin.confidence === undefined || coin.confidence >= minConfidence)) {
58
- result.set(key.slice(key.indexOf(':') + 1), coin.price)
59
- }
60
- }
61
- }),
62
- )
63
- return result
64
- },
65
- }
66
- }
41
+ // 价格源在 price.ts(子路径 /price);这里继续导出,旧的 import 不受影响
42
+ export { defillamaPrices, type SolanaPriceSource, type SolanaPriceSourceContext } from './price.js'
67
43
 
68
44
  // ---------------------------------------------------------------------------
69
45
  // 结果类型
@@ -133,6 +109,24 @@ export interface SolanaLpClmmPosition extends SolanaLpPositionBase {
133
109
  fees1Formatted: string | null
134
110
  /** 开启 prices 时:未领取手续费的美元价值(手续费为 null 或缺价格时为 null) */
135
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
136
130
  }
137
131
 
138
132
  export type SolanaLpPosition = SolanaLpAmmPosition | SolanaLpClmmPosition
@@ -152,7 +146,14 @@ export interface LpPositionsOptions extends RequestOptions {
152
146
  pools?: readonly string[]
153
147
  /** 返回已清空的仓位(流动性为 0、手续费也领完,NFT 还在)。默认 false */
154
148
  includeClosed?: boolean
155
- /** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource。默认不查 */
149
+ /**
150
+ * 集中流动性仓位同时算未领取的挖矿奖励(Raydium CLMM 农场奖励、Orca 的奖励 slot),放在 rewards 字段,照搬程序的结算:
151
+ * 池子的奖励增长按时间摊给当前流动性,再按仓位区间内的增长计算。默认 false。有奖励没领的仓位不算“已清空”
152
+ */
153
+ rewards?: boolean
154
+ /** rewards 结算到哪个时刻(unix 秒),默认当前时间;链上用的是区块时间,差几秒影响很小 */
155
+ now?: number
156
+ /** 查美元价值:true 用 DefiLlama,也可以传自己的 SolanaPriceSource(如 /price 的 combinePrices([defillamaPrices(), onchainPrices()]))。默认不查 */
156
157
  prices?: boolean | SolanaPriceSource
157
158
  /**
158
159
  * 价格源出错时调用(服务不可用、被拦截等):这时照样返回仓位,只是 valueUsd / feesUsd 为 null,
@@ -196,6 +197,7 @@ export class Bytes {
196
197
  }
197
198
  }
198
199
 
200
+ const ZERO_ADDRESS = '11111111111111111111111111111111'
199
201
  const RAYDIUM_POSITION_SIZE = 281
200
202
  const ORCA_POSITION_SIZE = 216
201
203
  /** 池子账户至少要读到的长度(Raydium PoolState 实际 1544 字节,Orca Whirlpool 653 字节) */
@@ -208,7 +210,7 @@ const RAYDIUM_TICK_SIZE = 168
208
210
  const ORCA_TICK_ARRAY_SIZE = 88
209
211
  const ORCA_TICK_SIZE = 113
210
212
 
211
- interface ClmmPositionState {
213
+ export interface ClmmPositionState {
212
214
  protocol: 'raydium-clmm' | 'orca-whirlpool'
213
215
  address: string
214
216
  mint: string
@@ -220,8 +222,12 @@ interface ClmmPositionState {
220
222
  growthLast1: bigint
221
223
  owed0: bigint
222
224
  owed1: bigint
225
+ /** 三个挖矿奖励:上次记录的区间内增长、已记账未领取的数量 */
226
+ rewards: Array<{ growthLast: bigint; owed: bigint }>
223
227
  }
224
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
+
225
231
  function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
226
232
  return {
227
233
  protocol: 'raydium-clmm',
@@ -235,6 +241,7 @@ function raydiumPosition(address: string, b: Bytes): ClmmPositionState {
235
241
  growthLast1: b.u128(113),
236
242
  owed0: b.u64(129),
237
243
  owed1: b.u64(137),
244
+ rewards: positionRewards(b, 145),
238
245
  }
239
246
  }
240
247
 
@@ -251,9 +258,21 @@ function orcaPosition(address: string, b: Bytes): ClmmPositionState {
251
258
  owed0: b.u64(112),
252
259
  growthLast1: b.u128(120),
253
260
  owed1: b.u64(136),
261
+ rewards: positionRewards(b, 144),
254
262
  }
255
263
  }
256
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
+
257
276
  interface ClmmPoolState {
258
277
  mint0: string
259
278
  mint1: string
@@ -267,6 +286,41 @@ interface ClmmPoolState {
267
286
  /** Orca:池子里的费率;Raydium:在 AmmConfig 里,ammConfig 为它的地址 */
268
287
  fee: number | null
269
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
+ })
270
324
  }
271
325
 
272
326
  function raydiumPool(b: Bytes): ClmmPoolState {
@@ -282,6 +336,8 @@ function raydiumPool(b: Bytes): ClmmPoolState {
282
336
  global0: b.u128(277),
283
337
  global1: b.u128(293),
284
338
  fee: null,
339
+ liquidity: b.u128(237),
340
+ rewards: raydiumRewards(b),
285
341
  }
286
342
  }
287
343
 
@@ -298,6 +354,8 @@ function orcaPool(b: Bytes): ClmmPoolState {
298
354
  global1: b.u128(245),
299
355
  decimals0: null,
300
356
  decimals1: null,
357
+ liquidity: b.u128(49),
358
+ rewards: orcaRewards(b),
301
359
  }
302
360
  }
303
361
 
@@ -382,7 +440,9 @@ export function orcaTickArray(pool: string, tick: number, spacing: number): { ad
382
440
  return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), utf8(String(start))], ORCA_WHIRLPOOL_PROGRAM_ID), start }
383
441
  }
384
442
 
385
- 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 {
386
446
  raydiumDiscriminator ??= discriminator('TickArrayState')
387
447
  const offset = 44 + ((tick - start) / spacing) * RAYDIUM_TICK_SIZE
388
448
  // 先认判别符、查长度再读字段(PDA 上可能是别的账户,如被转过 SOL 的系统账户)
@@ -394,7 +454,7 @@ function raydiumTick(account: AccountInfo, start: number, tick: number, spacing:
394
454
  return null
395
455
  }
396
456
  // Raydium 不区分是否初始化:没用过的 tick 各项都是 0,按程序的公式算出来就是对的
397
- 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) }
398
458
  }
399
459
 
400
460
  /** Anchor 账户判别符:sha256("account:<名称>") 的前 8 字节 */
@@ -403,7 +463,7 @@ let orcaDiscriminators: { fixed: Uint8Array; dynamic: Uint8Array } | undefined
403
463
  let raydiumDiscriminator: Uint8Array | undefined
404
464
  const startsWith = (data: Uint8Array, prefix: Uint8Array) => data.length >= prefix.length && prefix.every((byte, i) => data[i] === byte)
405
465
 
406
- 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 {
407
467
  orcaDiscriminators ??= { fixed: discriminator('TickArray'), dynamic: discriminator('DynamicTickArray') }
408
468
  if (account.data.length < 60) {
409
469
  return null // 两种格式的头部(判别符 + start + …)都放不下
@@ -416,7 +476,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
416
476
  if (b.i32(8) !== start || offset + ORCA_TICK_SIZE > account.data.length) {
417
477
  return null
418
478
  }
419
- 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) }
420
480
  }
421
481
  if (startsWith(account.data, orcaDiscriminators.dynamic)) {
422
482
  // 可变大小:start i32 | whirlpool | tick_bitmap u128 | ticks(未初始化 1 字节 0;初始化 1 字节 1 + 112 字节数据)
@@ -439,7 +499,7 @@ function orcaTick(account: AccountInfo, start: number, tick: number, spacing: nu
439
499
  if (offset + 113 > account.data.length) {
440
500
  return null
441
501
  }
442
- 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) }
443
503
  }
444
504
  return null
445
505
  }
@@ -462,6 +522,8 @@ interface RawClmm {
462
522
  amount1: bigint
463
523
  fees0: bigint | null
464
524
  fees1: bigint | null
525
+ /** rewards 选项开启时:每个已初始化的奖励的未领取数量(读不到 tick 时为 null) */
526
+ rewards?: Array<{ index: number; mint: string; amount: bigint | null }>
465
527
  }
466
528
 
467
529
  interface RawAmm {
@@ -534,11 +596,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
534
596
  if (!account) {
535
597
  return
536
598
  }
537
- const program = SOLANA_LP_PROTOCOLS[c.protocol].programId
538
- if (c.protocol === 'raydium-clmm' && account.owner === program && account.data.length === RAYDIUM_POSITION_SIZE) {
539
- positions.push(raydiumPosition(c.address, new Bytes(account.data)))
540
- } else if (c.protocol === 'orca-whirlpool' && account.owner === program && account.data.length === ORCA_POSITION_SIZE) {
541
- 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)
542
602
  }
543
603
  })
544
604
  // LP mint 的权限是 AMM v4 / CPMM 的固定权限地址时才去找池子
@@ -641,10 +701,32 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
641
701
  fees1 = null
642
702
  }
643
703
  }
644
- 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)) {
645
727
  continue
646
728
  }
647
- 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 })
648
730
  }
649
731
  for (const pool of ammHeld) {
650
732
  const balance = held.get(pool.lpMint)?.amount ?? 0n
@@ -671,9 +753,9 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
671
753
 
672
754
  /**
673
755
  * 节点不提供这个方法(方法不存在、被拦截、需要付费 / Token、403 等):这时报错里说明怎么绕过。
674
- * 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出
756
+ * 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出(资产总值 portfolio 也用)
675
757
  */
676
- function methodUnavailable(err: unknown): boolean {
758
+ export function methodUnavailable(err: unknown): boolean {
677
759
  if (err instanceof AllNodesFailedError) {
678
760
  return err.errors.length > 0 && err.errors.every((e) => methodUnavailable(e.error))
679
761
  }
@@ -742,7 +824,7 @@ async function findAmmPool(client: SolanaClient, protocol: 'raydium-amm' | 'rayd
742
824
 
743
825
  async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[], options: LpPositionsOptions): Promise<SolanaLpPosition[]> {
744
826
  const { signal } = options
745
- 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) : [])]))]
746
828
  const priceSource = options.prices === true ? defillamaPrices() : options.prices || null
747
829
  const fetchFn = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
748
830
  // 价格源出错(服务不可用、被拦截、同步抛错等)时照样返回仓位,只是不带价格;取消查询时照常 reject
@@ -751,8 +833,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
751
833
  return new Map()
752
834
  }
753
835
  try {
754
- // wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112 查得到
755
- return await priceSource.prices({ mints, fetch: fetchFn, signal })
836
+ // wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112 查得到;client 给链上价格源读池子
837
+ return await priceSource.prices({ mints, fetch: fetchFn, signal, client })
756
838
  } catch (err) {
757
839
  if (signal?.aborted) {
758
840
  throw err
@@ -820,6 +902,11 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
820
902
  const d1 = token1.decimals
821
903
  const fees0Formatted = r.fees0 === null ? null : fmt(r.fees0, token0)
822
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
+ })
823
910
  return {
824
911
  ...base,
825
912
  type: 'clmm',
@@ -840,6 +927,8 @@ async function finalize(client: SolanaClient, raw: readonly (RawClmm | RawAmm)[]
840
927
  fees0Formatted,
841
928
  fees1Formatted,
842
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) } : {}),
843
932
  }
844
933
  })
845
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 级别的整数转换丢精度后再平方