pump-trader 1.0.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.
package/index.js ADDED
@@ -0,0 +1,1294 @@
1
+ import {
2
+ Connection,
3
+ PublicKey,
4
+ Transaction,
5
+ TransactionInstruction,
6
+ SystemProgram,
7
+ ComputeBudgetProgram,
8
+ Keypair
9
+ } from "@solana/web3.js";
10
+
11
+ import {
12
+ ASSOCIATED_TOKEN_PROGRAM_ID,
13
+ getAssociatedTokenAddressSync,
14
+ createAssociatedTokenAccountInstruction,
15
+ createSyncNativeInstruction,
16
+ createCloseAccountInstruction,
17
+ getTokenMetadata,
18
+ TOKEN_2022_PROGRAM_ID,
19
+ TOKEN_PROGRAM_ID,
20
+ getMint
21
+ } from "@solana/spl-token";
22
+
23
+ import BN from "bn.js";
24
+ import bs58 from "bs58";
25
+
26
+ /* ================= 常量定义 ================= */
27
+
28
+ const PROGRAM_IDS = {
29
+ PUMP: new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"),
30
+ PUMP_AMM: new PublicKey("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"),
31
+ METADATA: new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
32
+ FEE: new PublicKey("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"),
33
+ EVENT_AUTHORITY: new PublicKey("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1")
34
+ };
35
+
36
+ const SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112");
37
+
38
+ const SEEDS = {
39
+ FEE_CONFIG: new Uint8Array([
40
+ 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, 91, 170,
41
+ 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, 182, 253, 109, 24, 176
42
+ ]),
43
+ AMM_FEE_CONFIG: Buffer.from([
44
+ 12, 20, 222, 252, 130, 94, 198, 118, 148, 37, 8, 24, 187, 101, 64, 101,
45
+ 244, 41, 141, 49, 86, 213, 113, 180, 212, 248, 9, 12, 24, 233, 168, 99
46
+ ]),
47
+ GLOBAL: Buffer.from("global"),
48
+ BONDING: Buffer.from("bonding-curve")
49
+ };
50
+
51
+ const DISCRIMINATORS = {
52
+ BUY: Buffer.from([102, 6, 61, 18, 1, 218, 235, 234]),
53
+ SELL: Buffer.from([51, 230, 133, 164, 1, 127, 131, 173]),
54
+ TRADE_EVENT: Buffer.from([189, 219, 127, 211, 78, 230, 97, 238])
55
+ };
56
+
57
+ const AMM_FEE_BPS = 100n; // 1%
58
+ const BPS_DENOMINATOR = 10000n;
59
+
60
+ /* ================= 工具函数 ================= */
61
+
62
+ const u64 = (v) => new BN(v.toString()).toArrayLike(Buffer, "le", 8);
63
+
64
+ const readU64 = (buf, offset) => {
65
+ const value = buf.readBigUInt64LE(offset);
66
+ return [value, offset + 8];
67
+ };
68
+
69
+ const readU32 = (buf, offsetObj) => {
70
+ const value = buf.readUInt32LE(offsetObj.offset);
71
+ offsetObj.offset += 4;
72
+ return value;
73
+ };
74
+
75
+ const readString = (buf, offsetObj) => {
76
+ const len = readU32(buf, offsetObj);
77
+ const str = buf.slice(offsetObj.offset, offsetObj.offset + len).toString("utf8");
78
+ offsetObj.offset += len;
79
+ return str;
80
+ };
81
+
82
+ /* ================= 解析函数 ================= */
83
+
84
+ function parseMetadataAccount(data) {
85
+ const offsetObj = { offset: 1 }; // 跳过 key
86
+
87
+ const updateAuthority = new PublicKey(data.slice(offsetObj.offset, offsetObj.offset + 32));
88
+ offsetObj.offset += 32;
89
+
90
+ const mint = new PublicKey(data.slice(offsetObj.offset, offsetObj.offset + 32));
91
+ offsetObj.offset += 32;
92
+
93
+ const name = readString(data, offsetObj);
94
+ const symbol = readString(data, offsetObj);
95
+ const uri = readString(data, offsetObj);
96
+
97
+ return {
98
+ updateAuthority: updateAuthority.toBase58(),
99
+ mint: mint.toBase58(),
100
+ name,
101
+ symbol,
102
+ uri
103
+ };
104
+ }
105
+
106
+ function parsePoolKeys(data) {
107
+ if (!data || data.length < 280) {
108
+ throw new Error('Invalid pool account data');
109
+ }
110
+
111
+ let offset = 8; // 跳过 discriminator
112
+
113
+ const poolBump = data.readUInt8(offset);
114
+ offset += 1;
115
+
116
+ const index = data.readUInt16LE(offset);
117
+ offset += 2;
118
+
119
+ const creator = new PublicKey(data.slice(offset, offset + 32));
120
+ offset += 32;
121
+
122
+ const baseMint = new PublicKey(data.slice(offset, offset + 32));
123
+ offset += 32;
124
+
125
+ const quoteMint = new PublicKey(data.slice(offset, offset + 32));
126
+ offset += 32;
127
+
128
+ const lpMint = new PublicKey(data.slice(offset, offset + 32));
129
+ offset += 32;
130
+
131
+ const poolBaseTokenAccount = new PublicKey(data.slice(offset, offset + 32));
132
+ offset += 32;
133
+
134
+ const poolQuoteTokenAccount = new PublicKey(data.slice(offset, offset + 32));
135
+ offset += 32;
136
+
137
+ const lpSupply = data.readBigUInt64LE(offset);
138
+ offset += 8;
139
+
140
+ const coinCreator = new PublicKey(data.slice(offset, offset + 32));
141
+ offset += 32;
142
+
143
+ const isMayhemMode = data.readUInt8(offset) === 1;
144
+
145
+ return {
146
+ creator,
147
+ baseMint,
148
+ quoteMint,
149
+ lpMint,
150
+ poolBaseTokenAccount,
151
+ poolQuoteTokenAccount,
152
+ coinCreator,
153
+ isMayhemMode
154
+ };
155
+ }
156
+
157
+ /* ================= PumpTrader 类 ================= */
158
+
159
+ export class PumpTrader {
160
+ constructor(rpc, privateKey) {
161
+ this.connection = new Connection(rpc, "confirmed");
162
+ this.wallet = Keypair.fromSecretKey(bs58.decode(privateKey));
163
+ this.global = PublicKey.findProgramAddressSync([SEEDS.GLOBAL], PROGRAM_IDS.PUMP)[0];
164
+ this.globalState = null;
165
+ this.tokenProgramCache = new Map(); // 缓存token program检测结果
166
+ }
167
+
168
+ /* ---------- Token Program 检测 ---------- */
169
+
170
+ /**
171
+ * 自动检测代币使用的 token program (TOKEN_PROGRAM_ID 或 TOKEN_2022_PROGRAM_ID)
172
+ * @param {string} tokenAddr - 代币地址
173
+ * @returns {Promise<{type: string, programId: PublicKey}>}
174
+ */
175
+ async detectTokenProgram(tokenAddr) {
176
+ // 检查缓存
177
+ if (this.tokenProgramCache.has(tokenAddr)) {
178
+ return this.tokenProgramCache.get(tokenAddr);
179
+ }
180
+
181
+ const mint = new PublicKey(tokenAddr);
182
+
183
+ try {
184
+ // 首先尝试获取 TOKEN_2022 的代币信息
185
+ const mintData = await getMint(this.connection, mint, "confirmed", TOKEN_2022_PROGRAM_ID);
186
+ const result = {
187
+ type: "TOKEN_2022_PROGRAM_ID",
188
+ programId: TOKEN_2022_PROGRAM_ID
189
+ };
190
+ this.tokenProgramCache.set(tokenAddr, result);
191
+ return result;
192
+ } catch (e) {
193
+ try {
194
+ // 如果失败,尝试标准 TOKEN_PROGRAM_ID
195
+ const mintData = await getMint(this.connection, mint, "confirmed", TOKEN_PROGRAM_ID);
196
+ const result = {
197
+ type: "TOKEN_PROGRAM_ID",
198
+ programId: TOKEN_PROGRAM_ID
199
+ };
200
+ this.tokenProgramCache.set(tokenAddr, result);
201
+ return result;
202
+ } catch (error) {
203
+ throw new Error(`Failed to detect token program for ${tokenAddr}: ${error}`);
204
+ }
205
+ }
206
+ }
207
+
208
+ /* ---------- 内盘/外盘检测 ---------- */
209
+
210
+ /**
211
+ * 检测代币是否已完成内盘绑定曲线(已进入外盘AMM)
212
+ * @param {string} tokenAddr - 代币地址
213
+ * @returns {Promise<boolean>} true表示在外盘,false表示还在内盘
214
+ */
215
+ async isAmmCompleted(tokenAddr) {
216
+ try {
217
+ const mint = new PublicKey(tokenAddr);
218
+ const { state } = await this.loadBonding(mint);
219
+ return state.complete;
220
+ } catch (error) {
221
+ // 如果无法加载内盘,说明可能已经在外盘或代币不存在
222
+ return true;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * 自动判断应该使用内盘还是外盘交易
228
+ * @param {string} tokenAddr - 代币地址
229
+ * @returns {Promise<string>} "bonding" 或 "amm"
230
+ */
231
+ async getTradeMode(tokenAddr) {
232
+ const isAmmMode = await this.isAmmCompleted(tokenAddr);
233
+ return isAmmMode ? "amm" : "bonding";
234
+ }
235
+
236
+ /* ---------- Global State ---------- */
237
+
238
+ async loadGlobal() {
239
+ const acc = await this.connection.getAccountInfo(this.global);
240
+ if (!acc) throw new Error("Global account not found");
241
+
242
+ const data = acc.data;
243
+ let offset = 8;
244
+
245
+ const readBool = () => data[offset++] === 1;
246
+ const readPk = () => {
247
+ const pk = new PublicKey(data.slice(offset, offset + 32));
248
+ offset += 32;
249
+ return pk;
250
+ };
251
+ const readU64 = () => {
252
+ const v = data.readBigUInt64LE(offset);
253
+ offset += 8;
254
+ return v;
255
+ };
256
+
257
+ this.globalState = {
258
+ initialized: readBool(),
259
+ authority: readPk(),
260
+ feeRecipient: readPk(),
261
+ withdrawAuthority: readPk(),
262
+ initialVirtualTokenReserves: readU64(),
263
+ initialVirtualSolReserves: readU64(),
264
+ initialRealTokenReserves: readU64(),
265
+ tokenTotalSupply: readU64(),
266
+ feeBasisPoints: readU64()
267
+ };
268
+
269
+ return this.globalState;
270
+ }
271
+
272
+ /* ---------- Bonding Curve ---------- */
273
+
274
+ getBondingPda(mint) {
275
+ return PublicKey.findProgramAddressSync(
276
+ [SEEDS.BONDING, mint.toBuffer()],
277
+ PROGRAM_IDS.PUMP
278
+ )[0];
279
+ }
280
+
281
+ async loadBonding(mint) {
282
+ const bonding = this.getBondingPda(mint);
283
+ const acc = await this.connection.getAccountInfo(bonding);
284
+ if (!acc) throw new Error("Bonding curve not found");
285
+
286
+ let offset = 8;
287
+ const data = acc.data;
288
+
289
+ const state = {};
290
+ [state.virtualTokenReserves, offset] = readU64(data, offset);
291
+ [state.virtualSolReserves, offset] = readU64(data, offset);
292
+ [state.realTokenReserves, offset] = readU64(data, offset);
293
+ [state.realSolReserves, offset] = readU64(data, offset);
294
+ [state.tokenTotalSupply, offset] = readU64(data, offset);
295
+ state.complete = data[offset] === 1;
296
+ offset += 1;
297
+
298
+ const creator = new PublicKey(data.slice(offset, offset + 32));
299
+
300
+ return { bonding, state, creator };
301
+ }
302
+
303
+ /* ---------- 价格计算 ---------- */
304
+
305
+ calcBuy(solIn, state) {
306
+ const newVirtualSol = state.virtualSolReserves + solIn;
307
+ const newVirtualToken = (state.virtualSolReserves * state.virtualTokenReserves) / newVirtualSol;
308
+ return state.virtualTokenReserves - newVirtualToken;
309
+ }
310
+
311
+ calcSell(tokenIn, state) {
312
+ const newVirtualToken = state.virtualTokenReserves + tokenIn;
313
+ const newVirtualSol = (state.virtualSolReserves * state.virtualTokenReserves) / newVirtualToken;
314
+ return state.virtualSolReserves - newVirtualSol;
315
+ }
316
+
317
+ calculateAmmBuyOutput(quoteIn, reserves) {
318
+ const quoteInAfterFee = (quoteIn * (BPS_DENOMINATOR - AMM_FEE_BPS)) / BPS_DENOMINATOR;
319
+ const numerator = reserves.baseAmount * quoteInAfterFee;
320
+ const denominator = reserves.quoteAmount + quoteInAfterFee;
321
+ return numerator / denominator;
322
+ }
323
+
324
+ calculateAmmSellOutput(baseIn, reserves) {
325
+ const baseInAfterFee = (baseIn * (BPS_DENOMINATOR - AMM_FEE_BPS)) / BPS_DENOMINATOR;
326
+ const numerator = reserves.quoteAmount * baseInAfterFee;
327
+ const denominator = reserves.baseAmount + baseInAfterFee;
328
+ return numerator / denominator;
329
+ }
330
+
331
+ /* ---------- 价格查询 ---------- */
332
+
333
+ async getPriceAndStatus(tokenAddr) {
334
+ const mint = new PublicKey(tokenAddr);
335
+ const { state } = await this.loadBonding(mint);
336
+
337
+ if (state.complete) {
338
+ const price = await this.getAmmPrice(mint);
339
+ return { price, completed: true };
340
+ }
341
+
342
+ const oneToken = BigInt(1_000_000);
343
+ const solOut = this.calcSell(oneToken, state);
344
+ const price = Number(solOut) / 1e9;
345
+ return { price, completed: false };
346
+ }
347
+
348
+ async getAmmPrice(mint) {
349
+ const [poolCreator] = PublicKey.findProgramAddressSync(
350
+ [Buffer.from("pool-authority"), mint.toBuffer()],
351
+ PROGRAM_IDS.PUMP
352
+ );
353
+
354
+ const indexBuffer = new BN(0).toArrayLike(Buffer, "le", 2);
355
+ const [pool] = PublicKey.findProgramAddressSync(
356
+ [Buffer.from("pool"), indexBuffer, poolCreator.toBuffer(), mint.toBuffer(), SOL_MINT.toBuffer()],
357
+ PROGRAM_IDS.PUMP_AMM
358
+ );
359
+
360
+ const acc = await this.connection.getAccountInfo(pool);
361
+ if (!acc) throw new Error("Pool not found");
362
+
363
+ const poolKeys = parsePoolKeys(acc.data);
364
+ const [baseInfo, quoteInfo] = await Promise.all([
365
+ this.connection.getTokenAccountBalance(poolKeys.poolBaseTokenAccount),
366
+ this.connection.getTokenAccountBalance(poolKeys.poolQuoteTokenAccount)
367
+ ]);
368
+
369
+ return quoteInfo.value.uiAmount / baseInfo.value.uiAmount;
370
+ }
371
+
372
+ /* ---------- 余额查询 ---------- */
373
+
374
+ async tokenBalance(tokenAddr) {
375
+ const mint = new PublicKey(tokenAddr);
376
+ const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner(
377
+ this.wallet.publicKey,
378
+ { mint }
379
+ );
380
+
381
+ return tokenAccounts.value[0]?.account.data.parsed.info.tokenAmount.uiAmount || 0;
382
+ }
383
+
384
+ async solBalance() {
385
+ const balance = await this.connection.getBalance(this.wallet.publicKey);
386
+ return balance / 1e9;
387
+ }
388
+
389
+ /* ---------- ATA 管理 ---------- */
390
+
391
+ async ensureAta(tx, mint, tokenProgram = null) {
392
+ const program = tokenProgram || TOKEN_2022_PROGRAM_ID;
393
+ const ata = getAssociatedTokenAddressSync(
394
+ mint,
395
+ this.wallet.publicKey,
396
+ false,
397
+ program,
398
+ ASSOCIATED_TOKEN_PROGRAM_ID
399
+ );
400
+
401
+ const acc = await this.connection.getAccountInfo(ata);
402
+ if (!acc) {
403
+ tx.add(
404
+ createAssociatedTokenAccountInstruction(
405
+ this.wallet.publicKey,
406
+ ata,
407
+ this.wallet.publicKey,
408
+ mint,
409
+ program,
410
+ ASSOCIATED_TOKEN_PROGRAM_ID
411
+ )
412
+ );
413
+ }
414
+
415
+ return ata;
416
+ }
417
+
418
+ async ensureWSOLAta(tx, owner, mode, lamports = 0n) {
419
+ const wsolAta = getAssociatedTokenAddressSync(
420
+ SOL_MINT,
421
+ owner,
422
+ false,
423
+ TOKEN_PROGRAM_ID,
424
+ ASSOCIATED_TOKEN_PROGRAM_ID
425
+ );
426
+
427
+ const acc = await this.connection.getAccountInfo(wsolAta);
428
+
429
+ if (!acc) {
430
+ tx.add(
431
+ createAssociatedTokenAccountInstruction(
432
+ owner,
433
+ wsolAta,
434
+ owner,
435
+ SOL_MINT,
436
+ TOKEN_PROGRAM_ID,
437
+ ASSOCIATED_TOKEN_PROGRAM_ID
438
+ )
439
+ );
440
+ }
441
+
442
+ if (mode === 'buy') {
443
+ tx.add(
444
+ SystemProgram.transfer({
445
+ fromPubkey: owner,
446
+ toPubkey: wsolAta,
447
+ lamports: Number(lamports)
448
+ })
449
+ );
450
+ tx.add(createSyncNativeInstruction(wsolAta));
451
+ }
452
+
453
+ return wsolAta;
454
+ }
455
+
456
+ /* ---------- 交易参数处理 ---------- */
457
+
458
+ genPriority(priorityOpt) {
459
+ if (!priorityOpt?.enableRandom || !priorityOpt.randomRange) {
460
+ return priorityOpt.base;
461
+ }
462
+ return priorityOpt.base + Math.floor(Math.random() * priorityOpt.randomRange);
463
+ }
464
+
465
+ calcSlippage({ tradeSize, reserve, slippageOpt }) {
466
+ const impact = Number(tradeSize) / Math.max(Number(reserve), 1);
467
+ const factor = slippageOpt.impactFactor ?? 1;
468
+
469
+ let slip = slippageOpt.base + Math.floor(impact * 10_000 * factor);
470
+
471
+ if (slippageOpt.max !== undefined) {
472
+ slip = Math.min(slip, slippageOpt.max);
473
+ }
474
+ if (slippageOpt.min !== undefined) {
475
+ slip = Math.max(slip, slippageOpt.min);
476
+ }
477
+
478
+ return slip;
479
+ }
480
+
481
+ splitByMax(total, max) {
482
+ const chunks = [];
483
+ let remaining = total;
484
+
485
+ while (remaining > 0n) {
486
+ const chunk = remaining > max ? max : remaining;
487
+ chunks.push(chunk);
488
+ remaining -= chunk;
489
+ }
490
+ return chunks;
491
+ }
492
+
493
+ splitIntoN(total, n) {
494
+ const chunks = [];
495
+ const part = total / BigInt(n);
496
+ let remaining = total;
497
+
498
+ for (let i = 0; i < n; i++) {
499
+ const chunk = i === n - 1 ? remaining : part;
500
+ chunks.push(chunk);
501
+ remaining -= chunk;
502
+ }
503
+ return chunks;
504
+ }
505
+
506
+ /* ---------- 内盘交易 ---------- */
507
+
508
+ /**
509
+ * 统一的自动买入接口,自动判断内盘/外盘
510
+ */
511
+ async autoBuy(tokenAddr, totalSolIn, tradeOpt) {
512
+ const mode = await this.getTradeMode(tokenAddr);
513
+ if (mode === "bonding") {
514
+ return this.buy(tokenAddr, totalSolIn, tradeOpt);
515
+ } else {
516
+ return this.ammBuy(tokenAddr, totalSolIn, tradeOpt);
517
+ }
518
+ }
519
+
520
+ /**
521
+ * 统一的自动卖出接口,自动判断内盘/外盘
522
+ */
523
+ async autoSell(tokenAddr, totalTokenIn, tradeOpt) {
524
+ const mode = await this.getTradeMode(tokenAddr);
525
+ if (mode === "bonding") {
526
+ return this.sell(tokenAddr, totalTokenIn, tradeOpt);
527
+ } else {
528
+ return this.ammSell(tokenAddr, totalTokenIn, tradeOpt);
529
+ }
530
+ }
531
+
532
+ async buy(tokenAddr, totalSolIn, tradeOpt) {
533
+ const mint = new PublicKey(tokenAddr);
534
+ const tokenProgram = await this.detectTokenProgram(tokenAddr);
535
+
536
+ if (!this.globalState) await this.loadGlobal();
537
+
538
+ const { bonding, state, creator } = await this.loadBonding(mint);
539
+ if (state.complete) throw new Error("Bonding curve already completed");
540
+ const solChunks = this.splitByMax(totalSolIn, tradeOpt.maxSolPerTx);
541
+ const pendingTransactions = []; // 待确认的交易
542
+ const failedTransactions = []; // 发送失败的交易
543
+ const associatedBondingCurve = getAssociatedTokenAddressSync(
544
+ mint,
545
+ bonding,
546
+ true,
547
+ tokenProgram.programId,
548
+ ASSOCIATED_TOKEN_PROGRAM_ID
549
+ );
550
+
551
+ const [creatorVault] = PublicKey.findProgramAddressSync(
552
+ [Buffer.from("creator-vault"), creator.toBuffer()],
553
+ PROGRAM_IDS.PUMP
554
+ );
555
+
556
+ const [globalVolumeAccumulator] = PublicKey.findProgramAddressSync(
557
+ [Buffer.from("global_volume_accumulator")],
558
+ PROGRAM_IDS.PUMP
559
+ );
560
+
561
+ const [userVolumeAccumulator] = PublicKey.findProgramAddressSync(
562
+ [Buffer.from("user_volume_accumulator"), this.wallet.publicKey.toBuffer()],
563
+ PROGRAM_IDS.PUMP
564
+ );
565
+
566
+ const [feeConfig] = PublicKey.findProgramAddressSync(
567
+ [Buffer.from("fee_config"), SEEDS.FEE_CONFIG],
568
+ PROGRAM_IDS.FEE
569
+ );
570
+
571
+ for (let i = 0; i < solChunks.length; i++) {
572
+ try {
573
+ const solIn = solChunks[i];
574
+ const tokenOut = this.calcBuy(solIn, state);
575
+ const slippageBps = this.calcSlippage({
576
+ tradeSize: solIn,
577
+ reserve: state.virtualSolReserves,
578
+ slippageOpt: tradeOpt.slippage
579
+ });
580
+ const maxSol = (solIn * BigInt(10_000 + slippageBps)) / 10_000n;
581
+ const priority = this.genPriority(tradeOpt.priority);
582
+
583
+ const tx = new Transaction().add(
584
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
585
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priority })
586
+ );
587
+
588
+ const userAta = await this.ensureAta(tx, mint, tokenProgram.programId);
589
+
590
+ tx.add(
591
+ new TransactionInstruction({
592
+ programId: PROGRAM_IDS.PUMP,
593
+ keys: [
594
+ { pubkey: this.global, isSigner: false, isWritable: false },
595
+ { pubkey: this.globalState.feeRecipient, isSigner: false, isWritable: true },
596
+ { pubkey: mint, isSigner: false, isWritable: false },
597
+ { pubkey: bonding, isSigner: false, isWritable: true },
598
+ { pubkey: associatedBondingCurve, isSigner: false, isWritable: true },
599
+ { pubkey: userAta, isSigner: false, isWritable: true },
600
+ { pubkey: this.wallet.publicKey, isSigner: true, isWritable: true },
601
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
602
+ { pubkey: tokenProgram.programId, isSigner: false, isWritable: false },
603
+ { pubkey: creatorVault, isSigner: false, isWritable: true },
604
+ { pubkey: PROGRAM_IDS.EVENT_AUTHORITY, isSigner: false, isWritable: false },
605
+ { pubkey: PROGRAM_IDS.PUMP, isSigner: false, isWritable: false },
606
+ { pubkey: globalVolumeAccumulator, isSigner: false, isWritable: false },
607
+ { pubkey: userVolumeAccumulator, isSigner: false, isWritable: true },
608
+ { pubkey: feeConfig, isSigner: false, isWritable: false },
609
+ { pubkey: PROGRAM_IDS.FEE, isSigner: false, isWritable: false }
610
+ ],
611
+ data: Buffer.concat([DISCRIMINATORS.BUY, u64(tokenOut), u64(maxSol)])
612
+ })
613
+ );
614
+
615
+ const { blockhash, lastValidBlockHeight } =
616
+ await this.connection.getLatestBlockhash('finalized');
617
+ tx.recentBlockhash = blockhash;
618
+ tx.feePayer = this.wallet.publicKey;
619
+ tx.sign(this.wallet);
620
+
621
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
622
+ skipPreflight: false,
623
+ maxRetries: 2
624
+ });
625
+
626
+ pendingTransactions.push({
627
+ signature,
628
+ lastValidBlockHeight,
629
+ index: i
630
+ });
631
+ } catch (e) {
632
+ failedTransactions.push({
633
+ index: i,
634
+ error: e.message
635
+ });
636
+ }
637
+ }
638
+
639
+ return { pendingTransactions, failedTransactions };
640
+ }
641
+
642
+ async sell(tokenAddr, totalTokenIn, tradeOpt) {
643
+ const mint = new PublicKey(tokenAddr);
644
+ const tokenProgram = await this.detectTokenProgram(tokenAddr);
645
+
646
+ if (!this.globalState) await this.loadGlobal();
647
+
648
+ const { bonding, state, creator } = await this.loadBonding(mint);
649
+ if (state.complete) throw new Error("Bonding curve already completed");
650
+
651
+ const totalSolOut = this.calcSell(totalTokenIn, state);
652
+ const tokenChunks = totalSolOut <= tradeOpt.maxSolPerTx
653
+ ? [totalTokenIn]
654
+ : this.splitIntoN(
655
+ totalTokenIn,
656
+ Number((totalSolOut + tradeOpt.maxSolPerTx - 1n) / tradeOpt.maxSolPerTx)
657
+ );
658
+
659
+ const pendingTransactions = []; // 待确认的交易
660
+ const failedTransactions = []; // 发送失败的交易
661
+
662
+ const associatedBondingCurve = getAssociatedTokenAddressSync(
663
+ mint,
664
+ bonding,
665
+ true,
666
+ tokenProgram.programId,
667
+ ASSOCIATED_TOKEN_PROGRAM_ID
668
+ );
669
+
670
+ const userAta = getAssociatedTokenAddressSync(
671
+ mint,
672
+ this.wallet.publicKey,
673
+ false,
674
+ tokenProgram.programId,
675
+ ASSOCIATED_TOKEN_PROGRAM_ID
676
+ );
677
+
678
+ const [creatorVault] = PublicKey.findProgramAddressSync(
679
+ [Buffer.from("creator-vault"), creator.toBuffer()],
680
+ PROGRAM_IDS.PUMP
681
+ );
682
+
683
+ const [feeConfig] = PublicKey.findProgramAddressSync(
684
+ [Buffer.from("fee_config"), SEEDS.FEE_CONFIG],
685
+ PROGRAM_IDS.FEE
686
+ );
687
+
688
+ for (let i = 0; i < tokenChunks.length; i++) {
689
+ try {
690
+ const tokenIn = tokenChunks[i];
691
+ const solOut = this.calcSell(tokenIn, state);
692
+ const slippageBps = this.calcSlippage({
693
+ tradeSize: tokenIn,
694
+ reserve: state.virtualTokenReserves,
695
+ slippageOpt: tradeOpt.slippage
696
+ });
697
+ const minSol = (solOut * BigInt(10_000 - slippageBps)) / 10_000n;
698
+ const priority = this.genPriority(tradeOpt.priority);
699
+
700
+ const tx = new Transaction().add(
701
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
702
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priority })
703
+ );
704
+
705
+ tx.add(
706
+ new TransactionInstruction({
707
+ programId: PROGRAM_IDS.PUMP,
708
+ keys: [
709
+ { pubkey: this.global, isSigner: false, isWritable: false },
710
+ { pubkey: this.globalState.feeRecipient, isSigner: false, isWritable: true },
711
+ { pubkey: mint, isSigner: false, isWritable: false },
712
+ { pubkey: bonding, isSigner: false, isWritable: true },
713
+ { pubkey: associatedBondingCurve, isSigner: false, isWritable: true },
714
+ { pubkey: userAta, isSigner: false, isWritable: true },
715
+ { pubkey: this.wallet.publicKey, isSigner: true, isWritable: true },
716
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
717
+ { pubkey: creatorVault, isSigner: false, isWritable: true },
718
+ { pubkey: tokenProgram.programId, isSigner: false, isWritable: false },
719
+ { pubkey: PROGRAM_IDS.EVENT_AUTHORITY, isSigner: false, isWritable: false },
720
+ { pubkey: PROGRAM_IDS.PUMP, isSigner: false, isWritable: false },
721
+ { pubkey: feeConfig, isSigner: false, isWritable: false },
722
+ { pubkey: PROGRAM_IDS.FEE, isSigner: false, isWritable: false }
723
+ ],
724
+ data: Buffer.concat([
725
+ DISCRIMINATORS.SELL,
726
+ u64(tokenIn),
727
+ u64(minSol > 0n ? minSol : 1n)
728
+ ])
729
+ })
730
+ );
731
+
732
+ const { blockhash, lastValidBlockHeight } =
733
+ await this.connection.getLatestBlockhash("finalized");
734
+ tx.recentBlockhash = blockhash;
735
+ tx.feePayer = this.wallet.publicKey;
736
+ tx.sign(this.wallet);
737
+
738
+ const signature = await this.connection.sendRawTransaction(tx.serialize());
739
+ pendingTransactions.push({
740
+ signature,
741
+ lastValidBlockHeight,
742
+ index: i
743
+ });
744
+ } catch (e) {
745
+ failedTransactions.push({
746
+ index: i,
747
+ error: e.message
748
+ });
749
+ }
750
+ }
751
+
752
+ return { pendingTransactions, failedTransactions };
753
+ }
754
+
755
+ /* ---------- 外盘交易 ---------- */
756
+
757
+ async ammBuy(tokenAddr, totalSolIn, tradeOpt) {
758
+ const mint = new PublicKey(tokenAddr);
759
+ const poolInfo = await this.getAmmPoolInfo(mint);
760
+ const reserves = await this.getAmmPoolReserves(poolInfo.poolKeys);
761
+ const solChunks = this.splitByMax(totalSolIn, tradeOpt.maxSolPerTx);
762
+
763
+ const pendingTransactions = [];
764
+ const failedTransactions = [];
765
+
766
+ for (let i = 0; i < solChunks.length; i++) {
767
+ try {
768
+ const solIn = solChunks[i];
769
+ const baseAmountOut = this.calculateAmmBuyOutput(solIn, reserves);
770
+ const slippageBps = this.calcSlippage({
771
+ tradeSize: solIn,
772
+ reserve: reserves.quoteAmount,
773
+ slippageOpt: tradeOpt.slippage
774
+ });
775
+ const maxQuoteIn = (solIn * BigInt(10_000 + slippageBps)) / 10_000n;
776
+ const priority = this.genPriority(tradeOpt.priority);
777
+
778
+ const tx = new Transaction().add(
779
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
780
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priority })
781
+ );
782
+
783
+ const userBaseAta = await this.ensureAta(tx, poolInfo.poolKeys.baseMint);
784
+ const userQuoteAta = await this.ensureWSOLAta(
785
+ tx,
786
+ this.wallet.publicKey,
787
+ "buy",
788
+ maxQuoteIn
789
+ );
790
+
791
+ const buyIx = this.createAmmBuyInstruction(
792
+ poolInfo,
793
+ userBaseAta,
794
+ userQuoteAta,
795
+ baseAmountOut,
796
+ maxQuoteIn
797
+ );
798
+
799
+ tx.add(buyIx);
800
+ tx.add(
801
+ createCloseAccountInstruction(
802
+ userQuoteAta,
803
+ this.wallet.publicKey,
804
+ this.wallet.publicKey
805
+ )
806
+ );
807
+
808
+ const { blockhash, lastValidBlockHeight } =
809
+ await this.connection.getLatestBlockhash('finalized');
810
+ tx.recentBlockhash = blockhash;
811
+ tx.feePayer = this.wallet.publicKey;
812
+ tx.sign(this.wallet);
813
+
814
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
815
+ skipPreflight: false,
816
+ maxRetries: 2
817
+ });
818
+
819
+ pendingTransactions.push({
820
+ signature,
821
+ lastValidBlockHeight,
822
+ index: i
823
+ });
824
+ } catch (e) {
825
+ failedTransactions.push({
826
+ index: i,
827
+ error: e.message
828
+ });
829
+ }
830
+ }
831
+
832
+ return { pendingTransactions, failedTransactions };
833
+ }
834
+
835
+ async ammSell(tokenAddr, totalTokenIn, tradeOpt) {
836
+ const mint = new PublicKey(tokenAddr);
837
+ const poolInfo = await this.getAmmPoolInfo(mint);
838
+ const reserves = await this.getAmmPoolReserves(poolInfo.poolKeys);
839
+ const totalSolOut = this.calculateAmmSellOutput(totalTokenIn, reserves);
840
+
841
+ const tokenChunks = totalSolOut <= tradeOpt.maxSolPerTx
842
+ ? [totalTokenIn]
843
+ : this.splitIntoN(
844
+ totalTokenIn,
845
+ Number((totalSolOut + tradeOpt.maxSolPerTx - 1n) / tradeOpt.maxSolPerTx)
846
+ );
847
+
848
+ const pendingTransactions = [];
849
+ const failedTransactions = [];
850
+
851
+ for (let i = 0; i < tokenChunks.length; i++) {
852
+ try {
853
+ const tokenIn = tokenChunks[i];
854
+ const solOut = this.calculateAmmSellOutput(tokenIn, reserves);
855
+ const slippageBps = this.calcSlippage({
856
+ tradeSize: tokenIn,
857
+ reserve: reserves.baseAmount,
858
+ slippageOpt: tradeOpt.slippage
859
+ });
860
+ const minQuoteOut = (solOut * BigInt(10_000 - slippageBps)) / 10_000n;
861
+ const priority = this.genPriority(tradeOpt.priority);
862
+
863
+ const tx = new Transaction().add(
864
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
865
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priority })
866
+ );
867
+
868
+ const userBaseAta = await this.ensureAta(tx, poolInfo.poolKeys.baseMint);
869
+ const userQuoteAta = await this.ensureWSOLAta(tx, this.wallet.publicKey, "sell");
870
+
871
+ const sellIx = this.createAmmSellInstruction(
872
+ poolInfo,
873
+ userBaseAta,
874
+ userQuoteAta,
875
+ tokenIn,
876
+ minQuoteOut
877
+ );
878
+
879
+ tx.add(sellIx);
880
+ tx.add(
881
+ createCloseAccountInstruction(
882
+ userQuoteAta,
883
+ this.wallet.publicKey,
884
+ this.wallet.publicKey
885
+ )
886
+ );
887
+
888
+ const { blockhash, lastValidBlockHeight } =
889
+ await this.connection.getLatestBlockhash('finalized');
890
+ tx.recentBlockhash = blockhash;
891
+ tx.feePayer = this.wallet.publicKey;
892
+ tx.sign(this.wallet);
893
+
894
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
895
+ skipPreflight: false,
896
+ maxRetries: 2
897
+ });
898
+
899
+ pendingTransactions.push({
900
+ signature,
901
+ lastValidBlockHeight,
902
+ index: i
903
+ });
904
+ } catch (e) {
905
+ failedTransactions.push({
906
+ index: i,
907
+ error: e.message
908
+ });
909
+ }
910
+ }
911
+
912
+ return { pendingTransactions, failedTransactions };
913
+ }
914
+
915
+ /* ---------- AMM 池信息 ---------- */
916
+
917
+ async getAmmPoolInfo(mint) {
918
+ const [poolAuthority] = PublicKey.findProgramAddressSync(
919
+ [Buffer.from("pool-authority"), mint.toBuffer()],
920
+ PROGRAM_IDS.PUMP
921
+ );
922
+
923
+ const [pool] = PublicKey.findProgramAddressSync(
924
+ [
925
+ Buffer.from("pool"),
926
+ new BN(0).toArrayLike(Buffer, "le", 2),
927
+ poolAuthority.toBuffer(),
928
+ mint.toBuffer(),
929
+ SOL_MINT.toBuffer()
930
+ ],
931
+ PROGRAM_IDS.PUMP_AMM
932
+ );
933
+
934
+ const acc = await this.connection.getAccountInfo(pool);
935
+ if (!acc) throw new Error("AMM pool not found");
936
+
937
+ const poolKeys = parsePoolKeys(acc.data);
938
+
939
+ const [globalConfigPda] = PublicKey.findProgramAddressSync(
940
+ [Buffer.from("global_config")],
941
+ PROGRAM_IDS.PUMP_AMM
942
+ );
943
+
944
+ const globalConfigAcc = await this.connection.getAccountInfo(globalConfigPda);
945
+ if (!globalConfigAcc) throw new Error("Global config not found");
946
+
947
+ const globalConfig = this.parseAmmGlobalConfig(globalConfigAcc.data, globalConfigPda);
948
+
949
+ return { pool, poolAuthority, poolKeys, globalConfig };
950
+ }
951
+
952
+ parseAmmGlobalConfig(data, address) {
953
+ let offset = 8; // discriminator
954
+
955
+ const admin = new PublicKey(data.slice(offset, offset + 32));
956
+ offset += 32;
957
+
958
+ offset += 8; // lpFeeBasisPoints
959
+ offset += 8; // protocolFeeBasisPoints
960
+ offset += 1; // disableFlags
961
+
962
+ const protocolFeeRecipients = [];
963
+ for (let i = 0; i < 8; i++) {
964
+ protocolFeeRecipients.push(new PublicKey(data.slice(offset, offset + 32)));
965
+ offset += 32;
966
+ }
967
+
968
+ return { address, admin, protocolFeeRecipients };
969
+ }
970
+
971
+ async getAmmPoolReserves(poolKeys) {
972
+ const [baseInfo, quoteInfo] = await Promise.all([
973
+ this.connection.getTokenAccountBalance(poolKeys.poolBaseTokenAccount),
974
+ this.connection.getTokenAccountBalance(poolKeys.poolQuoteTokenAccount)
975
+ ]);
976
+
977
+ return {
978
+ baseAmount: BigInt(baseInfo.value.amount),
979
+ quoteAmount: BigInt(quoteInfo.value.amount),
980
+ baseDecimals: baseInfo.value.decimals,
981
+ quoteDecimals: quoteInfo.value.decimals
982
+ };
983
+ }
984
+
985
+ /* ---------- AMM 指令构建 ---------- */
986
+
987
+ createAmmBuyInstruction(poolInfo, userBaseAta, userQuoteAta, baseAmountOut, maxQuoteAmountIn) {
988
+ const { pool, poolKeys, globalConfig } = poolInfo;
989
+
990
+ const [eventAuthority] = PublicKey.findProgramAddressSync(
991
+ [Buffer.from("__event_authority")],
992
+ PROGRAM_IDS.PUMP_AMM
993
+ );
994
+
995
+ const [coinCreatorVaultAuthority] = PublicKey.findProgramAddressSync(
996
+ [Buffer.from("creator_vault"), poolKeys.coinCreator.toBuffer()],
997
+ PROGRAM_IDS.PUMP_AMM
998
+ );
999
+
1000
+ const coinCreatorVaultAta = getAssociatedTokenAddressSync(
1001
+ SOL_MINT,
1002
+ coinCreatorVaultAuthority,
1003
+ true,
1004
+ TOKEN_PROGRAM_ID,
1005
+ ASSOCIATED_TOKEN_PROGRAM_ID
1006
+ );
1007
+
1008
+ const [globalVolumeAccumulator] = PublicKey.findProgramAddressSync(
1009
+ [Buffer.from("global_volume_accumulator")],
1010
+ PROGRAM_IDS.PUMP_AMM
1011
+ );
1012
+
1013
+ const [userVolumeAccumulator] = PublicKey.findProgramAddressSync(
1014
+ [Buffer.from("user_volume_accumulator"), this.wallet.publicKey.toBuffer()],
1015
+ PROGRAM_IDS.PUMP_AMM
1016
+ );
1017
+
1018
+ const [feeConfig] = PublicKey.findProgramAddressSync(
1019
+ [Buffer.from("fee_config"), SEEDS.AMM_FEE_CONFIG],
1020
+ PROGRAM_IDS.FEE
1021
+ );
1022
+
1023
+ const protocolFeeRecipient = globalConfig.protocolFeeRecipients[0];
1024
+ const protocolFeeRecipientTokenAccount = getAssociatedTokenAddressSync(
1025
+ SOL_MINT,
1026
+ protocolFeeRecipient,
1027
+ true,
1028
+ TOKEN_PROGRAM_ID,
1029
+ ASSOCIATED_TOKEN_PROGRAM_ID
1030
+ );
1031
+
1032
+ return new TransactionInstruction({
1033
+ programId: PROGRAM_IDS.PUMP_AMM,
1034
+ keys: [
1035
+ { pubkey: pool, isSigner: false, isWritable: true },
1036
+ { pubkey: this.wallet.publicKey, isSigner: true, isWritable: true },
1037
+ { pubkey: globalConfig.address, isSigner: false, isWritable: false },
1038
+ { pubkey: poolKeys.baseMint, isSigner: false, isWritable: false },
1039
+ { pubkey: poolKeys.quoteMint, isSigner: false, isWritable: false },
1040
+ { pubkey: userBaseAta, isSigner: false, isWritable: true },
1041
+ { pubkey: userQuoteAta, isSigner: false, isWritable: true },
1042
+ { pubkey: poolKeys.poolBaseTokenAccount, isSigner: false, isWritable: true },
1043
+ { pubkey: poolKeys.poolQuoteTokenAccount, isSigner: false, isWritable: true },
1044
+ { pubkey: protocolFeeRecipient, isSigner: false, isWritable: false },
1045
+ { pubkey: protocolFeeRecipientTokenAccount, isSigner: false, isWritable: true },
1046
+ { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
1047
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1048
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1049
+ { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1050
+ { pubkey: eventAuthority, isSigner: false, isWritable: false },
1051
+ { pubkey: PROGRAM_IDS.PUMP_AMM, isSigner: false, isWritable: false },
1052
+ { pubkey: coinCreatorVaultAta, isSigner: false, isWritable: true },
1053
+ { pubkey: coinCreatorVaultAuthority, isSigner: false, isWritable: false },
1054
+ { pubkey: globalVolumeAccumulator, isSigner: false, isWritable: false },
1055
+ { pubkey: userVolumeAccumulator, isSigner: false, isWritable: true },
1056
+ { pubkey: feeConfig, isSigner: false, isWritable: false },
1057
+ { pubkey: PROGRAM_IDS.FEE, isSigner: false, isWritable: false }
1058
+ ],
1059
+ data: Buffer.concat([
1060
+ DISCRIMINATORS.BUY,
1061
+ u64(baseAmountOut),
1062
+ u64(maxQuoteAmountIn),
1063
+ Buffer.from([1, 1]) // trackVolume = Some(true)
1064
+ ])
1065
+ });
1066
+ }
1067
+
1068
+ createAmmSellInstruction(poolInfo, userBaseAta, userQuoteAta, baseAmountIn, minQuoteAmountOut) {
1069
+ const { pool, poolKeys, globalConfig } = poolInfo;
1070
+
1071
+ const [eventAuthority] = PublicKey.findProgramAddressSync(
1072
+ [Buffer.from("__event_authority")],
1073
+ PROGRAM_IDS.PUMP_AMM
1074
+ );
1075
+
1076
+ const [coinCreatorVaultAuthority] = PublicKey.findProgramAddressSync(
1077
+ [Buffer.from("creator_vault"), poolKeys.coinCreator.toBuffer()],
1078
+ PROGRAM_IDS.PUMP_AMM
1079
+ );
1080
+
1081
+ const coinCreatorVaultAta = getAssociatedTokenAddressSync(
1082
+ SOL_MINT,
1083
+ coinCreatorVaultAuthority,
1084
+ true,
1085
+ TOKEN_PROGRAM_ID,
1086
+ ASSOCIATED_TOKEN_PROGRAM_ID
1087
+ );
1088
+
1089
+ const [feeConfig] = PublicKey.findProgramAddressSync(
1090
+ [Buffer.from("fee_config"), SEEDS.AMM_FEE_CONFIG],
1091
+ PROGRAM_IDS.FEE
1092
+ );
1093
+
1094
+ const protocolFeeRecipient = globalConfig.protocolFeeRecipients[0];
1095
+ const protocolFeeRecipientTokenAccount = getAssociatedTokenAddressSync(
1096
+ SOL_MINT,
1097
+ protocolFeeRecipient,
1098
+ true,
1099
+ TOKEN_PROGRAM_ID,
1100
+ ASSOCIATED_TOKEN_PROGRAM_ID
1101
+ );
1102
+
1103
+ return new TransactionInstruction({
1104
+ programId: PROGRAM_IDS.PUMP_AMM,
1105
+ keys: [
1106
+ { pubkey: pool, isSigner: false, isWritable: true },
1107
+ { pubkey: this.wallet.publicKey, isSigner: true, isWritable: true },
1108
+ { pubkey: globalConfig.address, isSigner: false, isWritable: false },
1109
+ { pubkey: poolKeys.baseMint, isSigner: false, isWritable: false },
1110
+ { pubkey: poolKeys.quoteMint, isSigner: false, isWritable: false },
1111
+ { pubkey: userBaseAta, isSigner: false, isWritable: true },
1112
+ { pubkey: userQuoteAta, isSigner: false, isWritable: true },
1113
+ { pubkey: poolKeys.poolBaseTokenAccount, isSigner: false, isWritable: true },
1114
+ { pubkey: poolKeys.poolQuoteTokenAccount, isSigner: false, isWritable: true },
1115
+ { pubkey: protocolFeeRecipient, isSigner: false, isWritable: false },
1116
+ { pubkey: protocolFeeRecipientTokenAccount, isSigner: false, isWritable: true },
1117
+ { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
1118
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1119
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1120
+ { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1121
+ { pubkey: eventAuthority, isSigner: false, isWritable: false },
1122
+ { pubkey: PROGRAM_IDS.PUMP_AMM, isSigner: false, isWritable: false },
1123
+ { pubkey: coinCreatorVaultAta, isSigner: false, isWritable: true },
1124
+ { pubkey: coinCreatorVaultAuthority, isSigner: false, isWritable: false },
1125
+ { pubkey: feeConfig, isSigner: false, isWritable: false },
1126
+ { pubkey: PROGRAM_IDS.FEE, isSigner: false, isWritable: false }
1127
+ ],
1128
+ data: Buffer.concat([
1129
+ DISCRIMINATORS.SELL,
1130
+ u64(baseAmountIn),
1131
+ u64(minQuoteAmountOut > 0n ? minQuoteAmountOut : 1n)
1132
+ ])
1133
+ });
1134
+ }
1135
+
1136
+ /* ---------- 交易确认 ---------- */
1137
+
1138
+ async confirmTransactionWithPolling(signature, lastValidBlockHeight, maxAttempts = 5, delayMs = 2000) {
1139
+ console.log('✅ 交易已发送:', signature);
1140
+ console.log('🔗 查看交易: https://solscan.io/tx/' + signature);
1141
+
1142
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1143
+ await new Promise(resolve => setTimeout(resolve, delayMs));
1144
+
1145
+ try {
1146
+ console.log(`🔍 检查交易状态 (${attempt}/${maxAttempts})...`);
1147
+
1148
+ const txInfo = await this.connection.getTransaction(signature, {
1149
+ commitment: 'confirmed',
1150
+ maxSupportedTransactionVersion: 0
1151
+ });
1152
+
1153
+ if (txInfo) {
1154
+ if (txInfo.meta?.err) {
1155
+ console.error('❌ 交易失败:', txInfo.meta.err);
1156
+ throw new Error('交易失败: ' + JSON.stringify(txInfo.meta.err));
1157
+ }
1158
+
1159
+ console.log('✅ 交易已确认!');
1160
+ return signature;
1161
+ }
1162
+
1163
+ const currentBlockHeight = await this.connection.getBlockHeight('finalized');
1164
+ if (currentBlockHeight > lastValidBlockHeight) {
1165
+ console.log('⚠️ 交易已过期(超过有效区块高度)');
1166
+ throw new Error('交易过期:未在有效区块高度内确认');
1167
+ }
1168
+
1169
+ } catch (error) {
1170
+ if (error.message?.includes('交易失败') || error.message?.includes('交易过期')) {
1171
+ throw error;
1172
+ }
1173
+ console.log(`⚠️ 查询出错,继续重试: ${error.message}`);
1174
+ }
1175
+ }
1176
+
1177
+ throw new Error(
1178
+ `交易确认超时(已尝试 ${maxAttempts} 次),签名: ${signature}。请手动检查交易状态。`
1179
+ );
1180
+ }
1181
+
1182
+ /* ---------- 事件监听 ---------- */
1183
+
1184
+ listenTrades(callback, mintFilter = null) {
1185
+ return this.connection.onLogs(
1186
+ PROGRAM_IDS.PUMP,
1187
+ (log) => {
1188
+ for (const logLine of log.logs) {
1189
+ if (!logLine.startsWith("Program data: ")) continue;
1190
+
1191
+ const buf = Buffer.from(logLine.replace("Program data: ", ""), "base64");
1192
+
1193
+ if (!buf.subarray(0, 8).equals(DISCRIMINATORS.TRADE_EVENT)) continue;
1194
+
1195
+ let offset = 8;
1196
+ const mint = new PublicKey(buf.slice(offset, offset + 32));
1197
+ offset += 32;
1198
+
1199
+ if (mintFilter && !mint.equals(mintFilter)) return;
1200
+
1201
+ const solAmount = buf.readBigUInt64LE(offset);
1202
+ offset += 8;
1203
+ const tokenAmount = buf.readBigUInt64LE(offset);
1204
+ offset += 8;
1205
+ const isBuy = buf[offset++] === 1;
1206
+ const user = new PublicKey(buf.slice(offset, offset + 32));
1207
+ offset += 32;
1208
+ const timestamp = Number(buf.readBigInt64LE(offset));
1209
+
1210
+ callback({
1211
+ mint: mint.toBase58(),
1212
+ solAmount,
1213
+ tokenAmount,
1214
+ isBuy,
1215
+ user: user.toBase58(),
1216
+ timestamp,
1217
+ signature: log.signature
1218
+ });
1219
+ }
1220
+ },
1221
+ "confirmed"
1222
+ );
1223
+ }
1224
+
1225
+ /* ---------- 元数据查询 ---------- */
1226
+
1227
+ async fetchMeta(tokenAddr) {
1228
+ const mint = new PublicKey(tokenAddr);
1229
+
1230
+ try {
1231
+ const metadata = await getTokenMetadata(
1232
+ this.connection,
1233
+ mint,
1234
+ TOKEN_2022_PROGRAM_ID
1235
+ );
1236
+
1237
+ return {
1238
+ name: metadata?.name || "",
1239
+ symbol: metadata?.symbol || "",
1240
+ uri: metadata?.uri || ""
1241
+ };
1242
+ } catch (e) {
1243
+ const metadataPda = PublicKey.findProgramAddressSync(
1244
+ [Buffer.from("metadata"), PROGRAM_IDS.METADATA.toBuffer(), mint.toBuffer()],
1245
+ PROGRAM_IDS.METADATA
1246
+ )[0];
1247
+
1248
+ const acc = await this.connection.getAccountInfo(metadataPda);
1249
+ if (!acc) return null;
1250
+
1251
+ const meta = parseMetadataAccount(acc.data);
1252
+
1253
+ return {
1254
+ name: meta?.name?.replace(/\u0000/g, '') || "",
1255
+ symbol: meta?.symbol?.replace(/\u0000/g, '') || "",
1256
+ uri: meta?.uri?.replace(/\u0000/g, '') || ""
1257
+ };
1258
+ }
1259
+ }
1260
+
1261
+ /* ---------- 辅助方法 ---------- */
1262
+
1263
+ /**
1264
+ * 获取钱包信息
1265
+ */
1266
+ getWallet() {
1267
+ return this.wallet;
1268
+ }
1269
+
1270
+ /**
1271
+ * 获取连接信息
1272
+ */
1273
+ getConnection() {
1274
+ return this.connection;
1275
+ }
1276
+
1277
+ /**
1278
+ * 清除token program缓存
1279
+ */
1280
+ clearTokenProgramCache(tokenAddr = null) {
1281
+ if (tokenAddr) {
1282
+ this.tokenProgramCache.delete(tokenAddr);
1283
+ } else {
1284
+ this.tokenProgramCache.clear();
1285
+ }
1286
+ }
1287
+
1288
+ /**
1289
+ * 获取缓存的token program信息
1290
+ */
1291
+ getCachedTokenProgram(tokenAddr) {
1292
+ return this.tokenProgramCache.get(tokenAddr);
1293
+ }
1294
+ }