feat: PumpSwap/PumpFun improvements, align with pump-public-docs, sync IDL
- Re-apply PumpSwap pool lookup, lib/utils/seed changes (no Fastlane) - pump-public-docs: Mayhem fee recipient at index 11 use WSOL ATA; random mayhem fee recipients (PumpSwap + PumpFun); Pool decode 244 bytes - Sync idl (pump.json, pump_amm.json, pump_fees.json) from pump-fun/pump-public-docs Made-with: Cursor
This commit is contained in:
@@ -768,21 +768,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "bonding_curve_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -1179,21 +1164,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "bonding_curve_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -3987,21 +3957,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "bonding_curve_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
|
||||
+1
-46
@@ -688,21 +688,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "pool_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [112, 111, 111, 108, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "base_mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -1134,21 +1119,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "pool_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [112, 111, 111, 108, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "base_mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -3192,21 +3162,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "pool_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [112, 111, 111, 108, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "base_mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -4880,7 +4835,7 @@
|
||||
},
|
||||
{
|
||||
"code": 6052,
|
||||
"name": "TokensInVaultLessThanCashbackEarned"
|
||||
"name": "CashbackEarnedDoesNotMatchTokenInVault"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
|
||||
+1157
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,15 @@ pub async fn update_rents(client: &SolanaRpcClient) -> Result<(), anyhow::Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 165 字节 Token 账户的典型租金(lamports),RPC 超时时用作回退
|
||||
const DEFAULT_TOKEN_ACCOUNT_RENT: u64 = 2_039_280;
|
||||
|
||||
/// 当 RPC 超时或不可用时设置默认租金,避免客户端创建卡死
|
||||
pub fn set_default_rents() {
|
||||
SPL_TOKEN_RENT.store(DEFAULT_TOKEN_ACCOUNT_RENT, Ordering::Release);
|
||||
SPL_TOKEN_2022_RENT.store(DEFAULT_TOKEN_ACCOUNT_RENT, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn start_rent_updater(client: Arc<SolanaRpcClient>) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::{
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::{
|
||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
|
||||
get_user_volume_accumulator_pda, global_constants::{self}, BUY_DISCRIMINATOR,
|
||||
BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
||||
get_mayhem_fee_recipient_meta_random, get_user_volume_accumulator_pda,
|
||||
global_constants::{self}, BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
||||
},
|
||||
utils::calc::{
|
||||
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
|
||||
@@ -137,9 +137,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
buy_data[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
global_constants::MAYHEM_FEE_RECIPIENT_META
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
global_constants::FEE_RECIPIENT_META
|
||||
};
|
||||
@@ -259,9 +259,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
|
||||
// Determine fee recipient based on mayhem mode
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
global_constants::MAYHEM_FEE_RECIPIENT_META
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
global_constants::FEE_RECIPIENT_META
|
||||
};
|
||||
|
||||
+20
-16
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_pool_v2_pda, get_user_volume_accumulator_pda,
|
||||
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR,
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR,
|
||||
BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
},
|
||||
trading::{
|
||||
@@ -119,16 +119,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
// Determine fee recipient based on mayhem mode
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
|
||||
let is_mayhem_mode = protocol_params.is_mayhem_mode;
|
||||
let fee_recipient =
|
||||
if is_mayhem_mode { accounts::MAYHEM_FEE_RECIPIENT } else { accounts::FEE_RECIPIENT };
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
accounts::MAYHEM_FEE_RECIPIENT_META
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
accounts::FEE_RECIPIENT_META
|
||||
(accounts::FEE_RECIPIENT, accounts::FEE_RECIPIENT_META)
|
||||
};
|
||||
let fee_recipient_ata = if is_mayhem_mode {
|
||||
fee_recipient_ata(fee_recipient, crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
fee_recipient_ata(fee_recipient, quote_mint)
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
@@ -320,16 +322,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
sol_amount = params.fixed_output_amount.unwrap();
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
|
||||
let is_mayhem_mode = protocol_params.is_mayhem_mode;
|
||||
let fee_recipient =
|
||||
if is_mayhem_mode { accounts::MAYHEM_FEE_RECIPIENT } else { accounts::FEE_RECIPIENT };
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
accounts::MAYHEM_FEE_RECIPIENT_META
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
accounts::FEE_RECIPIENT_META
|
||||
(accounts::FEE_RECIPIENT, accounts::FEE_RECIPIENT_META)
|
||||
};
|
||||
let fee_recipient_ata = if is_mayhem_mode {
|
||||
fee_recipient_ata(fee_recipient, crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
fee_recipient_ata(fee_recipient, quote_mint)
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
@@ -59,8 +60,18 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey =
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS");
|
||||
/// Mayhem fee recipients (pump-public-docs: use any one randomly)
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"),
|
||||
pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"),
|
||||
pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"),
|
||||
pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"),
|
||||
pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"),
|
||||
pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"),
|
||||
];
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
pub const MAYHEM_FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: MAYHEM_FEE_RECIPIENT,
|
||||
@@ -161,6 +172,19 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
/// Returns a random Mayhem fee recipient AccountMeta (pump-public-docs: Bonding Curve 2nd account = Mayhem fee recipient; use any one randomly).
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::MAYHEM_FEE_RECIPIENTS[0]);
|
||||
AccountMeta {
|
||||
pubkey: recipient,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Symbol;
|
||||
|
||||
impl Symbol {
|
||||
|
||||
@@ -2,12 +2,17 @@ use crate::{
|
||||
common::{
|
||||
spl_associated_token_account::get_associated_token_address_with_program_id, SolanaRpcClient,
|
||||
},
|
||||
constants::TOKEN_PROGRAM,
|
||||
constants::{TOKEN_PROGRAM, WSOL_TOKEN_ACCOUNT},
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
|
||||
/// PumpSwap 池账户总长度(见 pump-public-docs Breaking Change):8 字节 discriminator + 244 字节 Pool。
|
||||
/// 官方文档:pool structure needs to be 244 bytes (was 243),含 is_mayhem_mode。DataSize 必须与此一致,否则 getProgramAccounts 会返回 0。
|
||||
const POOL_ACCOUNT_DATA_LEN: u64 = 8 + 244;
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
@@ -29,6 +34,10 @@ pub mod seeds {
|
||||
|
||||
/// Seed for pool v2 PDA (required by program upgrade, readonly at end of account list)
|
||||
pub const POOL_V2_SEED: &[u8] = b"pool-v2";
|
||||
/// Legacy pool PDA seed (used with index, creator, base_mint, quote_mint)
|
||||
pub const POOL_SEED: &[u8] = b"pool";
|
||||
/// Pump program: pool-authority PDA seed (creator for canonical pool)
|
||||
pub const POOL_AUTHORITY_SEED: &[u8] = b"pool-authority";
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
@@ -53,6 +62,8 @@ pub mod accounts {
|
||||
pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
|
||||
/// Pump Bonding Curve program(canonical pool 的 creator 来自此程序的 pool-authority PDA)
|
||||
pub const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
pub const LP_FEE_BASIS_POINTS: u64 = 25;
|
||||
pub const PROTOCOL_FEE_BASIS_POINTS: u64 = 5;
|
||||
@@ -68,9 +79,19 @@ pub mod accounts {
|
||||
pub const DEFAULT_COIN_CREATOR_VAULT_AUTHORITY: Pubkey =
|
||||
pubkey!("8N3GDaZ2iwN65oxVatKTLPNooAVUJTbfiVJ1ahyqwjSk");
|
||||
|
||||
/// Mayhem fee recipient (for mayhem mode coins)
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey =
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS");
|
||||
/// Mayhem fee recipients (pump-public-docs: use any one randomly for throughput)
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"),
|
||||
pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"),
|
||||
pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"),
|
||||
pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"),
|
||||
pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"),
|
||||
pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"),
|
||||
];
|
||||
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
|
||||
// META
|
||||
|
||||
@@ -142,6 +163,20 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
/// Returns a random Mayhem fee recipient and its AccountMeta (pump-public-docs: use any one randomly).
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
|
||||
let recipient = *accounts::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0]);
|
||||
let meta = AccountMeta {
|
||||
pubkey: recipient,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
(recipient, meta)
|
||||
}
|
||||
|
||||
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
|
||||
#[inline]
|
||||
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||
@@ -152,6 +187,35 @@ pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||
Some(pda)
|
||||
}
|
||||
|
||||
/// Pump 程序上的 pool-authority PDA(canonical pool 的 creator),与 @pump-fun/pump-swap-sdk 一致。
|
||||
#[inline]
|
||||
pub fn get_pump_pool_authority_pda(mint: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[seeds::POOL_AUTHORITY_SEED, mint.as_ref()],
|
||||
&accounts::PUMP_PROGRAM_ID,
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Canonical Pump 池 PDA:index=0,creator=pumpPoolAuthorityPda(mint),base_mint=mint,quote_mint=WSOL。
|
||||
/// 与 @pump-fun/pump-swap-sdk 的 canonicalPumpPoolPda(mint) 一致,用于从 bonding curve 迁移后的标准池查找。
|
||||
#[inline]
|
||||
pub fn get_canonical_pool_pda(mint: &Pubkey) -> Pubkey {
|
||||
const CANONICAL_POOL_INDEX: u16 = 0;
|
||||
let authority = get_pump_pool_authority_pda(mint);
|
||||
let (pda, _) = Pubkey::find_program_address(
|
||||
&[
|
||||
seeds::POOL_SEED,
|
||||
&CANONICAL_POOL_INDEX.to_le_bytes(),
|
||||
authority.as_ref(),
|
||||
mint.as_ref(),
|
||||
WSOL_TOKEN_ACCOUNT.as_ref(),
|
||||
],
|
||||
&accounts::AMM_PROGRAM,
|
||||
);
|
||||
pda
|
||||
}
|
||||
|
||||
// Find a pool for a specific mint
|
||||
pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<Pubkey, anyhow::Error> {
|
||||
let (pool_address, _) = find_by_mint(rpc, mint).await?;
|
||||
@@ -231,11 +295,12 @@ pub async fn find_by_base_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// Use getProgramAccounts to find pools for the given mint
|
||||
// Use getProgramAccounts to find pools for the given mint.
|
||||
// base_mint 在账户布局中的偏移:8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let filters = vec![
|
||||
// solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool account size
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &base_mint.to_bytes()),
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, base_mint.as_ref()),
|
||||
),
|
||||
];
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
@@ -282,11 +347,12 @@ pub async fn find_by_quote_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// Use getProgramAccounts to find pools for the given mint
|
||||
// Use getProgramAccounts to find pools for the given mint.
|
||||
// quote_mint 在账户布局中的偏移:8 + 1 + 2 + 32 + 32 = 75
|
||||
let filters = vec![
|
||||
// solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool account size
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(75, "e_mint.to_bytes()),
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(75, quote_mint.as_ref()),
|
||||
),
|
||||
];
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
@@ -329,17 +395,52 @@ pub async fn find_by_quote_mint(
|
||||
Ok((address, pool))
|
||||
}
|
||||
|
||||
/// 按 mint 查找 PumpSwap 池(本函数仅用于 PumpSwap,其他 DEX 勿用)。
|
||||
///
|
||||
/// 查找顺序(与 @pump-fun/pump-swap-sdk 一致):
|
||||
/// 1. Pool v2 PDA ["pool-v2", base_mint] — 一次 getAccount
|
||||
/// 2. Canonical pool PDA ["pool", 0, pumpPoolAuthority(mint), mint, WSOL] — 迁移后的标准池
|
||||
/// 3. getProgramAccounts 按 base_mint / quote_mint 过滤
|
||||
pub async fn find_by_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
if let Ok((address, pool)) = find_by_base_mint(rpc, mint).await {
|
||||
return Ok((address, pool));
|
||||
let mut diag = Vec::<String>::new();
|
||||
|
||||
// 1. PumpSwap v2 PDA(seeds: ["pool-v2", base_mint])
|
||||
if let Some(pool_address) = get_pool_v2_pda(mint) {
|
||||
diag.push(format!("PDA(v2)={}", pool_address));
|
||||
match fetch_pool(rpc, &pool_address).await {
|
||||
Ok(pool) if pool.base_mint == *mint => return Ok((pool_address, pool)),
|
||||
Ok(_) => diag.push("PDA(v2) 账户存在但 base_mint 不匹配".into()),
|
||||
Err(e) => diag.push(format!("PDA(v2) get_account/decode 失败: {}", e)),
|
||||
}
|
||||
}
|
||||
if let Ok((address, pool)) = find_by_quote_mint(rpc, mint).await {
|
||||
return Ok((address, pool));
|
||||
|
||||
// 2. Canonical pool PDA(与 pump-swap-sdk canonicalPumpPoolPda(mint) 一致)
|
||||
let canonical_address = get_canonical_pool_pda(mint);
|
||||
diag.push(format!("canonical={}", canonical_address));
|
||||
match fetch_pool(rpc, &canonical_address).await {
|
||||
Ok(pool) if pool.base_mint == *mint => return Ok((canonical_address, pool)),
|
||||
Ok(_) => diag.push("canonical 账户存在但 base_mint 不匹配".into()),
|
||||
Err(e) => diag.push(format!("canonical get_account/decode 失败: {}", e)),
|
||||
}
|
||||
Err(anyhow!("No pool found for mint {}", mint))
|
||||
|
||||
// 3. 回退:getProgramAccounts 按 base_mint / quote_mint
|
||||
match find_by_base_mint(rpc, mint).await {
|
||||
Ok((address, pool)) => return Ok((address, pool)),
|
||||
Err(e) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
|
||||
}
|
||||
match find_by_quote_mint(rpc, mint).await {
|
||||
Ok((address, pool)) => return Ok((address, pool)),
|
||||
Err(e) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"No pool found for mint {}. 诊断: {}。若使用自建 RPC 请确认已开启 getProgramAccounts 或换用公共 RPC 重试;若代币未在 PumpSwap 建池请先在 pump.fun/DEX 上确认",
|
||||
mint,
|
||||
diag.join("; ")
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
|
||||
@@ -17,9 +17,12 @@ pub struct Pool {
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Whether this pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
/// Reserved for future fields (pump-public-docs: pool structure = 244 bytes total)
|
||||
pub _reserved: [u8; 7],
|
||||
}
|
||||
|
||||
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||
/// Borsh 解码用的 Pool 长度。链上池为 244 字节(pump-public-docs Breaking Change),与 POOL_SIZE 一致。
|
||||
pub const POOL_SIZE: usize = 244;
|
||||
|
||||
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||
if data.len() < POOL_SIZE {
|
||||
|
||||
+86
-14
@@ -54,6 +54,25 @@ fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。
|
||||
///
|
||||
/// * `dex_type`:PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。
|
||||
pub async fn find_pool_by_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
dex_type: DexType,
|
||||
) -> Result<Pubkey, anyhow::Error> {
|
||||
match dex_type {
|
||||
DexType::PumpSwap => {
|
||||
crate::instruction::utils::pumpswap::find_pool(rpc, mint).await
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"find_pool_by_mint not implemented for {:?}",
|
||||
dex_type
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the token to buy
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum TradeTokenType {
|
||||
@@ -97,27 +116,47 @@ impl TradingInfrastructure {
|
||||
config.commitment.clone(),
|
||||
));
|
||||
|
||||
// Initialize rent cache and start background updater
|
||||
common::seed::update_rents(&rpc).await.unwrap();
|
||||
// Initialize rent cache (with timeout so slow RPC doesn't block forever)
|
||||
const RENT_UPDATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
match tokio::time::timeout(RENT_UPDATE_TIMEOUT, common::seed::update_rents(&rpc)).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(target: "sol_trade_sdk", "rent update failed: {}, using defaults", e);
|
||||
}
|
||||
common::seed::set_default_rents();
|
||||
}
|
||||
Err(_) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(target: "sol_trade_sdk", "rent update timed out ({}s), using defaults; check RPC", RENT_UPDATE_TIMEOUT.as_secs());
|
||||
}
|
||||
common::seed::set_default_rents();
|
||||
}
|
||||
}
|
||||
common::seed::start_rent_updater(rpc.clone());
|
||||
|
||||
// Create SWQOS clients with blacklist checking
|
||||
// Create SWQOS clients with blacklist checking(单节点超时 5s,避免某一家卡死整段初始化)
|
||||
const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
|
||||
for swqos in &config.swqos_configs {
|
||||
// Check blacklist, skip disabled providers
|
||||
if swqos.is_blacklisted() {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match SwqosConfig::get_swqos_client(
|
||||
config.rpc_url.clone(),
|
||||
config.commitment.clone(),
|
||||
swqos.clone(),
|
||||
).await {
|
||||
Ok(swqos_client) => swqos_clients.push(swqos_client),
|
||||
Err(err) => {
|
||||
match tokio::time::timeout(
|
||||
SWQOS_CLIENT_TIMEOUT,
|
||||
SwqosConfig::get_swqos_client(
|
||||
config.rpc_url.clone(),
|
||||
config.commitment.clone(),
|
||||
swqos.clone(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(swqos_client)) => swqos_clients.push(swqos_client),
|
||||
Ok(Err(err)) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(
|
||||
target: "sol_trade_sdk",
|
||||
@@ -126,6 +165,16 @@ impl TradingInfrastructure {
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(
|
||||
target: "sol_trade_sdk",
|
||||
"swqos {:?} init timed out ({}s), skipping",
|
||||
swqos.swqos_type(),
|
||||
SWQOS_CLIENT_TIMEOUT.as_secs()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +397,7 @@ impl TradingClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to ensure WSOL ATA exists for a wallet
|
||||
/// 确保钱包存在 WSOL ATA;不存在则发交易创建(会花费租金 + 手续费,初始化阶段唯一会扣钱的逻辑)
|
||||
async fn ensure_wsol_ata(payer: &Arc<Keypair>, rpc: &Arc<SolanaRpcClient>) {
|
||||
let wsol_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
@@ -496,9 +545,32 @@ impl TradingClient {
|
||||
// Initialize wallet-specific caches
|
||||
crate::common::fast_fn::fast_init(&payer.pubkey());
|
||||
|
||||
// Handle WSOL ATA creation if configured
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 初始化阶段会花费租金/手续费的唯一路径:创建 WSOL ATA(ensure_wsol_ata)
|
||||
// - 触发条件:create_wsol_ata_on_startup == true 且钱包 SOL >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS
|
||||
// - 花费:ATA 租金(约 0.00203928 SOL)+ 交易手续费;钱包不足时已跳过
|
||||
// - 其它初始化(TradingInfrastructure::new、update_rents、get_swqos_client)仅 RPC/HTTP,不发送交易
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
if trade_config.create_wsol_ata_on_startup {
|
||||
Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await;
|
||||
const MIN_SOL_FOR_WSOL_ATA_LAMPORTS: u64 = 500_000; // 约 0.0005 SOL,用于 ATA 租金 + 手续费
|
||||
const BALANCE_CHECK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
let balance = tokio::time::timeout(
|
||||
BALANCE_CHECK_TIMEOUT,
|
||||
infrastructure.rpc.get_balance(&payer.pubkey()),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(Ok(0))
|
||||
.unwrap_or(0);
|
||||
if balance >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS {
|
||||
Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await;
|
||||
} else if sdk_log::sdk_log_enabled() {
|
||||
info!(
|
||||
target: "sol_trade_sdk",
|
||||
"⏭️ 跳过创建 WSOL ATA:钱包 SOL 不足(当前 {} lamports,需要至少 {})",
|
||||
balance,
|
||||
MIN_SOL_FOR_WSOL_ATA_LAMPORTS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let instance = Self {
|
||||
|
||||
@@ -2,7 +2,11 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction
|
||||
use solana_system_interface::instruction::transfer;
|
||||
|
||||
use crate::common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast, spl_token::close_account,
|
||||
fast_fn::{
|
||||
get_associated_token_address_with_program_id_fast,
|
||||
get_associated_token_address_with_program_id_fast_use_seed,
|
||||
},
|
||||
spl_token::close_account,
|
||||
SolanaRpcClient,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -36,10 +40,30 @@ pub async fn get_token_balance(
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
get_token_balance_with_options(
|
||||
rpc,
|
||||
payer,
|
||||
mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 使用与交易指令一致的 ATA 推导(可选 seed)查询余额;卖出/余额查询应与买入使用同一 ATA 地址。
|
||||
#[inline]
|
||||
pub async fn get_token_balance_with_options(
|
||||
rpc: &SolanaRpcClient,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
use_seed: bool,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let ata = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
payer,
|
||||
mint,
|
||||
token_program,
|
||||
use_seed,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&ata).await?;
|
||||
let balance_u64 =
|
||||
|
||||
@@ -31,6 +31,23 @@ impl TradingClient {
|
||||
trading::common::utils::get_token_balance(&self.infrastructure.rpc, &self.payer.pubkey(), mint).await
|
||||
}
|
||||
|
||||
/// 使用与交易一致的 ATA 推导(含 seed 优化)查询 payer 某 mint 的余额;卖出前查余额应使用此接口并传入池的 base_token_program,否则若使用 seed ATA 会查错账户。
|
||||
#[inline]
|
||||
pub async fn get_payer_token_balance_with_program(
|
||||
&self,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
trading::common::utils::get_token_balance_with_options(
|
||||
&self.infrastructure.rpc,
|
||||
&self.payer.pubkey(),
|
||||
mint,
|
||||
token_program,
|
||||
self.use_seed_optimize,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_payer_pubkey(&self) -> Pubkey {
|
||||
self.payer.pubkey()
|
||||
|
||||
Reference in New Issue
Block a user