feat(pumpswap): support virtual quote reserves
This commit is contained in:
@@ -54,7 +54,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let base_mint = protocol_params.base_mint;
|
||||
let quote_mint = protocol_params.quote_mint;
|
||||
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?;
|
||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
|
||||
let create_input_ata = params.create_input_mint_ata;
|
||||
@@ -283,7 +283,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let base_mint = protocol_params.base_mint;
|
||||
let quote_mint = protocol_params.quote_mint;
|
||||
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?;
|
||||
let pool_base_token_account = protocol_params.pool_base_token_account;
|
||||
let pool_quote_token_account = protocol_params.pool_quote_token_account;
|
||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||
@@ -535,6 +535,7 @@ mod tests {
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
@@ -627,6 +628,7 @@ mod tests {
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
@@ -653,8 +655,9 @@ mod tests {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
params.input_amount = Some(1_000_000);
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
params.protocol_params =
|
||||
DexParamEnum::PumpSwap(pumpswap_params().with_fee_basis_points(20, 5, 75));
|
||||
let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 75);
|
||||
protocol_params.virtual_quote_reserves = 500_000_000;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
@@ -667,10 +670,33 @@ mod tests {
|
||||
1_000_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
2_500_000_000,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(base_amount_out, expected.base);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_prices_with_effective_quote_reserves() {
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 0);
|
||||
protocol_params.virtual_quote_reserves = 500_000_000;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let min_quote_amount_out = u64::from_le_bytes(ix.data[16..24].try_into().unwrap());
|
||||
|
||||
let expected = crate::utils::calc::pumpswap::sell_base_input_internal_with_fees(
|
||||
100_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_500_000_000,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(min_quote_amount_out, expected.min_quote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::sync::{
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::warn;
|
||||
|
||||
// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22)
|
||||
// Pool account sizes are handled by find_by_base_mint/find_by_quote_mint.
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
@@ -740,12 +740,14 @@ pub async fn fetch_pool(
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022)
|
||||
const POOL_DATA_LEN_SPL: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_T22: u64 = 643;
|
||||
/// Known allocated Pool account sizes. The July 2026 layout carrying
|
||||
/// `virtual_quote_reserves` is allocated to 300 bytes on-chain.
|
||||
const POOL_DATA_LEN_LEGACY: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_CURRENT: u64 = 300;
|
||||
const POOL_DATA_LEN_EXTENDED: u64 = 643;
|
||||
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel.
|
||||
async fn get_program_accounts_both_sizes(
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel.
|
||||
async fn get_program_accounts_known_sizes(
|
||||
rpc: &SolanaRpcClient,
|
||||
memcmp_offset: usize,
|
||||
mint: &Pubkey,
|
||||
@@ -768,12 +770,14 @@ async fn get_program_accounts_both_sizes(
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let (spl_result, t22_result) = tokio::join!(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)),
|
||||
let (legacy_result, current_result, extended_result) = tokio::join!(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_LEGACY)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_CURRENT)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)),
|
||||
);
|
||||
let mut all = spl_result.unwrap_or_default();
|
||||
all.extend(t22_result.unwrap_or_default());
|
||||
let mut all = legacy_result.unwrap_or_default();
|
||||
all.extend(current_result.unwrap_or_default());
|
||||
all.extend(extended_result.unwrap_or_default());
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
@@ -797,7 +801,7 @@ pub async fn find_by_base_mint(
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?;
|
||||
let accounts = get_program_accounts_known_sizes(rpc, 43, base_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||
}
|
||||
@@ -814,7 +818,7 @@ pub async fn find_by_quote_mint(
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?;
|
||||
let accounts = get_program_accounts_known_sizes(rpc, 75, quote_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", quote_mint));
|
||||
}
|
||||
|
||||
@@ -17,16 +17,121 @@ 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],
|
||||
/// Virtual quote reserves appended to the Pool account.
|
||||
///
|
||||
/// Quotes must use `quote_vault_balance + virtual_quote_reserves`.
|
||||
pub virtual_quote_reserves: i128,
|
||||
}
|
||||
|
||||
/// Borsh 解码用的 Pool 长度。链上池为 244 字节(pump-public-docs Breaking Change),与 POOL_SIZE 一致。
|
||||
pub const POOL_SIZE: usize = 244;
|
||||
/// Minimum Borsh payload length for the current Pool layout, excluding the
|
||||
/// 8-byte Anchor account discriminator.
|
||||
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 + 16;
|
||||
const LEGACY_POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||
|
||||
#[derive(BorshDeserialize)]
|
||||
struct LegacyPool {
|
||||
pool_bump: u8,
|
||||
index: u16,
|
||||
creator: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
lp_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
lp_supply: u64,
|
||||
coin_creator: Pubkey,
|
||||
is_mayhem_mode: bool,
|
||||
is_cashback_coin: bool,
|
||||
}
|
||||
|
||||
impl From<LegacyPool> for Pool {
|
||||
fn from(pool: LegacyPool) -> Self {
|
||||
Self {
|
||||
pool_bump: pool.pool_bump,
|
||||
index: pool.index,
|
||||
creator: pool.creator,
|
||||
base_mint: pool.base_mint,
|
||||
quote_mint: pool.quote_mint,
|
||||
lp_mint: pool.lp_mint,
|
||||
pool_base_token_account: pool.pool_base_token_account,
|
||||
pool_quote_token_account: pool.pool_quote_token_account,
|
||||
lp_supply: pool.lp_supply,
|
||||
coin_creator: pool.coin_creator,
|
||||
is_mayhem_mode: pool.is_mayhem_mode,
|
||||
is_cashback_coin: pool.is_cashback_coin,
|
||||
virtual_quote_reserves: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||
if data.len() < POOL_SIZE {
|
||||
return None;
|
||||
if data.len() >= POOL_SIZE {
|
||||
return borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok();
|
||||
}
|
||||
|
||||
if data.len() >= LEGACY_POOL_SIZE {
|
||||
return borsh::from_slice::<LegacyPool>(&data[..LEGACY_POOL_SIZE]).ok().map(Into::into);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Compute the quote reserves used by PumpSwap pricing.
|
||||
///
|
||||
/// Returns `None` when the signed sum is negative or cannot fit in a `u64`.
|
||||
#[inline]
|
||||
pub fn effective_quote_reserves(
|
||||
quote_vault_balance: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
) -> Option<u64> {
|
||||
i128::from(quote_vault_balance)
|
||||
.checked_add(virtual_quote_reserves)
|
||||
.and_then(|reserves| u64::try_from(reserves).ok())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pool_payload(virtual_quote_reserves: i128) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(POOL_SIZE);
|
||||
data.push(7);
|
||||
data.extend_from_slice(&42u16.to_le_bytes());
|
||||
for seed in 1..=6 {
|
||||
data.extend_from_slice(Pubkey::new_from_array([seed; 32]).as_ref());
|
||||
}
|
||||
data.extend_from_slice(&123_456u64.to_le_bytes());
|
||||
data.extend_from_slice(Pubkey::new_from_array([7; 32]).as_ref());
|
||||
data.push(1);
|
||||
data.push(0);
|
||||
data.extend_from_slice(&virtual_quote_reserves.to_le_bytes());
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_current_pool_virtual_quote_reserves() {
|
||||
let pool = pool_decode(&pool_payload(987_654_321)).unwrap();
|
||||
|
||||
assert_eq!(pool.virtual_quote_reserves, 987_654_321);
|
||||
assert!(pool.is_mayhem_mode);
|
||||
assert!(!pool.is_cashback_coin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_legacy_pool_with_zero_virtual_quote_reserves() {
|
||||
let mut data = pool_payload(0);
|
||||
data.truncate(LEGACY_POOL_SIZE);
|
||||
data.extend_from_slice(&[0; 7]);
|
||||
|
||||
let pool = pool_decode(&data).unwrap();
|
||||
assert_eq!(pool.virtual_quote_reserves, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
|
||||
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
|
||||
assert_eq!(effective_quote_reserves(1_000, -250), Some(750));
|
||||
assert_eq!(effective_quote_reserves(100, -101), None);
|
||||
assert_eq!(effective_quote_reserves(u64::MAX, 1), None);
|
||||
}
|
||||
borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok()
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ pub struct PumpSwapParams {
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
/// Raw quote-vault token balance. Pricing uses this plus [`Self::virtual_quote_reserves`].
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Signed virtual quote reserves from the PumpSwap Pool account or trade event.
|
||||
pub virtual_quote_reserves: i128,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
@@ -75,6 +77,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
@@ -99,6 +102,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
@@ -127,6 +131,21 @@ impl PumpSwapParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Quote reserves used by PumpSwap pricing and fee-tier selection.
|
||||
pub fn effective_quote_reserves(&self) -> Result<u64, anyhow::Error> {
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
self.pool_quote_token_reserves,
|
||||
self.virtual_quote_reserves,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Invalid PumpSwap effective quote reserves: vault={} virtual={}",
|
||||
self.pool_quote_token_reserves,
|
||||
self.virtual_quote_reserves
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_fee_basis_points(
|
||||
mut self,
|
||||
lp_fee_basis_points: u64,
|
||||
@@ -161,6 +180,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
@@ -178,6 +198,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
@@ -202,6 +223,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
@@ -223,6 +245,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
@@ -277,6 +300,18 @@ impl PumpSwapParams {
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||
let effective_quote_token_reserves =
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
pool_quote_token_reserves,
|
||||
pool_data.virtual_quote_reserves,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Invalid PumpSwap effective quote reserves: vault={} virtual={}",
|
||||
pool_quote_token_reserves,
|
||||
pool_data.virtual_quote_reserves
|
||||
)
|
||||
})?;
|
||||
let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok();
|
||||
let fee_config = crate::instruction::utils::pumpswap::fetch_fee_config(rpc).await;
|
||||
let raw_fee_basis_points = crate::instruction::utils::pumpswap::compute_fee_basis_points(
|
||||
@@ -285,7 +320,7 @@ impl PumpSwapParams {
|
||||
pool_data.base_mint,
|
||||
base_mint_supply,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
effective_quote_token_reserves,
|
||||
);
|
||||
let creator_fee_basis_points = if pool_data.coin_creator == Pubkey::default() {
|
||||
0
|
||||
@@ -319,6 +354,7 @@ impl PumpSwapParams {
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves: pool_data.virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
|
||||
Reference in New Issue
Block a user