@w3lib/solana-batch-call 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +205 -2
  3. package/dist/address.d.ts +2 -0
  4. package/dist/address.d.ts.map +1 -1
  5. package/dist/address.js +8 -0
  6. package/dist/address.js.map +1 -1
  7. package/dist/client.d.ts +4 -0
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +8 -0
  10. package/dist/client.js.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/layout.d.ts +5 -0
  16. package/dist/layout.d.ts.map +1 -1
  17. package/dist/layout.js +16 -0
  18. package/dist/layout.js.map +1 -1
  19. package/dist/liquidity/amm.d.ts +7 -0
  20. package/dist/liquidity/amm.d.ts.map +1 -0
  21. package/dist/liquidity/amm.js +307 -0
  22. package/dist/liquidity/amm.js.map +1 -0
  23. package/dist/liquidity/clmm.d.ts +18 -0
  24. package/dist/liquidity/clmm.d.ts.map +1 -0
  25. package/dist/liquidity/clmm.js +85 -0
  26. package/dist/liquidity/clmm.js.map +1 -0
  27. package/dist/liquidity/common.d.ts +94 -0
  28. package/dist/liquidity/common.d.ts.map +1 -0
  29. package/dist/liquidity/common.js +228 -0
  30. package/dist/liquidity/common.js.map +1 -0
  31. package/dist/liquidity/index.d.ts +34 -0
  32. package/dist/liquidity/index.d.ts.map +1 -0
  33. package/dist/liquidity/index.js +157 -0
  34. package/dist/liquidity/index.js.map +1 -0
  35. package/dist/liquidity/orca.d.ts +6 -0
  36. package/dist/liquidity/orca.d.ts.map +1 -0
  37. package/dist/liquidity/orca.js +303 -0
  38. package/dist/liquidity/orca.js.map +1 -0
  39. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  40. package/dist/liquidity/raydiumClmm.d.ts.map +1 -0
  41. package/dist/liquidity/raydiumClmm.js +307 -0
  42. package/dist/liquidity/raydiumClmm.js.map +1 -0
  43. package/dist/liquidity/types.d.ts +150 -0
  44. package/dist/liquidity/types.d.ts.map +1 -0
  45. package/dist/liquidity/types.js +2 -0
  46. package/dist/liquidity/types.js.map +1 -0
  47. package/dist/lp.d.ts +168 -0
  48. package/dist/lp.d.ts.map +1 -0
  49. package/dist/lp.js +632 -0
  50. package/dist/lp.js.map +1 -0
  51. package/dist/lpMath.d.ts +82 -0
  52. package/dist/lpMath.d.ts.map +1 -0
  53. package/dist/lpMath.js +365 -0
  54. package/dist/lpMath.js.map +1 -0
  55. package/dist/shortcuts.d.ts +8 -0
  56. package/dist/shortcuts.d.ts.map +1 -1
  57. package/dist/shortcuts.js +1 -1
  58. package/dist/shortcuts.js.map +1 -1
  59. package/dist/subpaths/liquidity.d.ts +23 -0
  60. package/dist/subpaths/liquidity.d.ts.map +1 -0
  61. package/dist/subpaths/liquidity.js +19 -0
  62. package/dist/subpaths/liquidity.js.map +1 -0
  63. package/dist/subpaths/lp.d.ts +31 -0
  64. package/dist/subpaths/lp.d.ts.map +1 -0
  65. package/dist/subpaths/lp.js +33 -0
  66. package/dist/subpaths/lp.js.map +1 -0
  67. package/dist/subpaths/write.d.ts +22 -0
  68. package/dist/subpaths/write.d.ts.map +1 -0
  69. package/dist/subpaths/write.js +19 -0
  70. package/dist/subpaths/write.js.map +1 -0
  71. package/dist/write/batch.d.ts +41 -0
  72. package/dist/write/batch.d.ts.map +1 -0
  73. package/dist/write/batch.js +46 -0
  74. package/dist/write/batch.js.map +1 -0
  75. package/dist/write/collect.d.ts +50 -0
  76. package/dist/write/collect.d.ts.map +1 -0
  77. package/dist/write/collect.js +220 -0
  78. package/dist/write/collect.js.map +1 -0
  79. package/dist/write/instructions.d.ts +16 -0
  80. package/dist/write/instructions.d.ts.map +1 -0
  81. package/dist/write/instructions.js +89 -0
  82. package/dist/write/instructions.js.map +1 -0
  83. package/dist/write/send.d.ts +115 -0
  84. package/dist/write/send.d.ts.map +1 -0
  85. package/dist/write/send.js +720 -0
  86. package/dist/write/send.js.map +1 -0
  87. package/dist/write/signer.d.ts +94 -0
  88. package/dist/write/signer.d.ts.map +1 -0
  89. package/dist/write/signer.js +197 -0
  90. package/dist/write/signer.js.map +1 -0
  91. package/dist/write/transaction.d.ts +75 -0
  92. package/dist/write/transaction.d.ts.map +1 -0
  93. package/dist/write/transaction.js +133 -0
  94. package/dist/write/transaction.js.map +1 -0
  95. package/dist/write/transfer.d.ts +72 -0
  96. package/dist/write/transfer.d.ts.map +1 -0
  97. package/dist/write/transfer.js +127 -0
  98. package/dist/write/transfer.js.map +1 -0
  99. package/package.json +23 -5
  100. package/src/address.ts +9 -0
  101. package/src/client.ts +10 -0
  102. package/src/index.ts +2 -1
  103. package/src/layout.ts +17 -0
  104. package/src/liquidity/amm.ts +353 -0
  105. package/src/liquidity/clmm.ts +108 -0
  106. package/src/liquidity/common.ts +285 -0
  107. package/src/liquidity/index.ts +177 -0
  108. package/src/liquidity/orca.ts +344 -0
  109. package/src/liquidity/raydiumClmm.ts +348 -0
  110. package/src/liquidity/types.ts +158 -0
  111. package/src/lp.ts +850 -0
  112. package/src/lpMath.ts +420 -0
  113. package/src/shortcuts.ts +1 -1
  114. package/src/subpaths/liquidity.ts +45 -0
  115. package/src/subpaths/lp.ts +71 -0
  116. package/src/subpaths/write.ts +29 -0
  117. package/src/write/batch.ts +78 -0
  118. package/src/write/collect.ts +286 -0
  119. package/src/write/instructions.ts +98 -0
  120. package/src/write/send.ts +876 -0
  121. package/src/write/signer.ts +314 -0
  122. package/src/write/transaction.ts +181 -0
  123. package/src/write/transfer.ts +189 -0
package/dist/lp.js ADDED
@@ -0,0 +1,632 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { base64 } from '@scure/base';
3
+ import { decodeAddress, encodeAddress, findProgramAddress, isAddress } from './address.js';
4
+ import { formatUnits, parseMint, parseTokenAccount } from './layout.js';
5
+ import { AllNodesFailedError, HttpError, RpcError } from './rpc.js';
6
+ import { getAmountsForLiquidity, getFeeGrowthInside, getFeesOwed, orcaSqrtPriceAtTick, raydiumSqrtPriceAtTick, sqrtPriceToPrice, tickToPrice } from './lpMath.js';
7
+ export const RAYDIUM_CLMM_PROGRAM_ID = 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK';
8
+ export const ORCA_WHIRLPOOL_PROGRAM_ID = 'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc';
9
+ export const RAYDIUM_AMM_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8';
10
+ export const RAYDIUM_CPMM_PROGRAM_ID = 'CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C';
11
+ /** AMM v4 的 LP mint 权限(固定地址) */
12
+ export const RAYDIUM_AMM_AUTHORITY = '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1';
13
+ /** CPMM 的 LP mint 权限:PDA ["vault_and_lp_mint_auth_seed"] */
14
+ export const RAYDIUM_CPMM_AUTHORITY = 'GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL';
15
+ export const SOLANA_LP_PROTOCOLS = {
16
+ 'raydium-clmm': { name: 'Raydium CLMM', type: 'clmm', programId: RAYDIUM_CLMM_PROGRAM_ID },
17
+ 'orca-whirlpool': { name: 'Orca Whirlpool', type: 'clmm', programId: ORCA_WHIRLPOOL_PROGRAM_ID },
18
+ 'raydium-amm': { name: 'Raydium AMM v4', type: 'amm', programId: RAYDIUM_AMM_PROGRAM_ID },
19
+ 'raydium-cpmm': { name: 'Raydium CPMM', type: 'amm', programId: RAYDIUM_CPMM_PROGRAM_ID },
20
+ };
21
+ /** DefiLlama 价格(免费免 Key,按 100 个一批查询);confidence 低于 minConfidence(默认 0.9)的价格不用 */
22
+ export function defillamaPrices(options = {}) {
23
+ const minConfidence = options.minConfidence ?? 0.9;
24
+ return {
25
+ name: 'defillama',
26
+ async prices({ mints, fetch: fetchFn, signal }) {
27
+ const result = new Map();
28
+ const batches = [];
29
+ for (let i = 0; i < mints.length; i += 100) {
30
+ batches.push(mints.slice(i, i + 100));
31
+ }
32
+ await Promise.all(batches.map(async (batch) => {
33
+ const res = await fetchFn(`https://coins.llama.fi/prices/current/${batch.map((m) => `solana:${m}`).join(',')}`, { signal });
34
+ if (!res.ok) {
35
+ throw new Error(`DefiLlama prices: HTTP ${res.status}`);
36
+ }
37
+ const body = (await res.json());
38
+ for (const [key, coin] of Object.entries(body.coins ?? {})) {
39
+ if (typeof coin.price === 'number' && Number.isFinite(coin.price) && (coin.confidence === undefined || coin.confidence >= minConfidence)) {
40
+ result.set(key.slice(key.indexOf(':') + 1), coin.price);
41
+ }
42
+ }
43
+ }));
44
+ return result;
45
+ },
46
+ };
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // 账户布局(各程序的 Anchor / 原生账户,偏移量含 8 字节判别符)
50
+ // ---------------------------------------------------------------------------
51
+ /** 小端读取账户数据(添加 / 移除流动性也用) */
52
+ export class Bytes {
53
+ data;
54
+ #view;
55
+ constructor(data) {
56
+ this.data = data;
57
+ this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength);
58
+ }
59
+ u8(o) {
60
+ return this.#view.getUint8(o);
61
+ }
62
+ u16(o) {
63
+ return this.#view.getUint16(o, true);
64
+ }
65
+ u32(o) {
66
+ return this.#view.getUint32(o, true);
67
+ }
68
+ i32(o) {
69
+ return this.#view.getInt32(o, true);
70
+ }
71
+ u64(o) {
72
+ return this.#view.getBigUint64(o, true);
73
+ }
74
+ u128(o) {
75
+ return (this.#view.getBigUint64(o + 8, true) << 64n) | this.#view.getBigUint64(o, true);
76
+ }
77
+ key(o) {
78
+ return encodeAddress(this.data.subarray(o, o + 32));
79
+ }
80
+ }
81
+ const RAYDIUM_POSITION_SIZE = 281;
82
+ const ORCA_POSITION_SIZE = 216;
83
+ /** 池子账户至少要读到的长度(Raydium PoolState 实际 1544 字节,Orca Whirlpool 653 字节) */
84
+ const RAYDIUM_POOL_MIN_SIZE = 309;
85
+ const ORCA_POOL_MIN_SIZE = 261;
86
+ const AMM_POOL_SIZE = 752;
87
+ const CPMM_POOL_SIZE = 637;
88
+ const RAYDIUM_TICK_ARRAY_SIZE = 60;
89
+ const RAYDIUM_TICK_SIZE = 168;
90
+ const ORCA_TICK_ARRAY_SIZE = 88;
91
+ const ORCA_TICK_SIZE = 113;
92
+ function raydiumPosition(address, b) {
93
+ return {
94
+ protocol: 'raydium-clmm',
95
+ address,
96
+ mint: b.key(9),
97
+ pool: b.key(41),
98
+ tickLower: b.i32(73),
99
+ tickUpper: b.i32(77),
100
+ liquidity: b.u128(81),
101
+ growthLast0: b.u128(97),
102
+ growthLast1: b.u128(113),
103
+ owed0: b.u64(129),
104
+ owed1: b.u64(137),
105
+ };
106
+ }
107
+ function orcaPosition(address, b) {
108
+ return {
109
+ protocol: 'orca-whirlpool',
110
+ address,
111
+ pool: b.key(8),
112
+ mint: b.key(40),
113
+ liquidity: b.u128(72),
114
+ tickLower: b.i32(88),
115
+ tickUpper: b.i32(92),
116
+ growthLast0: b.u128(96),
117
+ owed0: b.u64(112),
118
+ growthLast1: b.u128(120),
119
+ owed1: b.u64(136),
120
+ };
121
+ }
122
+ function raydiumPool(b) {
123
+ return {
124
+ ammConfig: b.key(9),
125
+ mint0: b.key(73),
126
+ mint1: b.key(105),
127
+ decimals0: b.u8(233),
128
+ decimals1: b.u8(234),
129
+ tickSpacing: b.u16(235),
130
+ sqrtPrice: b.u128(253),
131
+ tick: b.i32(269),
132
+ global0: b.u128(277),
133
+ global1: b.u128(293),
134
+ fee: null,
135
+ };
136
+ }
137
+ function orcaPool(b) {
138
+ return {
139
+ ammConfig: null,
140
+ tickSpacing: b.u16(41),
141
+ fee: b.u16(45),
142
+ sqrtPrice: b.u128(65),
143
+ tick: b.i32(81),
144
+ mint0: b.key(101),
145
+ global0: b.u128(165),
146
+ mint1: b.key(181),
147
+ global1: b.u128(245),
148
+ decimals0: null,
149
+ decimals1: null,
150
+ };
151
+ }
152
+ function ammPool(address, b) {
153
+ return {
154
+ protocol: 'raydium-amm',
155
+ address,
156
+ decimals0: Number(b.u64(32)),
157
+ decimals1: Number(b.u64(40)),
158
+ reserved0: b.u64(192),
159
+ reserved1: b.u64(200),
160
+ vault0: b.key(336),
161
+ vault1: b.key(368),
162
+ mint0: b.key(400),
163
+ mint1: b.key(432),
164
+ lpMint: b.key(464),
165
+ lpSupply: b.u64(720),
166
+ };
167
+ }
168
+ function cpmmPool(address, b) {
169
+ return {
170
+ protocol: 'raydium-cpmm',
171
+ address,
172
+ vault0: b.key(72),
173
+ vault1: b.key(104),
174
+ lpMint: b.key(136),
175
+ mint0: b.key(168),
176
+ mint1: b.key(200),
177
+ decimals0: b.u8(331),
178
+ decimals1: b.u8(332),
179
+ lpSupply: b.u64(333),
180
+ reserved0: b.u64(341) + b.u64(357) + b.u64(397),
181
+ reserved1: b.u64(349) + b.u64(365) + b.u64(405),
182
+ };
183
+ }
184
+ export function parseAmmPool(account) {
185
+ const b = new Bytes(account.data);
186
+ if (account.owner === RAYDIUM_AMM_PROGRAM_ID && account.data.length === AMM_POOL_SIZE) {
187
+ return ammPool(account.address, b);
188
+ }
189
+ if (account.owner === RAYDIUM_CPMM_PROGRAM_ID && account.data.length === CPMM_POOL_SIZE) {
190
+ return cpmmPool(account.address, b);
191
+ }
192
+ return null;
193
+ }
194
+ export const utf8 = (s) => new TextEncoder().encode(s);
195
+ function floorDiv(a, b) {
196
+ return Math.floor(a / b) * b;
197
+ }
198
+ /** Raydium tick 数组:PDA ["tick_array", pool, start(i32 大端)],每个数组 60 个 tick */
199
+ export function raydiumTickArray(pool, tick, spacing) {
200
+ const start = floorDiv(tick, spacing * RAYDIUM_TICK_ARRAY_SIZE);
201
+ const be = new Uint8Array(4);
202
+ new DataView(be.buffer).setInt32(0, start, false);
203
+ return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), be], RAYDIUM_CLMM_PROGRAM_ID), start };
204
+ }
205
+ /** Orca tick 数组:PDA ["tick_array", whirlpool, start(十进制字符串)],每个数组 88 个 tick */
206
+ export function orcaTickArray(pool, tick, spacing) {
207
+ const start = floorDiv(tick, spacing * ORCA_TICK_ARRAY_SIZE);
208
+ return { address: findProgramAddress([utf8('tick_array'), decodeAddress(pool), utf8(String(start))], ORCA_WHIRLPOOL_PROGRAM_ID), start };
209
+ }
210
+ function raydiumTick(account, start, tick, spacing) {
211
+ raydiumDiscriminator ??= discriminator('TickArrayState');
212
+ const offset = 44 + ((tick - start) / spacing) * RAYDIUM_TICK_SIZE;
213
+ // 先认判别符、查长度再读字段(PDA 上可能是别的账户,如被转过 SOL 的系统账户)
214
+ if (!startsWith(account.data, raydiumDiscriminator) || offset + RAYDIUM_TICK_SIZE > account.data.length) {
215
+ return null;
216
+ }
217
+ const b = new Bytes(account.data);
218
+ if (b.i32(40) !== start) {
219
+ return null;
220
+ }
221
+ // Raydium 不区分是否初始化:没用过的 tick 各项都是 0,按程序的公式算出来就是对的
222
+ return { initialized: true, feeGrowthOutside0: b.u128(offset + 36), feeGrowthOutside1: b.u128(offset + 52) };
223
+ }
224
+ /** Anchor 账户判别符:sha256("account:<名称>") 的前 8 字节 */
225
+ const discriminator = (name) => sha256(utf8(`account:${name}`)).subarray(0, 8);
226
+ let orcaDiscriminators;
227
+ let raydiumDiscriminator;
228
+ const startsWith = (data, prefix) => data.length >= prefix.length && prefix.every((byte, i) => data[i] === byte);
229
+ function orcaTick(account, start, tick, spacing) {
230
+ orcaDiscriminators ??= { fixed: discriminator('TickArray'), dynamic: discriminator('DynamicTickArray') };
231
+ if (account.data.length < 60) {
232
+ return null; // 两种格式的头部(判别符 + start + …)都放不下
233
+ }
234
+ const b = new Bytes(account.data);
235
+ const index = (tick - start) / spacing;
236
+ if (startsWith(account.data, orcaDiscriminators.fixed)) {
237
+ // 固定大小:start i32 | ticks [Tick; 88](每个 113 字节:initialized u8、liquidity_net、liquidity_gross、两个手续费增长…)| whirlpool
238
+ const offset = 12 + index * ORCA_TICK_SIZE;
239
+ if (b.i32(8) !== start || offset + ORCA_TICK_SIZE > account.data.length) {
240
+ return null;
241
+ }
242
+ return { initialized: b.u8(offset) === 1, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) };
243
+ }
244
+ if (startsWith(account.data, orcaDiscriminators.dynamic)) {
245
+ // 可变大小:start i32 | whirlpool | tick_bitmap u128 | ticks(未初始化 1 字节 0;初始化 1 字节 1 + 112 字节数据)
246
+ if (b.i32(8) !== start) {
247
+ return null;
248
+ }
249
+ let offset = 60;
250
+ for (let i = 0; i < index; i++) {
251
+ if (offset >= account.data.length) {
252
+ return null; // 数据被截断
253
+ }
254
+ offset += b.u8(offset) === 1 ? 113 : 1;
255
+ }
256
+ if (offset >= account.data.length) {
257
+ return null;
258
+ }
259
+ if (b.u8(offset) !== 1) {
260
+ return { initialized: false, feeGrowthOutside0: 0n, feeGrowthOutside1: 0n };
261
+ }
262
+ if (offset + 113 > account.data.length) {
263
+ return null;
264
+ }
265
+ return { initialized: true, feeGrowthOutside0: b.u128(offset + 33), feeGrowthOutside1: b.u128(offset + 49) };
266
+ }
267
+ return null;
268
+ }
269
+ /**
270
+ * 查一个地址的 LP 仓位:Raydium CLMM / Orca Whirlpool 的仓位 NFT,Raydium AMM v4 / CPMM 的 LP 代币。
271
+ * 返回两种代币的数量、未领取的手续费(集中流动性),可选美元价值。
272
+ *
273
+ * - 默认扫描地址的全部代币账户找仓位 NFT 和 LP 代币(需要支持 getTokenAccountsByOwner 的节点);传 mints 时只看这些代币(ATA,免费节点也能用)
274
+ * - Raydium AMM v4 / CPMM 的 LP 代币按 LP mint 用 getProgramAccounts 找池子(需要支持它的节点),传 pools 时这些池子不用找
275
+ * - 质押在农场里的 LP、Orca 的仓位组合(Position Bundle)、锁仓的 Raydium 仓位不在用户名下,查不到
276
+ */
277
+ export async function lpPositions(client, owner, options = {}) {
278
+ if (!isAddress(owner)) {
279
+ throw new Error(`Invalid owner address: ${owner}`);
280
+ }
281
+ const protocols = new Set(options.protocols ?? Object.keys(SOLANA_LP_PROTOCOLS));
282
+ for (const p of protocols) {
283
+ if (!Object.hasOwn(SOLANA_LP_PROTOCOLS, p)) {
284
+ throw new Error(`Unknown LP protocol: ${p}`);
285
+ }
286
+ }
287
+ for (const address of [...(options.mints ?? []), ...(options.pools ?? [])]) {
288
+ if (!isAddress(address)) {
289
+ throw new Error(`Invalid address: ${address}`);
290
+ }
291
+ }
292
+ const { signal } = options;
293
+ const wantClmm = protocols.has('raydium-clmm') || protocols.has('orca-whirlpool');
294
+ const wantAmm = protocols.has('raydium-amm') || protocols.has('raydium-cpmm');
295
+ if (!wantClmm && !wantAmm) {
296
+ return [];
297
+ }
298
+ // 1. 传入的池子(AMM v4 / CPMM),以及持有的代币
299
+ const knownPools = [];
300
+ if (wantAmm && options.pools?.length) {
301
+ const accounts = await client.accounts(options.pools, { signal });
302
+ accounts.forEach((account, i) => {
303
+ const pool = account ? parseAmmPool(account) : null;
304
+ if (!pool) {
305
+ throw new Error(`Not a Raydium AMM v4 / CPMM pool: ${options.pools?.[i]}`);
306
+ }
307
+ if (protocols.has(pool.protocol)) {
308
+ knownPools.push(pool);
309
+ }
310
+ });
311
+ }
312
+ const holdings = await listHoldings(client, owner, options.mints, knownPools.map((p) => p.lpMint), signal);
313
+ // 2. 一次读:可能是仓位 NFT 的代币(精度 0、数量 1)推导出的仓位账户,其余代币的 mint(看是不是 Raydium 的 LP)
314
+ const nfts = wantClmm ? holdings.filter((h) => h.decimals === 0 && h.amount === 1n) : [];
315
+ const candidates = nfts.flatMap((h) => [
316
+ ...(protocols.has('raydium-clmm') ? [{ protocol: 'raydium-clmm', address: findProgramAddress([utf8('position'), decodeAddress(h.mint)], RAYDIUM_CLMM_PROGRAM_ID) }] : []),
317
+ ...(protocols.has('orca-whirlpool') ? [{ protocol: 'orca-whirlpool', address: findProgramAddress([utf8('position'), decodeAddress(h.mint)], ORCA_WHIRLPOOL_PROGRAM_ID) }] : []),
318
+ ]);
319
+ const poolByLp = new Map(knownPools.map((p) => [p.lpMint, p]));
320
+ const fungible = wantAmm ? holdings.filter((h) => !(h.decimals === 0 && h.amount === 1n) && !poolByLp.has(h.mint)) : [];
321
+ const first = await client.accounts([...candidates.map((c) => c.address), ...fungible.map((h) => h.mint)], { signal });
322
+ const positions = [];
323
+ candidates.forEach((c, i) => {
324
+ const account = first[i];
325
+ if (!account) {
326
+ return;
327
+ }
328
+ const program = SOLANA_LP_PROTOCOLS[c.protocol].programId;
329
+ if (c.protocol === 'raydium-clmm' && account.owner === program && account.data.length === RAYDIUM_POSITION_SIZE) {
330
+ positions.push(raydiumPosition(c.address, new Bytes(account.data)));
331
+ }
332
+ else if (c.protocol === 'orca-whirlpool' && account.owner === program && account.data.length === ORCA_POSITION_SIZE) {
333
+ positions.push(orcaPosition(c.address, new Bytes(account.data)));
334
+ }
335
+ });
336
+ // LP mint 的权限是 AMM v4 / CPMM 的固定权限地址时才去找池子
337
+ const lookups = [];
338
+ fungible.forEach((h, i) => {
339
+ const account = first[candidates.length + i];
340
+ if (!account) {
341
+ return;
342
+ }
343
+ let authority;
344
+ try {
345
+ authority = parseMint(account.data).mintAuthority;
346
+ }
347
+ catch {
348
+ return;
349
+ }
350
+ if (authority === RAYDIUM_AMM_AUTHORITY && protocols.has('raydium-amm')) {
351
+ lookups.push({ protocol: 'raydium-amm', lpMint: h.mint });
352
+ }
353
+ else if (authority === RAYDIUM_CPMM_AUTHORITY && protocols.has('raydium-cpmm')) {
354
+ lookups.push({ protocol: 'raydium-cpmm', lpMint: h.mint });
355
+ }
356
+ });
357
+ // 3. 池子:集中流动性的池子账户;LP 代币按 LP mint 找池子
358
+ const poolAddresses = [...new Set(positions.map((p) => p.pool))];
359
+ const [poolAccounts, found] = await Promise.all([
360
+ client.accounts(poolAddresses, { signal }),
361
+ Promise.all(lookups.map((l) => findAmmPool(client, l.protocol, l.lpMint, signal))),
362
+ ]);
363
+ for (const pool of found) {
364
+ if (pool) {
365
+ poolByLp.set(pool.lpMint, pool);
366
+ }
367
+ }
368
+ const pools = new Map();
369
+ poolAddresses.forEach((address, i) => {
370
+ const account = poolAccounts[i];
371
+ if (account?.owner === RAYDIUM_CLMM_PROGRAM_ID && account.data.length >= RAYDIUM_POOL_MIN_SIZE) {
372
+ pools.set(address, raydiumPool(new Bytes(account.data)));
373
+ }
374
+ else if (account?.owner === ORCA_WHIRLPOOL_PROGRAM_ID && account.data.length >= ORCA_POOL_MIN_SIZE) {
375
+ pools.set(address, orcaPool(new Bytes(account.data)));
376
+ }
377
+ });
378
+ const held = new Map(holdings.map((h) => [h.mint, h]));
379
+ const ammHeld = [...poolByLp.values()].filter((p) => (held.get(p.lpMint)?.amount ?? 0n) > 0n);
380
+ // 4. 一次读:有流动性的仓位两端的 tick 数组、Raydium 的费率配置、AMM 池子的金库
381
+ const tickArrays = positions.flatMap((p) => {
382
+ const pool = pools.get(p.pool);
383
+ if (!pool || p.liquidity === 0n) {
384
+ return [];
385
+ }
386
+ const derive = p.protocol === 'raydium-clmm' ? raydiumTickArray : orcaTickArray;
387
+ return [{ position: p, lower: derive(p.pool, p.tickLower, pool.tickSpacing), upper: derive(p.pool, p.tickUpper, pool.tickSpacing) }];
388
+ });
389
+ const configs = [...new Set([...pools.values()].flatMap((p) => (p.ammConfig ? [p.ammConfig] : [])))];
390
+ const vaults = ammHeld.flatMap((p) => [p.vault0, p.vault1]);
391
+ const tickAddresses = [...new Set(tickArrays.flatMap((t) => [t.lower.address, t.upper.address]))];
392
+ const second = await client.accounts([...tickAddresses, ...configs, ...vaults], { signal });
393
+ const byAddress = new Map();
394
+ [...tickAddresses, ...configs, ...vaults].forEach((address, i) => {
395
+ const account = second[i];
396
+ if (account) {
397
+ byAddress.set(address, account);
398
+ }
399
+ });
400
+ const feeRates = new Map();
401
+ for (const config of configs) {
402
+ const account = byAddress.get(config);
403
+ // AmmConfig:bump u8 | index u16 | owner | protocol_fee_rate u32 | trade_fee_rate u32
404
+ if (account?.owner === RAYDIUM_CLMM_PROGRAM_ID && account.data.length >= 51) {
405
+ feeRates.set(config, new Bytes(account.data).u32(47));
406
+ }
407
+ }
408
+ const raw = [];
409
+ for (const position of positions) {
410
+ const pool = pools.get(position.pool);
411
+ if (!pool) {
412
+ continue;
413
+ }
414
+ const sqrtAt = position.protocol === 'raydium-clmm' ? raydiumSqrtPriceAtTick : orcaSqrtPriceAtTick;
415
+ const [amount0, amount1] = getAmountsForLiquidity(pool.tick, pool.sqrtPrice, sqrtAt(position.tickLower), sqrtAt(position.tickUpper), position.liquidity, position.tickLower, position.tickUpper);
416
+ // 没有流动性时只有已记账的部分
417
+ let fees0 = position.owed0;
418
+ let fees1 = position.owed1;
419
+ const arrays = tickArrays.find((t) => t.position === position);
420
+ if (arrays) {
421
+ const read = position.protocol === 'raydium-clmm' ? raydiumTick : orcaTick;
422
+ const lowerAccount = byAddress.get(arrays.lower.address);
423
+ const upperAccount = byAddress.get(arrays.upper.address);
424
+ const lower = lowerAccount ? read(lowerAccount, arrays.lower.start, position.tickLower, pool.tickSpacing) : null;
425
+ const upper = upperAccount ? read(upperAccount, arrays.upper.start, position.tickUpper, pool.tickSpacing) : null;
426
+ // 有流动性时两端的 tick 数组一定存在;读不到 / 解析不了时不猜,手续费为 null
427
+ if (lower && upper) {
428
+ const [inside0, inside1] = getFeeGrowthInside(pool.tick, position.tickLower, lower, position.tickUpper, upper, pool.global0, pool.global1);
429
+ fees0 = getFeesOwed(position.owed0, position.liquidity, inside0, position.growthLast0);
430
+ fees1 = getFeesOwed(position.owed1, position.liquidity, inside1, position.growthLast1);
431
+ }
432
+ else {
433
+ fees0 = null;
434
+ fees1 = null;
435
+ }
436
+ }
437
+ if (!options.includeClosed && position.liquidity === 0n && fees0 === 0n && fees1 === 0n) {
438
+ continue;
439
+ }
440
+ raw.push({ type: 'clmm', position, pool: { ...pool, fee: pool.fee ?? (pool.ammConfig ? (feeRates.get(pool.ammConfig) ?? null) : null) }, amount0, amount1, fees0, fees1 });
441
+ }
442
+ for (const pool of ammHeld) {
443
+ const balance = held.get(pool.lpMint)?.amount ?? 0n;
444
+ const vault = (address) => {
445
+ const account = byAddress.get(address);
446
+ try {
447
+ return account ? parseTokenAccount(account.data).amount : null;
448
+ }
449
+ catch {
450
+ return null;
451
+ }
452
+ };
453
+ const v0 = vault(pool.vault0);
454
+ const v1 = vault(pool.vault1);
455
+ if (v0 === null || v1 === null || pool.lpSupply === 0n) {
456
+ continue;
457
+ }
458
+ const sub = (a, b) => (a > b ? a - b : 0n);
459
+ // 与程序撤出流动性的计算相同:份额 × (金库余额 − 不属于 LP 的部分),向下取整。
460
+ // AMM v4 的池子还能用 OpenBook 挂单时(status 不是 6 SwapOnly),挂单里的资金没算进来,数量会略少
461
+ raw.push({ type: 'amm', pool, balance, amount0: (balance * sub(v0, pool.reserved0)) / pool.lpSupply, amount1: (balance * sub(v1, pool.reserved1)) / pool.lpSupply });
462
+ }
463
+ return finalize(client, raw, options);
464
+ }
465
+ /**
466
+ * 节点不提供这个方法(方法不存在、被拦截、需要付费 / Token、403 等):这时报错里说明怎么绕过。
467
+ * 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出
468
+ */
469
+ function methodUnavailable(err) {
470
+ if (err instanceof AllNodesFailedError) {
471
+ return err.errors.length > 0 && err.errors.every((e) => methodUnavailable(e.error));
472
+ }
473
+ if (err instanceof HttpError) {
474
+ return err.status === 401 || err.status === 403 || err.status === 405;
475
+ }
476
+ if (err instanceof RpcError) {
477
+ // 限频不算“不支持”(如 QuickNode:“… request limit reached - … upgrade your account”)
478
+ if (err.code === 429 || err.code === -32007 || /rate.?limit|too many requests|limit reached/i.test(err.message)) {
479
+ return false;
480
+ }
481
+ return err.code === -32601 || err.code === 403 || /blocked|not (supported|available|allowed|enabled)|method not found|disabled|excluded|personal token|api key|upgrade|paid plan/i.test(err.message);
482
+ }
483
+ return false;
484
+ }
485
+ /** 按 mint 列出持有的代币:传 mints 时读这些代币的 ATA,否则扫描全部代币账户 */
486
+ async function listHoldings(client, owner, mints, extra, signal) {
487
+ if (mints) {
488
+ const list = [...new Set([...mints, ...extra])];
489
+ const balances = list.length ? await client.balances(owner, list, { signal }) : [];
490
+ return balances.filter((b) => b.success && !b.native && b.balance !== '0').map((b) => ({ mint: b.token, amount: BigInt(b.balance), decimals: b.decimals }));
491
+ }
492
+ const tokens = await client.ownerTokens(owner, { signal, metadata: false, includeNative: false, includeNfts: true }).catch((err) => {
493
+ if (signal?.aborted || !methodUnavailable(err)) {
494
+ throw err; // 取消、限频、超时等原样抛出(保留错误类型)
495
+ }
496
+ throw new Error(`Scanning the token accounts of ${owner} failed (${err instanceof Error ? err.message : String(err)}). ` +
497
+ 'Scanning needs getTokenAccountsByOwner (most free public nodes do not support it): pass the position NFT / LP mints in `mints`, or use a node that supports it', { cause: err });
498
+ });
499
+ return tokens.map((t) => ({ mint: t.token, amount: BigInt(t.balance), decimals: t.decimals }));
500
+ }
501
+ /** 按 LP mint 找 AMM v4 / CPMM 的池子(getProgramAccounts + memcmp),找不到时为 null */
502
+ async function findAmmPool(client, protocol, lpMint, signal) {
503
+ const [programId, size, offset] = protocol === 'raydium-amm' ? [RAYDIUM_AMM_PROGRAM_ID, AMM_POOL_SIZE, 464] : [RAYDIUM_CPMM_PROGRAM_ID, CPMM_POOL_SIZE, 136];
504
+ const res = await client
505
+ .request('getProgramAccounts', [programId, { encoding: 'base64', commitment: client.commitment, filters: [{ dataSize: size }, { memcmp: { offset, bytes: lpMint } }] }], { signal })
506
+ .catch((err) => {
507
+ if (signal?.aborted || !methodUnavailable(err)) {
508
+ throw err;
509
+ }
510
+ // 免费公共节点大多不支持 getProgramAccounts:说明怎么绕过
511
+ throw new Error(`Finding the ${SOLANA_LP_PROTOCOLS[protocol].name} pool of LP token ${lpMint} uses getProgramAccounts, which failed (${err instanceof Error ? err.message : String(err)}; most free public nodes do not support it). ` +
512
+ "Pass the pool address in `pools`, use a node that supports getProgramAccounts, or skip LP tokens with `protocols: ['raydium-clmm', 'orca-whirlpool']`", { cause: err });
513
+ });
514
+ for (const item of res ?? []) {
515
+ const data = base64.decode(item.account.data[0]);
516
+ const pool = parseAmmPool({ address: item.pubkey, owner: item.account.owner, data, lamports: 0n, executable: false });
517
+ if (pool?.lpMint === lpMint) {
518
+ return pool;
519
+ }
520
+ }
521
+ return null;
522
+ }
523
+ async function finalize(client, raw, options) {
524
+ const { signal } = options;
525
+ const mints = [...new Set(raw.flatMap((r) => [r.pool.mint0, r.pool.mint1]))];
526
+ const priceSource = options.prices === true ? defillamaPrices() : options.prices || null;
527
+ const fetchFn = options.fetch ?? ((...args) => fetch(...args));
528
+ // 价格源出错(服务不可用、被拦截、同步抛错等)时照样返回仓位,只是不带价格;取消查询时照常 reject
529
+ const loadPrices = async () => {
530
+ if (!priceSource || !mints.length) {
531
+ return new Map();
532
+ }
533
+ try {
534
+ // wSOL 的价格就是 SOL 的价格,DefiLlama 用 So111…112 查得到
535
+ return await priceSource.prices({ mints, fetch: fetchFn, signal });
536
+ }
537
+ catch (err) {
538
+ if (signal?.aborted) {
539
+ throw err;
540
+ }
541
+ try {
542
+ options.onPriceError?.(err);
543
+ }
544
+ catch {
545
+ // 回调出错不影响查询结果
546
+ }
547
+ return new Map();
548
+ }
549
+ };
550
+ const [details, priceMap] = await Promise.all([
551
+ mints.length ? client.tokens(mints, { fields: ['symbol', 'decimals'], signal }) : Promise.resolve([]),
552
+ loadPrices(),
553
+ ]);
554
+ const meta = new Map(details.map((d) => [d.address, d]));
555
+ const token = (mint, poolDecimals) => {
556
+ const d = meta.get(mint);
557
+ return { mint, symbol: d?.symbol ?? null, decimals: d?.decimals ?? poolDecimals };
558
+ };
559
+ const fmt = (value, t) => (t.decimals === null ? null : formatUnits(value, t.decimals));
560
+ const usd = (t, formatted) => {
561
+ const price = priceMap.get(t.mint);
562
+ return formatted === null || price === undefined ? null : Number(formatted) * price;
563
+ };
564
+ const sum = (a, b, amountA, amountB) => {
565
+ // 数量为 0 的一边没有价格也不影响
566
+ if ((a === null && amountA > 0n) || (b === null && amountB > 0n)) {
567
+ return null;
568
+ }
569
+ return (a ?? 0) + (b ?? 0);
570
+ };
571
+ const list = raw.map((r) => {
572
+ const protocol = r.type === 'clmm' ? r.position.protocol : r.pool.protocol;
573
+ const token0 = token(r.pool.mint0, r.pool.decimals0);
574
+ const token1 = token(r.pool.mint1, r.pool.decimals1);
575
+ const amount0Formatted = fmt(r.amount0, token0);
576
+ const amount1Formatted = fmt(r.amount1, token1);
577
+ const base = {
578
+ protocol,
579
+ protocolName: SOLANA_LP_PROTOCOLS[protocol].name,
580
+ pool: r.type === 'clmm' ? r.position.pool : r.pool.address,
581
+ token0,
582
+ token1,
583
+ amount0: r.amount0.toString(),
584
+ amount1: r.amount1.toString(),
585
+ amount0Formatted,
586
+ amount1Formatted,
587
+ ...(priceSource ? { valueUsd: sum(usd(token0, amount0Formatted), usd(token1, amount1Formatted), r.amount0, r.amount1) } : {}),
588
+ };
589
+ if (r.type === 'amm') {
590
+ return {
591
+ ...base,
592
+ type: 'amm',
593
+ lpMint: r.pool.lpMint,
594
+ balance: r.balance.toString(),
595
+ totalSupply: r.pool.lpSupply.toString(),
596
+ share: Number(r.balance) / Number(r.pool.lpSupply),
597
+ };
598
+ }
599
+ const { position: p, pool } = r;
600
+ const d0 = token0.decimals;
601
+ const d1 = token1.decimals;
602
+ const fees0Formatted = r.fees0 === null ? null : fmt(r.fees0, token0);
603
+ const fees1Formatted = r.fees1 === null ? null : fmt(r.fees1, token1);
604
+ return {
605
+ ...base,
606
+ type: 'clmm',
607
+ positionMint: p.mint,
608
+ position: p.address,
609
+ fee: pool.fee,
610
+ tickSpacing: pool.tickSpacing,
611
+ tickLower: p.tickLower,
612
+ tickUpper: p.tickUpper,
613
+ tick: pool.tick,
614
+ inRange: pool.tick >= p.tickLower && pool.tick < p.tickUpper,
615
+ liquidity: p.liquidity.toString(),
616
+ price: d0 === null || d1 === null ? null : sqrtPriceToPrice(pool.sqrtPrice, d0, d1),
617
+ priceLower: d0 === null || d1 === null ? null : tickToPrice(p.tickLower, d0, d1),
618
+ priceUpper: d0 === null || d1 === null ? null : tickToPrice(p.tickUpper, d0, d1),
619
+ fees0: r.fees0 === null ? null : r.fees0.toString(),
620
+ fees1: r.fees1 === null ? null : r.fees1.toString(),
621
+ fees0Formatted,
622
+ fees1Formatted,
623
+ ...(priceSource ? { feesUsd: r.fees0 === null || r.fees1 === null ? null : sum(usd(token0, fees0Formatted), usd(token1, fees1Formatted), r.fees0, r.fees1) } : {}),
624
+ };
625
+ });
626
+ if (priceSource) {
627
+ const total = (p) => (p.valueUsd === null || p.valueUsd === undefined ? -1 : p.valueUsd + (p.type === 'clmm' ? (p.feesUsd ?? 0) : 0));
628
+ list.sort((a, b) => total(b) - total(a));
629
+ }
630
+ return list;
631
+ }
632
+ //# sourceMappingURL=lp.js.map