diff --git a/Cargo.toml b/Cargo.toml index 26231a4..a536123 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "0.3.4" +version = "0.3.5" edition = "2021" authors = ["William ", "sgxiang ", "wei <1415121722@qq.com>"] repository = "https://github.com/0xfnzero/sol-trade-sdk" diff --git a/README.md b/README.md index d01fe14..0ae1207 100755 --- a/README.md +++ b/README.md @@ -31,14 +31,14 @@ Add the dependency to your `Cargo.toml`: ```toml # Add to your Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.3.4" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.3.5" } ``` ### Use crates.io ```toml # Add to your Cargo.toml -sol-trade-sdk = "0.3.4" +sol-trade-sdk = "0.3.5" ``` ## Usage Examples @@ -737,6 +737,10 @@ let trade_config = TradeConfig { - **Protocol Abstraction**: Supports trading operations across multiple protocols - **Concurrent Execution**: Supports sending transactions to multiple MEV services simultaneously +## Price Calculation Utilities + +The SDK includes price calculation utilities for all supported protocols in `src/utils/price/`. + ## Project Structure ``` @@ -753,6 +757,15 @@ src/ │ ├── pumpswap/ # PumpSwap trading implementation │ ├── raydium_cpmm/ # Raydium CPMM trading implementation │ └── factory.rs # Trading factory +├── utils/ # Utility functions +│ └── price/ # Price calculation utilities +│ ├── common.rs # Common price functions +│ ├── bonk.rs # Bonk price calculations +│ ├── pumpfun.rs # PumpFun price calculations +│ ├── pumpswap.rs # PumpSwap price calculations +│ ├── raydium_cpmm.rs # Raydium CPMM price calculations +│ ├── raydium_clmm.rs # Raydium CLMM price calculations +│ └── raydium_amm_v4.rs # Raydium AMM V4 price calculations ├── lib.rs # Main library file └── main.rs # Example program ``` diff --git a/README_CN.md b/README_CN.md index 65ea322..84aa4b3 100755 --- a/README_CN.md +++ b/README_CN.md @@ -31,14 +31,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.3.4" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "0.3.5" } ``` ### 使用 crates.io ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = "0.3.4" +sol-trade-sdk = "0.3.5" ``` ## 使用示例 @@ -735,6 +735,10 @@ let trade_config = TradeConfig { - **协议抽象**: 支持多个协议的交易操作 - **并发执行**: 支持同时向多个 MEV 服务发送交易 +## 价格计算工具 + +SDK 包含所有支持协议的价格计算工具,位于 `src/utils/price/` 目录。 + ## 项目结构 ``` @@ -751,6 +755,15 @@ src/ │ ├── pumpswap/ # PumpSwap交易实现 │ ├── raydium_cpmm/ # Raydium CPMM交易实现 │ └── factory.rs # 交易工厂 +├── utils/ # 工具函数 +│ └── price/ # 价格计算工具 +│ ├── common.rs # 通用价格函数 +│ ├── bonk.rs # Bonk 价格计算 +│ ├── pumpfun.rs # PumpFun 价格计算 +│ ├── pumpswap.rs # PumpSwap 价格计算 +│ ├── raydium_cpmm.rs # Raydium CPMM 价格计算 +│ ├── raydium_clmm.rs # Raydium CLMM 价格计算 +│ └── raydium_amm_v4.rs # Raydium AMM V4 价格计算 ├── lib.rs # 主库文件 └── main.rs # 示例程序 ``` diff --git a/src/constants/decimals.rs b/src/constants/decimals.rs new file mode 100644 index 0000000..ea62371 --- /dev/null +++ b/src/constants/decimals.rs @@ -0,0 +1,2 @@ +pub const SOL_DECIMALS: u8 = 9; +pub const DEFAULT_TOKEN_DECIMALS: u8 = 6; diff --git a/src/constants/mod.rs b/src/constants/mod.rs index 9f9f1e0..9135b76 100755 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -4,6 +4,7 @@ pub mod pumpswap; pub mod swqos; pub mod trade; pub mod raydium_cpmm; +pub mod decimals; pub mod trade_platform { pub const PUMPFUN: &'static str = "pumpfun"; diff --git a/src/instruction/bonk.rs b/src/instruction/bonk.rs index 3a23377..6dd05ca 100755 --- a/src/instruction/bonk.rs +++ b/src/instruction/bonk.rs @@ -5,16 +5,17 @@ use spl_associated_token_account::instruction::create_associated_token_account_i use spl_token::instruction::close_account; use crate::{ - constants::bonk::{accounts, BUY_EXECT_IN_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR}, - constants::trade::trade::DEFAULT_SLIPPAGE, - trading::bonk::{ - common::{get_amount_out, get_pool_pda, get_vault_pda}, - pool::Pool, + constants::{ + bonk::{accounts, BUY_EXECT_IN_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR}, + trade::trade::DEFAULT_SLIPPAGE, }, - trading::common::utils::get_token_balance, - trading::core::{ - params::{BonkParams, BuyParams, SellParams}, - traits::InstructionBuilder, + trading::{ + bonk::common::{fetch_pool_state, get_amount_out, get_pool_pda, get_vault_pda}, + common::utils::get_token_balance, + core::{ + params::{BonkParams, BuyParams, SellParams}, + traits::InstructionBuilder, + }, }, }; @@ -70,7 +71,7 @@ impl BonkInstructionBuilder { let mut real_quote = protocol_params.real_quote.unwrap_or(0); if virtual_base == 0 || virtual_quote == 0 || real_base == 0 || real_quote == 0 { - let pool = Pool::fetch(params.rpc.as_ref().unwrap(), &pool_state).await?; + let pool = fetch_pool_state(params.rpc.as_ref().unwrap(), &pool_state).await?; virtual_base = pool.virtual_base as u128; virtual_quote = pool.virtual_quote as u128; real_base = pool.real_base as u128; diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index 0f0fdb6..e2a0f3a 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -20,9 +20,7 @@ use crate::{ pumpswap::{ self, common::{ - coin_creator_vault_ata, coin_creator_vault_authority, fee_recipient_ata, find_pool, - get_global_volume_accumulator_pda, get_token_amount, - get_user_volume_accumulator_pda, get_wsol_amount, + coin_creator_vault_ata, coin_creator_vault_authority, fee_recipient_ata, fetch_pool, find_pool, get_global_volume_accumulator_pda, get_token_amount, get_user_volume_accumulator_pda, get_wsol_amount }, }, }, @@ -138,7 +136,7 @@ impl PumpSwapInstructionBuilder { let rpc = params.rpc.as_ref().unwrap().clone(); // Find pool let pool = find_pool(rpc.as_ref(), ¶ms.mint).await?; - let pool_data = pumpswap::pool::Pool::fetch(rpc.as_ref(), &pool).await?; + let pool_data = fetch_pool(rpc.as_ref(), &pool).await?; let pool_base_token_reserves = get_token_balance(rpc.as_ref(), &pool, &pool_data.base_mint).await?; let pool_quote_token_reserves = @@ -169,7 +167,7 @@ impl PumpSwapInstructionBuilder { let rpc = params.rpc.as_ref().unwrap().clone(); // Find pool let pool = find_pool(rpc.as_ref(), ¶ms.mint).await?; - let pool_data = pumpswap::pool::Pool::fetch(rpc.as_ref(), &pool).await?; + let pool_data = fetch_pool(rpc.as_ref(), &pool).await?; let pool_base_token_reserves = get_token_balance(rpc.as_ref(), &pool, &pool_data.base_mint).await?; let pool_quote_token_reserves = diff --git a/src/trading/bonk/common.rs b/src/trading/bonk/common.rs index f89ffef..c2f0dc9 100755 --- a/src/trading/bonk/common.rs +++ b/src/trading/bonk/common.rs @@ -1,5 +1,25 @@ -use crate::constants; +use crate::{ + common::SolanaRpcClient, + constants::{self, bonk::accounts}, +}; +use anyhow::anyhow; use solana_sdk::pubkey::Pubkey; +use solana_streamer_sdk::streaming::event_parser::protocols::bonk::{ + pool_state_decode, types::PoolState, +}; + +pub async fn fetch_pool_state( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, +) -> Result { + let account = rpc.get_account(pool_address).await?; + if account.owner != accounts::BONK { + return Err(anyhow!("Account is not owned by Bonk program")); + } + let pool_state = pool_state_decode(&account.data[8..]) + .ok_or_else(|| anyhow!("Failed to decode pool state"))?; + Ok(pool_state) +} pub fn get_amount_in_net( amount_in: u64, @@ -41,9 +61,7 @@ pub fn get_amount_in( // 根据 AMM 公式反推: amount_in_net = (amount_out * input_reserve) / (output_reserve - amount_out) let numerator = amount_out_with_slippage.checked_mul(input_reserve).unwrap(); - let denominator = output_reserve - .checked_sub(amount_out_with_slippage) - .unwrap(); + let denominator = output_reserve.checked_sub(amount_out_with_slippage).unwrap(); let amount_in_net = numerator.checked_div(denominator).unwrap(); // 计算总费用率 @@ -87,53 +105,21 @@ pub fn get_amount_out( } pub fn get_pool_pda(base_mint: &Pubkey, quote_mint: &Pubkey) -> Option { - let seeds: &[&[u8]; 3] = &[ - constants::bonk::seeds::POOL_SEED, - base_mint.as_ref(), - quote_mint.as_ref(), - ]; + let seeds: &[&[u8]; 3] = + &[constants::bonk::seeds::POOL_SEED, base_mint.as_ref(), quote_mint.as_ref()]; let program_id: &Pubkey = &constants::bonk::accounts::BONK; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) } pub fn get_vault_pda(pool_state: &Pubkey, mint: &Pubkey) -> Option { - let seeds: &[&[u8]; 3] = &[ - constants::bonk::seeds::POOL_VAULT_SEED, - pool_state.as_ref(), - mint.as_ref(), - ]; + let seeds: &[&[u8]; 3] = + &[constants::bonk::seeds::POOL_VAULT_SEED, pool_state.as_ref(), mint.as_ref()]; let program_id: &Pubkey = &constants::bonk::accounts::BONK; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) } -pub fn get_token_price( - virtual_base: u128, - virtual_quote: u128, - real_base: u128, - real_quote: u128, - decimal_base: u64, - decimal_quote: u64, -) -> f64 { - // 计算小数位数差异 - let decimal_diff = decimal_quote as i32 - decimal_base as i32; - let decimal_factor = if decimal_diff >= 0 { - 10_f64.powi(decimal_diff) - } else { - 1.0 / 10_f64.powi(-decimal_diff) - }; - - // 计算价格前的状态 - let quote_reserves = virtual_quote.checked_add(real_quote).unwrap(); - let base_reserves = virtual_base.checked_sub(real_base).unwrap(); - - // 使用浮点数计算价格,避免整数除法的精度丢失 - let price = (quote_reserves as f64) / (base_reserves as f64) / decimal_factor; - - price -} - #[cfg(test)] mod tests { use crate::constants::bonk::accounts::{PLATFORM_FEE_RATE, PROTOCOL_FEE_RATE, SHARE_FEE_RATE}; diff --git a/src/trading/bonk/mod.rs b/src/trading/bonk/mod.rs index 53868a7..34994bf 100755 --- a/src/trading/bonk/mod.rs +++ b/src/trading/bonk/mod.rs @@ -1,2 +1 @@ pub mod common; -pub mod pool; diff --git a/src/trading/bonk/pool.rs b/src/trading/bonk/pool.rs deleted file mode 100755 index 57ec80c..0000000 --- a/src/trading/bonk/pool.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{common::SolanaRpcClient, constants::bonk::accounts}; -use anyhow::anyhow; -use borsh::BorshDeserialize; -use solana_sdk::pubkey::Pubkey; - -#[derive(Debug, Clone, BorshDeserialize)] -pub struct VestingSchedule { - pub total_locked_amount: u64, - pub cliff_period: u64, - pub unlock_period: u64, - pub start_time: u64, - pub allocated_share_amount: u64, -} - -#[derive(Debug, Clone, BorshDeserialize)] -pub struct Pool { - pub epoch: u64, - pub auth_bump: u8, - pub status: u8, - pub base_decimals: u8, - pub quote_decimals: u8, - pub migrate_type: u8, - pub supply: u64, - pub total_base_sell: u64, - pub virtual_base: u64, - pub virtual_quote: u64, - pub real_base: u64, - pub real_quote: u64, - pub total_quote_fund_raising: u64, - pub quote_protocol_fee: u64, - pub platform_fee: u64, - pub migrate_fee: u64, - pub vesting_schedule: VestingSchedule, - pub global_config: Pubkey, - pub platform_config: Pubkey, - pub base_mint: Pubkey, - pub quote_mint: Pubkey, - pub base_vault: Pubkey, - pub quote_vault: Pubkey, - pub creator: Pubkey, - pub padding: [u64; 8], -} - -impl Pool { - pub fn from_bytes(data: &[u8]) -> Result { - let pool = Pool::try_from_slice(&data[8..])?; - Ok(pool) - } - - pub async fn fetch( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - ) -> Result { - let account = rpc.get_account(pool_address).await?; - - if account.owner != accounts::BONK { - return Err(anyhow!("Account is not owned by Bonk program")); - } - - Self::from_bytes(&account.data) - } -} diff --git a/src/trading/pumpfun/bonding_curve.rs b/src/trading/pumpfun/bonding_curve.rs deleted file mode 100644 index c58238d..0000000 --- a/src/trading/pumpfun/bonding_curve.rs +++ /dev/null @@ -1,174 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use solana_sdk::pubkey::Pubkey; - -/// Represents a bonding curve for token pricing and liquidity management -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] -pub struct PumpfunBondingCurveAccount { - /// Unique identifier for the bonding curve - pub discriminator: u64, - /// Virtual token reserves used for price calculations - pub virtual_token_reserves: u64, - /// Virtual SOL reserves used for price calculations - pub virtual_sol_reserves: u64, - /// Actual token reserves available for trading - pub real_token_reserves: u64, - /// Actual SOL reserves available for trading - pub real_sol_reserves: u64, - /// Total supply of tokens - pub token_total_supply: u64, - /// Whether the bonding curve is complete/finalized - pub complete: bool, - /// Token creator's address - pub creator: Pubkey, -} - -impl PumpfunBondingCurveAccount { - /// Creates a new bonding curve instance - /// - /// # Arguments - /// * `discriminator` - Unique identifier for the curve - /// * `virtual_token_reserves` - Virtual token reserves for price calculations - /// * `virtual_sol_reserves` - Virtual SOL reserves for price calculations - /// * `real_token_reserves` - Actual token reserves available - /// * `real_sol_reserves` - Actual SOL reserves available - /// * `token_total_supply` - Total supply of tokens - /// * `complete` - Whether the curve is complete - #[allow(clippy::too_many_arguments)] - pub fn new( - discriminator: u64, - virtual_token_reserves: u64, - virtual_sol_reserves: u64, - real_token_reserves: u64, - real_sol_reserves: u64, - token_total_supply: u64, - complete: bool, - creator: Pubkey, - ) -> Self { - Self { - discriminator, - virtual_token_reserves, - virtual_sol_reserves, - real_token_reserves, - real_sol_reserves, - token_total_supply, - complete, - creator, - } - } - - /// Calculates the amount of tokens received for a given SOL amount - /// - /// # Arguments - /// * `amount` - Amount of SOL to spend - /// - /// # Returns - /// * `Ok(u64)` - Amount of tokens that would be received - /// * `Err(&str)` - Error message if curve is complete - pub fn get_buy_price(&self, amount: u64) -> Result { - if self.complete { - return Err("Curve is complete"); - } - - if amount == 0 { - return Ok(0); - } - - // Calculate the product of virtual reserves using u128 to avoid overflow - let n: u128 = (self.virtual_sol_reserves as u128) * (self.virtual_token_reserves as u128); - - // Calculate the new virtual sol reserves after the purchase - let i: u128 = (self.virtual_sol_reserves as u128) + (amount as u128); - - // Calculate the new virtual token reserves after the purchase - let r: u128 = n / i + 1; - - // Calculate the amount of tokens to be purchased - let s: u128 = (self.virtual_token_reserves as u128) - r; - - // Convert back to u64 and return the minimum of calculated tokens and real reserves - let s_u64 = s as u64; - Ok(if s_u64 < self.real_token_reserves { s_u64 } else { self.real_token_reserves }) - } - - /// Calculates the amount of SOL received for selling tokens - /// - /// # Arguments - /// * `amount` - Amount of tokens to sell - /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) - /// - /// # Returns - /// * `Ok(u64)` - Amount of SOL that would be received after fees - /// * `Err(&str)` - Error message if curve is complete - pub fn get_sell_price(&self, amount: u64, fee_basis_points: u64) -> Result { - if self.complete { - return Err("Curve is complete"); - } - - if amount == 0 { - return Ok(0); - } - - // Calculate the proportional amount of virtual sol reserves to be received using u128 - let n: u128 = ((amount as u128) * (self.virtual_sol_reserves as u128)) - / ((self.virtual_token_reserves as u128) + (amount as u128)); - - // Calculate the fee amount in the same units - let a: u128 = (n * (fee_basis_points as u128)) / 10000; - - // Return the net amount after deducting the fee, converting back to u64 - Ok((n - a) as u64) - } - - /// Calculates the current market cap in SOL - pub fn get_market_cap_sol(&self) -> u64 { - if self.virtual_token_reserves == 0 { - return 0; - } - - ((self.token_total_supply as u128) * (self.virtual_sol_reserves as u128) - / (self.virtual_token_reserves as u128)) as u64 - } - - /// Calculates the final market cap in SOL after all tokens are sold - /// - /// # Arguments - /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) - pub fn get_final_market_cap_sol(&self, fee_basis_points: u64) -> u64 { - let total_sell_value: u128 = - self.get_buy_out_price(self.real_token_reserves, fee_basis_points) as u128; - let total_virtual_value: u128 = (self.virtual_sol_reserves as u128) + total_sell_value; - let total_virtual_tokens: u128 = - (self.virtual_token_reserves as u128) - (self.real_token_reserves as u128); - - if total_virtual_tokens == 0 { - return 0; - } - - ((self.token_total_supply as u128) * total_virtual_value / total_virtual_tokens) as u64 - } - - /// Calculates the price to buy out all remaining tokens - /// - /// # Arguments - /// * `amount` - Amount of tokens to buy - /// * `fee_basis_points` - Fee in basis points (1/100th of a percent) - pub fn get_buy_out_price(&self, amount: u64, fee_basis_points: u64) -> u64 { - // Get the effective amount of sol tokens - let sol_tokens: u128 = if amount < self.real_sol_reserves { - self.real_sol_reserves as u128 - } else { - amount as u128 - }; - - // Calculate total sell value - let total_sell_value: u128 = (sol_tokens * (self.virtual_sol_reserves as u128)) - / ((self.virtual_token_reserves as u128) - sol_tokens) - + 1; - - // Calculate fee - let fee: u128 = (total_sell_value * (fee_basis_points as u128)) / 10000; - - // Return total including fee, converting back to u64 - (total_sell_value + fee) as u64 - } -} diff --git a/src/trading/pumpfun/common.rs b/src/trading/pumpfun/common.rs index 7b584be..07ce482 100755 --- a/src/trading/pumpfun/common.rs +++ b/src/trading/pumpfun/common.rs @@ -4,7 +4,6 @@ use std::{collections::HashMap, sync::Arc}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey }; -use crate::trading::pumpfun::bonding_curve::PumpfunBondingCurveAccount; use crate::{ common::{ bonding_curve::BondingCurveAccount, global::GlobalAccount, PriorityFee, SolanaRpcClient @@ -122,11 +121,11 @@ pub async fn get_bonding_curve_account( } #[inline] -pub async fn get_bonding_curve_account_v2( +pub async fn fetch_bonding_curve_account( rpc: &SolanaRpcClient, mint: &Pubkey, -) -> Result<(Arc, Pubkey), anyhow::Error> { - let bonding_curve_pda = get_bonding_curve_pda(mint) +) -> Result<(Arc, Pubkey), anyhow::Error> { + let bonding_curve_pda: Pubkey = get_bonding_curve_pda(mint) .ok_or(anyhow!("Bonding curve not found"))?; let account = rpc.get_account(&bonding_curve_pda).await?; @@ -134,7 +133,7 @@ pub async fn get_bonding_curve_account_v2( return Err(anyhow!("Bonding curve not found")); } - let bonding_curve = solana_sdk::borsh1::try_from_slice_unchecked::(&account.data) + let bonding_curve = solana_sdk::borsh1::try_from_slice_unchecked::(&account.data[8..]) .map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?; Ok((Arc::new(bonding_curve), bonding_curve_pda)) @@ -215,13 +214,6 @@ pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Opti amount_sol + (amount_sol * slippage / 10000) } -#[inline] -pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { - let v_sol = virtual_sol_reserves as f64 / 100_000_000.0; - let v_tokens = virtual_token_reserves as f64 / 100_000.0; - v_sol / v_tokens -} - #[inline] pub fn get_buy_price(amount: u64, trade_info: &PumpFunTradeEvent) -> u64 { if amount == 0 { diff --git a/src/trading/pumpfun/mod.rs b/src/trading/pumpfun/mod.rs index bac8705..34994bf 100755 --- a/src/trading/pumpfun/mod.rs +++ b/src/trading/pumpfun/mod.rs @@ -1,2 +1 @@ pub mod common; -pub mod bonding_curve; \ No newline at end of file diff --git a/src/trading/pumpswap/common.rs b/src/trading/pumpswap/common.rs index 00980b5..d694ab8 100755 --- a/src/trading/pumpswap/common.rs +++ b/src/trading/pumpswap/common.rs @@ -1,10 +1,13 @@ use crate::common::SolanaRpcClient; -use crate::trading::pumpswap; +use crate::constants::pumpswap::accounts; +use anyhow::anyhow; +use solana_account_decoder::UiAccountEncoding; use solana_sdk::pubkey::Pubkey; +use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::types::{pool_decode, Pool}; // Find a pool for a specific mint pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result { - let (pool_address, _) = pumpswap::pool::Pool::find_by_mint(rpc, mint).await?; + let (pool_address, _) = find_by_mint(rpc, mint).await?; Ok(pool_address) } @@ -120,7 +123,6 @@ pub async fn get_wsol_amount( } } - pub(crate) fn coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey { let (pump_pool_authority, _) = Pubkey::find_program_address( &[b"creator_vault", &coin_creator.to_bytes()], @@ -151,10 +153,8 @@ pub(crate) fn fee_recipient_ata(fee_recipient: Pubkey, quote_mint: Pubkey) -> Pu } pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[ - &crate::constants::pumpswap::seeds::USER_VOLUME_ACCUMULATOR_SEED, - user.as_ref(), - ]; + let seeds: &[&[u8]; 2] = + &[&crate::constants::pumpswap::seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()]; let program_id: &Pubkey = &&crate::constants::pumpswap::accounts::AMM_PROGRAM; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) @@ -166,3 +166,147 @@ pub fn get_global_volume_accumulator_pda() -> Option { let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) } + +pub async fn fetch_pool( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, +) -> Result { + let account = rpc.get_account(pool_address).await?; + if account.owner != accounts::AMM_PROGRAM { + return Err(anyhow!("Account is not owned by PumpSwap program")); + } + let pool = pool_decode(&account.data[8..]).ok_or_else(|| anyhow!("Failed to decode pool"))?; + Ok(pool) +} + +pub async fn find_by_base_mint( + rpc: &SolanaRpcClient, + base_mint: &Pubkey, +) -> Result<(Pubkey, Pool), anyhow::Error> { + // 使用getProgramAccounts查找给定mint的池子 + let filters = vec![ + // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 + solana_rpc_client_api::filter::RpcFilterType::Memcmp( + solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &base_mint.to_bytes()), + ), + ]; + let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { + filters: Some(filters), + account_config: solana_rpc_client_api::config::RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: None, + commitment: None, + min_context_slot: None, + }, + with_context: None, + sort_results: None, + }; + let program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; + let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?; + if accounts.is_empty() { + return Err(anyhow!("No pool found for mint {}", base_mint)); + } + let mut pools: Vec<_> = accounts + .into_iter() + .filter_map(|(addr, acc)| pool_decode(&acc.data).map(|pool| (addr, pool))) + .collect(); + pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); + let (address, pool) = pools[0].clone(); + Ok((address, pool)) +} + +pub async fn find_by_quote_mint( + rpc: &SolanaRpcClient, + quote_mint: &Pubkey, +) -> Result<(Pubkey, Pool), anyhow::Error> { + // 使用getProgramAccounts查找给定mint的池子 + let filters = vec![ + // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 + solana_rpc_client_api::filter::RpcFilterType::Memcmp( + solana_client::rpc_filter::Memcmp::new_base58_encoded(75, "e_mint.to_bytes()), + ), + ]; + let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { + filters: Some(filters), + account_config: solana_rpc_client_api::config::RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: None, + commitment: None, + min_context_slot: None, + }, + with_context: None, + sort_results: None, + }; + let program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; + let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?; + if accounts.is_empty() { + return Err(anyhow!("No pool found for mint {}", quote_mint)); + } + let mut pools: Vec<_> = accounts + .into_iter() + .filter_map(|(addr, acc)| pool_decode(&acc.data).map(|pool| (addr, pool))) + .collect(); + pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); + let (address, pool) = pools[0].clone(); + Ok((address, pool)) +} + +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)); + } + if let Ok((address, pool)) = find_by_quote_mint(rpc, mint).await { + return Ok((address, pool)); + } + Err(anyhow!("No pool found for mint {}", mint)) +} + +pub async fn get_token_balances( + pool: &Pool, + rpc: &SolanaRpcClient, +) -> Result<(u64, u64), anyhow::Error> { + let base_balance = rpc.get_token_account_balance(&pool.pool_base_token_account).await?; + let quote_balance = rpc.get_token_account_balance(&pool.pool_quote_token_account).await?; + + let base_amount = base_balance.amount.parse::().map_err(|e| anyhow!(e))?; + let quote_amount = quote_balance.amount.parse::().map_err(|e| anyhow!(e))?; + + Ok((base_amount, quote_amount)) +} + +pub async fn calculate_buy_amount( + pool: &Pool, + rpc: &SolanaRpcClient, + sol_amount: u64, +) -> Result { + let (base_amount, quote_amount) = get_token_balances(pool, rpc).await?; + + // 使用常数乘积公式 (x * y = k) 计算 + let product = base_amount as u128 * quote_amount as u128; + let new_quote_amount = quote_amount as u128 + sol_amount as u128; + let new_base_amount = product / new_quote_amount; + + let token_amount = base_amount as u128 - new_base_amount; + + Ok(token_amount as u64) +} + +pub async fn calculate_sell_amount( + pool: &Pool, + rpc: &SolanaRpcClient, + token_amount: u64, +) -> Result { + let (base_amount, quote_amount) = get_token_balances(pool, rpc).await?; + + // 使用常数乘积公式 (x * y = k) 计算 + let product = base_amount as u128 * quote_amount as u128; + let new_base_amount = base_amount as u128 + token_amount as u128; + let new_quote_amount = product / new_base_amount; + + let sol_amount = quote_amount as u128 - new_quote_amount; + + Ok(sol_amount as u64) +} diff --git a/src/trading/pumpswap/mod.rs b/src/trading/pumpswap/mod.rs index 53868a7..34994bf 100755 --- a/src/trading/pumpswap/mod.rs +++ b/src/trading/pumpswap/mod.rs @@ -1,2 +1 @@ pub mod common; -pub mod pool; diff --git a/src/trading/pumpswap/pool.rs b/src/trading/pumpswap/pool.rs deleted file mode 100755 index a72c416..0000000 --- a/src/trading/pumpswap/pool.rs +++ /dev/null @@ -1,245 +0,0 @@ -use crate::{common::SolanaRpcClient, constants::pumpswap::accounts}; -use anyhow::anyhow; -use solana_account_decoder::UiAccountEncoding; -use solana_sdk::pubkey::Pubkey; - -#[derive(Debug, Clone)] -pub struct Pool { - pub pool_bump: u8, - pub index: u16, - pub creator: Pubkey, - pub base_mint: Pubkey, - pub quote_mint: Pubkey, - pub lp_mint: Pubkey, - pub pool_base_token_account: Pubkey, - pub pool_quote_token_account: Pubkey, - pub lp_supply: u64, - pub coin_creator: Pubkey, -} - -impl Pool { - pub fn from_bytes(data: &[u8]) -> Result { - if data.len() < 211 { - return Err(anyhow!("Data too short for Pool account")); - } - - // 跳过discriminator (8字节) - let data = &data[8..]; - - let pool_bump = data[0]; - let index = u16::from_le_bytes([data[1], data[2]]); - - let creator = Pubkey::new_from_array( - data[3..35] - .try_into() - .map_err(|e| anyhow!("Failed to convert creator: {:?}", e))?, - ); - let base_mint = Pubkey::new_from_array( - data[35..67] - .try_into() - .map_err(|e| anyhow!("Failed to convert base_mint: {:?}", e))?, - ); - let quote_mint = Pubkey::new_from_array( - data[67..99] - .try_into() - .map_err(|e| anyhow!("Failed to convert quote_mint: {:?}", e))?, - ); - let lp_mint = Pubkey::new_from_array( - data[99..131] - .try_into() - .map_err(|e| anyhow!("Failed to convert lp_mint: {:?}", e))?, - ); - let pool_base_token_account = Pubkey::new_from_array( - data[131..163] - .try_into() - .map_err(|e| anyhow!("Failed to convert pool_base_token_account: {:?}", e))?, - ); - let pool_quote_token_account = Pubkey::new_from_array( - data[163..195] - .try_into() - .map_err(|e| anyhow!("Failed to convert pool_quote_token_account: {:?}", e))?, - ); - - let lp_supply = u64::from_le_bytes([ - data[195], data[196], data[197], data[198], data[199], data[200], data[201], data[202], - ]); - - let mut coin_creator = Pubkey::default(); - if data.len() >= 203 + 32 { - coin_creator = Pubkey::new_from_array( - data[203..203 + 32] - .try_into() - .map_err(|e| anyhow!("Failed to convert coin_creator: {:?}", e))?, - ); - } - - Ok(Self { - pool_bump, - index, - creator, - base_mint, - quote_mint, - lp_mint, - pool_base_token_account, - pool_quote_token_account, - lp_supply, - coin_creator, - }) - } - - pub async fn fetch( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - ) -> Result { - let account = rpc.get_account(pool_address).await?; - - if account.owner != accounts::AMM_PROGRAM { - return Err(anyhow!("Account is not owned by PumpSwap program")); - } - - Self::from_bytes(&account.data) - } - - pub async fn find_by_base_mint( - rpc: &SolanaRpcClient, - base_mint: &Pubkey, - ) -> Result<(Pubkey, Self), anyhow::Error> { - // 使用getProgramAccounts查找给定mint的池子 - let filters = vec![ - // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 - solana_rpc_client_api::filter::RpcFilterType::Memcmp( - solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &base_mint.to_bytes()), - ), - ]; - let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { - filters: Some(filters), - account_config: solana_rpc_client_api::config::RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::Base64), - data_slice: None, - commitment: None, - min_context_slot: None, - }, - with_context: None, - sort_results: None, - }; - let program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; - let accounts = rpc - .get_program_accounts_with_config(&program_id, config) - .await?; - if accounts.is_empty() { - return Err(anyhow!("No pool found for mint {}", base_mint)); - } - let mut pools: Vec<_> = accounts - .into_iter() - .filter_map(|(addr, acc)| Self::from_bytes(&acc.data).map(|pool| (addr, pool)).ok()) - .collect(); - pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); - let (address, pool) = pools[0].clone(); - Ok((address, pool)) - } - - pub async fn find_by_quote_mint( - rpc: &SolanaRpcClient, - quote_mint: &Pubkey, - ) -> Result<(Pubkey, Self), anyhow::Error> { - // 使用getProgramAccounts查找给定mint的池子 - let filters = vec![ - // solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小 - solana_rpc_client_api::filter::RpcFilterType::Memcmp( - solana_client::rpc_filter::Memcmp::new_base58_encoded(75, "e_mint.to_bytes()), - ), - ]; - let config = solana_rpc_client_api::config::RpcProgramAccountsConfig { - filters: Some(filters), - account_config: solana_rpc_client_api::config::RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::Base64), - data_slice: None, - commitment: None, - min_context_slot: None, - }, - with_context: None, - sort_results: None, - }; - let program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM; - let accounts = rpc - .get_program_accounts_with_config(&program_id, config) - .await?; - if accounts.is_empty() { - return Err(anyhow!("No pool found for mint {}", quote_mint)); - } - let mut pools: Vec<_> = accounts - .into_iter() - .filter_map(|(addr, acc)| Self::from_bytes(&acc.data).map(|pool| (addr, pool)).ok()) - .collect(); - pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); - let (address, pool) = pools[0].clone(); - Ok((address, pool)) - } - - pub async fn find_by_mint( - rpc: &SolanaRpcClient, - mint: &Pubkey, - ) -> Result<(Pubkey, Self), anyhow::Error> { - if let Ok((address, pool)) = Self::find_by_base_mint(rpc, mint).await { - return Ok((address, pool)); - } - if let Ok((address, pool)) = Self::find_by_quote_mint(rpc, mint).await { - return Ok((address, pool)); - } - Err(anyhow!("No pool found for mint {}", mint)) - } - - pub async fn get_token_balances( - &self, - rpc: &SolanaRpcClient, - ) -> Result<(u64, u64), anyhow::Error> { - let base_balance = rpc - .get_token_account_balance(&self.pool_base_token_account) - .await?; - let quote_balance = rpc - .get_token_account_balance(&self.pool_quote_token_account) - .await?; - - let base_amount = base_balance.amount.parse::().map_err(|e| anyhow!(e))?; - let quote_amount = quote_balance - .amount - .parse::() - .map_err(|e| anyhow!(e))?; - - Ok((base_amount, quote_amount)) - } - - pub async fn calculate_buy_amount( - &self, - rpc: &SolanaRpcClient, - sol_amount: u64, - ) -> Result { - let (base_amount, quote_amount) = self.get_token_balances(rpc).await?; - - // 使用常数乘积公式 (x * y = k) 计算 - let product = base_amount as u128 * quote_amount as u128; - let new_quote_amount = quote_amount as u128 + sol_amount as u128; - let new_base_amount = product / new_quote_amount; - - let token_amount = base_amount as u128 - new_base_amount; - - Ok(token_amount as u64) - } - - pub async fn calculate_sell_amount( - &self, - rpc: &SolanaRpcClient, - token_amount: u64, - ) -> Result { - let (base_amount, quote_amount) = self.get_token_balances(rpc).await?; - - // 使用常数乘积公式 (x * y = k) 计算 - let product = base_amount as u128 * quote_amount as u128; - let new_base_amount = base_amount as u128 + token_amount as u128; - let new_quote_amount = product / new_base_amount; - - let sol_amount = quote_amount as u128 - new_quote_amount; - - Ok(sol_amount as u64) - } -} diff --git a/src/trading/raydium_cpmm/common.rs b/src/trading/raydium_cpmm/common.rs index 6a67058..f321e80 100755 --- a/src/trading/raydium_cpmm/common.rs +++ b/src/trading/raydium_cpmm/common.rs @@ -1,10 +1,28 @@ use crate::{ common::SolanaRpcClient, - constants::{self, raydium_cpmm::accounts::WSOL_TOKEN_ACCOUNT}, - trading::raydium_cpmm::pool::Pool, + constants::{ + self, + raydium_cpmm::accounts::{self, WSOL_TOKEN_ACCOUNT}, + }, }; use anyhow::anyhow; use solana_sdk::pubkey::Pubkey; +use solana_streamer_sdk::streaming::event_parser::protocols::raydium_cpmm::types::{ + pool_state_decode, PoolState, +}; + +pub async fn fetch_pool_state( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, +) -> Result { + let account = rpc.get_account(pool_address).await?; + if account.owner != accounts::RAYDIUM_CPMM { + return Err(anyhow!("Account is not owned by Raydium Cpmm program")); + } + let pool_state = pool_state_decode(&account.data[8..]) + .ok_or_else(|| anyhow!("Failed to decode pool state"))?; + Ok(pool_state) +} pub fn get_pool_pda(amm_config: &Pubkey, mint1: &Pubkey, mint2: &Pubkey) -> Option { let seeds: &[&[u8]; 4] = &[ @@ -19,21 +37,16 @@ pub fn get_pool_pda(amm_config: &Pubkey, mint1: &Pubkey, mint2: &Pubkey) -> Opti } pub fn get_vault_pda(pool_state: &Pubkey, mint: &Pubkey) -> Option { - let seeds: &[&[u8]; 3] = &[ - constants::raydium_cpmm::seeds::POOL_VAULT_SEED, - pool_state.as_ref(), - mint.as_ref(), - ]; + let seeds: &[&[u8]; 3] = + &[constants::raydium_cpmm::seeds::POOL_VAULT_SEED, pool_state.as_ref(), mint.as_ref()]; let program_id: &Pubkey = &constants::raydium_cpmm::accounts::RAYDIUM_CPMM; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) } pub fn get_observation_state_pda(pool_state: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[ - constants::raydium_cpmm::seeds::OBSERVATION_STATE_SEED, - pool_state.as_ref(), - ]; + let seeds: &[&[u8]; 2] = + &[constants::raydium_cpmm::seeds::OBSERVATION_STATE_SEED, pool_state.as_ref()]; let program_id: &Pubkey = &constants::raydium_cpmm::accounts::RAYDIUM_CPMM; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) @@ -44,12 +57,8 @@ pub async fn get_buy_token_amount( pool_state: &Pubkey, sol_amount: u64, ) -> Result { - let pool = Pool::fetch(rpc, pool_state).await?; - let is_token0_input = if pool.token0_mint == WSOL_TOKEN_ACCOUNT { - true - } else { - false - }; + let pool = fetch_pool_state(rpc, pool_state).await?; + let is_token0_input = if pool.token0_mint == WSOL_TOKEN_ACCOUNT { true } else { false }; let (token0_balance, token1_balance) = get_pool_token_balances(rpc, pool_state, &pool.token0_mint, &pool.token1_mint).await?; @@ -93,12 +102,8 @@ pub async fn get_sell_sol_amount( pool_state: &Pubkey, token_amount: u64, ) -> Result { - let pool = Pool::fetch(rpc, pool_state).await?; - let is_token0_sol = if pool.token0_mint == WSOL_TOKEN_ACCOUNT { - true - } else { - false - }; + let pool = fetch_pool_state(rpc, pool_state).await?; + let is_token0_sol = if pool.token0_mint == WSOL_TOKEN_ACCOUNT { true } else { false }; let (token0_balance, token1_balance) = get_pool_token_balances(rpc, pool_state, &pool.token0_mint, &pool.token1_mint).await?; @@ -151,15 +156,11 @@ pub async fn get_pool_token_balances( let token1_balance = rpc.get_token_account_balance(&token1_vault).await?; // 解析余额字符串为 u64 - let token0_amount = token0_balance - .amount - .parse::() - .map_err(|e| anyhow!("解析 token0 余额失败: {}", e))?; + let token0_amount = + token0_balance.amount.parse::().map_err(|e| anyhow!("解析 token0 余额失败: {}", e))?; - let token1_amount = token1_balance - .amount - .parse::() - .map_err(|e| anyhow!("解析 token1 余额失败: {}", e))?; + let token1_amount = + token1_balance.amount.parse::().map_err(|e| anyhow!("解析 token1 余额失败: {}", e))?; Ok((token0_amount, token1_amount)) } diff --git a/src/trading/raydium_cpmm/mod.rs b/src/trading/raydium_cpmm/mod.rs index 53868a7..34994bf 100755 --- a/src/trading/raydium_cpmm/mod.rs +++ b/src/trading/raydium_cpmm/mod.rs @@ -1,2 +1 @@ pub mod common; -pub mod pool; diff --git a/src/trading/raydium_cpmm/pool.rs b/src/trading/raydium_cpmm/pool.rs deleted file mode 100755 index 939bffb..0000000 --- a/src/trading/raydium_cpmm/pool.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::{common::SolanaRpcClient, constants::raydium_cpmm::accounts}; -use anyhow::anyhow; -use borsh::BorshDeserialize; -use solana_sdk::pubkey::Pubkey; - -#[derive(Debug, Clone, BorshDeserialize)] -pub struct Pool { - pub amm_config: Pubkey, - pub pool_creator: Pubkey, - pub token0_vault: Pubkey, - pub token1_vault: Pubkey, - pub lp_mint: Pubkey, - pub token0_mint: Pubkey, - pub token1_mint: Pubkey, - pub token0_program: Pubkey, - pub token1_program: Pubkey, - pub observation_key: Pubkey, - pub auth_bump: u8, - pub status: u8, - pub lp_mint_decimals: u8, - pub mint0_decimals: u8, - pub mint1_decimals: u8, - pub lp_supply: u64, - pub protocol_fees_token0: u64, - pub protocol_fees_token1: u64, - pub fund_fees_token0: u64, - pub fund_fees_token1: u64, - pub open_time: u64, - pub recent_epoch: u64, - pub padding: [u64; 31], -} - -impl Pool { - pub fn from_bytes(data: &[u8]) -> Result { - let pool = Pool::try_from_slice(&data[8..])?; - Ok(pool) - } - - pub async fn fetch( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - ) -> Result { - let account = rpc.get_account(pool_address).await?; - - if account.owner != accounts::RAYDIUM_CPMM { - return Err(anyhow!("Account is not owned by Raydium Cpmm program")); - } - - Self::from_bytes(&account.data) - } -} diff --git a/src/utils.rs b/src/utils/mod.rs similarity index 67% rename from src/utils.rs rename to src/utils/mod.rs index bcdd63e..d3769f7 100644 --- a/src/utils.rs +++ b/src/utils/mod.rs @@ -1,3 +1,5 @@ +pub mod price; + use crate::solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; use crate::trading; use crate::SolanaTrade; @@ -57,15 +59,6 @@ impl SolanaTrade { // -------------------------------- PumpFun -------------------------------- - #[inline] - pub fn get_pumpfun_token_price( - &self, - virtual_sol_reserves: u64, - virtual_token_reserves: u64, - ) -> f64 { - trading::pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) - } - #[inline] pub fn get_pumpfun_token_buy_price(&self, amount: u64, trade_info: &PumpFunTradeEvent) -> u64 { trading::pumpfun::common::get_buy_price(amount, trade_info) @@ -77,15 +70,12 @@ impl SolanaTrade { mint: &Pubkey, ) -> Result { let (bonding_curve, _) = - trading::pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + trading::pumpfun::common::fetch_bonding_curve_account(&self.rpc, mint).await?; let virtual_sol_reserves = bonding_curve.virtual_sol_reserves; let virtual_token_reserves = bonding_curve.virtual_token_reserves; - Ok(trading::pumpfun::common::get_token_price( - virtual_sol_reserves, - virtual_token_reserves, - )) + Ok(price::pumpfun::price_token_in_sol(virtual_sol_reserves, virtual_token_reserves)) } #[inline] @@ -94,7 +84,7 @@ impl SolanaTrade { mint: &Pubkey, ) -> Result { let (bonding_curve, _) = - trading::pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + trading::pumpfun::common::fetch_bonding_curve_account(&self.rpc, mint).await?; let actual_sol_reserves = bonding_curve.real_sol_reserves; @@ -104,7 +94,7 @@ impl SolanaTrade { #[inline] pub async fn get_pumpfun_token_creator(&self, mint: &Pubkey) -> Result { let (bonding_curve, _) = - trading::pumpfun::common::get_bonding_curve_account_v2(&self.rpc, mint).await?; + trading::pumpfun::common::fetch_bonding_curve_account(&self.rpc, mint).await?; let creator = bonding_curve.creator; @@ -118,16 +108,15 @@ impl SolanaTrade { &self, pool_address: &Pubkey, ) -> Result { - let pool = trading::pumpswap::pool::Pool::fetch(&self.rpc, pool_address).await?; + let pool = trading::pumpswap::common::fetch_pool(&self.rpc, pool_address).await?; - let (base_amount, quote_amount) = pool.get_token_balances(&self.rpc).await?; + let (base_amount, quote_amount) = + trading::pumpswap::common::get_token_balances(&pool, &self.rpc).await?; // Calculate price using constant product formula (x * y = k) // Price = quote_amount / base_amount if base_amount == 0 { - return Err(anyhow::anyhow!( - "Base amount is zero, cannot calculate price" - )); + return Err(anyhow::anyhow!("Base amount is zero, cannot calculate price")); } let price = quote_amount as f64 / base_amount as f64; @@ -140,9 +129,10 @@ impl SolanaTrade { &self, pool_address: &Pubkey, ) -> Result { - let pool = trading::pumpswap::pool::Pool::fetch(&self.rpc, pool_address).await?; + let pool = trading::pumpswap::common::fetch_pool(&self.rpc, pool_address).await?; - let (_, quote_amount) = pool.get_token_balances(&self.rpc).await?; + let (_, quote_amount) = + trading::pumpswap::common::get_token_balances(&pool, &self.rpc).await?; Ok(quote_amount) } @@ -152,32 +142,11 @@ impl SolanaTrade { &self, pool_address: &Pubkey, ) -> Result { - let pool = trading::pumpswap::pool::Pool::fetch(&self.rpc, pool_address).await?; + let pool = trading::pumpswap::common::fetch_pool(&self.rpc, pool_address).await?; - let (base_amount, _) = pool.get_token_balances(&self.rpc).await?; + let (base_amount, _) = + trading::pumpswap::common::get_token_balances(&pool, &self.rpc).await?; Ok(base_amount) } - - // -------------------------------- Bonk -------------------------------- - - #[inline] - pub fn get_bonk_token_price( - &self, - virtual_base: u128, - virtual_quote: u128, - real_base: u128, - real_quote: u128, - decimal_base: u64, - decimal_quote: u64, - ) -> f64 { - trading::bonk::common::get_token_price( - virtual_base, - virtual_quote, - real_base, - real_quote, - decimal_base, - decimal_quote, - ) - } } diff --git a/src/utils/price/bonk.rs b/src/utils/price/bonk.rs new file mode 100644 index 0000000..103fdb7 --- /dev/null +++ b/src/utils/price/bonk.rs @@ -0,0 +1,117 @@ +use solana_streamer_sdk::streaming::event_parser::protocols::bonk::types::PoolState; + +use crate::constants::{ + bonk::accounts::WSOL_TOKEN_ACCOUNT, + decimals::{DEFAULT_TOKEN_DECIMALS, SOL_DECIMALS}, +}; + +/// Calculate the token price in WSOL based on pool state +/// +/// # Arguments +/// * `pool_state` - Pool state +/// +/// # Returns +/// Token price in WSOL as f64 +pub fn price_token_in_wsol_with_pool_state(pool_state: &PoolState) -> f64 { + if pool_state.quote_mint != WSOL_TOKEN_ACCOUNT { + log::error!("Quote mint is not WSOL: {:?}", pool_state.quote_mint); + return 0.0; + } + price_base_in_quote( + pool_state.virtual_base, + pool_state.virtual_quote, + pool_state.real_base, + pool_state.real_quote, + pool_state.base_decimals, + pool_state.quote_decimals, + ) +} + +/// Calculate the price of base in quote based on pool state +/// +/// # Arguments +/// * `pool_state` - Pool state +/// +/// # Returns +/// The price of base in quote +pub fn price_base_in_quote_with_pool_state(pool_state: &PoolState) -> f64 { + price_base_in_quote( + pool_state.virtual_base, + pool_state.virtual_quote, + pool_state.real_base, + pool_state.real_quote, + pool_state.base_decimals, + pool_state.quote_decimals, + ) +} + +/// Calculate the price of token in WSOL +/// +/// # Arguments +/// * `virtual_base` - Virtual base reserves +/// * `virtual_quote` - Virtual quote reserves +/// * `real_base` - Real base reserves +/// * `real_quote` - Real quote reserves +/// +/// # Returns +/// The price of token in WSOL +pub fn price_token_in_wsol( + virtual_base: u64, + virtual_quote: u64, + real_base: u64, + real_quote: u64, +) -> f64 { + price_base_in_quote( + virtual_base, + virtual_quote, + real_base, + real_quote, + DEFAULT_TOKEN_DECIMALS, + SOL_DECIMALS, + ) +} + +/// Calculate the price of base in quote +/// +/// # Arguments +/// * `virtual_base` - Virtual base reserves +/// * `virtual_quote` - Virtual quote reserves +/// * `real_base` - Real base reserves +/// * `real_quote` - Real quote reserves +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// The price of base in quote +pub fn price_base_in_quote( + virtual_base: u64, + virtual_quote: u64, + real_base: u64, + real_quote: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + // Calculate decimal places difference + let decimal_diff = quote_decimals as i32 - base_decimals as i32; + let decimal_factor = if decimal_diff >= 0 { + 10_f64.powi(decimal_diff) + } else { + 1.0 / 10_f64.powi(-decimal_diff) + }; + // Calculate reserves state before price calculation + let quote_reserves = virtual_quote.checked_add(real_quote).unwrap_or(0); + let base_reserves = virtual_base.checked_sub(real_base).unwrap_or(0); + + if base_reserves == 0 { + return 0.0; + } + + if decimal_factor == 0.0 { + return 0.0; + } + + // Use floating point calculation to avoid precision loss from integer division + let price = (quote_reserves as f64) / (base_reserves as f64) / decimal_factor; + + price +} diff --git a/src/utils/price/common.rs b/src/utils/price/common.rs new file mode 100644 index 0000000..af0d5c8 --- /dev/null +++ b/src/utils/price/common.rs @@ -0,0 +1,47 @@ +/// Calculate the token price in quote based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in quote as f64 +pub fn price_base_in_quote( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + let base = base_reserve as f64 / 10f64.powi(base_decimals as i32); + let quote = quote_reserve as f64 / 10f64.powi(quote_decimals as i32); + if base == 0.0 { + return 0.0; + } + quote / base +} + +/// Calculate the token price in base based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in base as f64 +pub fn price_quote_in_base( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + let base = base_reserve as f64 / 10f64.powi(base_decimals as i32); + let quote = quote_reserve as f64 / 10f64.powi(quote_decimals as i32); + if quote == 0.0 { + return 0.0; + } + base / quote +} diff --git a/src/utils/price/mod.rs b/src/utils/price/mod.rs new file mode 100644 index 0000000..4f65c85 --- /dev/null +++ b/src/utils/price/mod.rs @@ -0,0 +1,7 @@ +pub mod bonk; +pub mod pumpfun; +pub mod pumpswap; +pub mod raydium_amm_v4; +pub mod raydium_clmm; +pub mod raydium_cpmm; +pub mod common; diff --git a/src/utils/price/pumpfun.rs b/src/utils/price/pumpfun.rs new file mode 100644 index 0000000..3f6a0e4 --- /dev/null +++ b/src/utils/price/pumpfun.rs @@ -0,0 +1,31 @@ +use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::types::BondingCurve; + +use crate::constants::pumpfun::global_constants::{LAMPORTS_PER_SOL, SCALE}; + +/// Calculate the token price in SOL based on virtual reserves +/// +/// # Arguments +/// * `bonding_curve` - Bonding curve account +/// +/// # Returns +/// Token price in SOL as f64 +pub fn price_token_in_sol_with_bonding_curve(bonding_curve: &BondingCurve) -> f64 { + price_token_in_sol(bonding_curve.virtual_sol_reserves, bonding_curve.virtual_token_reserves) +} + +/// Calculate the token price in SOL based on virtual reserves +/// +/// # Arguments +/// * `virtual_sol_reserves` - Virtual SOL reserves in the bonding curve +/// * `virtual_token_reserves` - Virtual token reserves in the bonding curve +/// +/// # Returns +/// Token price in SOL as f64 +pub fn price_token_in_sol(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { + let v_sol = virtual_sol_reserves as f64 / LAMPORTS_PER_SOL as f64; + let v_tokens = virtual_token_reserves as f64 / SCALE as f64; + if v_tokens == 0.0 { + return 0.0; + } + v_sol / v_tokens +} diff --git a/src/utils/price/pumpswap.rs b/src/utils/price/pumpswap.rs new file mode 100644 index 0000000..276dcf0 --- /dev/null +++ b/src/utils/price/pumpswap.rs @@ -0,0 +1,47 @@ +/// Calculate the token price in quote based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in quote as f64 +pub fn price_base_in_quote( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + crate::utils::price::common::price_base_in_quote( + base_reserve, + quote_reserve, + base_decimals, + quote_decimals, + ) +} + +/// Calculate the token price in base based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in base as f64 +pub fn price_quote_in_base( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + crate::utils::price::common::price_quote_in_base( + base_reserve, + quote_reserve, + base_decimals, + quote_decimals, + ) +} diff --git a/src/utils/price/raydium_amm_v4.rs b/src/utils/price/raydium_amm_v4.rs new file mode 100644 index 0000000..276dcf0 --- /dev/null +++ b/src/utils/price/raydium_amm_v4.rs @@ -0,0 +1,47 @@ +/// Calculate the token price in quote based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in quote as f64 +pub fn price_base_in_quote( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + crate::utils::price::common::price_base_in_quote( + base_reserve, + quote_reserve, + base_decimals, + quote_decimals, + ) +} + +/// Calculate the token price in base based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in base as f64 +pub fn price_quote_in_base( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + crate::utils::price::common::price_quote_in_base( + base_reserve, + quote_reserve, + base_decimals, + quote_decimals, + ) +} diff --git a/src/utils/price/raydium_clmm.rs b/src/utils/price/raydium_clmm.rs new file mode 100644 index 0000000..84a2db4 --- /dev/null +++ b/src/utils/price/raydium_clmm.rs @@ -0,0 +1,68 @@ +use solana_streamer_sdk::streaming::event_parser::protocols::raydium_clmm::types::PoolState; + +/// Calculate the price of token0 in token1 +/// +/// # Arguments +/// * `sqrt_price_x64` - The sqrt price of the pool +/// * `decimals_token0` - The decimals of token0 +/// * `decimals_token1` - The decimals of token1 +/// +/// # Returns +/// The price of token0 in token1 +pub fn price_token0_in_token1( + sqrt_price_x64: u128, + decimals_token0: u8, + decimals_token1: u8, +) -> f64 { + let sqrt_price = sqrt_price_x64 as f64 / (1u128 << 64) as f64; // Q64.64 转浮点 + let price_raw = sqrt_price * sqrt_price; // 未调整小数位的价格 + let scale = 10f64.powi((decimals_token0 as i32) - (decimals_token1 as i32)); + price_raw * scale +} + +/// Calculate the price of token1 in token0 +/// +/// # Arguments +/// * `sqrt_price_x64` - The sqrt price of the pool +/// * `decimals_token0` - The decimals of token0 +/// * `decimals_token1` - The decimals of token1 +/// +/// # Returns +/// The price of token1 in token0 +pub fn price_token1_in_token0( + sqrt_price_x64: u128, + decimals_token0: u8, + decimals_token1: u8, +) -> f64 { + 1.0 / price_token0_in_token1(sqrt_price_x64, decimals_token0, decimals_token1) +} + +/// Calculate the price of token0 in token1 based on pool state +/// +/// # Arguments +/// * `pool_state` - The pool state +/// +/// # Returns +/// The price of token0 in token1 +pub fn price_token0_in_token1_with_pool_state(pool_state: &PoolState) -> f64 { + price_token0_in_token1( + pool_state.sqrt_price_x64, + pool_state.mint_decimals0, + pool_state.mint_decimals1, + ) +} + +/// Calculate the price of token1 in token0 based on pool state +/// +/// # Arguments +/// * `pool_state` - The pool state +/// +/// # Returns +/// The price of token1 in token0 +pub fn price_token1_in_token0_with_pool_state(pool_state: &PoolState) -> f64 { + price_token1_in_token0( + pool_state.sqrt_price_x64, + pool_state.mint_decimals0, + pool_state.mint_decimals1, + ) +} diff --git a/src/utils/price/raydium_cpmm.rs b/src/utils/price/raydium_cpmm.rs new file mode 100644 index 0000000..276dcf0 --- /dev/null +++ b/src/utils/price/raydium_cpmm.rs @@ -0,0 +1,47 @@ +/// Calculate the token price in quote based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in quote as f64 +pub fn price_base_in_quote( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + crate::utils::price::common::price_base_in_quote( + base_reserve, + quote_reserve, + base_decimals, + quote_decimals, + ) +} + +/// Calculate the token price in base based on base and quote reserves +/// +/// # Arguments +/// * `base_reserve` - Base reserve in the pool +/// * `quote_reserve` - Quote reserve in the pool +/// * `base_decimals` - Base decimals +/// * `quote_decimals` - Quote decimals +/// +/// # Returns +/// Token price in base as f64 +pub fn price_quote_in_base( + base_reserve: u64, + quote_reserve: u64, + base_decimals: u8, + quote_decimals: u8, +) -> f64 { + crate::utils::price::common::price_quote_in_base( + base_reserve, + quote_reserve, + base_decimals, + quote_decimals, + ) +}