@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
package/src/price.ts ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * 价格(子路径 `@w3lib/solana-batch-call/price`):DefiLlama(当前 / 历史)、链上池子价格(Raydium CLMM / CPMM、Orca Whirlpool,按 PDA 推导池子地址,
3
+ * 不需要 getProgramAccounts)、组合多个价格源。价格只用于展示和估值,不要用来计算交易的最小数量
4
+ */
5
+ import { isAddress } from './address.js'
6
+ import type { RequestOptions, SolanaClient } from './client.js'
7
+ import { NATIVE_MINT } from './constants.js'
8
+ import { formatUnits, parseMint, parseTokenAccount, type AccountInfo } from './layout.js'
9
+ import { Bytes, ORCA_WHIRLPOOL_PROGRAM_ID, RAYDIUM_CLMM_PROGRAM_ID, parseAmmPool } from './lp.js'
10
+ import { sqrtPriceToPrice } from './lpMath.js'
11
+ import { poolAddresses, type PdaPoolProtocol } from './pools.js'
12
+
13
+ export interface SolanaPriceSourceContext {
14
+ /** 代币 mint(原生 SOL 用 NATIVE_MINT So111…112 查) */
15
+ mints: readonly string[]
16
+ fetch: typeof fetch
17
+ signal?: AbortSignal
18
+ /** 查询用的客户端(链上价格源 onchainPrices 用它读池子) */
19
+ client?: SolanaClient
20
+ }
21
+
22
+ export interface SolanaPriceSource {
23
+ readonly name: string
24
+ /** 返回 mint → 美元价格;没有价格的不返回 */
25
+ prices(ctx: SolanaPriceSourceContext): Promise<Map<string, number>>
26
+ }
27
+
28
+ export interface DefillamaPriceOptions {
29
+ /** 置信度下限(0~1),DefiLlama 给出的置信度低于它视为没有价格,默认 0.9 */
30
+ minConfidence?: number
31
+ /** 历史价格:这个时间点(unix 秒或 Date)附近的价格;不传为当前价格 */
32
+ at?: number | Date
33
+ }
34
+
35
+ /**
36
+ * DefiLlama 价格(免费、不需要 Key,按 100 个一批查询)。
37
+ * 注意:流动性很差的代币价格可能虚高,展示总资产时建议配合 minUsd 或自己的价格源。
38
+ *
39
+ * ```ts
40
+ * defillamaPrices() // 当前价格
41
+ * defillamaPrices({ at: new Date('2026-01-01') }) // 历史价格
42
+ * ```
43
+ */
44
+ export function defillamaPrices(options: DefillamaPriceOptions = {}): SolanaPriceSource {
45
+ const minConfidence = options.minConfidence ?? 0.9
46
+ const at = options.at instanceof Date ? Math.floor(options.at.getTime() / 1000) : options.at === undefined ? undefined : Math.floor(options.at)
47
+ const endpoint = at === undefined ? 'https://coins.llama.fi/prices/current' : `https://coins.llama.fi/prices/historical/${at}`
48
+ return {
49
+ name: at === undefined ? 'defillama' : `defillama@${at}`,
50
+ async prices({ mints, fetch: fetchFn, signal }) {
51
+ const result = new Map<string, number>()
52
+ const batches: string[][] = []
53
+ for (let i = 0; i < mints.length; i += 100) {
54
+ batches.push(mints.slice(i, i + 100))
55
+ }
56
+ await Promise.all(
57
+ batches.map(async (batch) => {
58
+ const res = await fetchFn(`${endpoint}/${batch.map((m) => `solana:${m}`).join(',')}`, { signal })
59
+ if (!res.ok) {
60
+ throw new Error(`DefiLlama prices: HTTP ${res.status}`)
61
+ }
62
+ const body = (await res.json()) as { coins?: Record<string, { price?: number; confidence?: number }> }
63
+ for (const [key, coin] of Object.entries(body.coins ?? {})) {
64
+ if (typeof coin.price === 'number' && Number.isFinite(coin.price) && (coin.confidence === undefined || coin.confidence >= minConfidence)) {
65
+ result.set(key.slice(key.indexOf(':') + 1), coin.price)
66
+ }
67
+ }
68
+ }),
69
+ )
70
+ return result
71
+ },
72
+ }
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // 组合
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /**
80
+ * 依次使用多个价格源:前面的源没给出价格的代币交给下一个源。某个源出错时跳过它(错误交给 onError;取消查询时照常 reject)。
81
+ *
82
+ * ```ts
83
+ * combinePrices([defillamaPrices(), onchainPrices()])
84
+ * ```
85
+ */
86
+ export function combinePrices(sources: readonly SolanaPriceSource[], options: { onError?: (source: SolanaPriceSource, err: unknown) => void } = {}): SolanaPriceSource {
87
+ return {
88
+ name: sources.map((s) => s.name).join('+'),
89
+ async prices(ctx) {
90
+ const found = await runSources(sources, ctx, options.onError)
91
+ return new Map([...found].map(([k, v]) => [k, v.price]))
92
+ },
93
+ }
94
+ }
95
+
96
+ /** 依次查询,记下每个价格来自哪个源 */
97
+ async function runSources(
98
+ sources: readonly SolanaPriceSource[],
99
+ ctx: SolanaPriceSourceContext,
100
+ onError?: (source: SolanaPriceSource, err: unknown) => void,
101
+ ): Promise<Map<string, { price: number; source: string }>> {
102
+ const result = new Map<string, { price: number; source: string }>()
103
+ let mints = [...ctx.mints]
104
+ for (const source of sources) {
105
+ if (!mints.length) break
106
+ let found: Map<string, number>
107
+ try {
108
+ found = await source.prices({ ...ctx, mints })
109
+ } catch (err) {
110
+ if (ctx.signal?.aborted) {
111
+ throw err
112
+ }
113
+ try {
114
+ onError?.(source, err)
115
+ } catch {
116
+ // 回调出错不影响查询结果
117
+ }
118
+ continue
119
+ }
120
+ for (const [mint, price] of found) {
121
+ if (!result.has(mint) && Number.isFinite(price)) result.set(mint, { price, source: source.name })
122
+ }
123
+ mints = mints.filter((m) => !result.has(m))
124
+ }
125
+ return result
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // 链上价格
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /** 按 1 美元计的稳定币(链上价格的计价锚点之一;另一个是 Wrapped SOL):USDC、USDT */
133
+ export const STABLECOINS: readonly string[] = ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB']
134
+
135
+ export type OnchainPriceProtocol = PdaPoolProtocol
136
+
137
+ export interface OnchainPriceOptions {
138
+ /** 池子里计价币一侧的价值(美元)下限,低于它的池子不用(防止小池子的价格被操纵),默认 10000 */
139
+ minLiquidityUsd?: number
140
+ /** 按 1 美元计的稳定币,默认 STABLECOINS(USDC、USDT) */
141
+ stablecoins?: readonly string[]
142
+ /** 用哪些协议的池子,默认全部:'raydium-clmm'、'orca-whirlpool'、'raydium-cpmm' */
143
+ protocols?: readonly OnchainPriceProtocol[]
144
+ }
145
+
146
+ /** 账户至少要读到的长度(Raydium PoolState 1544 字节,Orca Whirlpool 653 字节) */
147
+ const RAYDIUM_POOL_SIZE = 1544
148
+ const ORCA_POOL_SIZE = 653
149
+
150
+ interface Candidate {
151
+ protocol: OnchainPriceProtocol
152
+ address: string
153
+ token: string
154
+ anchor: string
155
+ }
156
+
157
+ /** 两个 mint 在所有协议里可能的池子地址 */
158
+ function candidates(token: string, anchor: string, protocols: ReadonlySet<OnchainPriceProtocol>): Candidate[] {
159
+ return poolAddresses(token, anchor, protocols).map((p) => ({ ...p, token, anchor }))
160
+ }
161
+
162
+ /** 池子里定价要用的字段:集中流动性读当前价格和活跃流动性,CPMM 读金库扣掉手续费后的余额 */
163
+ type PoolRead =
164
+ | { kind: 'clmm'; mint0: string; mint1: string; vault0: string; vault1: string; sqrtPrice: bigint; liquidity: bigint }
165
+ | { kind: 'cpmm'; mint0: string; mint1: string; vault0: string; vault1: string; reserved0: bigint; reserved1: bigint }
166
+
167
+ function readPool(c: Candidate, account: AccountInfo): PoolRead | null {
168
+ const b = new Bytes(account.data)
169
+ if (c.protocol === 'raydium-clmm' && account.owner === RAYDIUM_CLMM_PROGRAM_ID && account.data.length >= RAYDIUM_POOL_SIZE) {
170
+ // PoolState:mint 73 / 105、vault 137 / 169、liquidity 237、sqrt_price 253
171
+ return { kind: 'clmm', mint0: b.key(73), mint1: b.key(105), vault0: b.key(137), vault1: b.key(169), liquidity: b.u128(237), sqrtPrice: b.u128(253) }
172
+ }
173
+ if (c.protocol === 'orca-whirlpool' && account.owner === ORCA_WHIRLPOOL_PROGRAM_ID && account.data.length >= ORCA_POOL_SIZE) {
174
+ // Whirlpool:liquidity 49、sqrt_price 65、mint_a 101、vault_a 133、mint_b 181、vault_b 213
175
+ return { kind: 'clmm', mint0: b.key(101), mint1: b.key(181), vault0: b.key(133), vault1: b.key(213), liquidity: b.u128(49), sqrtPrice: b.u128(65) }
176
+ }
177
+ if (c.protocol === 'raydium-cpmm') {
178
+ const pool = parseAmmPool(account)
179
+ return pool?.protocol === 'raydium-cpmm'
180
+ ? { kind: 'cpmm', mint0: pool.mint0, mint1: pool.mint1, vault0: pool.vault0, vault1: pool.vault1, reserved0: pool.reserved0, reserved1: pool.reserved1 }
181
+ : null
182
+ }
183
+ return null
184
+ }
185
+
186
+ interface Quote {
187
+ token: string
188
+ anchor: string
189
+ /** 1 个代币值多少个计价币 */
190
+ price: number
191
+ /** 池子里计价币的数量(按精度换算) */
192
+ depth: number
193
+ }
194
+
195
+ /**
196
+ * 链上价格:按 PDA 推导代币对 Wrapped SOL、稳定币的池子(Raydium CLMM 的每个 AmmConfig、Orca 的每个费率档位、Raydium CPMM 的每个 AmmConfig),
197
+ * 用计价币一侧最深的池子的价格。稳定币按 1 美元;Wrapped SOL(和原生 SOL)按它对稳定币最深的池子。
198
+ * 不需要 getProgramAccounts(免费公共节点大多不支持);每个代币约推导 190 个地址,读取用 getMultipleAccounts 合并(一共两轮:池子和 mint、金库)。
199
+ * 深度:CPMM 是金库余额;集中流动性取金库余额和当前活跃流动性折算的储备中较小的(流动性都在区间外的池子价格不可信,不会被选中)。
200
+ * 适合 DefiLlama 没有收录的新币;价格是池子当前的价格,可能被单个区块内的交易影响,只用于展示。
201
+ * Raydium AMM v4、Meteora、PumpSwap 等池子地址不能只靠两个 mint 推导,不在查找范围内
202
+ */
203
+ export function onchainPrices(options: OnchainPriceOptions = {}): SolanaPriceSource {
204
+ return {
205
+ name: 'onchain',
206
+ async prices({ mints, client, signal }) {
207
+ if (!client) throw new Error('onchainPrices needs a SolanaClient (ctx.client)')
208
+ const stables = options.stablecoins ?? STABLECOINS
209
+ const protocols = new Set(options.protocols ?? (['raydium-clmm', 'orca-whirlpool', 'raydium-cpmm'] as const))
210
+ const minLiquidity = options.minLiquidityUsd ?? 10_000
211
+ const result = new Map<string, number>()
212
+ const stableSet = new Set(stables)
213
+ for (const m of mints) if (stableSet.has(m)) result.set(m, 1)
214
+ const rest = [...new Set(mints)].filter((m) => !stableSet.has(m) && m !== NATIVE_MINT && isAddress(m))
215
+ const wantSol = mints.includes(NATIVE_MINT)
216
+ if (!stables.length || !protocols.size || (!rest.length && !wantSol)) return result
217
+
218
+ // 1. 一次读:Wrapped SOL 对稳定币、其他代币对 Wrapped SOL 和稳定币的候选池子,以及这些代币的 mint(精度)
219
+ const list = [
220
+ ...stables.flatMap((s) => candidates(NATIVE_MINT, s, protocols)),
221
+ ...rest.flatMap((t) => [NATIVE_MINT, ...stables].flatMap((anchor) => candidates(t, anchor, protocols))),
222
+ ]
223
+ const mintList = [...new Set([NATIVE_MINT, ...stables, ...rest])]
224
+ const first = await client.accounts([...list.map((c) => c.address), ...mintList], { signal })
225
+ const decimals = new Map<string, number>()
226
+ mintList.forEach((m, i) => {
227
+ const account = first[list.length + i]
228
+ try {
229
+ if (account) decimals.set(m, parseMint(account.data).decimals)
230
+ } catch {
231
+ // 不是代币 mint
232
+ }
233
+ })
234
+ const pools = list.flatMap((c, i) => {
235
+ const account = first[i]
236
+ const pool = account ? readPool(c, account) : null
237
+ // 池子里的两个 mint 要和推导时的一致(PDA 上可能是别的账户)
238
+ const ok = pool && ((pool.mint0 === c.token && pool.mint1 === c.anchor) || (pool.mint1 === c.token && pool.mint0 === c.anchor))
239
+ return ok ? [{ ...c, pool }] : []
240
+ })
241
+ if (!pools.length) return result
242
+
243
+ // 2. 一次读:金库(集中流动性只读计价币一侧,CPMM 两侧都读)
244
+ const vaultsOf = (p: (typeof pools)[number]) => {
245
+ const anchorIs0 = p.pool.mint0 === p.anchor
246
+ return p.pool.kind === 'cpmm' ? [p.pool.vault0, p.pool.vault1] : [anchorIs0 ? p.pool.vault0 : p.pool.vault1]
247
+ }
248
+ const vaultAddresses = [...new Set(pools.flatMap(vaultsOf))]
249
+ const vaultAccounts = await client.accounts(vaultAddresses, { signal })
250
+ const balances = new Map<string, bigint>()
251
+ vaultAddresses.forEach((address, i) => {
252
+ const account = vaultAccounts[i]
253
+ try {
254
+ if (account) balances.set(address, parseTokenAccount(account.data).amount)
255
+ } catch {
256
+ // 不是代币账户
257
+ }
258
+ })
259
+ const quotes = pools.flatMap((p): Quote[] => {
260
+ const q = quote(p.pool, p.token, p.anchor, decimals, balances)
261
+ return q && Number.isFinite(q.price) && q.price > 0 && Number.isFinite(q.depth) ? [q] : []
262
+ })
263
+
264
+ // 3. 先算 Wrapped SOL(对稳定币),再按 SOL 和稳定币的美元价格算其他代币
265
+ const anchorsUsd = new Map(stables.map((s) => [s, 1]))
266
+ const sol = best(quotes.filter((q) => q.token === NATIVE_MINT), anchorsUsd, minLiquidity)
267
+ if (sol !== null) {
268
+ anchorsUsd.set(NATIVE_MINT, sol)
269
+ if (wantSol) result.set(NATIVE_MINT, sol)
270
+ }
271
+ for (const t of rest) {
272
+ const usd = best(quotes.filter((q) => q.token === t), anchorsUsd, minLiquidity)
273
+ if (usd !== null) result.set(t, usd)
274
+ }
275
+ return result
276
+ },
277
+ }
278
+ }
279
+
280
+ /** 一个池子给出的价格(计价币 / 代币)和计价币一侧的深度;缺精度、金库时为 null */
281
+ function quote(pool: PoolRead, token: string, anchor: string, decimals: ReadonlyMap<string, number>, balances: ReadonlyMap<string, bigint>): Quote | null {
282
+ const d0 = decimals.get(pool.mint0)
283
+ const d1 = decimals.get(pool.mint1)
284
+ if (d0 === undefined || d1 === undefined) return null
285
+ const tokenIs0 = pool.mint0 === token
286
+ const da = tokenIs0 ? d1 : d0
287
+ if (pool.kind === 'cpmm') {
288
+ const v0 = balances.get(pool.vault0)
289
+ const v1 = balances.get(pool.vault1)
290
+ if (v0 === undefined || v1 === undefined) return null
291
+ // 与程序一致:金库余额扣掉协议 / 基金 / 创建者手续费才是储备
292
+ const r0 = v0 > pool.reserved0 ? v0 - pool.reserved0 : 0n
293
+ const r1 = v1 > pool.reserved1 ? v1 - pool.reserved1 : 0n
294
+ if (r0 === 0n || r1 === 0n) return null
295
+ const [rt, ra, dt] = tokenIs0 ? [r0, r1, d0] : [r1, r0, d1]
296
+ const depth = Number(formatUnits(ra, da))
297
+ return { token, anchor, price: depth / Number(formatUnits(rt, dt)), depth }
298
+ }
299
+ const vault = balances.get(tokenIs0 ? pool.vault1 : pool.vault0)
300
+ if (vault === undefined || pool.sqrtPrice === 0n) return null
301
+ // price = token1 / token0(按精度换算)
302
+ const price1Per0 = sqrtPriceToPrice(pool.sqrtPrice, d0, d1)
303
+ // 当前活跃流动性折算的计价币储备(最小单位):token1 = L × √P,token0 = L / √P(√P = sqrtPrice / 2^64)
304
+ const sqrt = Number(pool.sqrtPrice) / 2 ** 64
305
+ const virtual = (tokenIs0 ? Number(pool.liquidity) * sqrt : Number(pool.liquidity) / sqrt) / 10 ** da
306
+ return { token, anchor, price: tokenIs0 ? price1Per0 : 1 / price1Per0, depth: Math.min(Number(formatUnits(vault, da)), virtual) }
307
+ }
308
+
309
+ /** 深度(美元)达到下限的池子里最深的一个给出的美元价格;没有时为 null */
310
+ function best(quotes: readonly Quote[], anchorsUsd: ReadonlyMap<string, number>, minLiquidityUsd: number): number | null {
311
+ let found: { usd: number; depthUsd: number } | null = null
312
+ for (const q of quotes) {
313
+ const anchorUsd = anchorsUsd.get(q.anchor)
314
+ if (anchorUsd === undefined) continue
315
+ const depthUsd = q.depth * anchorUsd
316
+ if (depthUsd >= minLiquidityUsd && (!found || depthUsd > found.depthUsd)) found = { usd: q.price * anchorUsd, depthUsd }
317
+ }
318
+ return found ? found.usd : null
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // tokenPrices
323
+ // ---------------------------------------------------------------------------
324
+
325
+ export interface TokenPricesOptions extends RequestOptions {
326
+ /** 价格源,依次使用(前面没有价格的交给后面的),默认 [defillamaPrices(), onchainPrices()] */
327
+ sources?: readonly SolanaPriceSource[]
328
+ /** 某个价格源出错时调用(继续用下一个源);回调本身出错会被忽略 */
329
+ onError?: (source: SolanaPriceSource, err: unknown) => void
330
+ /** 查价格用的 fetch(代理等场景),默认全局 fetch */
331
+ fetch?: typeof fetch
332
+ }
333
+
334
+ export interface TokenPrice {
335
+ /** 传入的 mint(原样) */
336
+ token: string
337
+ /** 按原生 SOL 处理(客户端的 nativeMints,默认 So111…112 和 11111…1),价格就是 SOL 的价格 */
338
+ native: boolean
339
+ /** 美元价格;所有价格源都没有时为 null */
340
+ price: number | null
341
+ /** 给出价格的价格源(如 'defillama'、'onchain') */
342
+ source: string | null
343
+ }
344
+
345
+ /**
346
+ * 查代币的美元价格(SOL 用 NATIVE_MINT)。默认先查 DefiLlama,没有收录的再从链上池子算。
347
+ *
348
+ * ```ts
349
+ * await tokenPrices(client, [NATIVE_MINT, USDC, newToken])
350
+ * // [{ token: 'So111…112', native: true, price: 187.4, source: 'defillama' }, …, { token: newToken, price: 0.0012, source: 'onchain' }]
351
+ * await tokenPrices(client, mints, { sources: [defillamaPrices({ at: new Date('2026-01-01') })] }) // 历史价格
352
+ * ```
353
+ */
354
+ export async function tokenPrices(client: SolanaClient, mints: readonly string[], options: TokenPricesOptions = {}): Promise<TokenPrice[]> {
355
+ const { sources = [defillamaPrices(), onchainPrices()], onError, fetch: fetchOption, signal } = options
356
+ const fetchFn = fetchOption ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
357
+ const native = mints.map((m) => client.isNativeMint(m))
358
+ // 原生 SOL 按 Wrapped SOL 的 mint 查;不是合法地址的不查
359
+ const keys = mints.map((m, i) => (native[i] ? NATIVE_MINT : m))
360
+ const query = [...new Set(keys.filter((k) => isAddress(k)))]
361
+ const found = await runSources(sources, { mints: query, fetch: fetchFn, signal, client }, onError)
362
+ return mints.map((token, i) => {
363
+ const hit = found.get(keys[i] as string)
364
+ return { token, native: native[i] as boolean, price: hit?.price ?? null, source: hit?.source ?? null }
365
+ })
366
+ }
@@ -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
+ }