feat: integrate Raydium CPMM trading protocol support

- Add Raydium CPMM constants, instruction builder and pool implementation
- Implement unified buy/sell trading interface and event parsing system
- Update documentation and examples to support new protocol
This commit is contained in:
ysq
2025-07-16 22:54:23 +08:00
parent aef9fe6b14
commit 75a29cd7ee
20 changed files with 1119 additions and 50 deletions
+28
View File
@@ -215,6 +215,34 @@ impl ProtocolParams for BonkParams {
}
}
/// RaydiumCpmm协议特定参数
#[derive(Clone)]
pub struct RaydiumCpmmParams {
pub pool_state: Option<Pubkey>,
pub minimum_amount_out: Option<u64>,
pub auto_handle_wsol: bool,
}
impl RaydiumCpmmParams {
pub fn default() -> Self {
Self {
pool_state: None,
minimum_amount_out: None,
auto_handle_wsol: true,
}
}
}
impl ProtocolParams for RaydiumCpmmParams {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn clone_box(&self) -> Box<dyn ProtocolParams> {
Box::new(self.clone())
}
}
impl BuyParams {
/// 转换为BuyWithTipParams
pub fn with_tip(self, swqos_clients: Vec<Arc<SwqosClient>>) -> BuyWithTipParams {
+14 -6
View File
@@ -1,18 +1,20 @@
use anyhow::{anyhow, Result};
use std::sync::Arc;
use crate::instruction::{bonk::BonkInstructionBuilder, pumpfun::PumpFunInstructionBuilder, pumpswap::PumpSwapInstructionBuilder};
use super::{
core::{executor::GenericTradeExecutor, traits::TradeExecutor},
use crate::instruction::{
bonk::BonkInstructionBuilder, pumpfun::PumpFunInstructionBuilder,
pumpswap::PumpSwapInstructionBuilder, raydium_cpmm::RaydiumCpmmInstructionBuilder,
};
use super::core::{executor::GenericTradeExecutor, traits::TradeExecutor};
/// 支持的交易协议
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DexType {
PumpFun,
PumpSwap,
Bonk,
RaydiumCpmm,
}
impl std::fmt::Display for DexType {
@@ -21,6 +23,7 @@ impl std::fmt::Display for DexType {
DexType::PumpFun => write!(f, "PumpFun"),
DexType::PumpSwap => write!(f, "PumpSwap"),
DexType::Bonk => write!(f, "Bonk"),
DexType::RaydiumCpmm => write!(f, "RaydiumCpmm"),
}
}
}
@@ -33,6 +36,7 @@ impl std::str::FromStr for DexType {
"pumpfun" => Ok(DexType::PumpFun),
"pumpswap" => Ok(DexType::PumpSwap),
"bonk" => Ok(DexType::Bonk),
"raydiumcpmm" => Ok(DexType::RaydiumCpmm),
_ => Err(anyhow!("Unsupported protocol: {}", s)),
}
}
@@ -55,9 +59,13 @@ impl TradeFactory {
}
DexType::Bonk => {
let instruction_builder = Arc::new(BonkInstructionBuilder);
Arc::new(GenericTradeExecutor::new(instruction_builder, "Bonk"))
}
DexType::RaydiumCpmm => {
let instruction_builder = Arc::new(RaydiumCpmmInstructionBuilder);
Arc::new(GenericTradeExecutor::new(
instruction_builder,
"Bonk",
"RaydiumCpmm",
))
}
}
@@ -65,7 +73,7 @@ impl TradeFactory {
/// 获取所有支持的协议
pub fn supported_dex_types() -> Vec<DexType> {
vec![DexType::PumpFun, DexType::PumpSwap, DexType::Bonk]
vec![DexType::PumpFun, DexType::PumpSwap, DexType::Bonk, DexType::RaydiumCpmm]
}
/// 检查协议是否支持
+1
View File
@@ -4,6 +4,7 @@ pub mod factory;
pub mod bonk;
pub mod pumpfun;
pub mod pumpswap;
pub mod raydium_cpmm;
pub use core::params::{BuyParams, BuyWithTipParams, SellParams, SellWithTipParams};
pub use core::traits::{InstructionBuilder, TradeExecutor};
+220
View File
@@ -0,0 +1,220 @@
use crate::{
common::SolanaRpcClient,
constants::{self, raydium_cpmm::accounts::WSOL_TOKEN_ACCOUNT},
trading::raydium_cpmm::pool::Pool,
};
use anyhow::anyhow;
use solana_sdk::pubkey::Pubkey;
pub fn get_pool_pda(amm_config: &Pubkey, mint1: &Pubkey, mint2: &Pubkey) -> Option<Pubkey> {
let seeds: &[&[u8]; 4] = &[
constants::raydium_cpmm::seeds::POOL_SEED,
amm_config.as_ref(),
mint1.as_ref(),
mint2.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_vault_pda(pool_state: &Pubkey, mint: &Pubkey) -> Option<Pubkey> {
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<Pubkey> {
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)
}
pub async fn get_buy_token_amount(
rpc: &SolanaRpcClient,
pool_state: &Pubkey,
sol_amount: u64,
) -> Result<u64, anyhow::Error> {
let pool = Pool::fetch(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?;
// 使用恒定乘积公式计算
let (reserve_in, reserve_out) = if is_token0_input {
(token0_balance, token1_balance)
} else {
(token1_balance, token0_balance)
};
if reserve_in == 0 || reserve_out == 0 {
return Err(anyhow!("池子储备金为零,无法进行交换"));
}
// 使用 u128 防止溢出
let amount_in_128 = sol_amount as u128;
let reserve_in_128 = reserve_in as u128;
let reserve_out_128 = reserve_out as u128;
// 恒定乘积公式: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in)
let numerator = amount_in_128 * reserve_out_128;
let denominator = reserve_in_128 + amount_in_128;
if denominator == 0 {
return Err(anyhow!("分母为零,计算错误"));
}
let amount_out = numerator / denominator;
// 检查是否超出储备金
if amount_out >= reserve_out_128 {
return Err(anyhow!("输出数量超过池子储备金"));
}
Ok(amount_out as u64)
}
pub async fn get_sell_sol_amount(
rpc: &SolanaRpcClient,
pool_state: &Pubkey,
token_amount: u64,
) -> Result<u64, anyhow::Error> {
let pool = Pool::fetch(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?;
let (reserve_in, reserve_out) = if is_token0_sol {
(token1_balance, token0_balance)
} else {
(token0_balance, token1_balance)
};
if reserve_in == 0 || reserve_out == 0 {
return Err(anyhow!("池子储备金为零,无法进行交换"));
}
// 使用 u128 防止溢出
let amount_in_128 = token_amount as u128;
let reserve_in_128 = reserve_in as u128;
let reserve_out_128 = reserve_out as u128;
// 恒定乘积公式: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in)
let numerator = amount_in_128 * reserve_out_128;
let denominator = reserve_in_128 + amount_in_128;
if denominator == 0 {
return Err(anyhow!("分母为零,计算错误"));
}
let amount_out = numerator / denominator;
// 检查是否超出储备金
if amount_out >= reserve_out_128 {
return Err(anyhow!("输出数量超过池子储备金"));
}
Ok(amount_out as u64)
}
/// 获取池子中两个代币的余额
///
/// # 返回值
/// 返回 token0_balance, token1_balance
pub async fn get_pool_token_balances(
rpc: &SolanaRpcClient,
pool_state: &Pubkey,
token0_mint: &Pubkey,
token1_mint: &Pubkey,
) -> Result<(u64, u64), anyhow::Error> {
let token0_vault = get_vault_pda(pool_state, token0_mint).unwrap();
let token0_balance = rpc.get_token_account_balance(&token0_vault).await?;
let token1_vault = get_vault_pda(pool_state, token1_mint).unwrap();
let token1_balance = rpc.get_token_account_balance(&token1_vault).await?;
// 解析余额字符串为 u64
let token0_amount = token0_balance
.amount
.parse::<u64>()
.map_err(|e| anyhow!("解析 token0 余额失败: {}", e))?;
let token1_amount = token1_balance
.amount
.parse::<u64>()
.map_err(|e| anyhow!("解析 token1 余额失败: {}", e))?;
Ok((token0_amount, token1_amount))
}
/// 计算代币价格 (token1/token0)
///
/// # 返回值
/// 返回 token1 相对于 token0 的价格
pub async fn calculate_price(
token0_amount: u64,
token1_amount: u64,
mint0_decimals: u8,
mint1_decimals: u8,
) -> Result<f64, anyhow::Error> {
if token0_amount == 0 {
return Err(anyhow!("Token0 余额为零,无法计算价格"));
}
// 考虑小数位精度
let token0_adjusted = token0_amount as f64 / 10_f64.powi(mint0_decimals as i32);
let token1_adjusted = token1_amount as f64 / 10_f64.powi(mint1_decimals as i32);
let price = token1_adjusted / token0_adjusted;
Ok(price)
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::pubkey;
#[test]
fn test_get_pool_pda() {
// 测试get_pool_pda函数
let amm_config = constants::raydium_cpmm::accounts::AMM_CONFIG;
let input_mint = pubkey!("So11111111111111111111111111111111111111112"); // WSOL
let output_mint = pubkey!("BnwbwoqPm5ZNx7YTJ8g9jR2qCpYeHBC7xxpU8zEtbonk"); // USDC
let pool_state = pubkey!("E9rRRpcdsKAseeLFbwC1Ewxd3aYG27meqwTTrMfCTbSG");
let result = get_pool_pda(&amm_config, &input_mint, &output_mint);
assert_eq!(result, Some(pool_state));
}
#[test]
fn test_get_vault_pda() {
// 测试get_vault_pda函数
let pool_state = pubkey!("HBMkgQvt4NAFx6XzNav23bNcv6K3oiC5UfY3JsE22scY");
let mint = pubkey!("DeESECsL3cLXno1LFquss98kNQSno1xpQC2ERCqSbonk"); // WSOL
let vault_pda = pubkey!("7rkgNG3A8z636DuzhchKeqAJTaH3H5ZFWmBQeStydovA");
let result = get_vault_pda(&pool_state, &mint);
assert_eq!(result, Some(vault_pda));
}
#[test]
fn test_get_observation_state_pda() {
let pool_state = pubkey!("HBMkgQvt4NAFx6XzNav23bNcv6K3oiC5UfY3JsE22scY");
let observation_state_pda = pubkey!("Gq8u9N18ASjq3AK2gCk6RtGSNyjXZf9EZDb6vTtB9JRs");
let result = get_observation_state_pda(&pool_state);
assert_eq!(result, Some(observation_state_pda));
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod common;
pub mod pool;
+51
View File
@@ -0,0 +1,51 @@
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<Self, anyhow::Error> {
let pool = Pool::try_from_slice(&data[8..])?;
Ok(pool)
}
pub async fn fetch(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
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)
}
}