four-flap-meme-sdk 1.3.90 → 1.3.92

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.
@@ -16,6 +16,19 @@ const QUOTE_TOKENS = {
16
16
  'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': { symbol: 'USDC', decimals: 6 },
17
17
  'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': { symbol: 'USDT', decimals: 6 },
18
18
  };
19
+ /** Raydium 程序地址 */
20
+ const RAYDIUM_PROGRAMS = {
21
+ /** LaunchLab */
22
+ LAUNCHLAB: 'LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj',
23
+ /** CPMM (Constant Product) */
24
+ CPMM: 'CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C',
25
+ /** Legacy AMM v4 */
26
+ AMM_V4: '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8',
27
+ /** Stable Swap AMM */
28
+ STABLE_SWAP: '5quBtoiQqxF9Jv6KYKctB59NT3gtJD2Y65kdnB1Uev3h',
29
+ /** CLMM (Concentrated Liquidity) */
30
+ CLMM: 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK',
31
+ };
19
32
  // ============================================================================
20
33
  // 动态导入(避免循环依赖和类型冲突)
21
34
  // ============================================================================
@@ -150,12 +163,97 @@ async function detectOrcaWavebreak(mint, connection, debug) {
150
163
  }
151
164
  /**
152
165
  * 检测 Raydium LaunchLab
153
- * 注意:LaunchLab API 可能需要特定的端点,这里做基础检测
166
+ * 使用 Raydium SDK 的官方 layout 解析链上数据
154
167
  */
155
- async function detectRaydiumLaunchLab(_mint, _connection, _debug) {
156
- // Raydium LaunchLab 检测暂未实现
157
- // 需要 Raydium 提供专门的 LaunchLab API
158
- return null;
168
+ async function detectRaydiumLaunchLab(mint, connection, debug) {
169
+ try {
170
+ // 动态导入 Raydium SDK
171
+ const { LaunchpadPool, LaunchpadConfig, getPdaLaunchpadPoolId, LAUNCHPAD_PROGRAM, Curve, } = await import('@raydium-io/raydium-sdk-v2');
172
+ const { NATIVE_MINT } = await import('@solana/spl-token');
173
+ const mintA = new PublicKey(mint);
174
+ const mintB = NATIVE_MINT; // 默认与 SOL 配对
175
+ // 获取池子 PDA
176
+ const poolId = getPdaLaunchpadPoolId(LAUNCHPAD_PROGRAM, mintA, mintB).publicKey;
177
+ // 获取池子账户数据
178
+ const poolAccountInfo = await connection.getAccountInfo(poolId);
179
+ if (!poolAccountInfo) {
180
+ if (debug)
181
+ console.log('[LP Inspect] LaunchLab pool not found for mint:', mint);
182
+ return null;
183
+ }
184
+ // 验证程序 ID
185
+ if (poolAccountInfo.owner.toBase58() !== LAUNCHPAD_PROGRAM.toBase58()) {
186
+ if (debug)
187
+ console.log('[LP Inspect] Pool owner is not LaunchLab program');
188
+ return null;
189
+ }
190
+ // 解码池子数据
191
+ const poolInfo = LaunchpadPool.decode(poolAccountInfo.data);
192
+ // 获取配置信息
193
+ const configAccountInfo = await connection.getAccountInfo(poolInfo.configId);
194
+ if (!configAccountInfo) {
195
+ if (debug)
196
+ console.log('[LP Inspect] LaunchLab config not found');
197
+ return null;
198
+ }
199
+ const configInfo = LaunchpadConfig.decode(configAccountInfo.data);
200
+ // 计算当前价格
201
+ const currentPrice = Curve.getPrice({
202
+ poolInfo,
203
+ curveType: configInfo.curveType,
204
+ decimalA: poolInfo.mintDecimalsA,
205
+ decimalB: poolInfo.mintDecimalsB,
206
+ }).toNumber();
207
+ // 计算毕业进度
208
+ let progress = 0;
209
+ try {
210
+ const initPrice = Curve.getPoolInitPriceByPool({
211
+ poolInfo,
212
+ decimalA: poolInfo.mintDecimalsA,
213
+ decimalB: poolInfo.mintDecimalsB,
214
+ curveType: configInfo.curveType,
215
+ }).toNumber();
216
+ const endPrice = Curve.getPoolEndPriceReal({
217
+ poolInfo,
218
+ curveType: configInfo.curveType,
219
+ decimalA: poolInfo.mintDecimalsA,
220
+ decimalB: poolInfo.mintDecimalsB,
221
+ }).toNumber();
222
+ const priceDiff = currentPrice - initPrice;
223
+ const totalDiff = endPrice - initPrice;
224
+ progress = totalDiff === 0 ? 0 : Math.min((priceDiff / totalDiff) * 100, 100);
225
+ }
226
+ catch (e) {
227
+ if (debug)
228
+ console.log('[LP Inspect] Failed to calculate LaunchLab progress:', e);
229
+ }
230
+ // 判断是否已毕业(迁移到 AMM/CPMM)
231
+ // 当池子状态变为已迁移或 progress >= 100 时为已完成
232
+ const isMigrated = poolInfo.status !== undefined && poolInfo.status >= 2; // status: 0=active, 1=waiting, 2+=migrated
233
+ // 计算储备量
234
+ // poolInfo 中包含 totalSellA (已卖出的代币) 和 totalFundRaisingB (已筹集的 SOL)
235
+ const reserveQuote = Number(poolInfo.totalFundRaisingB || 0) / Math.pow(10, poolInfo.mintDecimalsB);
236
+ const totalSupply = Number(poolInfo.supply || 0) / Math.pow(10, poolInfo.mintDecimalsA);
237
+ const soldAmount = Number(poolInfo.totalSellA || 0) / Math.pow(10, poolInfo.mintDecimalsA);
238
+ const reserveToken = totalSupply - soldAmount;
239
+ return {
240
+ type: 'raydium_launchlab',
241
+ address: poolId.toBase58(),
242
+ complete: isMigrated || progress >= 100,
243
+ reserveQuote: reserveQuote.toFixed(4),
244
+ reserveQuoteRaw: BigInt(poolInfo.totalFundRaisingB?.toString() || '0'),
245
+ reserveToken: reserveToken.toFixed(4),
246
+ reserveTokenRaw: BigInt(Math.floor(reserveToken * Math.pow(10, poolInfo.mintDecimalsA))),
247
+ price: currentPrice,
248
+ progress,
249
+ creator: poolInfo.creator?.toBase58(),
250
+ };
251
+ }
252
+ catch (err) {
253
+ if (debug)
254
+ console.log('[LP Inspect] Raydium LaunchLab check failed:', err?.message || err);
255
+ return null;
256
+ }
159
257
  }
160
258
  // ============================================================================
161
259
  // 外盘检测
@@ -281,60 +379,111 @@ async function detectOrcaWhirlpool(mint, connection, debug) {
281
379
  }
282
380
  }
283
381
  /**
284
- * 检测 Raydium 池子
382
+ * 检测 Raydium 池子(混合方案)
383
+ * 1. 先用 API 快速发现池子列表
384
+ * 2. 再用链上查询获取实时储备量
285
385
  */
286
386
  async function detectRaydiumPools(mint, connection, debug) {
287
387
  const pools = [];
288
388
  try {
289
- const sdk = await loadRaydiumSdk();
290
- if (!sdk)
389
+ // ==================== 步骤 1: 用 API 发现池子 ====================
390
+ const apiUrl = `https://api-v3.raydium.io/pools/info/mint?mint1=${mint}&poolType=all&poolSortField=default&sortType=desc&pageSize=20&page=1`;
391
+ const response = await fetch(apiUrl);
392
+ if (!response.ok) {
393
+ if (debug)
394
+ console.log('[LP Inspect] Raydium API request failed:', response.status);
291
395
  return pools;
292
- const { initRaydium } = sdk;
293
- const raydium = await initRaydium(connection);
294
- // 通过 API 查找所有包含该代币的池子
295
- const poolList = await raydium.api.fetchPoolByMints({
296
- mint1: mint,
297
- mint2: WSOL,
298
- });
299
- // poolList 可能是对象而不是数组,需要处理
300
- const poolArray = poolList?.data || [];
301
- if (!Array.isArray(poolArray))
396
+ }
397
+ const result = await response.json();
398
+ if (!result.success || !result.data?.data) {
399
+ if (debug)
400
+ console.log('[LP Inspect] Raydium API returned no data');
401
+ return pools;
402
+ }
403
+ const poolArray = result.data.data;
404
+ if (!Array.isArray(poolArray) || poolArray.length === 0)
302
405
  return pools;
406
+ if (debug)
407
+ console.log('[LP Inspect] Raydium API found', poolArray.length, 'pools');
408
+ // ==================== 步骤 2: 链上查询实时数据 ====================
303
409
  for (const pool of poolArray) {
304
- // 判断池子类型
305
- let platform = 'RAYDIUM_AMM';
306
- if (pool.programId?.includes('CPMMoo')) {
307
- platform = 'RAYDIUM_CPMM';
410
+ try {
411
+ // 判断池子类型和程序
412
+ let platform = 'RAYDIUM_AMM';
413
+ const programId = pool.programId;
414
+ if (programId === RAYDIUM_PROGRAMS.CPMM) {
415
+ platform = 'RAYDIUM_CPMM';
416
+ }
417
+ else if (programId === RAYDIUM_PROGRAMS.CLMM) {
418
+ platform = 'RAYDIUM_CLMM';
419
+ }
420
+ else if (programId === RAYDIUM_PROGRAMS.AMM_V4) {
421
+ platform = 'RAYDIUM_AMM';
422
+ }
423
+ // 判断哪个是 base token
424
+ const isBaseA = pool.mintA?.address === mint;
425
+ const quoteToken = isBaseA ? pool.mintB?.address : pool.mintA?.address;
426
+ const quoteSymbol = isBaseA ? pool.mintB?.symbol : pool.mintA?.symbol;
427
+ const quoteDecimals = isBaseA ? pool.mintB?.decimals : pool.mintA?.decimals;
428
+ const baseDecimals = isBaseA ? pool.mintA?.decimals : pool.mintB?.decimals;
429
+ if (!quoteToken)
430
+ continue;
431
+ // 尝试从链上获取实时储备量
432
+ let reserveQuote = pool.mintAmountA;
433
+ let reserveBase = pool.mintAmountB;
434
+ if (isBaseA) {
435
+ reserveQuote = pool.mintAmountB;
436
+ reserveBase = pool.mintAmountA;
437
+ }
438
+ // 尝试从链上获取实时数据(支持 CPMM, AMM V4, CLMM)
439
+ let onChainPrice;
440
+ try {
441
+ const onChainData = await fetchRaydiumPoolOnChain(connection, pool.id, programId, debug);
442
+ if (onChainData) {
443
+ // 使用链上数据更新储备量
444
+ if (isBaseA) {
445
+ reserveQuote = onChainData.reserveB;
446
+ reserveBase = onChainData.reserveA;
447
+ }
448
+ else {
449
+ reserveQuote = onChainData.reserveA;
450
+ reserveBase = onChainData.reserveB;
451
+ }
452
+ onChainPrice = onChainData.price;
453
+ if (debug)
454
+ console.log('[LP Inspect] Got on-chain data:', { reserveQuote, reserveBase, price: onChainPrice });
455
+ }
456
+ }
457
+ catch (e) {
458
+ if (debug)
459
+ console.log('[LP Inspect] Failed to get on-chain data, using API data:', e);
460
+ }
461
+ pools.push({
462
+ platform,
463
+ poolAddress: pool.id,
464
+ quoteToken: quoteToken || '',
465
+ quoteSymbol: quoteSymbol || 'UNKNOWN',
466
+ quoteDecimals: quoteDecimals || 9,
467
+ baseToken: mint,
468
+ reserveQuote: String(reserveQuote || 0),
469
+ reserveQuoteRaw: BigInt(Math.floor((reserveQuote || 0) * Math.pow(10, quoteDecimals || 9))),
470
+ reserveBase: String(reserveBase || 0),
471
+ reserveBaseRaw: BigInt(Math.floor((reserveBase || 0) * Math.pow(10, baseDecimals || 6))),
472
+ price: onChainPrice ?? pool.price, // 优先使用链上价格
473
+ feeBps: Math.floor((pool.feeRate || 0) * 10000),
474
+ extra: {
475
+ type: pool.type,
476
+ programId,
477
+ tvl: pool.tvl,
478
+ lpMint: pool.lpMint?.address,
479
+ onChainData: onChainPrice !== undefined, // 标记是否使用了链上数据
480
+ },
481
+ });
308
482
  }
309
- else if (pool.type === 'Concentrated') {
310
- platform = 'RAYDIUM_CLMM';
483
+ catch (poolErr) {
484
+ if (debug)
485
+ console.log('[LP Inspect] Error processing pool:', pool.id, poolErr);
311
486
  }
312
- const isBaseA = pool.mintA?.address === mint;
313
- const quoteToken = isBaseA ? pool.mintB?.address : pool.mintA?.address;
314
- const quoteSymbol = isBaseA ? pool.mintB?.symbol : pool.mintA?.symbol;
315
- const quoteDecimals = isBaseA ? pool.mintB?.decimals : pool.mintA?.decimals;
316
- const reserveQuoteNum = isBaseA ? pool.mintAmountB : pool.mintAmountA;
317
- const reserveBaseNum = isBaseA ? pool.mintAmountA : pool.mintAmountB;
318
- if (!quoteToken)
319
- continue;
320
- pools.push({
321
- platform,
322
- poolAddress: pool.id,
323
- quoteToken: quoteToken || '',
324
- quoteSymbol: quoteSymbol || 'UNKNOWN',
325
- quoteDecimals: quoteDecimals || 9,
326
- baseToken: mint,
327
- reserveQuote: String(reserveQuoteNum || 0),
328
- reserveQuoteRaw: BigInt(Math.floor((reserveQuoteNum || 0) * 1e9)),
329
- reserveBase: String(reserveBaseNum || 0),
330
- reserveBaseRaw: BigInt(Math.floor((reserveBaseNum || 0) * 1e9)),
331
- price: pool.price,
332
- feeBps: Math.floor((pool.feeRate || 0) * 10000),
333
- extra: {
334
- type: pool.type,
335
- lpMint: pool.lpMint?.address,
336
- },
337
- });
338
487
  }
339
488
  }
340
489
  catch (err) {
@@ -343,6 +492,132 @@ async function detectRaydiumPools(mint, connection, debug) {
343
492
  }
344
493
  return pools;
345
494
  }
495
+ /**
496
+ * 从链上获取 Raydium 池子的实时数据
497
+ * 使用 Raydium SDK 的官方 layout 解析
498
+ */
499
+ async function fetchRaydiumPoolOnChain(connection, poolId, programId, debug) {
500
+ try {
501
+ // 动态导入 Raydium SDK 的 layout
502
+ const { liquidityStateV4Layout, CpmmPoolInfoLayout, PoolInfoLayout, splAccountLayout, SqrtPriceMath, } = await import('@raydium-io/raydium-sdk-v2');
503
+ const poolPubkey = new PublicKey(poolId);
504
+ const accountInfo = await connection.getAccountInfo(poolPubkey);
505
+ if (!accountInfo) {
506
+ if (debug)
507
+ console.log('[LP Inspect] Pool account not found:', poolId);
508
+ return null;
509
+ }
510
+ // 验证程序 ID
511
+ if (accountInfo.owner.toBase58() !== programId) {
512
+ if (debug)
513
+ console.log('[LP Inspect] Pool owner mismatch');
514
+ return null;
515
+ }
516
+ const data = accountInfo.data;
517
+ // ==================== CPMM 池子 ====================
518
+ if (programId === RAYDIUM_PROGRAMS.CPMM) {
519
+ try {
520
+ const poolInfo = CpmmPoolInfoLayout.decode(data);
521
+ // 读取 vault 账户余额
522
+ const [vaultAInfo, vaultBInfo] = await connection.getMultipleAccountsInfo([
523
+ poolInfo.vaultA,
524
+ poolInfo.vaultB,
525
+ ]);
526
+ if (!vaultAInfo || !vaultBInfo)
527
+ return null;
528
+ const vaultAData = splAccountLayout.decode(vaultAInfo.data);
529
+ const vaultBData = splAccountLayout.decode(vaultBInfo.data);
530
+ // 计算实际储备 (扣除手续费)
531
+ const reserveA = Number(vaultAData.amount
532
+ .sub(poolInfo.fundFeesMintA)
533
+ .sub(poolInfo.protocolFeesMintA)) / Math.pow(10, poolInfo.mintDecimalA);
534
+ const reserveB = Number(vaultBData.amount
535
+ .sub(poolInfo.fundFeesMintB)
536
+ .sub(poolInfo.protocolFeesMintB)) / Math.pow(10, poolInfo.mintDecimalB);
537
+ return {
538
+ reserveA,
539
+ reserveB,
540
+ decimalsA: poolInfo.mintDecimalA,
541
+ decimalsB: poolInfo.mintDecimalB,
542
+ price: reserveB / reserveA,
543
+ };
544
+ }
545
+ catch (e) {
546
+ if (debug)
547
+ console.log('[LP Inspect] CPMM decode error:', e);
548
+ return null;
549
+ }
550
+ }
551
+ // ==================== AMM V4 池子 ====================
552
+ else if (programId === RAYDIUM_PROGRAMS.AMM_V4) {
553
+ try {
554
+ const poolInfo = liquidityStateV4Layout.decode(data);
555
+ // 读取 vault 账户余额
556
+ const [baseVaultInfo, quoteVaultInfo] = await connection.getMultipleAccountsInfo([
557
+ poolInfo.baseVault,
558
+ poolInfo.quoteVault,
559
+ ]);
560
+ if (!baseVaultInfo || !quoteVaultInfo)
561
+ return null;
562
+ const baseVaultData = splAccountLayout.decode(baseVaultInfo.data);
563
+ const quoteVaultData = splAccountLayout.decode(quoteVaultInfo.data);
564
+ // 计算实际储备 (扣除待提取的 PnL)
565
+ const reserveA = Number(baseVaultData.amount.sub(poolInfo.baseNeedTakePnl)) / Math.pow(10, poolInfo.baseDecimal.toNumber());
566
+ const reserveB = Number(quoteVaultData.amount.sub(poolInfo.quoteNeedTakePnl)) / Math.pow(10, poolInfo.quoteDecimal.toNumber());
567
+ return {
568
+ reserveA,
569
+ reserveB,
570
+ decimalsA: poolInfo.baseDecimal.toNumber(),
571
+ decimalsB: poolInfo.quoteDecimal.toNumber(),
572
+ price: reserveB / reserveA,
573
+ };
574
+ }
575
+ catch (e) {
576
+ if (debug)
577
+ console.log('[LP Inspect] AMM V4 decode error:', e);
578
+ return null;
579
+ }
580
+ }
581
+ // ==================== CLMM 池子 ====================
582
+ else if (programId === RAYDIUM_PROGRAMS.CLMM) {
583
+ try {
584
+ const poolInfo = PoolInfoLayout.decode(data);
585
+ // CLMM 使用 sqrtPriceX64 计算价格
586
+ const price = SqrtPriceMath.sqrtPriceX64ToPrice(poolInfo.sqrtPriceX64, poolInfo.mintDecimalsA, poolInfo.mintDecimalsB);
587
+ // CLMM 的储备量需要从 vault 读取
588
+ const [vaultAInfo, vaultBInfo] = await connection.getMultipleAccountsInfo([
589
+ poolInfo.vaultA,
590
+ poolInfo.vaultB,
591
+ ]);
592
+ let reserveA = 0, reserveB = 0;
593
+ if (vaultAInfo && vaultBInfo) {
594
+ const vaultAData = splAccountLayout.decode(vaultAInfo.data);
595
+ const vaultBData = splAccountLayout.decode(vaultBInfo.data);
596
+ reserveA = Number(vaultAData.amount) / Math.pow(10, poolInfo.mintDecimalsA);
597
+ reserveB = Number(vaultBData.amount) / Math.pow(10, poolInfo.mintDecimalsB);
598
+ }
599
+ return {
600
+ reserveA,
601
+ reserveB,
602
+ decimalsA: poolInfo.mintDecimalsA,
603
+ decimalsB: poolInfo.mintDecimalsB,
604
+ price: price.toNumber(),
605
+ };
606
+ }
607
+ catch (e) {
608
+ if (debug)
609
+ console.log('[LP Inspect] CLMM decode error:', e);
610
+ return null;
611
+ }
612
+ }
613
+ return null;
614
+ }
615
+ catch (e) {
616
+ if (debug)
617
+ console.log('[LP Inspect] On-chain fetch error:', e);
618
+ return null;
619
+ }
620
+ }
346
621
  // ============================================================================
347
622
  // 主函数
348
623
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.3.90",
3
+ "version": "1.3.92",
4
4
  "description": "SDK for Flap bonding curve, four.meme TokenManager, and Solana DEX",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",