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