pump-trader 1.2.1 → 1.2.4
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/dist/index.d.ts +4 -2
- package/dist/index.js +103 -18
- package/index.js +103 -18
- package/index.ts +110 -18
- package/package.json +1 -1
- package/tests/instruction-accounts.test.ts +18 -7
package/dist/index.d.ts
CHANGED
|
@@ -126,11 +126,13 @@ export declare class PumpTrader {
|
|
|
126
126
|
calcSell(tokenIn: bigint, state: BondingCurveState): bigint;
|
|
127
127
|
calculateAmmBuyOutput(quoteIn: bigint, reserves: PoolReserves): bigint;
|
|
128
128
|
calculateAmmSellOutput(baseIn: bigint, reserves: PoolReserves): bigint;
|
|
129
|
-
|
|
129
|
+
private solPriceCache;
|
|
130
|
+
getSolPriceInUsdc(): Promise<number>;
|
|
131
|
+
getPriceAndStatus(tokenAddr: string, quoteMint?: PublicKey): Promise<{
|
|
130
132
|
price: number;
|
|
131
133
|
completed: boolean;
|
|
132
134
|
}>;
|
|
133
|
-
getAmmPrice(mint: PublicKey): Promise<number>;
|
|
135
|
+
getAmmPrice(mint: PublicKey, quoteMint?: PublicKey): Promise<number>;
|
|
134
136
|
/**
|
|
135
137
|
* 查询代币余额
|
|
136
138
|
* @param tokenAddr - 代币地址(可选),如果不传则返回所有代币
|
package/dist/index.js
CHANGED
|
@@ -153,6 +153,8 @@ function parsePoolKeys(data) {
|
|
|
153
153
|
/* ================= PumpTrader 类 ================= */
|
|
154
154
|
class PumpTrader {
|
|
155
155
|
constructor(rpc, wallet) {
|
|
156
|
+
/* ---------- 价格查询 ---------- */
|
|
157
|
+
this.solPriceCache = null;
|
|
156
158
|
this.connection = new web3_js_1.Connection(rpc, "confirmed");
|
|
157
159
|
this._wallet = wallet;
|
|
158
160
|
this.publicKey = wallet.publicKey;
|
|
@@ -419,20 +421,77 @@ class PumpTrader {
|
|
|
419
421
|
const denominator = reserves.baseAmount + baseInAfterFee;
|
|
420
422
|
return numerator / denominator;
|
|
421
423
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
+
async getSolPriceInUsdc() {
|
|
425
|
+
if (this.solPriceCache && Date.now() - this.solPriceCache.timestamp < 60000) {
|
|
426
|
+
return this.solPriceCache.price;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
// Use Orca USDC/SOL whirlpool (7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm)
|
|
430
|
+
// Read token vault balances directly to compute price
|
|
431
|
+
const poolAddr = new web3_js_1.PublicKey("7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm");
|
|
432
|
+
const acc = await this.connection.getAccountInfo(poolAddr);
|
|
433
|
+
if (!acc || acc.data.length < 304)
|
|
434
|
+
throw new Error("Invalid pool data");
|
|
435
|
+
// Whirlpool: tokenMintA at offset 40, tokenMintB at offset 72, vaultA at 104, vaultB at 136
|
|
436
|
+
const tokenMintA = new web3_js_1.PublicKey(acc.data.slice(40, 72));
|
|
437
|
+
const tokenVaultA = new web3_js_1.PublicKey(acc.data.slice(104, 136));
|
|
438
|
+
const tokenVaultB = new web3_js_1.PublicKey(acc.data.slice(136, 168));
|
|
439
|
+
const [balanceA, balanceB] = await Promise.all([
|
|
440
|
+
this.connection.getTokenAccountBalance(tokenVaultA),
|
|
441
|
+
this.connection.getTokenAccountBalance(tokenVaultB),
|
|
442
|
+
]);
|
|
443
|
+
// Determine which vault holds SOL by checking tokenMintA
|
|
444
|
+
const SOL_ADDR = "So11111111111111111111111111111111111111112";
|
|
445
|
+
const solBalance = tokenMintA.toBase58() === SOL_ADDR
|
|
446
|
+
? Number(balanceA.value.amount)
|
|
447
|
+
: Number(balanceB.value.amount);
|
|
448
|
+
const usdcBalance = tokenMintA.toBase58() === SOL_ADDR
|
|
449
|
+
? Number(balanceB.value.amount)
|
|
450
|
+
: Number(balanceA.value.amount);
|
|
451
|
+
if (solBalance === 0)
|
|
452
|
+
throw new Error("Zero balance");
|
|
453
|
+
const price = usdcBalance / solBalance;
|
|
454
|
+
this.solPriceCache = { price, timestamp: Date.now() };
|
|
455
|
+
return price;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return 175;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async getPriceAndStatus(tokenAddr, quoteMint) {
|
|
424
462
|
const mint = new web3_js_1.PublicKey(tokenAddr);
|
|
425
463
|
const { state } = await this.loadBonding(mint);
|
|
426
464
|
if (state.complete) {
|
|
427
|
-
const
|
|
465
|
+
const qm = quoteMint || (state.quoteMint && !state.quoteMint.equals(SOL_MINT) ? state.quoteMint : SOL_MINT);
|
|
466
|
+
const price = await this.getAmmPrice(mint, qm);
|
|
428
467
|
return { price, completed: true };
|
|
429
468
|
}
|
|
469
|
+
const qm = quoteMint || state.quoteMint || SOL_MINT;
|
|
470
|
+
const isSolQuote = qm.equals(SOL_MINT);
|
|
430
471
|
const oneToken = BigInt(1_000_000);
|
|
431
|
-
|
|
432
|
-
|
|
472
|
+
if (isSolQuote) {
|
|
473
|
+
const solOut = this.calcSell(oneToken, state);
|
|
474
|
+
const price = Number(solOut) / 1e9;
|
|
475
|
+
return { price, completed: false };
|
|
476
|
+
}
|
|
477
|
+
// USDC-paired bonding curve: pump stores USDC raw amount (6 decimals) in the
|
|
478
|
+
// virtualSolReserves field. calcSell returns USDC raw, divide by 1e6 for USDC price.
|
|
479
|
+
// Then convert to SOL using Orca USDC/SOL pool.
|
|
480
|
+
let quotePrice;
|
|
481
|
+
if (state.virtualQuoteReserves !== undefined) {
|
|
482
|
+
const newVirtualToken = state.virtualTokenReserves + oneToken;
|
|
483
|
+
const quoteOut = state.virtualQuoteReserves - (state.virtualQuoteReserves * state.virtualTokenReserves) / newVirtualToken;
|
|
484
|
+
quotePrice = Number(quoteOut) / 1e6;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const rawOut = this.calcSell(oneToken, state);
|
|
488
|
+
quotePrice = Number(rawOut) / 1e6;
|
|
489
|
+
}
|
|
490
|
+
const solPrice = await this.getSolPriceInUsdc();
|
|
491
|
+
const price = quotePrice / solPrice;
|
|
433
492
|
return { price, completed: false };
|
|
434
493
|
}
|
|
435
|
-
async getAmmPrice(mint) {
|
|
494
|
+
async getAmmPrice(mint, quoteMint = SOL_MINT) {
|
|
436
495
|
const [poolCreator] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("pool-authority"), mint.toBuffer()], PROGRAM_IDS.PUMP);
|
|
437
496
|
const indexBuffer = new bn_js_1.default(0).toArrayLike(Buffer, "le", 2);
|
|
438
497
|
const [pool] = web3_js_1.PublicKey.findProgramAddressSync([
|
|
@@ -440,7 +499,7 @@ class PumpTrader {
|
|
|
440
499
|
indexBuffer,
|
|
441
500
|
poolCreator.toBuffer(),
|
|
442
501
|
mint.toBuffer(),
|
|
443
|
-
|
|
502
|
+
quoteMint.toBuffer(),
|
|
444
503
|
], PROGRAM_IDS.PUMP_AMM);
|
|
445
504
|
const acc = await this.connection.getAccountInfo(pool);
|
|
446
505
|
if (!acc)
|
|
@@ -450,7 +509,13 @@ class PumpTrader {
|
|
|
450
509
|
this.connection.getTokenAccountBalance(poolKeys.poolBaseTokenAccount),
|
|
451
510
|
this.connection.getTokenAccountBalance(poolKeys.poolQuoteTokenAccount),
|
|
452
511
|
]);
|
|
453
|
-
|
|
512
|
+
let price = quoteInfo.value.uiAmount / baseInfo.value.uiAmount;
|
|
513
|
+
// If pool is not SOL-quoted, convert to SOL price
|
|
514
|
+
if (!quoteMint.equals(SOL_MINT)) {
|
|
515
|
+
const solPrice = await this.getSolPriceInUsdc();
|
|
516
|
+
price = price / solPrice;
|
|
517
|
+
}
|
|
518
|
+
return price;
|
|
454
519
|
}
|
|
455
520
|
/* ---------- 余额查询 ---------- */
|
|
456
521
|
/**
|
|
@@ -983,8 +1048,8 @@ class PumpTrader {
|
|
|
983
1048
|
const [feeConfig] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("fee_config"), SEEDS.AMM_FEE_CONFIG], PROGRAM_IDS.FEE);
|
|
984
1049
|
const protocolFeeRecipient = globalConfig.protocolFeeRecipients[0];
|
|
985
1050
|
const protocolFeeRecipientTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(SOL_MINT, protocolFeeRecipient, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
986
|
-
const
|
|
987
|
-
const
|
|
1051
|
+
const buybackFeeRecipient = this.pickBuybackFeeRecipient();
|
|
1052
|
+
const buybackFeeRecipientTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(poolKeys.quoteMint, buybackFeeRecipient, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
988
1053
|
const remainingKeys = [];
|
|
989
1054
|
if (poolKeys.isCashbackCoin) {
|
|
990
1055
|
const userVolumeAccumulatorWsolAta = (0, spl_token_1.getAssociatedTokenAddressSync)(SOL_MINT, userVolumeAccumulator, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
@@ -994,9 +1059,19 @@ class PumpTrader {
|
|
|
994
1059
|
isWritable: true,
|
|
995
1060
|
});
|
|
996
1061
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
pubkey:
|
|
1062
|
+
const POOL_DEFAULT_COIN_CREATOR = new web3_js_1.PublicKey("11111111111111111111111111111111");
|
|
1063
|
+
if (poolKeys.coinCreator && !poolKeys.coinCreator.equals(POOL_DEFAULT_COIN_CREATOR)) {
|
|
1064
|
+
remainingKeys.push({ pubkey: poolV2, isSigner: false, isWritable: false });
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
remainingKeys.push({
|
|
1068
|
+
pubkey: PUMP_BUYBACK_FEE_RECIPIENTS[0],
|
|
1069
|
+
isSigner: false,
|
|
1070
|
+
isWritable: true,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
remainingKeys.push({ pubkey: buybackFeeRecipient, isSigner: false, isWritable: false }, {
|
|
1074
|
+
pubkey: buybackFeeRecipientTokenAccount,
|
|
1000
1075
|
isSigner: false,
|
|
1001
1076
|
isWritable: true,
|
|
1002
1077
|
});
|
|
@@ -1065,8 +1140,8 @@ class PumpTrader {
|
|
|
1065
1140
|
const [feeConfig] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("fee_config"), SEEDS.AMM_FEE_CONFIG], PROGRAM_IDS.FEE);
|
|
1066
1141
|
const protocolFeeRecipient = globalConfig.protocolFeeRecipients[0];
|
|
1067
1142
|
const protocolFeeRecipientTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(SOL_MINT, protocolFeeRecipient, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
1068
|
-
const
|
|
1069
|
-
const
|
|
1143
|
+
const buybackFeeRecipient = this.pickBuybackFeeRecipient();
|
|
1144
|
+
const buybackFeeRecipientTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(poolKeys.quoteMint, buybackFeeRecipient, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
1070
1145
|
const [userVolumeAccumulator] = web3_js_1.PublicKey.findProgramAddressSync([
|
|
1071
1146
|
Buffer.from("user_volume_accumulator"),
|
|
1072
1147
|
this.publicKey.toBuffer(),
|
|
@@ -1080,9 +1155,19 @@ class PumpTrader {
|
|
|
1080
1155
|
isWritable: true,
|
|
1081
1156
|
}, { pubkey: userVolumeAccumulator, isSigner: false, isWritable: true });
|
|
1082
1157
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
pubkey:
|
|
1158
|
+
const POOL_DEFAULT_COIN_CREATOR = new web3_js_1.PublicKey("11111111111111111111111111111111");
|
|
1159
|
+
if (poolKeys.coinCreator && !poolKeys.coinCreator.equals(POOL_DEFAULT_COIN_CREATOR)) {
|
|
1160
|
+
remainingKeys.push({ pubkey: poolV2, isSigner: false, isWritable: false });
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
remainingKeys.push({
|
|
1164
|
+
pubkey: PUMP_BUYBACK_FEE_RECIPIENTS[0],
|
|
1165
|
+
isSigner: false,
|
|
1166
|
+
isWritable: true,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
remainingKeys.push({ pubkey: buybackFeeRecipient, isSigner: false, isWritable: false }, {
|
|
1170
|
+
pubkey: buybackFeeRecipientTokenAccount,
|
|
1086
1171
|
isSigner: false,
|
|
1087
1172
|
isWritable: true,
|
|
1088
1173
|
});
|
package/index.js
CHANGED
|
@@ -58,6 +58,17 @@ const DISCRIMINATORS = {
|
|
|
58
58
|
const AMM_FEE_BPS = 100n; // 1%
|
|
59
59
|
const BPS_DENOMINATOR = 10000n;
|
|
60
60
|
const PUMP_NEW_FEE_RECIPIENTS = [
|
|
61
|
+
"62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV",
|
|
62
|
+
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ",
|
|
63
|
+
"7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX",
|
|
64
|
+
"9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz",
|
|
65
|
+
"AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY",
|
|
66
|
+
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM",
|
|
67
|
+
"FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz",
|
|
68
|
+
"G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP",
|
|
69
|
+
].map((value) => new PublicKey(value));
|
|
70
|
+
|
|
71
|
+
const PUMP_BUYBACK_FEE_RECIPIENTS = [
|
|
61
72
|
"5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD",
|
|
62
73
|
"9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7",
|
|
63
74
|
"GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL",
|
|
@@ -360,6 +371,12 @@ export class PumpTrader {
|
|
|
360
371
|
return PUMP_NEW_FEE_RECIPIENTS[index % PUMP_NEW_FEE_RECIPIENTS.length];
|
|
361
372
|
}
|
|
362
373
|
|
|
374
|
+
pickBuybackFeeRecipient(index = 0) {
|
|
375
|
+
return PUMP_BUYBACK_FEE_RECIPIENTS[
|
|
376
|
+
index % PUMP_BUYBACK_FEE_RECIPIENTS.length
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
|
|
363
380
|
buildBondingBuyKeys(args) {
|
|
364
381
|
const tokenProgramId = args.tokenProgramId ?? TOKEN_PROGRAM_ID;
|
|
365
382
|
|
|
@@ -492,22 +509,74 @@ export class PumpTrader {
|
|
|
492
509
|
|
|
493
510
|
/* ---------- 价格查询 ---------- */
|
|
494
511
|
|
|
495
|
-
async
|
|
512
|
+
async getSolPriceInUsdc() {
|
|
513
|
+
if (this._solPriceCache && Date.now() - this._solPriceCache.timestamp < 60000) {
|
|
514
|
+
return this._solPriceCache.price;
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
const poolAddr = new PublicKey("7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm");
|
|
518
|
+
const acc = await this.connection.getAccountInfo(poolAddr);
|
|
519
|
+
if (!acc || acc.data.length < 304) throw new Error("Invalid pool data");
|
|
520
|
+
const tokenMintA = new PublicKey(acc.data.slice(40, 72));
|
|
521
|
+
const tokenVaultA = new PublicKey(acc.data.slice(104, 136));
|
|
522
|
+
const tokenVaultB = new PublicKey(acc.data.slice(136, 168));
|
|
523
|
+
const [balanceA, balanceB] = await Promise.all([
|
|
524
|
+
this.connection.getTokenAccountBalance(tokenVaultA),
|
|
525
|
+
this.connection.getTokenAccountBalance(tokenVaultB),
|
|
526
|
+
]);
|
|
527
|
+
const SOL_ADDR = "So11111111111111111111111111111111111111112";
|
|
528
|
+
const solBalance = tokenMintA.toBase58() === SOL_ADDR
|
|
529
|
+
? Number(balanceA.value.amount)
|
|
530
|
+
: Number(balanceB.value.amount);
|
|
531
|
+
const usdcBalance = tokenMintA.toBase58() === SOL_ADDR
|
|
532
|
+
? Number(balanceB.value.amount)
|
|
533
|
+
: Number(balanceA.value.amount);
|
|
534
|
+
if (solBalance === 0) throw new Error("Zero balance");
|
|
535
|
+
const price = usdcBalance / solBalance;
|
|
536
|
+
this._solPriceCache = { price, timestamp: Date.now() };
|
|
537
|
+
return price;
|
|
538
|
+
} catch {
|
|
539
|
+
return 175;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async getPriceAndStatus(tokenAddr, quoteMint = null) {
|
|
496
544
|
const mint = new PublicKey(tokenAddr);
|
|
497
545
|
const { state } = await this.loadBonding(mint);
|
|
498
546
|
|
|
499
547
|
if (state.complete) {
|
|
500
|
-
const
|
|
548
|
+
const qm = quoteMint || (state.quoteMint && !state.quoteMint.equals(SOL_MINT) ? state.quoteMint : SOL_MINT);
|
|
549
|
+
const price = await this.getAmmPrice(mint, qm);
|
|
501
550
|
return { price, completed: true };
|
|
502
551
|
}
|
|
503
552
|
|
|
553
|
+
const qm = quoteMint || state.quoteMint || SOL_MINT;
|
|
554
|
+
const isSolQuote = qm.equals(SOL_MINT);
|
|
555
|
+
|
|
504
556
|
const oneToken = BigInt(1_000_000);
|
|
505
|
-
|
|
506
|
-
|
|
557
|
+
|
|
558
|
+
if (isSolQuote) {
|
|
559
|
+
const solOut = this.calcSell(oneToken, state);
|
|
560
|
+
const price = Number(solOut) / 1e9;
|
|
561
|
+
return { price, completed: false };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let quotePrice;
|
|
565
|
+
if (state.virtualQuoteReserves !== undefined) {
|
|
566
|
+
const newVirtualToken = state.virtualTokenReserves + oneToken;
|
|
567
|
+
const quoteOut = state.virtualQuoteReserves - (state.virtualQuoteReserves * state.virtualTokenReserves) / newVirtualToken;
|
|
568
|
+
quotePrice = Number(quoteOut) / 1e6;
|
|
569
|
+
} else {
|
|
570
|
+
const rawOut = this.calcSell(oneToken, state);
|
|
571
|
+
quotePrice = Number(rawOut) / 1e6;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const solPrice = await this.getSolPriceInUsdc();
|
|
575
|
+
const price = quotePrice / solPrice;
|
|
507
576
|
return { price, completed: false };
|
|
508
577
|
}
|
|
509
578
|
|
|
510
|
-
async getAmmPrice(mint) {
|
|
579
|
+
async getAmmPrice(mint, quoteMint = SOL_MINT) {
|
|
511
580
|
const [poolCreator] = PublicKey.findProgramAddressSync(
|
|
512
581
|
[Buffer.from("pool-authority"), mint.toBuffer()],
|
|
513
582
|
PROGRAM_IDS.PUMP,
|
|
@@ -520,7 +589,7 @@ export class PumpTrader {
|
|
|
520
589
|
indexBuffer,
|
|
521
590
|
poolCreator.toBuffer(),
|
|
522
591
|
mint.toBuffer(),
|
|
523
|
-
|
|
592
|
+
quoteMint.toBuffer(),
|
|
524
593
|
],
|
|
525
594
|
PROGRAM_IDS.PUMP_AMM,
|
|
526
595
|
);
|
|
@@ -534,7 +603,14 @@ export class PumpTrader {
|
|
|
534
603
|
this.connection.getTokenAccountBalance(poolKeys.poolQuoteTokenAccount),
|
|
535
604
|
]);
|
|
536
605
|
|
|
537
|
-
|
|
606
|
+
let price = quoteInfo.value.uiAmount / baseInfo.value.uiAmount;
|
|
607
|
+
|
|
608
|
+
if (!quoteMint.equals(SOL_MINT)) {
|
|
609
|
+
const solPrice = await this.getSolPriceInUsdc();
|
|
610
|
+
price = price / solPrice;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return price;
|
|
538
614
|
}
|
|
539
615
|
|
|
540
616
|
/* ---------- 余额查询 ---------- */
|
|
@@ -1393,10 +1469,10 @@ export class PumpTrader {
|
|
|
1393
1469
|
TOKEN_PROGRAM_ID,
|
|
1394
1470
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
1395
1471
|
);
|
|
1396
|
-
const
|
|
1397
|
-
const
|
|
1472
|
+
const buybackFeeRecipient = this.pickBuybackFeeRecipient();
|
|
1473
|
+
const buybackFeeRecipientTokenAccount = getAssociatedTokenAddressSync(
|
|
1398
1474
|
poolKeys.quoteMint,
|
|
1399
|
-
|
|
1475
|
+
buybackFeeRecipient,
|
|
1400
1476
|
true,
|
|
1401
1477
|
TOKEN_PROGRAM_ID,
|
|
1402
1478
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
@@ -1417,11 +1493,20 @@ export class PumpTrader {
|
|
|
1417
1493
|
isWritable: true,
|
|
1418
1494
|
});
|
|
1419
1495
|
}
|
|
1420
|
-
|
|
1496
|
+
const POOL_DEFAULT_COIN_CREATOR = new PublicKey("11111111111111111111111111111111");
|
|
1497
|
+
if (poolKeys.coinCreator && !poolKeys.coinCreator.equals(POOL_DEFAULT_COIN_CREATOR)) {
|
|
1498
|
+
remainingKeys.push({ pubkey: poolV2, isSigner: false, isWritable: false });
|
|
1499
|
+
} else {
|
|
1500
|
+
remainingKeys.push({
|
|
1501
|
+
pubkey: PUMP_BUYBACK_FEE_RECIPIENTS[0],
|
|
1502
|
+
isSigner: false,
|
|
1503
|
+
isWritable: true,
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1421
1506
|
remainingKeys.push(
|
|
1422
|
-
{ pubkey:
|
|
1507
|
+
{ pubkey: buybackFeeRecipient, isSigner: false, isWritable: false },
|
|
1423
1508
|
{
|
|
1424
|
-
pubkey:
|
|
1509
|
+
pubkey: buybackFeeRecipientTokenAccount,
|
|
1425
1510
|
isSigner: false,
|
|
1426
1511
|
isWritable: true,
|
|
1427
1512
|
},
|
|
@@ -1526,10 +1611,10 @@ export class PumpTrader {
|
|
|
1526
1611
|
TOKEN_PROGRAM_ID,
|
|
1527
1612
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
1528
1613
|
);
|
|
1529
|
-
const
|
|
1530
|
-
const
|
|
1614
|
+
const buybackFeeRecipient = this.pickBuybackFeeRecipient();
|
|
1615
|
+
const buybackFeeRecipientTokenAccount = getAssociatedTokenAddressSync(
|
|
1531
1616
|
poolKeys.quoteMint,
|
|
1532
|
-
|
|
1617
|
+
buybackFeeRecipient,
|
|
1533
1618
|
true,
|
|
1534
1619
|
TOKEN_PROGRAM_ID,
|
|
1535
1620
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
@@ -1564,9 +1649,9 @@ export class PumpTrader {
|
|
|
1564
1649
|
}
|
|
1565
1650
|
remainingKeys.push({ pubkey: poolV2, isSigner: false, isWritable: false });
|
|
1566
1651
|
remainingKeys.push(
|
|
1567
|
-
{ pubkey:
|
|
1652
|
+
{ pubkey: buybackFeeRecipient, isSigner: false, isWritable: false },
|
|
1568
1653
|
{
|
|
1569
|
-
pubkey:
|
|
1654
|
+
pubkey: buybackFeeRecipientTokenAccount,
|
|
1570
1655
|
isSigner: false,
|
|
1571
1656
|
isWritable: true,
|
|
1572
1657
|
},
|
package/index.ts
CHANGED
|
@@ -700,24 +700,90 @@ export class PumpTrader {
|
|
|
700
700
|
|
|
701
701
|
/* ---------- 价格查询 ---------- */
|
|
702
702
|
|
|
703
|
+
private solPriceCache: { price: number; timestamp: number } | null = null;
|
|
704
|
+
|
|
705
|
+
async getSolPriceInUsdc(): Promise<number> {
|
|
706
|
+
if (this.solPriceCache && Date.now() - this.solPriceCache.timestamp < 60000) {
|
|
707
|
+
return this.solPriceCache.price;
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
// Use Orca USDC/SOL whirlpool (7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm)
|
|
711
|
+
// Read token vault balances directly to compute price
|
|
712
|
+
const poolAddr = new PublicKey("7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm");
|
|
713
|
+
const acc = await this.connection.getAccountInfo(poolAddr);
|
|
714
|
+
if (!acc || acc.data.length < 304) throw new Error("Invalid pool data");
|
|
715
|
+
|
|
716
|
+
// Whirlpool: tokenMintA at offset 40, tokenMintB at offset 72, vaultA at 104, vaultB at 136
|
|
717
|
+
const tokenMintA = new PublicKey(acc.data.slice(40, 72));
|
|
718
|
+
const tokenVaultA = new PublicKey(acc.data.slice(104, 136));
|
|
719
|
+
const tokenVaultB = new PublicKey(acc.data.slice(136, 168));
|
|
720
|
+
|
|
721
|
+
const [balanceA, balanceB] = await Promise.all([
|
|
722
|
+
this.connection.getTokenAccountBalance(tokenVaultA),
|
|
723
|
+
this.connection.getTokenAccountBalance(tokenVaultB),
|
|
724
|
+
]);
|
|
725
|
+
|
|
726
|
+
// Determine which vault holds SOL by checking tokenMintA
|
|
727
|
+
const SOL_ADDR = "So11111111111111111111111111111111111111112";
|
|
728
|
+
const solBalance = tokenMintA.toBase58() === SOL_ADDR
|
|
729
|
+
? Number(balanceA.value.amount)
|
|
730
|
+
: Number(balanceB.value.amount);
|
|
731
|
+
const usdcBalance = tokenMintA.toBase58() === SOL_ADDR
|
|
732
|
+
? Number(balanceB.value.amount)
|
|
733
|
+
: Number(balanceA.value.amount);
|
|
734
|
+
|
|
735
|
+
if (solBalance === 0) throw new Error("Zero balance");
|
|
736
|
+
const price = usdcBalance / solBalance;
|
|
737
|
+
this.solPriceCache = { price, timestamp: Date.now() };
|
|
738
|
+
return price;
|
|
739
|
+
} catch {
|
|
740
|
+
return 175;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
703
744
|
async getPriceAndStatus(
|
|
704
745
|
tokenAddr: string,
|
|
746
|
+
quoteMint?: PublicKey,
|
|
705
747
|
): Promise<{ price: number; completed: boolean }> {
|
|
706
748
|
const mint = new PublicKey(tokenAddr);
|
|
707
749
|
const { state } = await this.loadBonding(mint);
|
|
708
750
|
|
|
709
751
|
if (state.complete) {
|
|
710
|
-
const
|
|
752
|
+
const qm = quoteMint || (state.quoteMint && !state.quoteMint.equals(SOL_MINT) ? state.quoteMint : SOL_MINT);
|
|
753
|
+
const price = await this.getAmmPrice(mint, qm);
|
|
711
754
|
return { price, completed: true };
|
|
712
755
|
}
|
|
713
756
|
|
|
757
|
+
const qm = quoteMint || state.quoteMint || SOL_MINT;
|
|
758
|
+
const isSolQuote = qm.equals(SOL_MINT);
|
|
759
|
+
|
|
714
760
|
const oneToken = BigInt(1_000_000);
|
|
715
|
-
|
|
716
|
-
|
|
761
|
+
|
|
762
|
+
if (isSolQuote) {
|
|
763
|
+
const solOut = this.calcSell(oneToken, state);
|
|
764
|
+
const price = Number(solOut) / 1e9;
|
|
765
|
+
return { price, completed: false };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// USDC-paired bonding curve: pump stores USDC raw amount (6 decimals) in the
|
|
769
|
+
// virtualSolReserves field. calcSell returns USDC raw, divide by 1e6 for USDC price.
|
|
770
|
+
// Then convert to SOL using Orca USDC/SOL pool.
|
|
771
|
+
let quotePrice: number;
|
|
772
|
+
if (state.virtualQuoteReserves !== undefined) {
|
|
773
|
+
const newVirtualToken = state.virtualTokenReserves + oneToken;
|
|
774
|
+
const quoteOut = state.virtualQuoteReserves - (state.virtualQuoteReserves * state.virtualTokenReserves) / newVirtualToken;
|
|
775
|
+
quotePrice = Number(quoteOut) / 1e6;
|
|
776
|
+
} else {
|
|
777
|
+
const rawOut = this.calcSell(oneToken, state);
|
|
778
|
+
quotePrice = Number(rawOut) / 1e6;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const solPrice = await this.getSolPriceInUsdc();
|
|
782
|
+
const price = quotePrice / solPrice;
|
|
717
783
|
return { price, completed: false };
|
|
718
784
|
}
|
|
719
785
|
|
|
720
|
-
async getAmmPrice(mint: PublicKey): Promise<number> {
|
|
786
|
+
async getAmmPrice(mint: PublicKey, quoteMint: PublicKey = SOL_MINT): Promise<number> {
|
|
721
787
|
const [poolCreator] = PublicKey.findProgramAddressSync(
|
|
722
788
|
[Buffer.from("pool-authority"), mint.toBuffer()],
|
|
723
789
|
PROGRAM_IDS.PUMP,
|
|
@@ -730,7 +796,7 @@ export class PumpTrader {
|
|
|
730
796
|
indexBuffer,
|
|
731
797
|
poolCreator.toBuffer(),
|
|
732
798
|
mint.toBuffer(),
|
|
733
|
-
|
|
799
|
+
quoteMint.toBuffer(),
|
|
734
800
|
],
|
|
735
801
|
PROGRAM_IDS.PUMP_AMM,
|
|
736
802
|
);
|
|
@@ -744,7 +810,15 @@ export class PumpTrader {
|
|
|
744
810
|
this.connection.getTokenAccountBalance(poolKeys.poolQuoteTokenAccount),
|
|
745
811
|
]);
|
|
746
812
|
|
|
747
|
-
|
|
813
|
+
let price = quoteInfo.value.uiAmount! / baseInfo.value.uiAmount!;
|
|
814
|
+
|
|
815
|
+
// If pool is not SOL-quoted, convert to SOL price
|
|
816
|
+
if (!quoteMint.equals(SOL_MINT)) {
|
|
817
|
+
const solPrice = await this.getSolPriceInUsdc();
|
|
818
|
+
price = price / solPrice;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return price;
|
|
748
822
|
}
|
|
749
823
|
|
|
750
824
|
/* ---------- 余额查询 ---------- */
|
|
@@ -1657,10 +1731,10 @@ export class PumpTrader {
|
|
|
1657
1731
|
TOKEN_PROGRAM_ID,
|
|
1658
1732
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
1659
1733
|
);
|
|
1660
|
-
const
|
|
1661
|
-
const
|
|
1734
|
+
const buybackFeeRecipient = this.pickBuybackFeeRecipient();
|
|
1735
|
+
const buybackFeeRecipientTokenAccount = getAssociatedTokenAddressSync(
|
|
1662
1736
|
poolKeys.quoteMint,
|
|
1663
|
-
|
|
1737
|
+
buybackFeeRecipient,
|
|
1664
1738
|
true,
|
|
1665
1739
|
TOKEN_PROGRAM_ID,
|
|
1666
1740
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
@@ -1681,11 +1755,20 @@ export class PumpTrader {
|
|
|
1681
1755
|
isWritable: true,
|
|
1682
1756
|
});
|
|
1683
1757
|
}
|
|
1684
|
-
|
|
1758
|
+
const POOL_DEFAULT_COIN_CREATOR = new PublicKey("11111111111111111111111111111111");
|
|
1759
|
+
if (poolKeys.coinCreator && !poolKeys.coinCreator.equals(POOL_DEFAULT_COIN_CREATOR)) {
|
|
1760
|
+
remainingKeys.push({ pubkey: poolV2, isSigner: false, isWritable: false });
|
|
1761
|
+
} else {
|
|
1762
|
+
remainingKeys.push({
|
|
1763
|
+
pubkey: PUMP_BUYBACK_FEE_RECIPIENTS[0],
|
|
1764
|
+
isSigner: false,
|
|
1765
|
+
isWritable: true,
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1685
1768
|
remainingKeys.push(
|
|
1686
|
-
{ pubkey:
|
|
1769
|
+
{ pubkey: buybackFeeRecipient, isSigner: false, isWritable: false },
|
|
1687
1770
|
{
|
|
1688
|
-
pubkey:
|
|
1771
|
+
pubkey: buybackFeeRecipientTokenAccount,
|
|
1689
1772
|
isSigner: false,
|
|
1690
1773
|
isWritable: true,
|
|
1691
1774
|
},
|
|
@@ -1790,10 +1873,10 @@ export class PumpTrader {
|
|
|
1790
1873
|
TOKEN_PROGRAM_ID,
|
|
1791
1874
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
1792
1875
|
);
|
|
1793
|
-
const
|
|
1794
|
-
const
|
|
1876
|
+
const buybackFeeRecipient = this.pickBuybackFeeRecipient();
|
|
1877
|
+
const buybackFeeRecipientTokenAccount = getAssociatedTokenAddressSync(
|
|
1795
1878
|
poolKeys.quoteMint,
|
|
1796
|
-
|
|
1879
|
+
buybackFeeRecipient,
|
|
1797
1880
|
true,
|
|
1798
1881
|
TOKEN_PROGRAM_ID,
|
|
1799
1882
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
@@ -1826,11 +1909,20 @@ export class PumpTrader {
|
|
|
1826
1909
|
{ pubkey: userVolumeAccumulator, isSigner: false, isWritable: true },
|
|
1827
1910
|
);
|
|
1828
1911
|
}
|
|
1829
|
-
|
|
1912
|
+
const POOL_DEFAULT_COIN_CREATOR = new PublicKey("11111111111111111111111111111111");
|
|
1913
|
+
if (poolKeys.coinCreator && !poolKeys.coinCreator.equals(POOL_DEFAULT_COIN_CREATOR)) {
|
|
1914
|
+
remainingKeys.push({ pubkey: poolV2, isSigner: false, isWritable: false });
|
|
1915
|
+
} else {
|
|
1916
|
+
remainingKeys.push({
|
|
1917
|
+
pubkey: PUMP_BUYBACK_FEE_RECIPIENTS[0],
|
|
1918
|
+
isSigner: false,
|
|
1919
|
+
isWritable: true,
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1830
1922
|
remainingKeys.push(
|
|
1831
|
-
{ pubkey:
|
|
1923
|
+
{ pubkey: buybackFeeRecipient, isSigner: false, isWritable: false },
|
|
1832
1924
|
{
|
|
1833
|
-
pubkey:
|
|
1925
|
+
pubkey: buybackFeeRecipientTokenAccount,
|
|
1834
1926
|
isSigner: false,
|
|
1835
1927
|
isWritable: true,
|
|
1836
1928
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
|
-
import bs58 from "bs58";
|
|
5
4
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
|
6
5
|
import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
7
6
|
|
|
@@ -9,6 +8,17 @@ import { PumpTrader } from "../index";
|
|
|
9
8
|
|
|
10
9
|
const SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112");
|
|
11
10
|
const FEE_RECIPIENTS = [
|
|
11
|
+
"62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV",
|
|
12
|
+
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ",
|
|
13
|
+
"7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX",
|
|
14
|
+
"9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz",
|
|
15
|
+
"AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY",
|
|
16
|
+
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM",
|
|
17
|
+
"FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz",
|
|
18
|
+
"G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"
|
|
19
|
+
].map((value) => new PublicKey(value));
|
|
20
|
+
|
|
21
|
+
const BUYBACK_FEE_RECIPIENTS = [
|
|
12
22
|
"5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD",
|
|
13
23
|
"9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7",
|
|
14
24
|
"GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL",
|
|
@@ -20,7 +30,7 @@ const FEE_RECIPIENTS = [
|
|
|
20
30
|
].map((value) => new PublicKey(value));
|
|
21
31
|
|
|
22
32
|
function createTrader() {
|
|
23
|
-
return new PumpTrader("http://127.0.0.1:8899",
|
|
33
|
+
return new PumpTrader("http://127.0.0.1:8899", Keypair.generate());
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
function createPoolInfo(isCashbackCoin: boolean) {
|
|
@@ -127,7 +137,7 @@ test("amm buy places poolV2 before fee recipient pair for non-cashback coins", (
|
|
|
127
137
|
TOKEN_PROGRAM_ID
|
|
128
138
|
);
|
|
129
139
|
|
|
130
|
-
const feeRecipient =
|
|
140
|
+
const feeRecipient = BUYBACK_FEE_RECIPIENTS[0];
|
|
131
141
|
const feeRecipientAta = getAssociatedTokenAddressSync(
|
|
132
142
|
SOL_MINT,
|
|
133
143
|
feeRecipient,
|
|
@@ -136,6 +146,7 @@ test("amm buy places poolV2 before fee recipient pair for non-cashback coins", (
|
|
|
136
146
|
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
137
147
|
);
|
|
138
148
|
|
|
149
|
+
assert.equal(instruction.keys[9]?.pubkey.toBase58(), poolInfo.globalConfig.protocolFeeRecipients[0].toBase58());
|
|
139
150
|
assert.equal(instruction.keys.length, 26);
|
|
140
151
|
assert.equal(instruction.keys.at(-3)?.pubkey.toBase58(), trader.deriveAmmPoolV2(poolInfo.poolKeys.baseMint).toBase58());
|
|
141
152
|
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(), feeRecipient.toBase58());
|
|
@@ -159,7 +170,7 @@ test("amm buy places cashback account before poolV2 and fee recipient tail", ()
|
|
|
159
170
|
|
|
160
171
|
assert.equal(instruction.keys.length, 27);
|
|
161
172
|
assert.equal(instruction.keys.at(-3)?.isWritable, false);
|
|
162
|
-
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(),
|
|
173
|
+
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(), BUYBACK_FEE_RECIPIENTS[0].toBase58());
|
|
163
174
|
assert.equal(instruction.keys.at(-1)?.isWritable, true);
|
|
164
175
|
});
|
|
165
176
|
|
|
@@ -178,7 +189,7 @@ test("amm sell places poolV2 before fee recipient pair for non-cashback coins",
|
|
|
178
189
|
|
|
179
190
|
assert.equal(instruction.keys.length, 24);
|
|
180
191
|
assert.equal(instruction.keys.at(-3)?.pubkey.toBase58(), trader.deriveAmmPoolV2(poolInfo.poolKeys.baseMint).toBase58());
|
|
181
|
-
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(),
|
|
192
|
+
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(), BUYBACK_FEE_RECIPIENTS[0].toBase58());
|
|
182
193
|
assert.equal(instruction.keys.at(-1)?.isWritable, true);
|
|
183
194
|
});
|
|
184
195
|
|
|
@@ -197,10 +208,10 @@ test("amm sell places cashback accounts before poolV2 and fee recipient tail", (
|
|
|
197
208
|
|
|
198
209
|
assert.equal(instruction.keys.length, 26);
|
|
199
210
|
assert.equal(instruction.keys.at(-3)?.pubkey.toBase58(), trader.deriveAmmPoolV2(poolInfo.poolKeys.baseMint).toBase58());
|
|
200
|
-
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(),
|
|
211
|
+
assert.equal(instruction.keys.at(-2)?.pubkey.toBase58(), BUYBACK_FEE_RECIPIENTS[0].toBase58());
|
|
201
212
|
assert.equal(instruction.keys.at(-1)?.pubkey.toBase58(), getAssociatedTokenAddressSync(
|
|
202
213
|
SOL_MINT,
|
|
203
|
-
|
|
214
|
+
BUYBACK_FEE_RECIPIENTS[0],
|
|
204
215
|
true,
|
|
205
216
|
TOKEN_PROGRAM_ID,
|
|
206
217
|
ASSOCIATED_TOKEN_PROGRAM_ID
|