four-flap-meme-sdk 1.5.54 → 1.5.56
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.
|
@@ -5,125 +5,71 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Connection } from '@solana/web3.js';
|
|
7
7
|
/** Solana LP 平台类型 */
|
|
8
|
-
export type SolanaLPPlatform = 'PUMP_BONDING_CURVE' | 'METEORA_DBC' | '
|
|
8
|
+
export type SolanaLPPlatform = 'PUMP_BONDING_CURVE' | 'METEORA_DBC' | 'RAYDIUM_LAUNCHLAB' | 'JUPITER_AGGREGATED' | 'PUMP_SWAP' | 'MULTI_POOL' | 'UNKNOWN';
|
|
9
9
|
/** 内盘(Bonding Curve)信息 */
|
|
10
10
|
export interface SolanaBondingCurveInfo {
|
|
11
|
-
|
|
12
|
-
type: 'pump' | 'meteora_dbc' | 'orca_wavebreak' | 'raydium_launchlab';
|
|
13
|
-
/** Bonding Curve 地址 */
|
|
11
|
+
type: 'pump' | 'meteora_dbc' | 'raydium_launchlab';
|
|
14
12
|
address: string;
|
|
15
|
-
/** 是否已毕业/迁移 */
|
|
16
13
|
complete: boolean;
|
|
17
|
-
/** Quote Token 储备(SOL 或其他) */
|
|
18
14
|
reserveQuote: string;
|
|
19
15
|
reserveQuoteRaw: bigint;
|
|
20
|
-
/** Base Token 储备 */
|
|
21
16
|
reserveToken: string;
|
|
22
17
|
reserveTokenRaw: bigint;
|
|
23
|
-
/** 当前价格 */
|
|
24
18
|
price?: number;
|
|
25
|
-
/** 毕业进度百分比 */
|
|
26
19
|
progress?: number;
|
|
27
|
-
/** 创建者 */
|
|
28
20
|
creator?: string;
|
|
29
21
|
}
|
|
30
|
-
/** 外盘池子信息 */
|
|
22
|
+
/** 外盘池子信息 (整合 Jupiter 数据) */
|
|
31
23
|
export interface SolanaPoolInfo {
|
|
32
|
-
/** 平台类型 */
|
|
33
24
|
platform: SolanaLPPlatform;
|
|
34
|
-
/** 池子地址 */
|
|
35
25
|
poolAddress: string;
|
|
36
|
-
/** Quote Token(SOL/USDC 等)*/
|
|
37
26
|
quoteToken: string;
|
|
38
27
|
quoteSymbol: string;
|
|
39
28
|
quoteDecimals: number;
|
|
40
|
-
/** Base Token */
|
|
41
29
|
baseToken: string;
|
|
42
|
-
/** Quote 储备 */
|
|
43
30
|
reserveQuote: string;
|
|
44
31
|
reserveQuoteRaw: bigint;
|
|
45
|
-
/** Base 储备 */
|
|
46
|
-
reserveBase: string;
|
|
47
|
-
reserveBaseRaw: bigint;
|
|
48
|
-
/** 当前价格 */
|
|
49
32
|
price?: number;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
mcap?: number;
|
|
34
|
+
liquidity?: number;
|
|
35
|
+
fdv?: number;
|
|
36
|
+
isVerified?: boolean;
|
|
53
37
|
extra?: Record<string, any>;
|
|
54
38
|
}
|
|
55
39
|
/** Solana LP 检测结果 */
|
|
56
40
|
export interface SolanaLPInfo {
|
|
57
|
-
/** 代币 Mint 地址 */
|
|
58
41
|
mint: string;
|
|
59
|
-
/** 主平台类型 */
|
|
60
42
|
platform: SolanaLPPlatform;
|
|
61
|
-
/** 代币精度 */
|
|
62
43
|
decimals?: number;
|
|
63
|
-
/** 代币总供应量 */
|
|
64
44
|
totalSupply?: string;
|
|
65
45
|
totalSupplyRaw?: bigint;
|
|
66
|
-
/** 内盘信息(如果在内盘) */
|
|
67
46
|
bondingCurve?: SolanaBondingCurveInfo;
|
|
68
|
-
/** 所有发现的外盘池子 */
|
|
69
|
-
pools: SolanaPoolInfo[];
|
|
70
|
-
/** 最佳池子推荐(按流动性排序) */
|
|
71
47
|
bestPool?: SolanaPoolInfo;
|
|
72
|
-
|
|
48
|
+
pools: SolanaPoolInfo[];
|
|
73
49
|
elapsed?: number;
|
|
74
50
|
}
|
|
75
51
|
/** 检测选项 */
|
|
76
52
|
export interface SolanaInspectOptions {
|
|
77
|
-
/** 自定义 RPC URL */
|
|
78
53
|
rpcUrl?: string;
|
|
79
|
-
/** 自定义连接 */
|
|
80
54
|
connection?: Connection;
|
|
81
|
-
/** 是否跳过内盘检测 */
|
|
82
55
|
skipBondingCurve?: boolean;
|
|
83
|
-
/** 是否跳过外盘检测 */
|
|
84
56
|
skipPools?: boolean;
|
|
85
|
-
/** 调试模式 */
|
|
86
57
|
debug?: boolean;
|
|
87
|
-
/** 超时时间 (ms) */
|
|
88
58
|
timeout?: number;
|
|
89
59
|
}
|
|
90
60
|
/**
|
|
91
|
-
* 检测 Solana 代币的 LP
|
|
92
|
-
*
|
|
93
|
-
* @param mint 代币 Mint 地址
|
|
94
|
-
* @param options 检测选项
|
|
95
|
-
* @returns LP 信息
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```typescript
|
|
99
|
-
* const lpInfo = await inspectSolanaTokenLP('TokenMintAddress...', {
|
|
100
|
-
* rpcUrl: 'https://api.mainnet-beta.solana.com',
|
|
101
|
-
* debug: true,
|
|
102
|
-
* })
|
|
103
|
-
*
|
|
104
|
-
* if (lpInfo.platform === 'PUMP_BONDING_CURVE') {
|
|
105
|
-
* console.log('代币在 Pump.fun 内盘')
|
|
106
|
-
* console.log('毕业进度:', lpInfo.bondingCurve?.progress)
|
|
107
|
-
* } else if (lpInfo.bestPool) {
|
|
108
|
-
* console.log('最佳池子:', lpInfo.bestPool.platform)
|
|
109
|
-
* console.log('流动性:', lpInfo.bestPool.reserveQuote)
|
|
110
|
-
* }
|
|
111
|
-
* ```
|
|
61
|
+
* 检测 Solana 代币的 LP 信息
|
|
112
62
|
*/
|
|
113
63
|
export declare function inspectSolanaTokenLP(mint: string, options?: SolanaInspectOptions): Promise<SolanaLPInfo>;
|
|
114
64
|
/**
|
|
115
65
|
* 快速检测代币是否在内盘
|
|
116
|
-
*
|
|
117
|
-
* @param mint 代币 Mint 地址
|
|
118
|
-
* @param options 检测选项
|
|
119
|
-
* @returns 如果在内盘返回内盘信息,否则返回 null
|
|
120
66
|
*/
|
|
121
67
|
export declare function detectBondingCurve(mint: string, options?: SolanaInspectOptions): Promise<SolanaBondingCurveInfo | null>;
|
|
122
68
|
/**
|
|
123
|
-
*
|
|
69
|
+
* 获取代币流动性 (USD)
|
|
124
70
|
*/
|
|
125
|
-
export declare function
|
|
71
|
+
export declare function getTokenLiquidity(mint: string, options?: SolanaInspectOptions): Promise<number>;
|
|
126
72
|
/**
|
|
127
|
-
*
|
|
73
|
+
* 判断代币是否已毕业
|
|
128
74
|
*/
|
|
129
|
-
export declare function
|
|
75
|
+
export declare function isTokenGraduated(mint: string, options?: SolanaInspectOptions): Promise<boolean>;
|
|
@@ -5,66 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { PublicKey } from '@solana/web3.js';
|
|
7
7
|
import { createConnection } from './connection.js';
|
|
8
|
+
import { getTokenInfoV2 } from '../dex/jup/tokens.js';
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// 常量
|
|
10
11
|
// ============================================================================
|
|
11
|
-
/** Wrapped SOL */
|
|
12
12
|
const WSOL = 'So11111111111111111111111111111111111111112';
|
|
13
|
-
/** 常用 Quote Token */
|
|
14
|
-
const QUOTE_TOKENS = {
|
|
15
|
-
[WSOL]: { symbol: 'SOL', decimals: 9 },
|
|
16
|
-
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': { symbol: 'USDC', decimals: 6 },
|
|
17
|
-
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': { symbol: 'USDT', decimals: 6 },
|
|
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
|
-
};
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// 动态导入(避免循环依赖和类型冲突)
|
|
34
|
-
// ============================================================================
|
|
35
|
-
/** 懒加载 Pump.fun SDK */
|
|
36
|
-
async function loadPumpSdk() {
|
|
37
|
-
const { getBondingCurveInfo } = await import('../dex/pump/pump.js');
|
|
38
|
-
const { getPumpSwapPoolInfo } = await import('../dex/pump/pump-swap.js');
|
|
39
|
-
return { getBondingCurveInfo, getPumpSwapPoolInfo };
|
|
40
|
-
}
|
|
41
|
-
/** 懒加载 Meteora SDK */
|
|
42
|
-
async function loadMeteoraSdk() {
|
|
43
|
-
const { findDbcPoolByBaseMint, getDbcPoolInfo } = await import('../dex/meteora/dbc.js');
|
|
44
|
-
const { findDlmmPool, getDlmmPoolInfo, findDlmmPoolsByToken } = await import('../dex/meteora/dlmm.js');
|
|
45
|
-
return { findDbcPoolByBaseMint, getDbcPoolInfo, findDlmmPool, getDlmmPoolInfo, findDlmmPoolsByToken };
|
|
46
|
-
}
|
|
47
|
-
/** 懒加载 Orca SDK */
|
|
48
|
-
async function loadOrcaSdk() {
|
|
49
|
-
try {
|
|
50
|
-
const { getWavebreakBondingCurveInfo } = await import('../dex/orca/wavebreak.js');
|
|
51
|
-
const { findOrcaPool, getOrcaPoolInfo } = await import('../dex/orca/orca.js');
|
|
52
|
-
return { getWavebreakBondingCurveInfo, findOrcaPool, getOrcaPoolInfo };
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/** 懒加载 Raydium SDK */
|
|
59
|
-
async function loadRaydiumSdk() {
|
|
60
|
-
try {
|
|
61
|
-
const { initRaydium, getRaydiumPoolInfo } = await import('../dex/raydium/raydium.js');
|
|
62
|
-
return { initRaydium, getRaydiumPoolInfo };
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
13
|
// ============================================================================
|
|
69
14
|
// 内盘检测
|
|
70
15
|
// ============================================================================
|
|
@@ -73,14 +18,12 @@ async function loadRaydiumSdk() {
|
|
|
73
18
|
*/
|
|
74
19
|
async function detectPumpBondingCurve(mint, connection, debug) {
|
|
75
20
|
try {
|
|
76
|
-
const { getBondingCurveInfo } = await
|
|
21
|
+
const { getBondingCurveInfo } = await import('../dex/pump/pump.js');
|
|
77
22
|
const info = await getBondingCurveInfo(connection, mint);
|
|
78
23
|
if (!info)
|
|
79
24
|
return null;
|
|
80
|
-
// 计算价格 (virtualSolReserves / virtualTokenReserves)
|
|
81
25
|
const price = Number(info.virtualSolReserves) / Number(info.virtualTokenReserves);
|
|
82
|
-
|
|
83
|
-
const GRADUATION_THRESHOLD = 85000000000n; // 85 SOL in lamports
|
|
26
|
+
const GRADUATION_THRESHOLD = 85000000000n;
|
|
84
27
|
const progress = Math.min(100, Number(info.realSolReserves * 100n / GRADUATION_THRESHOLD));
|
|
85
28
|
return {
|
|
86
29
|
type: 'pump',
|
|
@@ -106,7 +49,7 @@ async function detectPumpBondingCurve(mint, connection, debug) {
|
|
|
106
49
|
*/
|
|
107
50
|
async function detectMeteoraDbc(mint, connection, debug) {
|
|
108
51
|
try {
|
|
109
|
-
const { findDbcPoolByBaseMint, getDbcPoolInfo } = await
|
|
52
|
+
const { findDbcPoolByBaseMint, getDbcPoolInfo } = await import('../dex/meteora/dbc.js');
|
|
110
53
|
const poolAddress = await findDbcPoolByBaseMint(mint, connection);
|
|
111
54
|
if (!poolAddress)
|
|
112
55
|
return null;
|
|
@@ -130,595 +73,83 @@ async function detectMeteoraDbc(mint, connection, debug) {
|
|
|
130
73
|
return null;
|
|
131
74
|
}
|
|
132
75
|
}
|
|
133
|
-
/**
|
|
134
|
-
* 检测 Orca Wavebreak
|
|
135
|
-
*/
|
|
136
|
-
async function detectOrcaWavebreak(mint, connection, debug) {
|
|
137
|
-
try {
|
|
138
|
-
const sdk = await loadOrcaSdk();
|
|
139
|
-
if (!sdk)
|
|
140
|
-
return null;
|
|
141
|
-
const { getWavebreakBondingCurveInfo } = sdk;
|
|
142
|
-
const info = await getWavebreakBondingCurveInfo(mint, connection);
|
|
143
|
-
if (!info)
|
|
144
|
-
return null;
|
|
145
|
-
return {
|
|
146
|
-
type: 'orca_wavebreak',
|
|
147
|
-
address: info.bondingCurveAddress,
|
|
148
|
-
complete: info.isGraduated,
|
|
149
|
-
reserveQuote: (Number(info.quoteAmount) / 1e9).toFixed(4),
|
|
150
|
-
reserveQuoteRaw: info.quoteAmount,
|
|
151
|
-
reserveToken: info.baseAmount.toString(),
|
|
152
|
-
reserveTokenRaw: info.baseAmount,
|
|
153
|
-
price: info.currentPrice,
|
|
154
|
-
progress: info.progressPercent,
|
|
155
|
-
creator: info.creator,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
if (debug)
|
|
160
|
-
console.log('[LP Inspect] Orca Wavebreak check failed:', err);
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
76
|
/**
|
|
165
77
|
* 检测 Raydium LaunchLab
|
|
166
|
-
* 使用 Raydium SDK 的官方 layout 解析链上数据
|
|
167
78
|
*/
|
|
168
79
|
async function detectRaydiumLaunchLab(mint, connection, debug) {
|
|
169
80
|
try {
|
|
170
|
-
// 动态导入 Raydium SDK
|
|
171
81
|
const { LaunchpadPool, LaunchpadConfig, getPdaLaunchpadPoolId, LAUNCHPAD_PROGRAM, Curve, } = await import('@raydium-io/raydium-sdk-v2');
|
|
172
82
|
const { NATIVE_MINT } = await import('@solana/spl-token');
|
|
173
|
-
const
|
|
174
|
-
const mintB = NATIVE_MINT; // 默认与 SOL 配对
|
|
175
|
-
// 获取池子 PDA
|
|
176
|
-
const poolId = getPdaLaunchpadPoolId(LAUNCHPAD_PROGRAM, mintA, mintB).publicKey;
|
|
177
|
-
// 获取池子账户数据
|
|
83
|
+
const poolId = getPdaLaunchpadPoolId(LAUNCHPAD_PROGRAM, new PublicKey(mint), NATIVE_MINT).publicKey;
|
|
178
84
|
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');
|
|
85
|
+
if (!poolAccountInfo)
|
|
188
86
|
return null;
|
|
189
|
-
}
|
|
190
|
-
// 解码池子数据
|
|
191
87
|
const poolInfo = LaunchpadPool.decode(poolAccountInfo.data);
|
|
192
|
-
// 获取配置信息
|
|
193
88
|
const configAccountInfo = await connection.getAccountInfo(poolInfo.configId);
|
|
194
|
-
if (!configAccountInfo)
|
|
195
|
-
if (debug)
|
|
196
|
-
console.log('[LP Inspect] LaunchLab config not found');
|
|
89
|
+
if (!configAccountInfo)
|
|
197
90
|
return null;
|
|
198
|
-
}
|
|
199
91
|
const configInfo = LaunchpadConfig.decode(configAccountInfo.data);
|
|
200
|
-
// 计算当前价格
|
|
201
92
|
const currentPrice = Curve.getPrice({
|
|
202
93
|
poolInfo,
|
|
203
94
|
curveType: configInfo.curveType,
|
|
204
95
|
decimalA: poolInfo.mintDecimalsA,
|
|
205
96
|
decimalB: poolInfo.mintDecimalsB,
|
|
206
97
|
}).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;
|
|
98
|
+
const isMigrated = poolInfo.status !== undefined && poolInfo.status >= 2;
|
|
239
99
|
return {
|
|
240
100
|
type: 'raydium_launchlab',
|
|
241
101
|
address: poolId.toBase58(),
|
|
242
|
-
complete: isMigrated
|
|
243
|
-
reserveQuote:
|
|
102
|
+
complete: isMigrated,
|
|
103
|
+
reserveQuote: (Number(poolInfo.totalFundRaisingB || 0) / 1e9).toFixed(4),
|
|
244
104
|
reserveQuoteRaw: BigInt(poolInfo.totalFundRaisingB?.toString() || '0'),
|
|
245
|
-
reserveToken:
|
|
246
|
-
reserveTokenRaw:
|
|
105
|
+
reserveToken: "0", // Simplified
|
|
106
|
+
reserveTokenRaw: 0n,
|
|
247
107
|
price: currentPrice,
|
|
248
|
-
progress,
|
|
249
108
|
creator: poolInfo.creator?.toBase58(),
|
|
250
109
|
};
|
|
251
110
|
}
|
|
252
111
|
catch (err) {
|
|
253
112
|
if (debug)
|
|
254
|
-
console.log('[LP Inspect] Raydium LaunchLab check failed:', err
|
|
113
|
+
console.log('[LP Inspect] Raydium LaunchLab check failed:', err);
|
|
255
114
|
return null;
|
|
256
115
|
}
|
|
257
116
|
}
|
|
258
117
|
// ============================================================================
|
|
259
|
-
// 外盘检测
|
|
118
|
+
// 外盘检测 (Jupiter API)
|
|
260
119
|
// ============================================================================
|
|
261
120
|
/**
|
|
262
|
-
*
|
|
263
|
-
*/
|
|
264
|
-
async function detectPumpSwapPool(mint, connection, debug) {
|
|
265
|
-
try {
|
|
266
|
-
const { getPumpSwapPoolInfo } = await loadPumpSdk();
|
|
267
|
-
const info = await getPumpSwapPoolInfo(connection, mint);
|
|
268
|
-
if (!info)
|
|
269
|
-
return null;
|
|
270
|
-
// 获取池子余额
|
|
271
|
-
const poolPubkey = new PublicKey(info.poolAddress);
|
|
272
|
-
const balance = await connection.getBalance(poolPubkey);
|
|
273
|
-
return {
|
|
274
|
-
platform: 'PUMP_SWAP',
|
|
275
|
-
poolAddress: info.poolAddress,
|
|
276
|
-
quoteToken: info.quoteMint,
|
|
277
|
-
quoteSymbol: 'SOL',
|
|
278
|
-
quoteDecimals: 9,
|
|
279
|
-
baseToken: info.baseMint,
|
|
280
|
-
reserveQuote: (balance / 1e9).toFixed(4),
|
|
281
|
-
reserveQuoteRaw: BigInt(balance),
|
|
282
|
-
reserveBase: info.lpSupply.toString(),
|
|
283
|
-
reserveBaseRaw: info.lpSupply,
|
|
284
|
-
extra: {
|
|
285
|
-
lpMint: info.lpMint,
|
|
286
|
-
creator: info.creator,
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
if (debug)
|
|
292
|
-
console.log('[LP Inspect] Pump Swap pool check failed:', err);
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* 检测 Meteora DLMM Pool
|
|
298
|
-
* 优化: 使用 Meteora API 替代全量链上查询
|
|
299
|
-
*/
|
|
300
|
-
async function detectMeteoraDlmm(mint, connection, debug) {
|
|
301
|
-
try {
|
|
302
|
-
const { findDlmmPoolsByToken, getDlmmPoolInfo } = await loadMeteoraSdk();
|
|
303
|
-
// 使用 API 查询所有包含该代币的池子
|
|
304
|
-
const pools = await findDlmmPoolsByToken(mint);
|
|
305
|
-
if (pools.length === 0) {
|
|
306
|
-
if (debug)
|
|
307
|
-
console.log('[LP Inspect] No Meteora DLMM pools found for mint:', mint);
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
// 优先选择与 SOL 配对的池子
|
|
311
|
-
let selectedPool = pools.find(p => p.tokenX === WSOL || p.tokenY === WSOL);
|
|
312
|
-
if (!selectedPool) {
|
|
313
|
-
selectedPool = pools[0]; // 如果没有 SOL 池子,选择第一个
|
|
314
|
-
}
|
|
315
|
-
if (debug)
|
|
316
|
-
console.log('[LP Inspect] Found Meteora DLMM pool:', selectedPool.address);
|
|
317
|
-
// 获取池子详细信息
|
|
318
|
-
const poolInfo = await getDlmmPoolInfo(selectedPool.address, connection);
|
|
319
|
-
// 判断哪个是 base token
|
|
320
|
-
const isTokenX = poolInfo.tokenXMint === mint;
|
|
321
|
-
const reserveQuote = isTokenX ? poolInfo.tokenYReserve : poolInfo.tokenXReserve;
|
|
322
|
-
const reserveBase = isTokenX ? poolInfo.tokenXReserve : poolInfo.tokenYReserve;
|
|
323
|
-
const quoteToken = isTokenX ? poolInfo.tokenYMint : poolInfo.tokenXMint;
|
|
324
|
-
return {
|
|
325
|
-
platform: 'METEORA_DLMM',
|
|
326
|
-
poolAddress: selectedPool.address,
|
|
327
|
-
quoteToken,
|
|
328
|
-
quoteSymbol: quoteToken === WSOL ? 'SOL' : 'UNKNOWN',
|
|
329
|
-
quoteDecimals: quoteToken === WSOL ? 9 : 6,
|
|
330
|
-
baseToken: mint,
|
|
331
|
-
reserveQuote: reserveQuote || '0',
|
|
332
|
-
reserveQuoteRaw: BigInt(reserveQuote || 0),
|
|
333
|
-
reserveBase: reserveBase || '0',
|
|
334
|
-
reserveBaseRaw: BigInt(reserveBase || 0),
|
|
335
|
-
price: poolInfo.currentPrice,
|
|
336
|
-
feeBps: poolInfo.feeBps,
|
|
337
|
-
extra: {
|
|
338
|
-
binStep: poolInfo.binStep,
|
|
339
|
-
activeId: poolInfo.activeId,
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
catch (err) {
|
|
344
|
-
if (debug)
|
|
345
|
-
console.log('[LP Inspect] Meteora DLMM check failed:', err);
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* 检测 Orca Whirlpool
|
|
121
|
+
* 使用 Jupiter API 检测外盘信息
|
|
351
122
|
*/
|
|
352
|
-
async function
|
|
123
|
+
async function detectWithJupiter(mint, debug) {
|
|
353
124
|
try {
|
|
354
|
-
const
|
|
355
|
-
if (!
|
|
125
|
+
const info = await getTokenInfoV2(mint);
|
|
126
|
+
if (!info || !info.liquidity || info.liquidity < 100) {
|
|
356
127
|
return null;
|
|
357
|
-
const { findOrcaPool, getOrcaPoolInfo } = sdk;
|
|
358
|
-
// 尝试与 SOL 配对(常见的 tick spacing: 64, 128)
|
|
359
|
-
let poolAddress = await findOrcaPool(mint, WSOL, 64, connection);
|
|
360
|
-
if (!poolAddress) {
|
|
361
|
-
poolAddress = await findOrcaPool(mint, WSOL, 128, connection);
|
|
362
128
|
}
|
|
363
|
-
if (!poolAddress)
|
|
364
|
-
return null;
|
|
365
|
-
const poolInfo = await getOrcaPoolInfo(poolAddress, connection);
|
|
366
|
-
// 判断哪个是 base token
|
|
367
|
-
const isTokenA = poolInfo.tokenMintA === mint;
|
|
368
129
|
return {
|
|
369
|
-
platform: '
|
|
370
|
-
poolAddress,
|
|
371
|
-
quoteToken:
|
|
130
|
+
platform: 'JUPITER_AGGREGATED',
|
|
131
|
+
poolAddress: info.graduatedPool || '',
|
|
132
|
+
quoteToken: WSOL,
|
|
372
133
|
quoteSymbol: 'SOL',
|
|
373
|
-
quoteDecimals:
|
|
134
|
+
quoteDecimals: 9,
|
|
374
135
|
baseToken: mint,
|
|
375
|
-
reserveQuote:
|
|
376
|
-
reserveQuoteRaw:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
136
|
+
reserveQuote: info.liquidity.toFixed(2),
|
|
137
|
+
reserveQuoteRaw: BigInt(Math.floor(info.liquidity)),
|
|
138
|
+
price: info.usdPrice ?? undefined,
|
|
139
|
+
mcap: info.mcap ?? undefined,
|
|
140
|
+
liquidity: info.liquidity,
|
|
141
|
+
fdv: info.fdv ?? undefined,
|
|
142
|
+
isVerified: info.isVerified ?? false,
|
|
381
143
|
extra: {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
144
|
+
launchpad: info.launchpad,
|
|
145
|
+
organicScore: info.organicScore,
|
|
146
|
+
stats24h: info.stats24h,
|
|
147
|
+
}
|
|
386
148
|
};
|
|
387
149
|
}
|
|
388
150
|
catch (err) {
|
|
389
151
|
if (debug)
|
|
390
|
-
console.log('[LP Inspect]
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* 检测 Raydium 池子(混合方案)
|
|
396
|
-
* 1. 先用 API 快速发现池子列表
|
|
397
|
-
* 2. 再用链上查询获取实时储备量
|
|
398
|
-
*/
|
|
399
|
-
async function detectRaydiumPools(mint, connection, debug) {
|
|
400
|
-
const pools = [];
|
|
401
|
-
try {
|
|
402
|
-
// ==================== 步骤 1: 用 API 发现池子 ====================
|
|
403
|
-
const apiUrl = `https://api-v3.raydium.io/pools/info/mint?mint1=${mint}&poolType=all&poolSortField=default&sortType=desc&pageSize=20&page=1`;
|
|
404
|
-
const response = await fetch(apiUrl);
|
|
405
|
-
if (!response.ok) {
|
|
406
|
-
if (debug)
|
|
407
|
-
console.log('[LP Inspect] Raydium API request failed:', response.status);
|
|
408
|
-
return pools;
|
|
409
|
-
}
|
|
410
|
-
const result = await response.json();
|
|
411
|
-
if (!result.success || !result.data?.data) {
|
|
412
|
-
if (debug)
|
|
413
|
-
console.log('[LP Inspect] Raydium API returned no data');
|
|
414
|
-
return pools;
|
|
415
|
-
}
|
|
416
|
-
const poolArray = result.data.data;
|
|
417
|
-
if (!Array.isArray(poolArray) || poolArray.length === 0)
|
|
418
|
-
return pools;
|
|
419
|
-
if (debug)
|
|
420
|
-
console.log('[LP Inspect] Raydium API found', poolArray.length, 'pools');
|
|
421
|
-
// ==================== 步骤 2: 链上查询实时数据 ====================
|
|
422
|
-
for (const pool of poolArray) {
|
|
423
|
-
try {
|
|
424
|
-
// 判断池子类型和程序
|
|
425
|
-
let platform = 'RAYDIUM_AMM';
|
|
426
|
-
const programId = pool.programId;
|
|
427
|
-
if (programId === RAYDIUM_PROGRAMS.CPMM) {
|
|
428
|
-
platform = 'RAYDIUM_CPMM';
|
|
429
|
-
}
|
|
430
|
-
else if (programId === RAYDIUM_PROGRAMS.CLMM) {
|
|
431
|
-
platform = 'RAYDIUM_CLMM';
|
|
432
|
-
}
|
|
433
|
-
else if (programId === RAYDIUM_PROGRAMS.AMM_V4) {
|
|
434
|
-
platform = 'RAYDIUM_AMM';
|
|
435
|
-
}
|
|
436
|
-
// 判断哪个是 base token
|
|
437
|
-
const isBaseA = pool.mintA?.address === mint;
|
|
438
|
-
const quoteToken = isBaseA ? pool.mintB?.address : pool.mintA?.address;
|
|
439
|
-
const quoteSymbol = isBaseA ? pool.mintB?.symbol : pool.mintA?.symbol;
|
|
440
|
-
const quoteDecimals = isBaseA ? pool.mintB?.decimals : pool.mintA?.decimals;
|
|
441
|
-
const baseDecimals = isBaseA ? pool.mintA?.decimals : pool.mintB?.decimals;
|
|
442
|
-
if (!quoteToken)
|
|
443
|
-
continue;
|
|
444
|
-
// 尝试从链上获取实时储备量
|
|
445
|
-
let reserveQuote = pool.mintAmountA;
|
|
446
|
-
let reserveBase = pool.mintAmountB;
|
|
447
|
-
if (isBaseA) {
|
|
448
|
-
reserveQuote = pool.mintAmountB;
|
|
449
|
-
reserveBase = pool.mintAmountA;
|
|
450
|
-
}
|
|
451
|
-
// 尝试从链上获取实时数据(支持 CPMM, AMM V4, CLMM)
|
|
452
|
-
let onChainPrice;
|
|
453
|
-
let usedOnChainData = false;
|
|
454
|
-
try {
|
|
455
|
-
const onChainData = await fetchRaydiumPoolOnChain(connection, pool.id, programId, debug); // 添加类型断言以访问额外属性
|
|
456
|
-
if (onChainData) {
|
|
457
|
-
usedOnChainData = true;
|
|
458
|
-
// CLMM 需要特殊处理:合约中 token 按地址排序
|
|
459
|
-
if (platform === 'RAYDIUM_CLMM' && onChainData.mintA && onChainData.mintB) {
|
|
460
|
-
// 合约中:mintA 是地址较小的 token,mintB 是地址较大的 token
|
|
461
|
-
// sqrtPriceX64ToPrice 返回 mintB/mintA
|
|
462
|
-
// 判断我们查询的代币在合约中的位置
|
|
463
|
-
const queryMintIsA = onChainData.mintA === mint;
|
|
464
|
-
const queryMintIsB = onChainData.mintB === mint;
|
|
465
|
-
if (queryMintIsA) {
|
|
466
|
-
// 查询的代币是 mintA(地址较小)
|
|
467
|
-
// sqrtPrice 返回 mintB/mintA = quote/base
|
|
468
|
-
reserveBase = onChainData.reserveA;
|
|
469
|
-
reserveQuote = onChainData.reserveB;
|
|
470
|
-
// 我们需要 quote/base 价格,sqrtPrice 已经是这个
|
|
471
|
-
onChainPrice = onChainData.price;
|
|
472
|
-
}
|
|
473
|
-
else if (queryMintIsB) {
|
|
474
|
-
// 查询的代币是 mintB(地址较大)
|
|
475
|
-
// sqrtPrice 返回 mintB/mintA = base/quote(反了)
|
|
476
|
-
reserveBase = onChainData.reserveB;
|
|
477
|
-
reserveQuote = onChainData.reserveA;
|
|
478
|
-
// 我们需要 quote/base = 1 / (base/quote) = 1 / sqrtPrice
|
|
479
|
-
onChainPrice = 1 / onChainData.price;
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
// mint 不在池子中,跳过
|
|
483
|
-
if (debug)
|
|
484
|
-
console.log('[LP Inspect] CLMM: mint not found in pool');
|
|
485
|
-
}
|
|
486
|
-
if (debug) {
|
|
487
|
-
console.log('[LP Inspect] CLMM 链上数据:', {
|
|
488
|
-
poolId: pool.id,
|
|
489
|
-
contractMintA: onChainData.mintA,
|
|
490
|
-
contractMintB: onChainData.mintB,
|
|
491
|
-
queryMint: mint,
|
|
492
|
-
queryMintIsA,
|
|
493
|
-
queryMintIsB,
|
|
494
|
-
rawPrice: onChainData.price,
|
|
495
|
-
adjustedPrice: onChainPrice,
|
|
496
|
-
reserveQuote,
|
|
497
|
-
reserveBase,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
// CPMM 和 AMM V4:使用原有逻辑
|
|
503
|
-
if (isBaseA) {
|
|
504
|
-
reserveQuote = onChainData.reserveB;
|
|
505
|
-
reserveBase = onChainData.reserveA;
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
reserveQuote = onChainData.reserveA;
|
|
509
|
-
reserveBase = onChainData.reserveB;
|
|
510
|
-
}
|
|
511
|
-
// price 已经是 reserveB/reserveA
|
|
512
|
-
onChainPrice = isBaseA ? onChainData.price : (onChainData.price ? 1 / onChainData.price : undefined);
|
|
513
|
-
}
|
|
514
|
-
if (debug) {
|
|
515
|
-
console.log('[LP Inspect] 链上数据 (实时):', {
|
|
516
|
-
poolId: pool.id,
|
|
517
|
-
platform,
|
|
518
|
-
reserveQuote,
|
|
519
|
-
reserveBase,
|
|
520
|
-
onChainPrice,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
catch (e) {
|
|
526
|
-
if (debug)
|
|
527
|
-
console.log('[LP Inspect] 链上数据获取失败,使用 API 数据:', e);
|
|
528
|
-
}
|
|
529
|
-
// 计算最终价格:优先使用链上价格,否则用 reserve 比例
|
|
530
|
-
let finalPrice;
|
|
531
|
-
if (onChainPrice !== undefined) {
|
|
532
|
-
finalPrice = onChainPrice;
|
|
533
|
-
}
|
|
534
|
-
else if (reserveBase > 0 && reserveQuote > 0) {
|
|
535
|
-
// 用储备量比例计算: SOL/代币
|
|
536
|
-
finalPrice = reserveQuote / reserveBase;
|
|
537
|
-
}
|
|
538
|
-
else {
|
|
539
|
-
// API 返回的价格是 代币/SOL,需要取倒数
|
|
540
|
-
finalPrice = pool.price ? (1 / pool.price) : undefined;
|
|
541
|
-
}
|
|
542
|
-
pools.push({
|
|
543
|
-
platform,
|
|
544
|
-
poolAddress: pool.id,
|
|
545
|
-
quoteToken: quoteToken || '',
|
|
546
|
-
quoteSymbol: quoteSymbol || 'UNKNOWN',
|
|
547
|
-
quoteDecimals: quoteDecimals || 9,
|
|
548
|
-
baseToken: mint,
|
|
549
|
-
reserveQuote: String(reserveQuote || 0),
|
|
550
|
-
reserveQuoteRaw: BigInt(Math.floor((reserveQuote || 0) * Math.pow(10, quoteDecimals || 9))),
|
|
551
|
-
reserveBase: String(reserveBase || 0),
|
|
552
|
-
reserveBaseRaw: BigInt(Math.floor((reserveBase || 0) * Math.pow(10, baseDecimals || 6))),
|
|
553
|
-
price: finalPrice,
|
|
554
|
-
feeBps: Math.floor((pool.feeRate || 0) * 10000),
|
|
555
|
-
extra: {
|
|
556
|
-
type: pool.type,
|
|
557
|
-
programId,
|
|
558
|
-
tvl: pool.tvl,
|
|
559
|
-
lpMint: pool.lpMint?.address,
|
|
560
|
-
onChainData: usedOnChainData,
|
|
561
|
-
apiPrice: pool.price, // 保留 API 价格用于对比
|
|
562
|
-
},
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
catch (poolErr) {
|
|
566
|
-
if (debug)
|
|
567
|
-
console.log('[LP Inspect] Error processing pool:', pool.id, poolErr);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
catch (err) {
|
|
572
|
-
if (debug)
|
|
573
|
-
console.log('[LP Inspect] Raydium pools check failed:', err);
|
|
574
|
-
}
|
|
575
|
-
return pools;
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* 从链上获取 Raydium 池子的实时数据
|
|
579
|
-
* 使用 Raydium SDK 的官方 layout 解析
|
|
580
|
-
*/
|
|
581
|
-
async function fetchRaydiumPoolOnChain(connection, poolId, programId, debug) {
|
|
582
|
-
try {
|
|
583
|
-
// 动态导入 Raydium SDK 的 layout
|
|
584
|
-
const { liquidityStateV4Layout, CpmmPoolInfoLayout, PoolInfoLayout, splAccountLayout, SqrtPriceMath, } = await import('@raydium-io/raydium-sdk-v2');
|
|
585
|
-
const poolPubkey = new PublicKey(poolId);
|
|
586
|
-
const accountInfo = await connection.getAccountInfo(poolPubkey);
|
|
587
|
-
if (!accountInfo) {
|
|
588
|
-
if (debug)
|
|
589
|
-
console.log('[LP Inspect] Pool account not found:', poolId);
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
// 验证程序 ID
|
|
593
|
-
if (accountInfo.owner.toBase58() !== programId) {
|
|
594
|
-
if (debug)
|
|
595
|
-
console.log('[LP Inspect] Pool owner mismatch');
|
|
596
|
-
return null;
|
|
597
|
-
}
|
|
598
|
-
const data = accountInfo.data;
|
|
599
|
-
// ==================== CPMM 池子 ====================
|
|
600
|
-
if (programId === RAYDIUM_PROGRAMS.CPMM) {
|
|
601
|
-
try {
|
|
602
|
-
const poolInfo = CpmmPoolInfoLayout.decode(data);
|
|
603
|
-
// 读取 vault 账户余额
|
|
604
|
-
const [vaultAInfo, vaultBInfo] = await connection.getMultipleAccountsInfo([
|
|
605
|
-
poolInfo.vaultA,
|
|
606
|
-
poolInfo.vaultB,
|
|
607
|
-
]);
|
|
608
|
-
if (!vaultAInfo || !vaultBInfo)
|
|
609
|
-
return null;
|
|
610
|
-
const vaultAData = splAccountLayout.decode(vaultAInfo.data);
|
|
611
|
-
const vaultBData = splAccountLayout.decode(vaultBInfo.data);
|
|
612
|
-
// 计算实际储备 (扣除手续费)
|
|
613
|
-
const reserveA = Number(vaultAData.amount
|
|
614
|
-
.sub(poolInfo.fundFeesMintA)
|
|
615
|
-
.sub(poolInfo.protocolFeesMintA)) / Math.pow(10, poolInfo.mintDecimalA);
|
|
616
|
-
const reserveB = Number(vaultBData.amount
|
|
617
|
-
.sub(poolInfo.fundFeesMintB)
|
|
618
|
-
.sub(poolInfo.protocolFeesMintB)) / Math.pow(10, poolInfo.mintDecimalB);
|
|
619
|
-
return {
|
|
620
|
-
reserveA,
|
|
621
|
-
reserveB,
|
|
622
|
-
decimalsA: poolInfo.mintDecimalA,
|
|
623
|
-
decimalsB: poolInfo.mintDecimalB,
|
|
624
|
-
price: reserveB / reserveA,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
catch (e) {
|
|
628
|
-
if (debug)
|
|
629
|
-
console.log('[LP Inspect] CPMM decode error:', e);
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
// ==================== AMM V4 池子 ====================
|
|
634
|
-
else if (programId === RAYDIUM_PROGRAMS.AMM_V4) {
|
|
635
|
-
try {
|
|
636
|
-
const poolInfo = liquidityStateV4Layout.decode(data);
|
|
637
|
-
// 读取 vault 账户余额
|
|
638
|
-
const [baseVaultInfo, quoteVaultInfo] = await connection.getMultipleAccountsInfo([
|
|
639
|
-
poolInfo.baseVault,
|
|
640
|
-
poolInfo.quoteVault,
|
|
641
|
-
]);
|
|
642
|
-
if (!baseVaultInfo || !quoteVaultInfo)
|
|
643
|
-
return null;
|
|
644
|
-
const baseVaultData = splAccountLayout.decode(baseVaultInfo.data);
|
|
645
|
-
const quoteVaultData = splAccountLayout.decode(quoteVaultInfo.data);
|
|
646
|
-
// 计算实际储备 (扣除待提取的 PnL)
|
|
647
|
-
const reserveA = Number(baseVaultData.amount.sub(poolInfo.baseNeedTakePnl)) / Math.pow(10, poolInfo.baseDecimal.toNumber());
|
|
648
|
-
const reserveB = Number(quoteVaultData.amount.sub(poolInfo.quoteNeedTakePnl)) / Math.pow(10, poolInfo.quoteDecimal.toNumber());
|
|
649
|
-
return {
|
|
650
|
-
reserveA,
|
|
651
|
-
reserveB,
|
|
652
|
-
decimalsA: poolInfo.baseDecimal.toNumber(),
|
|
653
|
-
decimalsB: poolInfo.quoteDecimal.toNumber(),
|
|
654
|
-
price: reserveB / reserveA,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
catch (e) {
|
|
658
|
-
if (debug)
|
|
659
|
-
console.log('[LP Inspect] AMM V4 decode error:', e);
|
|
660
|
-
return null;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// ==================== CLMM 池子 ====================
|
|
664
|
-
else if (programId === RAYDIUM_PROGRAMS.CLMM) {
|
|
665
|
-
try {
|
|
666
|
-
const poolInfo = PoolInfoLayout.decode(data);
|
|
667
|
-
// CLMM 使用 sqrtPriceX64 计算价格
|
|
668
|
-
// 注意:合约中 token 是按地址排序的 (token_mint_0 < token_mint_1)
|
|
669
|
-
// sqrtPriceX64ToPrice 返回 token_1 / token_0
|
|
670
|
-
const priceFromSqrt = SqrtPriceMath.sqrtPriceX64ToPrice(poolInfo.sqrtPriceX64, poolInfo.mintDecimalsA, poolInfo.mintDecimalsB);
|
|
671
|
-
// CLMM 的储备量需要从 vault 读取
|
|
672
|
-
const [vaultAInfo, vaultBInfo] = await connection.getMultipleAccountsInfo([
|
|
673
|
-
poolInfo.vaultA,
|
|
674
|
-
poolInfo.vaultB,
|
|
675
|
-
]);
|
|
676
|
-
let reserveA = 0, reserveB = 0;
|
|
677
|
-
if (vaultAInfo && vaultBInfo) {
|
|
678
|
-
const vaultAData = splAccountLayout.decode(vaultAInfo.data);
|
|
679
|
-
const vaultBData = splAccountLayout.decode(vaultBInfo.data);
|
|
680
|
-
reserveA = Number(vaultAData.amount) / Math.pow(10, poolInfo.mintDecimalsA);
|
|
681
|
-
reserveB = Number(vaultBData.amount) / Math.pow(10, poolInfo.mintDecimalsB);
|
|
682
|
-
}
|
|
683
|
-
// 获取 token mint 地址来判断 token 顺序
|
|
684
|
-
// poolInfo.mintA 是地址较小的 token (token_mint_0)
|
|
685
|
-
// poolInfo.mintB 是地址较大的 token (token_mint_1)
|
|
686
|
-
const mintA = poolInfo.mintA?.toBase58?.() || '';
|
|
687
|
-
const mintB = poolInfo.mintB?.toBase58?.() || '';
|
|
688
|
-
if (debug) {
|
|
689
|
-
console.log('[LP Inspect] CLMM pool data:', {
|
|
690
|
-
mintA,
|
|
691
|
-
mintB,
|
|
692
|
-
reserveA,
|
|
693
|
-
reserveB,
|
|
694
|
-
sqrtPriceX64: poolInfo.sqrtPriceX64?.toString(),
|
|
695
|
-
priceFromSqrt: priceFromSqrt.toNumber(),
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
return {
|
|
699
|
-
reserveA,
|
|
700
|
-
reserveB,
|
|
701
|
-
decimalsA: poolInfo.mintDecimalsA,
|
|
702
|
-
decimalsB: poolInfo.mintDecimalsB,
|
|
703
|
-
// 返回 token_1/token_0 的价格
|
|
704
|
-
// detectRaydiumPools 会根据查询的代币位置来决定是否取倒数
|
|
705
|
-
price: priceFromSqrt.toNumber(),
|
|
706
|
-
// 额外返回 mint 信息,便于 detectRaydiumPools 判断
|
|
707
|
-
mintA,
|
|
708
|
-
mintB,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
catch (e) {
|
|
712
|
-
if (debug)
|
|
713
|
-
console.log('[LP Inspect] CLMM decode error:', e);
|
|
714
|
-
return null;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
return null;
|
|
718
|
-
}
|
|
719
|
-
catch (e) {
|
|
720
|
-
if (debug)
|
|
721
|
-
console.log('[LP Inspect] On-chain fetch error:', e);
|
|
152
|
+
console.log('[LP Inspect] Jupiter check failed:', err);
|
|
722
153
|
return null;
|
|
723
154
|
}
|
|
724
155
|
}
|
|
@@ -726,128 +157,51 @@ async function fetchRaydiumPoolOnChain(connection, poolId, programId, debug) {
|
|
|
726
157
|
// 主函数
|
|
727
158
|
// ============================================================================
|
|
728
159
|
/**
|
|
729
|
-
* 检测 Solana 代币的 LP
|
|
730
|
-
*
|
|
731
|
-
* @param mint 代币 Mint 地址
|
|
732
|
-
* @param options 检测选项
|
|
733
|
-
* @returns LP 信息
|
|
734
|
-
*
|
|
735
|
-
* @example
|
|
736
|
-
* ```typescript
|
|
737
|
-
* const lpInfo = await inspectSolanaTokenLP('TokenMintAddress...', {
|
|
738
|
-
* rpcUrl: 'https://api.mainnet-beta.solana.com',
|
|
739
|
-
* debug: true,
|
|
740
|
-
* })
|
|
741
|
-
*
|
|
742
|
-
* if (lpInfo.platform === 'PUMP_BONDING_CURVE') {
|
|
743
|
-
* console.log('代币在 Pump.fun 内盘')
|
|
744
|
-
* console.log('毕业进度:', lpInfo.bondingCurve?.progress)
|
|
745
|
-
* } else if (lpInfo.bestPool) {
|
|
746
|
-
* console.log('最佳池子:', lpInfo.bestPool.platform)
|
|
747
|
-
* console.log('流动性:', lpInfo.bestPool.reserveQuote)
|
|
748
|
-
* }
|
|
749
|
-
* ```
|
|
160
|
+
* 检测 Solana 代币的 LP 信息
|
|
750
161
|
*/
|
|
751
162
|
export async function inspectSolanaTokenLP(mint, options = {}) {
|
|
752
163
|
const startTime = Date.now();
|
|
753
|
-
if (!options.connection && !options.rpcUrl) {
|
|
754
|
-
throw new Error('Either connection or rpcUrl is required in options');
|
|
755
|
-
}
|
|
756
164
|
const connection = options.connection || createConnection(options.rpcUrl);
|
|
757
165
|
const debug = options.debug;
|
|
758
|
-
// 初始化结果
|
|
759
166
|
const result = {
|
|
760
167
|
mint,
|
|
761
168
|
platform: 'UNKNOWN',
|
|
762
169
|
pools: [],
|
|
763
170
|
};
|
|
764
|
-
//
|
|
171
|
+
// 0. 获取代币基本信息
|
|
765
172
|
try {
|
|
766
|
-
const
|
|
767
|
-
const mintInfo = await connection.getParsedAccountInfo(mintPubkey);
|
|
173
|
+
const mintInfo = await connection.getParsedAccountInfo(new PublicKey(mint));
|
|
768
174
|
if (mintInfo.value?.data && 'parsed' in mintInfo.value.data) {
|
|
769
175
|
const parsed = mintInfo.value.data.parsed;
|
|
770
|
-
|
|
771
|
-
result.decimals = decimals;
|
|
176
|
+
result.decimals = parsed.info.decimals;
|
|
772
177
|
result.totalSupplyRaw = BigInt(parsed.info.supply);
|
|
773
|
-
result.totalSupply = (Number(result.totalSupplyRaw) / Math.pow(10, decimals)).toString();
|
|
178
|
+
result.totalSupply = (Number(result.totalSupplyRaw) / Math.pow(10, result.decimals)).toString();
|
|
774
179
|
}
|
|
775
180
|
}
|
|
776
|
-
catch
|
|
777
|
-
|
|
778
|
-
console.log('[LP Inspect] Failed to get mint info:', err);
|
|
779
|
-
}
|
|
780
|
-
// ========================================
|
|
781
|
-
// 1. 内盘检测(并行)
|
|
782
|
-
// ========================================
|
|
181
|
+
catch { }
|
|
182
|
+
// 1. 内盘检测
|
|
783
183
|
if (!options.skipBondingCurve) {
|
|
784
|
-
const
|
|
184
|
+
const [pump, meteora, raydium] = await Promise.all([
|
|
785
185
|
detectPumpBondingCurve(mint, connection, debug),
|
|
786
186
|
detectMeteoraDbc(mint, connection, debug),
|
|
787
|
-
detectOrcaWavebreak(mint, connection, debug),
|
|
788
187
|
detectRaydiumLaunchLab(mint, connection, debug),
|
|
789
188
|
]);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
result.platform = 'PUMP_BONDING_CURVE';
|
|
798
|
-
break;
|
|
799
|
-
case 'meteora_dbc':
|
|
800
|
-
result.platform = 'METEORA_DBC';
|
|
801
|
-
break;
|
|
802
|
-
case 'orca_wavebreak':
|
|
803
|
-
result.platform = 'ORCA_WAVEBREAK';
|
|
804
|
-
break;
|
|
805
|
-
case 'raydium_launchlab':
|
|
806
|
-
result.platform = 'RAYDIUM_LAUNCHLAB';
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
result.elapsed = Date.now() - startTime;
|
|
810
|
-
return result;
|
|
811
|
-
}
|
|
189
|
+
const bc = pump || meteora || raydium;
|
|
190
|
+
if (bc && !bc.complete) {
|
|
191
|
+
result.bondingCurve = bc;
|
|
192
|
+
result.platform = bc.type === 'pump' ? 'PUMP_BONDING_CURVE' :
|
|
193
|
+
bc.type === 'meteora_dbc' ? 'METEORA_DBC' : 'RAYDIUM_LAUNCHLAB';
|
|
194
|
+
result.elapsed = Date.now() - startTime;
|
|
195
|
+
return result;
|
|
812
196
|
}
|
|
813
197
|
}
|
|
814
|
-
//
|
|
815
|
-
// 2. 外盘检测(并行)
|
|
816
|
-
// ========================================
|
|
198
|
+
// 2. 外盘检测 (Jupiter)
|
|
817
199
|
if (!options.skipPools) {
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
]);
|
|
824
|
-
// 收集所有有效的池子
|
|
825
|
-
for (const r of poolResults) {
|
|
826
|
-
if (r.status === 'fulfilled' && r.value) {
|
|
827
|
-
if (Array.isArray(r.value)) {
|
|
828
|
-
result.pools.push(...r.value);
|
|
829
|
-
}
|
|
830
|
-
else {
|
|
831
|
-
result.pools.push(r.value);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
// 按流动性排序
|
|
836
|
-
result.pools.sort((a, b) => {
|
|
837
|
-
const liquidityA = Number(a.reserveQuoteRaw);
|
|
838
|
-
const liquidityB = Number(b.reserveQuoteRaw);
|
|
839
|
-
return liquidityB - liquidityA;
|
|
840
|
-
});
|
|
841
|
-
// 设置最佳池子
|
|
842
|
-
if (result.pools.length > 0) {
|
|
843
|
-
result.bestPool = result.pools[0];
|
|
844
|
-
// 设置平台类型
|
|
845
|
-
if (result.pools.length > 1) {
|
|
846
|
-
result.platform = 'MULTI_POOL';
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
849
|
-
result.platform = result.bestPool.platform;
|
|
850
|
-
}
|
|
200
|
+
const jupPool = await detectWithJupiter(mint, debug);
|
|
201
|
+
if (jupPool) {
|
|
202
|
+
result.bestPool = jupPool;
|
|
203
|
+
result.pools = [jupPool];
|
|
204
|
+
result.platform = jupPool.platform;
|
|
851
205
|
}
|
|
852
206
|
}
|
|
853
207
|
result.elapsed = Date.now() - startTime;
|
|
@@ -855,46 +209,27 @@ export async function inspectSolanaTokenLP(mint, options = {}) {
|
|
|
855
209
|
}
|
|
856
210
|
/**
|
|
857
211
|
* 快速检测代币是否在内盘
|
|
858
|
-
*
|
|
859
|
-
* @param mint 代币 Mint 地址
|
|
860
|
-
* @param options 检测选项
|
|
861
|
-
* @returns 如果在内盘返回内盘信息,否则返回 null
|
|
862
212
|
*/
|
|
863
213
|
export async function detectBondingCurve(mint, options = {}) {
|
|
864
|
-
if (!options.connection && !options.rpcUrl) {
|
|
865
|
-
throw new Error('Either connection or rpcUrl is required in options');
|
|
866
|
-
}
|
|
867
214
|
const connection = options.connection || createConnection(options.rpcUrl);
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
detectMeteoraDbc(mint, connection, debug),
|
|
873
|
-
detectOrcaWavebreak(mint, connection, debug),
|
|
874
|
-
detectRaydiumLaunchLab(mint, connection, debug),
|
|
215
|
+
const results = await Promise.all([
|
|
216
|
+
detectPumpBondingCurve(mint, connection, options.debug),
|
|
217
|
+
detectMeteoraDbc(mint, connection, options.debug),
|
|
218
|
+
detectRaydiumLaunchLab(mint, connection, options.debug),
|
|
875
219
|
]);
|
|
876
|
-
|
|
877
|
-
for (const r of results) {
|
|
878
|
-
if (r.status === 'fulfilled' && r.value && !r.value.complete) {
|
|
879
|
-
return r.value;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
return null;
|
|
220
|
+
return results.find(r => r && !r.complete) || null;
|
|
883
221
|
}
|
|
884
222
|
/**
|
|
885
|
-
*
|
|
223
|
+
* 获取代币流动性 (USD)
|
|
886
224
|
*/
|
|
887
|
-
export async function
|
|
888
|
-
const
|
|
889
|
-
return
|
|
225
|
+
export async function getTokenLiquidity(mint, options = {}) {
|
|
226
|
+
const info = await inspectSolanaTokenLP(mint, options);
|
|
227
|
+
return info.bestPool?.liquidity || 0;
|
|
890
228
|
}
|
|
891
229
|
/**
|
|
892
|
-
*
|
|
230
|
+
* 判断代币是否已毕业
|
|
893
231
|
*/
|
|
894
|
-
export async function
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
skipBondingCurve: true,
|
|
898
|
-
});
|
|
899
|
-
return lpInfo.bestPool || null;
|
|
232
|
+
export async function isTokenGraduated(mint, options = {}) {
|
|
233
|
+
const bc = await detectBondingCurve(mint, options);
|
|
234
|
+
return bc === null;
|
|
900
235
|
}
|
|
@@ -16,13 +16,13 @@ export declare const PROFIT_CONFIG: {
|
|
|
16
16
|
/** 利润接收地址 */
|
|
17
17
|
readonly RECIPIENT: "0x8a365e4359247D7FdbE8a73B547566A03E4e9Ed8";
|
|
18
18
|
/** 利润比例(基点):30 bps = 0.3% = 千分之三(普通模式) */
|
|
19
|
-
readonly RATE_BPS:
|
|
19
|
+
readonly RATE_BPS: 40;
|
|
20
20
|
/** 利润比例(基点):6 bps = 0.06% = 万分之六(捆绑换手模式) */
|
|
21
21
|
readonly RATE_BPS_SWAP: 6;
|
|
22
22
|
/** 利润比例(单边基点):6 bps = 0.06% = 万分之六(用户类型 v0) */
|
|
23
|
-
readonly RATE_BPS_V0:
|
|
23
|
+
readonly RATE_BPS_V0: 8;
|
|
24
24
|
/** 利润比例(双边基点):3 bps = 0.03% = 千分之三(用户类型 v0) */
|
|
25
|
-
readonly RATE_BPS_V0_DOUBLE:
|
|
25
|
+
readonly RATE_BPS_V0_DOUBLE: 4;
|
|
26
26
|
/** 利润比例(单边基点):5 bps = 0.05% = 万分之五(用户类型 v1) */
|
|
27
27
|
readonly RATE_BPS_V1: 10;
|
|
28
28
|
/** 利润比例(双边基点):2.5 bps = 0.025% = 千分之二点五(用户类型 v1) */
|
package/dist/utils/constants.js
CHANGED
|
@@ -22,13 +22,13 @@ export const PROFIT_CONFIG = {
|
|
|
22
22
|
/** 利润接收地址 */
|
|
23
23
|
RECIPIENT: '0x8a365e4359247D7FdbE8a73B547566A03E4e9Ed8',
|
|
24
24
|
/** 利润比例(基点):30 bps = 0.3% = 千分之三(普通模式) */
|
|
25
|
-
RATE_BPS:
|
|
25
|
+
RATE_BPS: 40,
|
|
26
26
|
/** 利润比例(基点):6 bps = 0.06% = 万分之六(捆绑换手模式) */
|
|
27
27
|
RATE_BPS_SWAP: 6,
|
|
28
28
|
/** 利润比例(单边基点):6 bps = 0.06% = 万分之六(用户类型 v0) */
|
|
29
|
-
RATE_BPS_V0:
|
|
29
|
+
RATE_BPS_V0: 8,
|
|
30
30
|
/** 利润比例(双边基点):3 bps = 0.03% = 千分之三(用户类型 v0) */
|
|
31
|
-
RATE_BPS_V0_DOUBLE:
|
|
31
|
+
RATE_BPS_V0_DOUBLE: 4,
|
|
32
32
|
/** 利润比例(单边基点):5 bps = 0.05% = 万分之五(用户类型 v1) */
|
|
33
33
|
RATE_BPS_V1: 10,
|
|
34
34
|
/** 利润比例(双边基点):2.5 bps = 0.025% = 千分之二点五(用户类型 v1) */
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -1462,7 +1462,7 @@ export class BundleExecutor {
|
|
|
1462
1462
|
const curveBuyData = curveBuyerWallets.map((wallet, i) => {
|
|
1463
1463
|
const buyWei = parseOkb(curveBuyAmounts[i]);
|
|
1464
1464
|
const isLast = i === curveBuyerWallets.length - 1;
|
|
1465
|
-
const gasLimit =
|
|
1465
|
+
const gasLimit = 8000000n; // 统一给 800W,不再区分是否最后一笔
|
|
1466
1466
|
return { wallet, info: curveBuyerInfos[i], buyWei, gasLimit };
|
|
1467
1467
|
});
|
|
1468
1468
|
totalCurveBuyWei = curveBuyData.reduce((sum, d) => sum + d.buyWei, 0n);
|