Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ed2aef931 | |||
| 0cf0064e80 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.10"
|
||||
version = "4.0.11"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
|
||||
@@ -152,7 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .check_min_tip(false) // default: false - filter SWQOS below min tip
|
||||
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
|
||||
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
|
||||
// .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
|
||||
// .use_pumpfun_v2(true) // PumpFun V2 (27 accounts, quote_mint); required for USDC-paired PumpFun coins
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
@@ -376,7 +376,7 @@ PumpFun has two instruction sets for bonding-curve trading:
|
||||
|
||||
**How to enable V2:**
|
||||
|
||||
**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet):
|
||||
**Method 1 — Global runtime flag** (recommended for `TradingClient`; required for USDC-paired coins):
|
||||
|
||||
```rust
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -384,7 +384,9 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.build();
|
||||
```
|
||||
|
||||
**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
|
||||
**Method 2 — Set the quote mint on `PumpFunParams`**:
|
||||
|
||||
When using the high-level `TradingClient`, keep `.use_pumpfun_v2(true)` enabled in `TradeConfig` and set `quote_mint` on the PumpFun params to select the pair:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
@@ -399,6 +401,8 @@ let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(USDC_TOKEN_ACCOUNT);
|
||||
```
|
||||
|
||||
`with_quote_mint(...)` also marks the params as V2-capable for lower-level instruction builders, but the high-level `TradingClient` uses the client-level `use_pumpfun_v2` runtime flag when choosing V1 vs V2.
|
||||
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
|
||||
#### PumpSwap: coin_creator_vault from events (no RPC)
|
||||
|
||||
+12
-1
@@ -152,6 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||
// .use_pumpfun_v2(true) // PumpFun V2(27 个账户,quote_mint);USDC 配对 PumpFun 币必须开启
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
@@ -372,7 +373,15 @@ Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过
|
||||
|
||||
**使用方式:**
|
||||
|
||||
设置 `PumpFunParams` 的 `quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局:
|
||||
高层 `TradingClient` 需要先在 `TradeConfig` 开启 PumpFun V2;USDC 配对币必须开启:
|
||||
|
||||
```rust
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.use_pumpfun_v2(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
然后在 `PumpFunParams` 设置 `quote_mint` 来选择 SOL/USDC 配对:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
@@ -391,6 +400,8 @@ client.buy(buy_params).await?;
|
||||
client.sell(sell_params).await?;
|
||||
```
|
||||
|
||||
`with_quote_mint(...)` 会把参数标记为可使用 V2,底层 instruction builder 可据此走 V2;但高层 `TradingClient` 选择 V1/V2 时使用的是 client 级别的 `use_pumpfun_v2` 运行时开关。
|
||||
|
||||
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|
||||
|-----------|-------------|---------|------|
|
||||
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
|
||||
|
||||
+64
-25
@@ -4,7 +4,7 @@ use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
|
||||
use crate::common::{
|
||||
spl_associated_token_account::get_associated_token_address_with_program_id,
|
||||
@@ -21,6 +21,22 @@ const MAX_PDA_CACHE_SIZE: usize = 100_000;
|
||||
const MAX_ATA_CACHE_SIZE: usize = 100_000;
|
||||
const MAX_INSTRUCTION_CACHE_SIZE: usize = 100_000;
|
||||
|
||||
#[inline]
|
||||
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
|
||||
where
|
||||
K: Eq + Hash + Clone,
|
||||
{
|
||||
let len = cache.len();
|
||||
if len <= max_size {
|
||||
return;
|
||||
}
|
||||
let remove_count = (len - max_size).max(max_size / 16).min(len);
|
||||
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
|
||||
for key in keys {
|
||||
cache.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------- Instruction Cache ---------------------
|
||||
|
||||
/// Instruction cache key for uniquely identifying instruction types and parameters
|
||||
@@ -66,7 +82,10 @@ where
|
||||
};
|
||||
|
||||
// Lock-free cache lookup with entry API
|
||||
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
|
||||
let instructions =
|
||||
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone();
|
||||
prune_cache(&INSTRUCTION_CACHE, MAX_INSTRUCTION_CACHE_SIZE);
|
||||
instructions
|
||||
}
|
||||
|
||||
// --------------------- Associated Token Account ---------------------
|
||||
@@ -106,8 +125,26 @@ pub fn _create_associated_token_account_idempotent_fast(
|
||||
use_seed,
|
||||
};
|
||||
|
||||
// Only use seed if the mint address is not wSOL or SOL
|
||||
// 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全)
|
||||
let build_standard_create = || {
|
||||
let associated_token_address =
|
||||
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
|
||||
vec![Instruction {
|
||||
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
accounts: vec![
|
||||
AccountMeta::new(*payer, true), // Payer (signer, writable)
|
||||
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
|
||||
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
|
||||
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
|
||||
],
|
||||
data: vec![1],
|
||||
}]
|
||||
};
|
||||
|
||||
// Only use seed if the mint address is not wSOL or SOL.
|
||||
// Seed accounts are deterministic token accounts, not ATAs; callers must ensure they are not
|
||||
// recreating an existing seeded account. Fall back to idempotent ATA if seed assembly fails.
|
||||
let arc_instructions = if use_seed
|
||||
&& !mint.eq(&crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
&& !mint.eq(&crate::constants::SOL_TOKEN_ACCOUNT)
|
||||
@@ -117,29 +154,18 @@ pub fn _create_associated_token_account_idempotent_fast(
|
||||
// Use cache to get instruction
|
||||
get_cached_instructions(cache_key, || {
|
||||
super::seed::create_associated_token_account_use_seed(payer, owner, mint, token_program)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
"seed token account setup failed for mint {}: {}; fallback to idempotent ATA",
|
||||
mint,
|
||||
err
|
||||
);
|
||||
build_standard_create()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Use cache to get instruction
|
||||
get_cached_instructions(cache_key, || {
|
||||
// Get Associated Token Address using cache
|
||||
let associated_token_address =
|
||||
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
|
||||
// Create Associated Token Account instruction
|
||||
// Reference implementation of spl_associated_token_account::instruction::create_associated_token_account
|
||||
vec![Instruction {
|
||||
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
accounts: vec![
|
||||
AccountMeta::new(*payer, true), // Payer (signer, writable)
|
||||
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
|
||||
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
|
||||
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
|
||||
],
|
||||
data: vec![1],
|
||||
}]
|
||||
})
|
||||
get_cached_instructions(cache_key, build_standard_create)
|
||||
};
|
||||
|
||||
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
|
||||
@@ -183,6 +209,7 @@ where
|
||||
|
||||
if let Some(pda) = pda_result {
|
||||
PDA_CACHE.insert(cache_key, pda);
|
||||
prune_cache(&PDA_CACHE, MAX_PDA_CACHE_SIZE);
|
||||
}
|
||||
|
||||
pda_result
|
||||
@@ -265,7 +292,18 @@ fn _get_associated_token_address_with_program_id_fast(
|
||||
token_mint_address,
|
||||
token_program_id,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
"seed token account address failed for mint {}: {}; fallback to ATA",
|
||||
token_mint_address,
|
||||
err
|
||||
);
|
||||
get_associated_token_address_with_program_id(
|
||||
wallet_address,
|
||||
token_mint_address,
|
||||
token_program_id,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
get_associated_token_address_with_program_id(
|
||||
wallet_address,
|
||||
@@ -276,6 +314,7 @@ fn _get_associated_token_address_with_program_id_fast(
|
||||
|
||||
// Store computation result in cache (lock-free)
|
||||
ATA_CACHE.insert(cache_key, ata);
|
||||
prune_cache(&ATA_CACHE, MAX_ATA_CACHE_SIZE);
|
||||
|
||||
ata
|
||||
}
|
||||
|
||||
+31
-34
@@ -1,5 +1,4 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use anyhow::anyhow;
|
||||
use fnv::FnvHasher;
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
@@ -50,6 +49,26 @@ async fn fetch_rent_for_token_account(
|
||||
Ok(client.get_minimum_balance_for_rent_exemption(165).await?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn derive_seed_from_mint(mint: &Pubkey) -> String {
|
||||
// Keep the legacy 8-hex seed stable. Changing this derivation changes the token account
|
||||
// address and can strand balances created by earlier buys.
|
||||
let mut hasher = FnvHasher::default();
|
||||
hasher.write(mint.as_ref());
|
||||
let hash = hasher.finish();
|
||||
let v = (hash & 0xFFFF_FFFF) as u32;
|
||||
let mut seed = String::with_capacity(8);
|
||||
for i in 0..8 {
|
||||
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
|
||||
let byte = match nibble {
|
||||
0..=9 => b'0' + nibble,
|
||||
_ => b'a' + (nibble - 10),
|
||||
};
|
||||
seed.push(byte as char);
|
||||
}
|
||||
seed
|
||||
}
|
||||
|
||||
pub fn create_associated_token_account_use_seed(
|
||||
payer: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
@@ -63,33 +82,23 @@ pub fn create_associated_token_account_use_seed(
|
||||
let rent = if is_2022_token {
|
||||
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
|
||||
if v == u64::MAX {
|
||||
return Err(anyhow!("Rent not initialized"));
|
||||
DEFAULT_TOKEN_ACCOUNT_RENT
|
||||
} else {
|
||||
v
|
||||
}
|
||||
v
|
||||
} else {
|
||||
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
|
||||
if v == u64::MAX {
|
||||
return Err(anyhow!("Rent not initialized"));
|
||||
DEFAULT_TOKEN_ACCOUNT_RENT
|
||||
} else {
|
||||
v
|
||||
}
|
||||
v
|
||||
};
|
||||
|
||||
let mut buf = [0u8; 8];
|
||||
let mut hasher = FnvHasher::default();
|
||||
hasher.write(mint.as_ref());
|
||||
let hash = hasher.finish();
|
||||
let v = (hash & 0xFFFF_FFFF) as u32;
|
||||
for i in 0..8 {
|
||||
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
|
||||
buf[i] = match nibble {
|
||||
0..=9 => b'0' + nibble,
|
||||
_ => b'a' + (nibble - 10),
|
||||
};
|
||||
}
|
||||
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
|
||||
let seed = derive_seed_from_mint(mint);
|
||||
// 🔧 修复:使用传入的 token_program 生成地址(支持 Token 和 Token-2022)
|
||||
// 买入和卖出只要都使用事件中的 token_program,地址自然一致
|
||||
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
|
||||
let ata_like = Pubkey::create_with_seed(payer, &seed, token_program)?;
|
||||
|
||||
let len = 165;
|
||||
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
|
||||
@@ -98,7 +107,7 @@ pub fn create_associated_token_account_use_seed(
|
||||
payer,
|
||||
&ata_like,
|
||||
payer,
|
||||
seed,
|
||||
&seed,
|
||||
rent,
|
||||
len,
|
||||
token_program,
|
||||
@@ -118,21 +127,9 @@ pub fn get_associated_token_address_with_program_id_use_seed(
|
||||
token_mint_address: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
) -> Result<Pubkey, anyhow::Error> {
|
||||
let mut buf = [0u8; 8];
|
||||
let mut hasher = FnvHasher::default();
|
||||
hasher.write(token_mint_address.as_ref());
|
||||
let hash = hasher.finish();
|
||||
let v = (hash & 0xFFFF_FFFF) as u32;
|
||||
for i in 0..8 {
|
||||
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
|
||||
buf[i] = match nibble {
|
||||
0..=9 => b'0' + nibble,
|
||||
_ => b'a' + (nibble - 10),
|
||||
};
|
||||
}
|
||||
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
|
||||
let seed = derive_seed_from_mint(token_mint_address);
|
||||
// 🔧 修复:使用传入的 token_program_id 生成地址(支持 Token 和 Token-2022)
|
||||
// 买入和卖出只要都使用事件中的 token_program_id,地址自然一致
|
||||
let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program_id)?;
|
||||
let ata_like = Pubkey::create_with_seed(wallet_address, &seed, token_program_id)?;
|
||||
Ok(ata_like)
|
||||
}
|
||||
|
||||
+18
-23
@@ -30,24 +30,20 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
fn is_pump_suffix_mint(mint: &Pubkey) -> bool {
|
||||
mint.to_string().ends_with("pump")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn effective_pump_mint_token_program(mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
|
||||
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
|
||||
let tp = protocol_params.token_program;
|
||||
if tp == Pubkey::default() || tp == TOKEN_PROGRAM_2022 {
|
||||
return TOKEN_PROGRAM_2022;
|
||||
if tp == Pubkey::default() {
|
||||
TOKEN_PROGRAM_2022
|
||||
} else {
|
||||
tp
|
||||
}
|
||||
if is_pump_suffix_mint(mint) {
|
||||
return TOKEN_PROGRAM_2022;
|
||||
}
|
||||
tp
|
||||
}
|
||||
|
||||
/// Resolve quote mint and its token program from PumpFunParams.
|
||||
@@ -123,7 +119,7 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = effective_pump_mint_token_program(¶ms.output_mint, protocol_params);
|
||||
let token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -277,7 +273,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = effective_pump_mint_token_program(¶ms.input_mint, protocol_params);
|
||||
let token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -391,8 +387,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let base_token_program =
|
||||
effective_pump_mint_token_program(¶ms.output_mint, protocol_params);
|
||||
let base_token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -612,7 +607,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?;
|
||||
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let base_token_program = effective_pump_mint_token_program(¶ms.input_mint, protocol_params);
|
||||
let base_token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -773,7 +768,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::{bonding_curve::BondingCurveAccount, GasFeeStrategy},
|
||||
constants::{TOKEN_PROGRAM, TOKEN_PROGRAM_2022},
|
||||
constants::TOKEN_PROGRAM,
|
||||
trading::core::params::{DexParamEnum, PumpFunParams, SwapParams},
|
||||
};
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -857,14 +852,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_suffix_buy_forces_token_2022_even_when_params_legacy() {
|
||||
fn pump_suffix_buy_respects_explicit_legacy_token_program() {
|
||||
crate::common::seed::set_default_rents();
|
||||
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
let instructions = build_buy_v1(¶ms).unwrap();
|
||||
|
||||
assert_eq!(instructions.len(), 3);
|
||||
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
|
||||
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM_2022);
|
||||
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
|
||||
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM);
|
||||
assert_eq!(instructions[2].accounts.len(), 18);
|
||||
assert_eq!(instructions[2].data.len(), 25);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use once_cell::sync::Lazy;
|
||||
use smallvec::SmallVec;
|
||||
use solana_compute_budget_interface::ComputeBudgetInstruction;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use std::sync::Arc;
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
|
||||
const MAX_COMPUTE_BUDGET_CACHE_SIZE: usize = 4_096;
|
||||
|
||||
/// Cache key containing all parameters for compute budget instructions
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -17,6 +19,22 @@ struct ComputeBudgetCacheKey {
|
||||
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, Arc<SmallVec<[Instruction; 2]>>>> =
|
||||
Lazy::new(|| DashMap::new());
|
||||
|
||||
#[inline]
|
||||
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
|
||||
where
|
||||
K: Eq + Hash + Clone,
|
||||
{
|
||||
let len = cache.len();
|
||||
if len <= max_size {
|
||||
return;
|
||||
}
|
||||
let remove_count = (len - max_size).max(max_size / 16).min(len);
|
||||
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
|
||||
for key in keys {
|
||||
cache.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend `instructions` with compute budget instructions; on cache hit extends from cached Arc (no SmallVec clone).
|
||||
#[inline(always)]
|
||||
pub fn extend_compute_budget_instructions(
|
||||
@@ -41,6 +59,7 @@ pub fn extend_compute_budget_instructions(
|
||||
let arc = Arc::new(insts);
|
||||
instructions.extend(arc.iter().cloned());
|
||||
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
|
||||
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
|
||||
}
|
||||
|
||||
/// Returns compute budget instructions (allocates on cache hit; prefer `extend_compute_budget_instructions` on hot path).
|
||||
@@ -59,5 +78,6 @@ pub fn compute_budget_instructions(unit_price: u64, unit_limit: u32) -> SmallVec
|
||||
}
|
||||
let arc = Arc::new(insts.clone());
|
||||
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
|
||||
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
|
||||
insts
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::anyhow;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
@@ -93,19 +94,19 @@ fn build_versioned_transaction(
|
||||
// 使用预分配的交易构建器以降低延迟
|
||||
let mut builder = acquire_builder();
|
||||
|
||||
let versioned_msg = builder.build_zero_alloc(
|
||||
let build_result = builder.build_zero_alloc(
|
||||
&payer.pubkey(),
|
||||
&full_instructions,
|
||||
address_lookup_table_account,
|
||||
blockhash,
|
||||
);
|
||||
release_builder(builder);
|
||||
let versioned_msg = build_result?;
|
||||
|
||||
let msg_bytes = versioned_msg.serialize();
|
||||
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
|
||||
let signature =
|
||||
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
|
||||
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
|
||||
|
||||
// 归还构建器到池
|
||||
release_builder(builder);
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
@@ -471,8 +471,31 @@ impl ResultCollector {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast submit mode for callers that do not wait for on-chain confirmation.
|
||||
/// Return as soon as one route accepts, a landed failure consumes the nonce, all routes finish,
|
||||
/// or a short submit window expires. Slow HTTP routes continue in worker tasks but no longer
|
||||
/// block post-buy monitoring / sell scheduling.
|
||||
async fn wait_for_first_submitted(
|
||||
&self,
|
||||
timeout: Duration,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
|
||||
let start = Instant::now();
|
||||
let poll_interval = Duration::from_millis(1);
|
||||
loop {
|
||||
if self.success_flag.load(Ordering::Acquire)
|
||||
|| self.landed_failed_flag.load(Ordering::Acquire)
|
||||
|| self.completed_count.load(Ordering::Acquire) >= self.total_tasks
|
||||
|| start.elapsed() >= timeout
|
||||
{
|
||||
return self.get_first();
|
||||
}
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
|
||||
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
|
||||
#[allow(dead_code)]
|
||||
async fn wait_for_all_submitted(
|
||||
&self,
|
||||
timeout_secs: u64,
|
||||
@@ -627,9 +650,6 @@ pub async fn execute_parallel(
|
||||
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
|
||||
let channel_count = selected_task_configs.len().max(1);
|
||||
let collector = Arc::new(ResultCollector::new(channel_count));
|
||||
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
|
||||
let submit_timeout_secs: u64 =
|
||||
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
|
||||
let shared = Arc::new(SwqosSharedContext {
|
||||
payer,
|
||||
instructions,
|
||||
@@ -714,12 +734,10 @@ pub async fn execute_parallel(
|
||||
// All jobs enqueued (no spawn on hot path)
|
||||
|
||||
if !wait_transaction_confirmed {
|
||||
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
|
||||
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
|
||||
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
|
||||
let ret = collector.wait_for_first_submitted(Duration::from_millis(500)).await.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
|
||||
Some(anyhow!("No SWQOS result within fast submit window")),
|
||||
vec![],
|
||||
));
|
||||
let (success, signatures, last_error, submit_timings) = ret;
|
||||
|
||||
@@ -32,12 +32,13 @@ pub struct PumpFunParams {
|
||||
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
|
||||
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
/// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache).
|
||||
/// **`Pubkey::default()`**:ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值。
|
||||
/// **`Pubkey::default()`**:ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致)。
|
||||
/// 显式传入 Legacy 或 Token-2022 id 时严格按该值组装,不再用 mint 字符串后缀猜测。
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
|
||||
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
|
||||
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值。
|
||||
pub fee_recipient: Pubkey,
|
||||
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
|
||||
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
|
||||
|
||||
@@ -17,6 +17,7 @@ const PARALLEL_SENDER_COUNT: usize = 18;
|
||||
/// 启动时预填充数量,必须 >= PARALLEL_SENDER_COUNT,否则 18 路并发 build 会触发分配或争抢
|
||||
const TX_BUILDER_POOL_PREFILL: usize = 64;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
@@ -82,7 +83,7 @@ impl PreallocatedTxBuilder {
|
||||
instructions: &[Instruction],
|
||||
address_lookup_table_account: Option<&AddressLookupTableAccount>,
|
||||
recent_blockhash: Hash,
|
||||
) -> VersionedMessage {
|
||||
) -> Result<VersionedMessage> {
|
||||
self.reset();
|
||||
self.instructions.extend_from_slice(instructions);
|
||||
|
||||
@@ -92,14 +93,13 @@ impl PreallocatedTxBuilder {
|
||||
&self.instructions,
|
||||
std::slice::from_ref(alt),
|
||||
recent_blockhash,
|
||||
)
|
||||
.expect("v0 message compile failed");
|
||||
VersionedMessage::V0(message)
|
||||
)?;
|
||||
Ok(VersionedMessage::V0(message))
|
||||
} else {
|
||||
// ✅ 没有查找表,使用 Legacy 消息(兼容所有 RPC)
|
||||
let message =
|
||||
Message::new_with_blockhash(&self.instructions, Some(payer), &recent_blockhash);
|
||||
VersionedMessage::Legacy(message)
|
||||
Ok(VersionedMessage::Legacy(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-29
@@ -1,22 +1,10 @@
|
||||
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
|
||||
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
fn sol_to_lamports(sol: f64) -> u64 {
|
||||
(sol * 1_000_000_000.0) as u64
|
||||
}
|
||||
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
|
||||
utils::calc::common::compute_fee,
|
||||
};
|
||||
|
||||
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
|
||||
#[inline]
|
||||
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
|
||||
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
|
||||
}
|
||||
|
||||
/// Calculates the amount of tokens that can be purchased with a given SOL amount
|
||||
/// using the bonding curve formula.
|
||||
///
|
||||
@@ -54,26 +42,21 @@ pub fn get_buy_token_amount_from_sol_amount(
|
||||
|
||||
let input_amount = amount_128
|
||||
.checked_mul(10_000)
|
||||
.unwrap()
|
||||
.checked_div(total_fee_basis_points_128 + 10_000)
|
||||
.unwrap();
|
||||
.and_then(|v| v.checked_div(total_fee_basis_points_128 + 10_000))
|
||||
.unwrap_or(0);
|
||||
|
||||
let denominator = virtual_sol_reserves + input_amount;
|
||||
|
||||
let mut tokens_received =
|
||||
input_amount.checked_mul(virtual_token_reserves).unwrap().checked_div(denominator).unwrap();
|
||||
|
||||
tokens_received = tokens_received.min(real_token_reserves);
|
||||
|
||||
if tokens_received <= 100 * 1_000_000_u128 {
|
||||
tokens_received = if amount > sol_str_to_lamports("0.01").unwrap_or(0) {
|
||||
25547619 * 1_000_000_u128
|
||||
} else {
|
||||
255476 * 1_000_000_u128
|
||||
};
|
||||
let Some(denominator) = virtual_sol_reserves.checked_add(input_amount) else { return 0 };
|
||||
if denominator == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
tokens_received as u64
|
||||
let tokens_received = input_amount
|
||||
.checked_mul(virtual_token_reserves)
|
||||
.and_then(|v| v.checked_div(denominator))
|
||||
.unwrap_or(0)
|
||||
.min(real_token_reserves);
|
||||
|
||||
tokens_received.min(u64::MAX as u128) as u64
|
||||
}
|
||||
|
||||
/// Calculates the amount of SOL that will be received when selling a given token amount
|
||||
|
||||
Reference in New Issue
Block a user