release: update DEX protocol support for v4.0.9
Sync supported DEX IDLs from bitquery/solana-idl-lib, upgrade protocol builders, and keep trade submission hot paths free of RPC query calls. Include exact-output coverage, token-account setup fixes for WSOL and stable pairs, and low-latency transaction build improvements.
This commit is contained in:
+4
-2
@@ -362,7 +362,8 @@ pub struct TradeBuyParams {
|
||||
pub create_mint_ata: bool,
|
||||
/// Durable nonce information
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
|
||||
/// Optional fixed output token amount. On exact-out capable DEXes this uses
|
||||
/// the exact-out instruction and treats `input_token_amount` as max input.
|
||||
pub fixed_output_token_amount: Option<u64>,
|
||||
/// Gas fee strategy
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
@@ -413,7 +414,8 @@ pub struct TradeSellParams {
|
||||
pub close_mint_token_ata: bool,
|
||||
/// Durable nonce information
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
|
||||
/// Optional fixed output token amount. On exact-out capable DEXes this uses
|
||||
/// the exact-out instruction and treats `input_token_amount` as max input.
|
||||
pub fixed_output_token_amount: Option<u64>,
|
||||
/// Gas fee strategy
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
|
||||
@@ -141,8 +141,8 @@ mod tests {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
let elapsed = fast_elapsed_nanos(start);
|
||||
|
||||
// 应该大约是 10ms = 10,000,000 纳秒
|
||||
assert!(elapsed >= 9_000_000 && elapsed <= 12_000_000);
|
||||
// Scheduler jitter can exceed the sleep duration; the fast timer must not under-report.
|
||||
assert!(elapsed >= 9_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -151,7 +151,7 @@ mod tests {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
let elapsed_ms = sw.elapsed_millis();
|
||||
|
||||
assert!(elapsed_ms >= 9 && elapsed_ms <= 12);
|
||||
assert!(elapsed_ms >= 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ impl TradeConfig {
|
||||
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// let config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
/// .mev_protection(true)
|
||||
/// .check_min_tip(true)
|
||||
|
||||
+171
-45
@@ -1,8 +1,14 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::bonk::{
|
||||
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
|
||||
SELL_EXECT_IN_DISCRIMINATOR,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::bonk::{
|
||||
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
|
||||
BUY_EXECT_OUT_DISCRIMINATOR, SELL_EXECT_IN_DISCRIMINATOR, SELL_EXECT_OUT_DISCRIMINATOR,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{BonkParams, SwapParams},
|
||||
@@ -55,6 +61,11 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
accounts::GLOBAL_CONFIG_META
|
||||
};
|
||||
|
||||
let quote_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
};
|
||||
let quote_token_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
@@ -88,11 +99,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
let user_quote_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -103,11 +110,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
protocol_params.base_vault
|
||||
};
|
||||
let quote_vault_account = if protocol_params.quote_vault == Pubkey::default() {
|
||||
if usd1_pool {
|
||||
get_vault_pda(&pool_state, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
|
||||
} else {
|
||||
get_vault_pda(&pool_state, &crate::constants::WSOL_TOKEN_ACCOUNT).unwrap()
|
||||
}
|
||||
get_vault_pda(&pool_state, "e_mint).unwrap()
|
||||
} else {
|
||||
protocol_params.quote_vault
|
||||
};
|
||||
@@ -117,9 +120,15 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata && !usd1_pool {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
if params.create_input_mint_ata {
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
@@ -135,12 +144,18 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
}
|
||||
|
||||
let mut data = [0u8; 32];
|
||||
data[..8].copy_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&BUY_EXECT_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_out.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_in.to_le_bytes());
|
||||
} else {
|
||||
data[..8].copy_from_slice(&BUY_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
data[24..32].copy_from_slice(&share_fee_rate.to_le_bytes());
|
||||
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
let accounts: [AccountMeta; 15] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
global_config, // Global Config (readonly)
|
||||
@@ -156,15 +171,12 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
crate::constants::TOKEN_PROGRAM_META, // Quote Token Program (readonly)
|
||||
accounts::EVENT_AUTHORITY_META, // Event Authority (readonly)
|
||||
accounts::BONK_META, // Program (readonly)
|
||||
crate::constants::SYSTEM_PROGRAM_META, // System Program (readonly)
|
||||
AccountMeta::new(protocol_params.platform_associated_account, false), // Platform Associated Account
|
||||
AccountMeta::new(protocol_params.creator_associated_account, false), // Creator Associated Account
|
||||
];
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::BONK, &data, accounts.to_vec()));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -203,6 +215,11 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
accounts::GLOBAL_CONFIG_META
|
||||
};
|
||||
|
||||
let quote_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
};
|
||||
let quote_token_mint = if usd1_pool {
|
||||
crate::constants::USD1_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
@@ -235,11 +252,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
let user_quote_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -250,11 +263,7 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
protocol_params.base_vault
|
||||
};
|
||||
let quote_vault_account = if protocol_params.quote_vault == Pubkey::default() {
|
||||
if usd1_pool {
|
||||
get_vault_pda(&pool_state, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
|
||||
} else {
|
||||
get_vault_pda(&pool_state, &crate::constants::WSOL_TOKEN_ACCOUNT).unwrap()
|
||||
}
|
||||
get_vault_pda(&pool_state, "e_mint).unwrap()
|
||||
} else {
|
||||
protocol_params.quote_vault
|
||||
};
|
||||
@@ -262,19 +271,31 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.close_output_mint_ata && !usd1_pool {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
if params.create_output_mint_ata {
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
let mut data = [0u8; 32];
|
||||
data[..8].copy_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&SELL_EXECT_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_out.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount.to_le_bytes());
|
||||
} else {
|
||||
data[..8].copy_from_slice(&SELL_EXECT_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
data[24..32].copy_from_slice(&share_fee_rate.to_le_bytes());
|
||||
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
let accounts: [AccountMeta; 15] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
global_config, // Global Config (readonly)
|
||||
@@ -290,15 +311,12 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
crate::constants::TOKEN_PROGRAM_META, // Quote Token Program (readonly)
|
||||
accounts::EVENT_AUTHORITY_META, // Event Authority (readonly)
|
||||
accounts::BONK_META, // Program (readonly)
|
||||
crate::constants::SYSTEM_PROGRAM_META, // System Program (readonly)
|
||||
AccountMeta::new(protocol_params.platform_associated_account, false), // Platform Associated Account
|
||||
AccountMeta::new(protocol_params.creator_associated_account, false), // Creator Associated Account
|
||||
];
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::BONK, &data, accounts.to_vec()));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
@@ -313,3 +331,111 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{common::GasFeeStrategy, swqos::TradeType, trading::core::params::DexParamEnum};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn bonk_params() -> BonkParams {
|
||||
BonkParams {
|
||||
mint_token_program: crate::constants::TOKEN_PROGRAM,
|
||||
platform_config: pk(8),
|
||||
platform_associated_account: pk(9),
|
||||
creator_associated_account: pk(10),
|
||||
global_config: accounts::GLOBAL_CONFIG,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_params(trade_type: TradeType) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type,
|
||||
input_mint: pk(3),
|
||||
input_token_program: None,
|
||||
output_mint: pk(3),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::Bonk(bonk_params()),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount: Some(42),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bonk_buy_uses_exact_out_when_fixed_output_is_set() {
|
||||
let instructions = BonkInstructionBuilder
|
||||
.build_buy_instructions(&swap_params(TradeType::Buy))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 15);
|
||||
assert_eq!(ix.accounts[14].pubkey, accounts::BONK);
|
||||
assert_eq!(&ix.data[..8], BUY_EXECT_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bonk_sell_uses_exact_out_when_fixed_output_is_set() {
|
||||
let instructions = BonkInstructionBuilder
|
||||
.build_sell_instructions(&swap_params(TradeType::Sell))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 15);
|
||||
assert_eq!(ix.accounts[14].pubkey, accounts::BONK);
|
||||
assert_eq!(&ix.data[..8], SELL_EXECT_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bonk_usd1_buy_create_input_builds_usd1_ata_not_wsol_wrap() {
|
||||
let mut params = swap_params(TradeType::Buy);
|
||||
if let DexParamEnum::Bonk(protocol_params) = &mut params.protocol_params {
|
||||
protocol_params.global_config = accounts::USD1_GLOBAL_CONFIG;
|
||||
}
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions = BonkInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USD1_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use crate::{
|
||||
instruction::utils::meteora_damm_v2::{accounts, get_event_authority_pda, SWAP_DISCRIMINATOR},
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::meteora_damm_v2::{
|
||||
accounts, get_event_authority_pda, SWAP2_DISCRIMINATOR, SWAP_MODE_EXACT_IN,
|
||||
SWAP_MODE_EXACT_OUT, SWAP_MODE_PARTIAL_FILL,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{MeteoraDammV2Params, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
@@ -42,32 +51,43 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
// ========================================
|
||||
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let input_mint =
|
||||
if is_a_in { protocol_params.token_a_mint } else { protocol_params.token_b_mint };
|
||||
let input_token_program =
|
||||
if is_a_in { protocol_params.token_a_program } else { protocol_params.token_b_program };
|
||||
let output_mint =
|
||||
if is_a_in { protocol_params.token_b_mint } else { protocol_params.token_a_mint };
|
||||
let output_token_program =
|
||||
if is_a_in { protocol_params.token_b_program } else { protocol_params.token_a_program };
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
|
||||
let (amount_0, amount_1) = match protocol_params.swap_mode {
|
||||
SWAP_MODE_EXACT_OUT => {
|
||||
let amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 exact-out swap2")
|
||||
})?;
|
||||
(amount_out, amount_in)
|
||||
}
|
||||
SWAP_MODE_EXACT_IN | SWAP_MODE_PARTIAL_FILL => {
|
||||
let minimum_amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap2 min output")
|
||||
})?;
|
||||
(amount_in, minimum_amount_out)
|
||||
}
|
||||
mode => return Err(anyhow!("Unsupported MeteoraDammV2 swap_mode {}", mode)),
|
||||
};
|
||||
|
||||
let input_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_a_program
|
||||
} else {
|
||||
&protocol_params.token_b_program
|
||||
},
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let output_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
@@ -77,28 +97,32 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 14] = [
|
||||
let mut account_metas = Vec::with_capacity(
|
||||
13 + usize::from(protocol_params.referral_token_account.is_some())
|
||||
+ usize::from(protocol_params.include_rate_limiter_sysvar),
|
||||
);
|
||||
account_metas.extend([
|
||||
accounts::AUTHORITY_META, // Pool Authority (readonly)
|
||||
AccountMeta::new(protocol_params.pool, false), // Pool
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
@@ -110,25 +134,32 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Transfer Authority
|
||||
AccountMeta::new_readonly(protocol_params.token_a_program, false), // Token Program (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.token_b_program, false), // Token Program (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Referral Token Account (readonly)
|
||||
]);
|
||||
if let Some(referral_token_account) = protocol_params.referral_token_account {
|
||||
account_metas.push(AccountMeta::new(referral_token_account, false));
|
||||
}
|
||||
account_metas.extend([
|
||||
AccountMeta::new_readonly(get_event_authority_pda(), false), // Event Authority (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Program (readonly)
|
||||
];
|
||||
]);
|
||||
if protocol_params.include_rate_limiter_sysvar {
|
||||
account_metas.push(accounts::SYSVAR_INSTRUCTIONS_META);
|
||||
}
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let mut data = [0u8; 25];
|
||||
data[..8].copy_from_slice(&SWAP2_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_0.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_1.to_le_bytes());
|
||||
data[24] = protocol_params.swap_mode;
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::METEORA_DAMM_V2,
|
||||
&data,
|
||||
accounts.to_vec(),
|
||||
account_metas,
|
||||
));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &input_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -161,45 +192,67 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
// ========================================
|
||||
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
|
||||
let input_mint =
|
||||
if is_a_in { protocol_params.token_a_mint } else { protocol_params.token_b_mint };
|
||||
let input_token_program =
|
||||
if is_a_in { protocol_params.token_a_program } else { protocol_params.token_b_program };
|
||||
let output_mint =
|
||||
if is_a_in { protocol_params.token_b_mint } else { protocol_params.token_a_mint };
|
||||
let output_token_program =
|
||||
if is_a_in { protocol_params.token_b_program } else { protocol_params.token_a_program };
|
||||
let amount_in = params.input_amount.unwrap_or(0);
|
||||
let (amount_0, amount_1) = match protocol_params.swap_mode {
|
||||
SWAP_MODE_EXACT_OUT => {
|
||||
let amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 exact-out swap2")
|
||||
})?;
|
||||
(amount_out, amount_in)
|
||||
}
|
||||
SWAP_MODE_EXACT_IN | SWAP_MODE_PARTIAL_FILL => {
|
||||
let minimum_amount_out = params.fixed_output_amount.ok_or_else(|| {
|
||||
anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap2 min output")
|
||||
})?;
|
||||
(amount_in, minimum_amount_out)
|
||||
}
|
||||
mode => return Err(anyhow!("Unsupported MeteoraDammV2 swap_mode {}", mode)),
|
||||
};
|
||||
|
||||
let input_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_a_program
|
||||
} else {
|
||||
&protocol_params.token_b_program
|
||||
},
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let output_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 14] = [
|
||||
let mut account_metas = Vec::with_capacity(
|
||||
13 + usize::from(protocol_params.referral_token_account.is_some())
|
||||
+ usize::from(protocol_params.include_rate_limiter_sysvar),
|
||||
);
|
||||
account_metas.extend([
|
||||
accounts::AUTHORITY_META, // Pool Authority (readonly)
|
||||
AccountMeta::new(protocol_params.pool, false), // Pool
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
@@ -211,32 +264,36 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Transfer Authority
|
||||
AccountMeta::new_readonly(protocol_params.token_a_program, false), // Token Program (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.token_b_program, false), // Token Program (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Referral Token Account (readonly)
|
||||
]);
|
||||
if let Some(referral_token_account) = protocol_params.referral_token_account {
|
||||
account_metas.push(AccountMeta::new(referral_token_account, false));
|
||||
}
|
||||
account_metas.extend([
|
||||
AccountMeta::new_readonly(get_event_authority_pda(), false), // Event Authority (readonly)
|
||||
accounts::METEORA_DAMM_V2_META, // Program (readonly)
|
||||
];
|
||||
]);
|
||||
if protocol_params.include_rate_limiter_sysvar {
|
||||
account_metas.push(accounts::SYSVAR_INSTRUCTIONS_META);
|
||||
}
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or_default().to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let mut data = [0u8; 25];
|
||||
data[..8].copy_from_slice(&SWAP2_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_0.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_1.to_le_bytes());
|
||||
data[24] = protocol_params.swap_mode;
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::METEORA_DAMM_V2,
|
||||
&data,
|
||||
accounts.to_vec(),
|
||||
account_metas,
|
||||
));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &output_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
if is_a_in {
|
||||
&protocol_params.token_a_program
|
||||
} else {
|
||||
&protocol_params.token_b_program
|
||||
},
|
||||
&input_token_program,
|
||||
&input_token_account,
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -247,3 +304,162 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn meteora_params(referral: Option<Pubkey>) -> MeteoraDammV2Params {
|
||||
let params = MeteoraDammV2Params::new(
|
||||
pk(1),
|
||||
pk(2),
|
||||
pk(3),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(4),
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
match referral {
|
||||
Some(account) => params.with_referral_token_account(account),
|
||||
None => params,
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_params(protocol_params: MeteoraDammV2Params) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
input_token_program: None,
|
||||
output_mint: pk(4),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::MeteoraDammV2(protocol_params),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount: Some(1),
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_omits_optional_referral_account() {
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(meteora_params(None)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 13);
|
||||
assert_eq!(ix.accounts[11].pubkey, get_event_authority_pda());
|
||||
assert_eq!(ix.accounts[12].pubkey, accounts::METEORA_DAMM_V2);
|
||||
assert_eq!(&ix.data[..8], SWAP2_DISCRIMINATOR);
|
||||
assert_eq!(ix.data[24], SWAP_MODE_PARTIAL_FILL);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_includes_writable_referral_account_when_set() {
|
||||
let referral = pk(9);
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(meteora_params(Some(referral))))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 14);
|
||||
assert_eq!(ix.accounts[11].pubkey, referral);
|
||||
assert!(ix.accounts[11].is_writable);
|
||||
assert_eq!(ix.accounts[12].pubkey, get_event_authority_pda());
|
||||
assert_eq!(ix.accounts[13].pubkey, accounts::METEORA_DAMM_V2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_includes_sysvar_only_when_rate_limiter_is_set() {
|
||||
let protocol_params = meteora_params(None).with_rate_limiter_sysvar(true);
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(protocol_params))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 14);
|
||||
assert_eq!(ix.accounts[11].pubkey, get_event_authority_pda());
|
||||
assert_eq!(ix.accounts[12].pubkey, accounts::METEORA_DAMM_V2);
|
||||
assert_eq!(ix.accounts[13].pubkey, accounts::SYSVAR_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_swap2_exact_out_uses_amount_out_then_max_input() {
|
||||
let protocol_params = meteora_params(None).with_swap_mode(SWAP_MODE_EXACT_OUT);
|
||||
let instructions = MeteoraDammV2InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(protocol_params))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], SWAP2_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 1);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
assert_eq!(ix.data[24], SWAP_MODE_EXACT_OUT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meteora_sol_buy_uses_pool_wsol_mint_for_user_input_account() {
|
||||
let mut params = swap_params(meteora_params(None));
|
||||
params.input_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
MeteoraDammV2InstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_wsol_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let wrong_sol_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
&crate::constants::SOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
assert_eq!(ix.accounts[2].pubkey, expected_wsol_ata);
|
||||
assert_ne!(ix.accounts[2].pubkey, wrong_sol_ata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ pub mod pumpswap;
|
||||
pub(crate) mod pumpswap_ix_data;
|
||||
pub mod raydium_amm_v4;
|
||||
pub mod raydium_cpmm;
|
||||
pub(crate) mod token_account_setup;
|
||||
pub mod utils;
|
||||
|
||||
+202
-90
@@ -13,10 +13,16 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::{
|
||||
accounts, get_bonding_curve_pda, get_user_volume_accumulator_pda,
|
||||
global_constants::{self},
|
||||
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::pumpfun::{
|
||||
accounts, get_bonding_curve_pda, get_user_volume_accumulator_pda,
|
||||
global_constants::{self},
|
||||
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing,
|
||||
},
|
||||
},
|
||||
utils::calc::{
|
||||
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
|
||||
@@ -28,10 +34,7 @@ use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
||||
|
||||
#[inline]
|
||||
fn effective_pump_mint_token_program(mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
|
||||
if mint.to_string().ends_with("pump") {
|
||||
return TOKEN_PROGRAM_2022;
|
||||
}
|
||||
fn effective_pump_mint_token_program(_mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
|
||||
let tp = protocol_params.token_program;
|
||||
if tp == Pubkey::default() {
|
||||
TOKEN_PROGRAM_2022
|
||||
@@ -108,19 +111,6 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
)
|
||||
.ok_or_else(|| anyhow!("creator_vault PDA derivation failed (creator={})", creator))?;
|
||||
|
||||
let buy_token_amount = match params.fixed_output_amount {
|
||||
Some(amount) => amount,
|
||||
None => get_buy_token_amount_from_sol_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
bonding_curve.real_token_reserves as u128,
|
||||
creator,
|
||||
lamports_in,
|
||||
),
|
||||
};
|
||||
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
@@ -167,11 +157,23 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
|
||||
// ── Legacy buy path ──
|
||||
let track_volume_val = if bonding_curve.is_cashback_coin { 1u8 } else { 0u8 };
|
||||
let buy_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data(lamports_in, min_tokens_out, track_volume_val)
|
||||
let buy_data = if let Some(token_amount) = params.fixed_output_amount {
|
||||
encode_pumpfun_buy_ix_data(token_amount, lamports_in, track_volume_val)
|
||||
} else {
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val)
|
||||
let buy_token_amount = get_buy_token_amount_from_sol_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
bonding_curve.real_token_reserves as u128,
|
||||
creator,
|
||||
lamports_in,
|
||||
);
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data(lamports_in, min_tokens_out, track_volume_val)
|
||||
} else {
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val)
|
||||
}
|
||||
};
|
||||
|
||||
let fee_recipient_meta =
|
||||
@@ -250,18 +252,17 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?
|
||||
};
|
||||
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
|
||||
let sol_amount = get_sell_sol_amount_from_token_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
creator,
|
||||
token_amount,
|
||||
);
|
||||
|
||||
let min_sol_output = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => calculate_with_slippage_sell(sol_amount, slippage_bp),
|
||||
let min_sol_output = if let Some(fixed) = params.fixed_output_amount {
|
||||
fixed
|
||||
} else {
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
let sol_amount = get_sell_sol_amount_from_token_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
creator,
|
||||
token_amount,
|
||||
);
|
||||
calculate_with_slippage_sell(sol_amount, slippage_bp)
|
||||
};
|
||||
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
@@ -378,19 +379,6 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
)
|
||||
.ok_or_else(|| anyhow!("creator_vault PDA derivation failed (creator={})", creator))?;
|
||||
|
||||
let buy_token_amount = match params.fixed_output_amount {
|
||||
Some(amount) => amount,
|
||||
None => get_buy_token_amount_from_sol_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
bonding_curve.real_token_reserves as u128,
|
||||
creator,
|
||||
lamports_in,
|
||||
),
|
||||
};
|
||||
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
@@ -472,7 +460,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
"e_token_program,
|
||||
);
|
||||
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
@@ -486,25 +474,36 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
);
|
||||
}
|
||||
|
||||
let (buy_data, quote_amount_to_fund) = if let Some(token_amount) = params.fixed_output_amount {
|
||||
(encode_pumpfun_buy_v2_ix_data(token_amount, lamports_in), lamports_in)
|
||||
} else {
|
||||
let buy_token_amount = get_buy_token_amount_from_sol_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
bonding_curve.real_token_reserves as u128,
|
||||
creator,
|
||||
lamports_in,
|
||||
);
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
(encode_pumpfun_buy_exact_quote_in_v2_ix_data(lamports_in, min_tokens_out), lamports_in)
|
||||
} else {
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
(encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost), max_sol_cost)
|
||||
}
|
||||
};
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
quote_amount_to_fund,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
let buy_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
encode_pumpfun_buy_exact_quote_in_v2_ix_data(lamports_in, min_tokens_out)
|
||||
} else {
|
||||
encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost)
|
||||
};
|
||||
|
||||
let metas: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
AccountMeta::new_readonly(params.output_mint, false),
|
||||
@@ -537,6 +536,10 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, metas));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
@@ -584,18 +587,17 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
})?
|
||||
};
|
||||
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
|
||||
let sol_amount = get_sell_sol_amount_from_token_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
creator,
|
||||
token_amount,
|
||||
);
|
||||
|
||||
let min_sol_output = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => calculate_with_slippage_sell(sol_amount, slippage_bp),
|
||||
let min_sol_output = if let Some(fixed) = params.fixed_output_amount {
|
||||
fixed
|
||||
} else {
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
let sol_amount = get_sell_sol_amount_from_token_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
creator,
|
||||
token_amount,
|
||||
);
|
||||
calculate_with_slippage_sell(sol_amount, slippage_bp)
|
||||
};
|
||||
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
@@ -678,17 +680,15 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
"e_token_program,
|
||||
);
|
||||
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -736,6 +736,10 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
)?);
|
||||
}
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
@@ -846,14 +850,122 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_suffix_buy_forces_token_2022_even_with_legacy_cached_program() {
|
||||
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[1].program_id, TOKEN_PROGRAM_2022);
|
||||
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
|
||||
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
|
||||
assert_eq!(instructions[2].accounts.len(), 18);
|
||||
assert_eq!(instructions[2].data.len(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v1_fixed_output_uses_buy_with_max_input_budget() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
params.use_exact_sol_amount = Some(true);
|
||||
|
||||
let instructions = build_buy_v1(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
|
||||
assert_eq!(ix.data.len(), 25);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(
|
||||
u64::from_le_bytes(ix.data[16..24].try_into().unwrap()),
|
||||
params.input_amount.unwrap()
|
||||
);
|
||||
assert_eq!(ix.data[24], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_fixed_output_uses_buy_with_max_input_budget() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
params.use_exact_sol_amount = Some(true);
|
||||
|
||||
let instructions = build_buy_v2(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(
|
||||
u64::from_le_bytes(ix.data[16..24].try_into().unwrap()),
|
||||
params.input_amount.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_wraps_max_quote_budget() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
|
||||
let instructions = build_buy_v2(¶ms).unwrap();
|
||||
let transfer_ix = &instructions[1];
|
||||
let system_ix = bincode::deserialize::<
|
||||
solana_system_interface::instruction::SystemInstruction,
|
||||
>(&transfer_ix.data)
|
||||
.unwrap();
|
||||
let expected = crate::utils::calc::common::calculate_with_slippage_buy(
|
||||
params.input_amount.unwrap(),
|
||||
params.slippage_basis_points.unwrap(),
|
||||
);
|
||||
|
||||
match system_ix {
|
||||
solana_system_interface::instruction::SystemInstruction::Transfer { lamports } => {
|
||||
assert_eq!(lamports, expected);
|
||||
}
|
||||
other => panic!("unexpected system instruction: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v1_sell_fixed_output_uses_min_sol_directly() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = mint;
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
|
||||
let instructions = build_sell_v1(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR);
|
||||
assert_eq!(ix.data.len(), 24);
|
||||
assert_eq!(
|
||||
u64::from_le_bytes(ix.data[8..16].try_into().unwrap()),
|
||||
params.input_amount.unwrap()
|
||||
);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_sell_fixed_output_uses_min_sol_directly() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = mint;
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
|
||||
let instructions = build_sell_v2(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR);
|
||||
assert_eq!(ix.data.len(), 24);
|
||||
assert_eq!(
|
||||
u64::from_le_bytes(ix.data[8..16].try_into().unwrap()),
|
||||
params.input_amount.unwrap()
|
||||
);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` 与 **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`**
|
||||
//! 的 instruction data 栈上编码(热路径零堆分配)。
|
||||
//!
|
||||
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 在 ix 参数中为
|
||||
//! **1 字节 option tag + 1 字节值**(Anchor `Option<bool>` = 2 字节),共 26 字节 ix data。
|
||||
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 是单字段
|
||||
//! struct(TypeScript 传 `[true]`),在 ix 参数中为 1 字节 bool,共 25 字节 ix data。
|
||||
//! `*_v2` 指令无 `track_volume` 字节(见 [pump-public-docs](https://github.com/pump-fun/pump-public-docs))。
|
||||
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
@@ -10,22 +10,17 @@ use crate::instruction::utils::pumpfun::{
|
||||
BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
/// 与官方 `getBuyInstructionInternal` 对齐:`OptionBool` 在 ix 中为 1 字节 option tag + 1 字节值。
|
||||
/// `track_volume` 在调用侧组合为 `(1u8, cashback as u8)`(Option tag + value),共 26 字节 ix data。
|
||||
pub const TRACK_VOLUME_OPTION_TAG: u8 = 1;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_ix_data(
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
track_volume_val: u8,
|
||||
) -> [u8; 26] {
|
||||
let mut d = [0u8; 26];
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
d[24] = TRACK_VOLUME_OPTION_TAG;
|
||||
d[25] = track_volume_val;
|
||||
d[24] = track_volume_val;
|
||||
d
|
||||
}
|
||||
|
||||
@@ -34,13 +29,12 @@ pub fn encode_pumpfun_buy_exact_sol_in_ix_data(
|
||||
spendable_sol_in: u64,
|
||||
min_tokens_out: u64,
|
||||
track_volume_val: u8,
|
||||
) -> [u8; 26] {
|
||||
let mut d = [0u8; 26];
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
d[24] = TRACK_VOLUME_OPTION_TAG;
|
||||
d[25] = track_volume_val;
|
||||
d[24] = track_volume_val;
|
||||
d
|
||||
}
|
||||
|
||||
|
||||
+206
-43
@@ -4,18 +4,21 @@ use crate::{
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
|
||||
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
instruction::utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
|
||||
get_user_volume_accumulator_wsol_ata, warm_pumpswap_global_config,
|
||||
},
|
||||
trading::{
|
||||
common::wsol_manager,
|
||||
core::{
|
||||
params::{PumpSwapParams, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
|
||||
get_user_volume_accumulator_wsol_ata,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{PumpSwapParams, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
utils::calc::pumpswap::{buy_quote_input_internal, sell_base_input_internal},
|
||||
};
|
||||
@@ -32,7 +35,6 @@ pub struct PumpSwapInstructionBuilder;
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
warm_pumpswap_global_config(params.rpc.as_ref()).await;
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
@@ -53,7 +55,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_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_wsol_ata = params.create_input_mint_ata;
|
||||
let create_input_ata = params.create_input_mint_ata;
|
||||
let close_wsol_ata = params.close_input_mint_ata;
|
||||
let base_token_program = protocol_params.base_token_program;
|
||||
let quote_token_program = protocol_params.quote_token_program;
|
||||
@@ -77,13 +79,21 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let input_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
|
||||
let input_stable_token_program =
|
||||
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
|
||||
let output_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
|
||||
let output_trade_token_program =
|
||||
if quote_is_wsol_or_usdc { base_token_program } else { quote_token_program };
|
||||
let mut creator = Pubkey::default();
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
(output_amount, params.input_amount.unwrap_or(0))
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal(
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
@@ -109,10 +119,6 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
(result.min_quote, params.input_amount.unwrap_or(0))
|
||||
};
|
||||
|
||||
if params.fixed_output_amount.is_some() {
|
||||
token_amount = params.fixed_output_amount.unwrap();
|
||||
}
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -143,7 +149,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if create_wsol_ata {
|
||||
if create_input_ata {
|
||||
// Determine wrap amount based on instruction type:
|
||||
// - buy_exact_quote_in: program spends exactly input_amount, wrap input_amount
|
||||
// - buy: program may spend up to max_quote, wrap max_quote
|
||||
@@ -153,19 +159,23 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
} else {
|
||||
sol_amount
|
||||
};
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), wrap_amount));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_stable_mint,
|
||||
&input_stable_token_program,
|
||||
wrap_amount,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
if quote_is_wsol_or_usdc { &base_mint } else { "e_mint },
|
||||
if quote_is_wsol_or_usdc { &base_token_program } else { "e_token_program },
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_trade_mint,
|
||||
&output_trade_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,7 +235,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let ix_data = if params.fixed_output_amount.is_some() {
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
} else if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
@@ -252,14 +264,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
));
|
||||
}
|
||||
if close_wsol_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_stable_mint,
|
||||
);
|
||||
}
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
warm_pumpswap_global_config(params.rpc.as_ref()).await;
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
@@ -278,7 +292,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let pool_quote_token_account = protocol_params.pool_quote_token_account;
|
||||
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_wsol_ata = params.create_output_mint_ata;
|
||||
let create_output_ata = params.create_output_mint_ata;
|
||||
let close_wsol_ata = params.close_output_mint_ata;
|
||||
let base_token_program = protocol_params.base_token_program;
|
||||
let quote_token_program = protocol_params.quote_token_program;
|
||||
@@ -304,13 +318,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let output_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
|
||||
let output_stable_token_program =
|
||||
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
|
||||
let mut creator = Pubkey::default();
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
(params.input_amount.unwrap(), output_amount)
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal(
|
||||
params.input_amount.unwrap(),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
@@ -336,10 +355,6 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
(result.max_quote, result.base)
|
||||
};
|
||||
|
||||
if params.fixed_output_amount.is_some() {
|
||||
sol_amount = params.fixed_output_amount.unwrap();
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
|
||||
let is_mayhem_mode = protocol_params.is_mayhem_mode;
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
@@ -368,10 +383,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if create_wsol_ata {
|
||||
instructions.extend(wsol_manager::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
if create_output_ata {
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_stable_mint,
|
||||
&output_stable_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
@@ -442,7 +463,11 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
|
||||
if close_wsol_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_stable_mint,
|
||||
);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
@@ -495,3 +520,141 @@ pub fn claim_cashback_pumpswap_instruction(
|
||||
accounts,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, PumpSwapParams, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn pumpswap_params() -> PumpSwapParams {
|
||||
PumpSwapParams::new(
|
||||
pk(1),
|
||||
pk(2),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
accounts::PROTOCOL_FEE_RECIPIENT,
|
||||
Pubkey::default(),
|
||||
false,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn swap_params(trade_type: TradeType, fixed_output_amount: Option<u64>) -> SwapParams {
|
||||
let (input_mint, output_mint) = if trade_type == TradeType::Sell {
|
||||
(pk(2), crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
(crate::constants::WSOL_TOKEN_ACCOUNT, pk(2))
|
||||
};
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type,
|
||||
input_mint,
|
||||
input_token_program: None,
|
||||
output_mint,
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::PumpSwap(pumpswap_params()),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount,
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: Some(true),
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_fixed_output_uses_buy_with_max_input_budget() {
|
||||
let instructions = PumpSwapInstructionBuilder
|
||||
.build_buy_instructions(&swap_params(TradeType::Buy, Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_fixed_output_uses_min_quote_directly() {
|
||||
let instructions = PumpSwapInstructionBuilder
|
||||
.build_sell_instructions(&swap_params(TradeType::Sell, Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_usdc_buy_create_input_builds_usdc_ata() {
|
||||
let mut params = swap_params(TradeType::Buy, Some(42));
|
||||
params.protocol_params = DexParamEnum::PumpSwap(PumpSwapParams::new(
|
||||
pk(1),
|
||||
pk(2),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
accounts::PROTOCOL_FEE_RECIPIENT,
|
||||
Pubkey::default(),
|
||||
false,
|
||||
0,
|
||||
));
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::raydium_amm_v4::{accounts, SWAP_BASE_IN_DISCRIMINATOR},
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::raydium_amm_v4::{
|
||||
accounts, SWAP_BASE_IN_DISCRIMINATOR, SWAP_BASE_OUT_DISCRIMINATOR,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{RaydiumAmmV4Params, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
@@ -10,12 +18,38 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
/// Instruction builder for RaydiumCpmm protocol
|
||||
pub struct RaydiumAmmV4InstructionBuilder;
|
||||
|
||||
fn ensure_market_accounts(params: &RaydiumAmmV4Params) -> Result<()> {
|
||||
let required = [
|
||||
("amm_open_orders", params.amm_open_orders),
|
||||
("amm_target_orders", params.amm_target_orders),
|
||||
("serum_program", params.serum_program),
|
||||
("serum_market", params.serum_market),
|
||||
("serum_bids", params.serum_bids),
|
||||
("serum_asks", params.serum_asks),
|
||||
("serum_event_queue", params.serum_event_queue),
|
||||
("serum_coin_vault_account", params.serum_coin_vault_account),
|
||||
("serum_pc_vault_account", params.serum_pc_vault_account),
|
||||
("serum_vault_signer", params.serum_vault_signer),
|
||||
];
|
||||
|
||||
for (name, account) in required {
|
||||
if account == Pubkey::default() {
|
||||
return Err(anyhow!(
|
||||
"Raydium AMM v4 requires {}; use RaydiumAmmV4Params::from_amm_address_by_rpc or with_market_accounts",
|
||||
name
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
@@ -30,6 +64,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumAmmV4Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
|
||||
ensure_market_accounts(protocol_params)?;
|
||||
|
||||
let is_wsol = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
@@ -47,33 +82,22 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.coin_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let swap_result = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let minimum_amount_out = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => swap_result.min_amount_out,
|
||||
};
|
||||
let input_mint =
|
||||
if is_base_in { protocol_params.coin_mint } else { protocol_params.pc_mint };
|
||||
let output_mint =
|
||||
if is_base_in { protocol_params.pc_mint } else { protocol_params.coin_mint };
|
||||
|
||||
let user_source_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&input_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let user_destination_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -84,47 +108,66 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 17] = [
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
crate::constants::TOKEN_PROGRAM_META, // Token Program (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_open_orders, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_target_orders, false), // Amm Target Orders
|
||||
AccountMeta::new(protocol_params.token_coin, false), // Pool Coin Token Account
|
||||
AccountMeta::new(protocol_params.token_pc, false), // Pool Pc Token Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Pc Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Vault Signer
|
||||
AccountMeta::new_readonly(protocol_params.serum_program, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.serum_market, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.serum_bids, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.serum_asks, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.serum_event_queue, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.serum_coin_vault_account, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.serum_pc_vault_account, false), // Serum Pc Vault Account
|
||||
AccountMeta::new_readonly(protocol_params.serum_vault_signer, false), // Serum Vault Signer
|
||||
AccountMeta::new(user_source_token_account, false), // User Source Token Account
|
||||
AccountMeta::new(user_destination_token_account, false), // User Destination Token Account
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Source Owner
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 17];
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..1].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_AMM_V4,
|
||||
@@ -133,8 +176,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &input_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -149,6 +191,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumAmmV4Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
|
||||
ensure_market_accounts(protocol_params)?;
|
||||
|
||||
if params.input_amount.is_none() || params.input_amount.unwrap_or(0) == 0 {
|
||||
return Err(anyhow!("Token amount is not set"));
|
||||
@@ -169,33 +212,22 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
// ========================================
|
||||
let is_base_in = protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.pc_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let swap_result = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let minimum_amount_out = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => swap_result.min_amount_out,
|
||||
};
|
||||
let input_mint =
|
||||
if is_base_in { protocol_params.coin_mint } else { protocol_params.pc_mint };
|
||||
let output_mint =
|
||||
if is_base_in { protocol_params.pc_mint } else { protocol_params.coin_mint };
|
||||
|
||||
let user_source_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
&input_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let user_destination_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -203,37 +235,59 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let accounts: [AccountMeta; 17] = [
|
||||
let accounts: [AccountMeta; 18] = [
|
||||
crate::constants::TOKEN_PROGRAM_META, // Token Program (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_open_orders, false), // Amm Open Orders
|
||||
AccountMeta::new(protocol_params.amm_target_orders, false), // Amm Target Orders
|
||||
AccountMeta::new(protocol_params.token_coin, false), // Pool Coin Token Account
|
||||
AccountMeta::new(protocol_params.token_pc, false), // Pool Pc Token Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Pc Vault Account
|
||||
AccountMeta::new(protocol_params.amm, false), // Serum Vault Signer
|
||||
AccountMeta::new_readonly(protocol_params.serum_program, false), // Serum Program
|
||||
AccountMeta::new(protocol_params.serum_market, false), // Serum Market
|
||||
AccountMeta::new(protocol_params.serum_bids, false), // Serum Bids
|
||||
AccountMeta::new(protocol_params.serum_asks, false), // Serum Asks
|
||||
AccountMeta::new(protocol_params.serum_event_queue, false), // Serum Event Queue
|
||||
AccountMeta::new(protocol_params.serum_coin_vault_account, false), // Serum Coin Vault Account
|
||||
AccountMeta::new(protocol_params.serum_pc_vault_account, false), // Serum Pc Vault Account
|
||||
AccountMeta::new_readonly(protocol_params.serum_vault_signer, false), // Serum Vault Signer
|
||||
AccountMeta::new(user_source_token_account, false), // User Source Token Account
|
||||
AccountMeta::new(user_destination_token_account, false), // User Destination Token Account
|
||||
AccountMeta::new(params.payer.pubkey(), true), // User Source Owner
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 17];
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let amount_in = params.input_amount.unwrap_or(0);
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..1].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.coin_reserve,
|
||||
protocol_params.pc_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..1].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[1..9].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[9..17].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_AMM_V4,
|
||||
@@ -242,7 +296,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &output_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
@@ -257,3 +311,160 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn market_params() -> RaydiumAmmV4Params {
|
||||
RaydiumAmmV4Params::new(
|
||||
pk(1),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(2),
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
)
|
||||
.with_market_accounts(
|
||||
pk(5),
|
||||
pk(6),
|
||||
pk(7),
|
||||
pk(8),
|
||||
pk(9),
|
||||
pk(10),
|
||||
pk(11),
|
||||
pk(12),
|
||||
pk(13),
|
||||
pk(14),
|
||||
)
|
||||
}
|
||||
|
||||
fn swap_params(
|
||||
protocol_params: RaydiumAmmV4Params,
|
||||
fixed_output_amount: Option<u64>,
|
||||
) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
input_token_program: None,
|
||||
output_mint: pk(2),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::RaydiumAmmV4(protocol_params),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount,
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_uses_idl_market_account_order() {
|
||||
let instructions = RaydiumAmmV4InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(market_params(), None))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(ix.accounts.len(), 18);
|
||||
assert_eq!(&ix.data[..1], SWAP_BASE_IN_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts[3].pubkey, pk(5));
|
||||
assert_eq!(ix.accounts[4].pubkey, pk(6));
|
||||
assert_eq!(ix.accounts[7].pubkey, pk(7));
|
||||
assert_eq!(ix.accounts[8].pubkey, pk(8));
|
||||
assert_eq!(ix.accounts[9].pubkey, pk(9));
|
||||
assert_eq!(ix.accounts[10].pubkey, pk(10));
|
||||
assert_eq!(ix.accounts[11].pubkey, pk(11));
|
||||
assert_eq!(ix.accounts[12].pubkey, pk(12));
|
||||
assert_eq!(ix.accounts[13].pubkey, pk(13));
|
||||
assert_eq!(ix.accounts[14].pubkey, pk(14));
|
||||
assert!(!ix.accounts[7].is_writable);
|
||||
assert!(!ix.accounts[14].is_writable);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_uses_base_out_when_fixed_output_is_set() {
|
||||
let instructions = RaydiumAmmV4InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(market_params(), Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..1], SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[1..9].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[9..17].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_rejects_placeholder_market_accounts() {
|
||||
let err = RaydiumAmmV4InstructionBuilder
|
||||
.build_buy_instructions(&swap_params(
|
||||
RaydiumAmmV4Params::new(
|
||||
pk(1),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
pk(2),
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("amm_open_orders"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_amm_v4_usdc_buy_create_input_builds_usdc_ata() {
|
||||
let mut protocol_params = market_params();
|
||||
protocol_params.coin_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let mut params = swap_params(protocol_params, Some(42));
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions =
|
||||
RaydiumAmmV4InstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
+229
-110
@@ -1,9 +1,15 @@
|
||||
use crate::{
|
||||
common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed,
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::utils::raydium_cpmm::{
|
||||
accounts, get_observation_state_pda, get_pool_pda, get_vault_account,
|
||||
SWAP_BASE_IN_DISCRIMINATOR,
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
push_close_wsol_if_needed, push_create_or_wrap_user_token_account,
|
||||
push_create_user_token_account,
|
||||
},
|
||||
utils::raydium_cpmm::{
|
||||
accounts, get_observation_state_pda, get_pool_pda, get_vault_account,
|
||||
SWAP_BASE_IN_DISCRIMINATOR, SWAP_BASE_OUT_DISCRIMINATOR,
|
||||
},
|
||||
},
|
||||
trading::core::{
|
||||
params::{RaydiumCpmmParams, SwapParams},
|
||||
@@ -63,53 +69,38 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let mint_token_program = if is_base_in {
|
||||
let input_mint =
|
||||
if is_base_in { protocol_params.base_mint } else { protocol_params.quote_mint };
|
||||
let input_token_program = if is_base_in {
|
||||
protocol_params.base_token_program
|
||||
} else {
|
||||
protocol_params.quote_token_program
|
||||
};
|
||||
let output_mint =
|
||||
if is_base_in { protocol_params.quote_mint } else { protocol_params.base_mint };
|
||||
let output_token_program = if is_base_in {
|
||||
protocol_params.quote_token_program
|
||||
} else {
|
||||
protocol_params.base_token_program
|
||||
};
|
||||
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let result = compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let minimum_amount_out = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => result.min_amount_out,
|
||||
};
|
||||
|
||||
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&mint_token_program,
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
let input_vault_account = get_vault_account(
|
||||
&pool_state,
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
protocol_params,
|
||||
);
|
||||
let output_vault_account =
|
||||
get_vault_account(&pool_state, ¶ms.output_mint, protocol_params);
|
||||
let input_vault_account = get_vault_account(&pool_state, &input_mint, protocol_params);
|
||||
let output_vault_account = get_vault_account(&pool_state, &output_mint, protocol_params);
|
||||
|
||||
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
|
||||
get_observation_state_pda(&pool_state).unwrap()
|
||||
@@ -123,19 +114,23 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if params.create_input_mint_ata {
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), amount_in));
|
||||
push_create_or_wrap_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
amount_in,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&mint_token_program,
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,27 +138,37 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
let accounts: [AccountMeta; 13] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new(pool_state, false), // Pool State
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
crate::constants::TOKEN_PROGRAM_META, // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(mint_token_program, false), // Output Token Program (readonly)
|
||||
if is_wsol {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
crate::constants::USDC_TOKEN_ACCOUNT_META
|
||||
}, // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(params.output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
AccountMeta::new_readonly(input_token_program, false), // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(output_token_program, false), // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(input_mint, false), // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_base_in,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_CPMM,
|
||||
@@ -172,8 +177,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_input_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &input_mint);
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
@@ -219,54 +223,36 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
let is_quote_out = protocol_params.quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let mint_token_program = if is_quote_out {
|
||||
let input_mint =
|
||||
if is_quote_out { protocol_params.base_mint } else { protocol_params.quote_mint };
|
||||
let input_token_program = if is_quote_out {
|
||||
protocol_params.base_token_program
|
||||
} else {
|
||||
protocol_params.quote_token_program
|
||||
};
|
||||
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => {
|
||||
compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_quote_out,
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out
|
||||
}
|
||||
let output_mint =
|
||||
if is_quote_out { protocol_params.quote_mint } else { protocol_params.base_mint };
|
||||
let output_token_program = if is_quote_out {
|
||||
protocol_params.quote_token_program
|
||||
} else {
|
||||
protocol_params.base_token_program
|
||||
};
|
||||
|
||||
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.input_mint,
|
||||
&mint_token_program,
|
||||
&input_mint,
|
||||
&input_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
let output_vault_account = get_vault_account(
|
||||
&pool_state,
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
protocol_params,
|
||||
);
|
||||
let input_vault_account =
|
||||
get_vault_account(&pool_state, ¶ms.input_mint, protocol_params);
|
||||
let output_vault_account = get_vault_account(&pool_state, &output_mint, protocol_params);
|
||||
let input_vault_account = get_vault_account(&pool_state, &input_mint, protocol_params);
|
||||
|
||||
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
|
||||
get_observation_state_pda(&pool_state).unwrap()
|
||||
@@ -277,37 +263,54 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(3);
|
||||
let mut instructions = Vec::with_capacity(4);
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(crate::trading::common::create_wsol_ata(¶ms.payer.pubkey()));
|
||||
push_create_user_token_account(
|
||||
&mut instructions,
|
||||
¶ms.payer.pubkey(),
|
||||
&output_mint,
|
||||
&output_token_program,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
let accounts: [AccountMeta; 13] = [
|
||||
AccountMeta::new(params.payer.pubkey(), true), // Payer (signer)
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new_readonly(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new(pool_state, false), // Pool State
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
AccountMeta::new_readonly(mint_token_program, false), // Input Token Program (readonly)
|
||||
crate::constants::TOKEN_PROGRAM_META, // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(params.input_mint, false), // Input token mint (readonly)
|
||||
if is_wsol {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
crate::constants::USDC_TOKEN_ACCOUNT_META
|
||||
}, // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
AccountMeta::new_readonly(input_token_program, false), // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(output_token_program, false), // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(input_mint, false), // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
];
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
let amount_in = params.input_amount.unwrap_or(0);
|
||||
if let Some(amount_out) = params.fixed_output_amount {
|
||||
data[..8].copy_from_slice(&SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&amount_out.to_le_bytes());
|
||||
} else {
|
||||
let minimum_amount_out = compute_swap_amount(
|
||||
protocol_params.base_reserve,
|
||||
protocol_params.quote_reserve,
|
||||
is_quote_out,
|
||||
amount_in,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
)
|
||||
.min_amount_out;
|
||||
data[..8].copy_from_slice(&SWAP_BASE_IN_DISCRIMINATOR);
|
||||
data[8..16].copy_from_slice(&amount_in.to_le_bytes());
|
||||
data[16..24].copy_from_slice(&minimum_amount_out.to_le_bytes());
|
||||
}
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::RAYDIUM_CPMM,
|
||||
@@ -316,12 +319,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
));
|
||||
|
||||
if params.close_output_mint_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), &output_mint);
|
||||
}
|
||||
if params.close_input_mint_ata {
|
||||
instructions.push(crate::common::spl_token::close_account(
|
||||
&mint_token_program,
|
||||
&input_token_program,
|
||||
&input_token_account,
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
@@ -332,3 +334,120 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::GasFeeStrategy,
|
||||
swqos::TradeType,
|
||||
trading::core::params::{DexParamEnum, SwapParams},
|
||||
};
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn pk(seed: u8) -> Pubkey {
|
||||
Pubkey::new_from_array([seed; 32])
|
||||
}
|
||||
|
||||
fn cpmm_params() -> RaydiumCpmmParams {
|
||||
RaydiumCpmmParams {
|
||||
pool_state: pk(1),
|
||||
amm_config: pk(2),
|
||||
base_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
quote_mint: pk(3),
|
||||
base_reserve: 1_000_000_000,
|
||||
quote_reserve: 2_000_000_000,
|
||||
base_vault: pk(4),
|
||||
quote_vault: pk(5),
|
||||
base_token_program: crate::constants::TOKEN_PROGRAM,
|
||||
quote_token_program: crate::constants::TOKEN_PROGRAM,
|
||||
observation_state: pk(6),
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_params(fixed_output_amount: Option<u64>) -> SwapParams {
|
||||
SwapParams {
|
||||
rpc: None,
|
||||
payer: Arc::new(Keypair::new()),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
input_token_program: None,
|
||||
output_mint: pk(3),
|
||||
output_token_program: None,
|
||||
input_amount: Some(100_000),
|
||||
slippage_basis_points: Some(100),
|
||||
address_lookup_table_account: None,
|
||||
recent_blockhash: None,
|
||||
wait_tx_confirmed: false,
|
||||
protocol_params: DexParamEnum::RaydiumCpmm(cpmm_params()),
|
||||
open_seed_optimize: true,
|
||||
swqos_clients: Arc::new(Vec::new()),
|
||||
middleware_manager: None,
|
||||
durable_nonce: None,
|
||||
with_tip: false,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: false,
|
||||
create_output_mint_ata: false,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount,
|
||||
gas_fee_strategy: GasFeeStrategy::new(),
|
||||
simulate: true,
|
||||
log_enabled: false,
|
||||
use_dedicated_sender_threads: false,
|
||||
sender_thread_cores: None,
|
||||
max_sender_concurrency: 0,
|
||||
effective_core_ids: Arc::new(Vec::new()),
|
||||
check_min_tip: false,
|
||||
grpc_recv_us: None,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_cpmm_uses_base_in_and_readonly_amm_config_by_default() {
|
||||
let instructions =
|
||||
RaydiumCpmmInstructionBuilder.build_buy_instructions(&swap_params(None)).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], SWAP_BASE_IN_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts[2].pubkey, pk(2));
|
||||
assert!(!ix.accounts[2].is_writable);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_cpmm_uses_base_output_when_fixed_output_is_set() {
|
||||
let instructions = RaydiumCpmmInstructionBuilder
|
||||
.build_buy_instructions(&swap_params(Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], SWAP_BASE_OUT_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raydium_cpmm_usdc_buy_create_input_uses_usdc_accounts() {
|
||||
let mut protocol_params = cpmm_params();
|
||||
protocol_params.base_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
protocol_params.quote_mint = pk(3);
|
||||
|
||||
let mut params = swap_params(Some(42));
|
||||
params.protocol_params = DexParamEnum::RaydiumCpmm(protocol_params);
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.open_seed_optimize = false;
|
||||
|
||||
let instructions =
|
||||
RaydiumCpmmInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let create_ix = instructions.first().unwrap();
|
||||
let swap_ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||
assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
assert_eq!(swap_ix.accounts[10].pubkey, crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_create_user_token_account(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
use_seed: bool,
|
||||
) {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
payer,
|
||||
payer,
|
||||
mint,
|
||||
token_program,
|
||||
use_seed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_create_or_wrap_user_token_account(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
amount: u64,
|
||||
use_seed: bool,
|
||||
) {
|
||||
if *mint == crate::constants::WSOL_TOKEN_ACCOUNT {
|
||||
instructions.extend(crate::trading::common::handle_wsol(payer, amount));
|
||||
} else {
|
||||
push_create_user_token_account(instructions, payer, mint, token_program, use_seed);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn push_close_wsol_if_needed(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
) {
|
||||
if *mint == crate::constants::WSOL_TOKEN_ACCOUNT {
|
||||
instructions.extend(crate::trading::common::close_wsol(payer));
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,9 @@ pub mod accounts {
|
||||
}
|
||||
|
||||
pub const BUY_EXECT_IN_DISCRIMINATOR: [u8; 8] = [250, 234, 13, 123, 213, 156, 19, 236];
|
||||
pub const BUY_EXECT_OUT_DISCRIMINATOR: [u8; 8] = [24, 211, 116, 40, 105, 3, 153, 56];
|
||||
pub const SELL_EXECT_IN_DISCRIMINATOR: [u8; 8] = [149, 39, 222, 155, 211, 124, 152, 26];
|
||||
pub const SELL_EXECT_OUT_DISCRIMINATOR: [u8; 8] = [95, 200, 71, 34, 8, 9, 11, 166];
|
||||
|
||||
pub async fn fetch_pool_state(
|
||||
rpc: &SolanaRpcClient,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod accounts {
|
||||
|
||||
pub const AUTHORITY: Pubkey = pubkey!("HLnpSz9h2S4hiLQ43rnSD9XkcUThA7B8hQMKmDaiTLcC");
|
||||
pub const METEORA_DAMM_V2: Pubkey = pubkey!("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG");
|
||||
pub const SYSVAR_INSTRUCTIONS: Pubkey = pubkey!("Sysvar1nstructions1111111111111111111111111");
|
||||
|
||||
// META
|
||||
|
||||
@@ -32,9 +33,20 @@ pub mod accounts {
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
pub const SYSVAR_INSTRUCTIONS_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: SYSVAR_INSTRUCTIONS,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
}
|
||||
|
||||
pub const SWAP_DISCRIMINATOR: &[u8] = &[248, 198, 158, 145, 225, 117, 135, 200];
|
||||
pub const SWAP2_DISCRIMINATOR: &[u8] = &[65, 75, 63, 76, 235, 91, 91, 136];
|
||||
pub const SWAP_MODE_EXACT_IN: u8 = 0;
|
||||
pub const SWAP_MODE_PARTIAL_FILL: u8 = 1;
|
||||
pub const SWAP_MODE_EXACT_OUT: u8 = 2;
|
||||
|
||||
pub async fn fetch_pool(
|
||||
rpc: &SolanaRpcClient,
|
||||
|
||||
@@ -11,7 +11,10 @@ use parking_lot::RwLock;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::warn;
|
||||
|
||||
@@ -205,6 +208,7 @@ struct CachedGlobalConfig {
|
||||
|
||||
static GLOBAL_CONFIG_CACHE: Lazy<RwLock<Option<CachedGlobalConfig>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
static GLOBAL_CONFIG_REFRESH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
|
||||
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
|
||||
@@ -294,8 +298,12 @@ pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
|
||||
.as_ref()
|
||||
.map(|c| c.fetched_at.elapsed() > PUMPSWAP_GLOBAL_CONFIG_TTL)
|
||||
.unwrap_or(true);
|
||||
if stale {
|
||||
let _ = refresh_global_config_once(rpc.as_ref()).await;
|
||||
if stale && !GLOBAL_CONFIG_REFRESH_IN_FLIGHT.swap(true, Ordering::AcqRel) {
|
||||
let rpc = Arc::clone(rpc);
|
||||
tokio::spawn(async move {
|
||||
let _ = refresh_global_config_once(rpc.as_ref()).await;
|
||||
GLOBAL_CONFIG_REFRESH_IN_FLIGHT.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
instruction::utils::raydium_amm_v4_types::{amm_info_decode, AmmInfo},
|
||||
instruction::utils::raydium_amm_v4_types::{
|
||||
amm_info_decode, market_state_decode, AmmInfo, MarketState,
|
||||
},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
@@ -40,3 +42,25 @@ pub async fn fetch_amm_info(rpc: &SolanaRpcClient, amm: Pubkey) -> Result<AmmInf
|
||||
amm_info_decode(&amm_info).ok_or_else(|| anyhow!("Failed to decode amm info"))?;
|
||||
Ok(amm_info)
|
||||
}
|
||||
|
||||
pub async fn fetch_market_state(
|
||||
rpc: &SolanaRpcClient,
|
||||
market: Pubkey,
|
||||
) -> Result<MarketState, anyhow::Error> {
|
||||
let market_data = rpc.get_account_data(&market).await?;
|
||||
market_state_decode(&market_data).ok_or_else(|| anyhow!("Failed to decode market state"))
|
||||
}
|
||||
|
||||
pub fn derive_serum_vault_signer(
|
||||
serum_program: &Pubkey,
|
||||
serum_market: &Pubkey,
|
||||
vault_signer_nonce: u64,
|
||||
) -> Result<Pubkey, anyhow::Error> {
|
||||
let nonce = vault_signer_nonce.to_le_bytes();
|
||||
Pubkey::create_program_address(&[serum_market.as_ref(), &nonce], serum_program)
|
||||
.or_else(|_| {
|
||||
let legacy_nonce = [vault_signer_nonce as u8];
|
||||
Pubkey::create_program_address(&[serum_market.as_ref(), &legacy_nonce], serum_program)
|
||||
})
|
||||
.map_err(|err| anyhow!("Failed to derive Serum vault signer: {}", err))
|
||||
}
|
||||
|
||||
@@ -77,3 +77,38 @@ pub fn amm_info_decode(data: &[u8]) -> Option<AmmInfo> {
|
||||
}
|
||||
borsh::from_slice::<AmmInfo>(&data[..AMM_INFO_SIZE]).ok()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct MarketState {
|
||||
pub padding: [u8; 5],
|
||||
pub account_flags: u64,
|
||||
pub own_address: Pubkey,
|
||||
pub vault_signer_nonce: u64,
|
||||
pub coin_mint: Pubkey,
|
||||
pub pc_mint: Pubkey,
|
||||
pub serum_coin_vault_account: Pubkey,
|
||||
pub coin_deposits_total: u64,
|
||||
pub coin_fees_accrued: u64,
|
||||
pub serum_pc_vault_account: Pubkey,
|
||||
pub pc_deposits_total: u64,
|
||||
pub pc_fees_accrued: u64,
|
||||
pub pc_dust_threshold: u64,
|
||||
pub request_queue: Pubkey,
|
||||
pub serum_event_queue: Pubkey,
|
||||
pub serum_bids: Pubkey,
|
||||
pub serum_asks: Pubkey,
|
||||
pub coin_lot_size: u64,
|
||||
pub pc_lot_size: u64,
|
||||
pub fee_rate_bps: u64,
|
||||
pub referrer_rebate_accrued: u64,
|
||||
pub padding2: [u8; 7],
|
||||
}
|
||||
|
||||
pub const MARKET_STATE_SIZE: usize = 388;
|
||||
|
||||
pub fn market_state_decode(data: &[u8]) -> Option<MarketState> {
|
||||
if data.len() < MARKET_STATE_SIZE {
|
||||
return None;
|
||||
}
|
||||
borsh::from_slice::<MarketState>(&data[..MARKET_STATE_SIZE]).ok()
|
||||
}
|
||||
|
||||
@@ -288,7 +288,10 @@ impl OptimizationFlags {
|
||||
"+popcnt".to_string(),
|
||||
];
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
let target_features = vec!["+neon".to_string()];
|
||||
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
let target_features = vec![];
|
||||
Self {
|
||||
opt_level: OptLevel::Aggressive,
|
||||
|
||||
@@ -172,10 +172,17 @@ impl SIMDMemoryOps {
|
||||
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
match len {
|
||||
1 => *a == *b,
|
||||
2 => *(a as *const u16) == *(b as *const u16),
|
||||
3 => *(a as *const u16) == *(b as *const u16) && *a.add(2) == *b.add(2),
|
||||
4 => *(a as *const u32) == *(b as *const u32),
|
||||
5..=8 => *(a as *const u64) == *(b as *const u64),
|
||||
2 => ptr::read_unaligned(a as *const u16) == ptr::read_unaligned(b as *const u16),
|
||||
3 => {
|
||||
ptr::read_unaligned(a as *const u16) == ptr::read_unaligned(b as *const u16)
|
||||
&& *a.add(2) == *b.add(2)
|
||||
}
|
||||
4 => ptr::read_unaligned(a as *const u32) == ptr::read_unaligned(b as *const u32),
|
||||
5..=7 => {
|
||||
ptr::read_unaligned(a as *const u32) == ptr::read_unaligned(b as *const u32)
|
||||
&& (4..len).all(|i| *a.add(i) == *b.add(i))
|
||||
}
|
||||
8 => ptr::read_unaligned(a as *const u64) == ptr::read_unaligned(b as *const u64),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl Default for SyscallBypassConfig {
|
||||
|
||||
pub struct SyscallBatchProcessor {
|
||||
pending_calls: crossbeam_queue::ArrayQueue<SyscallRequest>,
|
||||
_executor: tokio::runtime::Handle,
|
||||
_executor: Option<tokio::runtime::Handle>,
|
||||
batch_stats: CachePadded<AtomicU64>,
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ pub struct FastTimeProvider {
|
||||
/// 上次更新时间
|
||||
last_update: CachePadded<AtomicU64>,
|
||||
/// 启用vDSO
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
vdso_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -121,6 +122,7 @@ impl FastTimeProvider {
|
||||
/// 🚀 超快速获取当前时间 - 绕过系统调用
|
||||
#[inline(always)]
|
||||
pub fn fast_now_nanos(&self) -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
if self.vdso_enabled {
|
||||
// 使用vDSO快速获取时间
|
||||
return self.vdso_time_nanos();
|
||||
@@ -140,6 +142,7 @@ impl FastTimeProvider {
|
||||
|
||||
/// vDSO时间获取
|
||||
#[inline(always)]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn vdso_time_nanos(&self) -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@@ -371,7 +374,7 @@ impl SyscallBatchProcessor {
|
||||
/// 创建系统调用批处理器
|
||||
pub fn new(batch_size: usize) -> Result<Self> {
|
||||
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
|
||||
let executor = tokio::runtime::Handle::current();
|
||||
let executor = tokio::runtime::Handle::try_current().ok();
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Syscall batch processor created with batch size: {}", batch_size);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::sync::Arc;
|
||||
|
||||
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
|
||||
use crate::{
|
||||
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
|
||||
common::nonce_cache::DurableNonceInfo,
|
||||
trading::{
|
||||
core::transaction_pool::{acquire_builder, release_builder},
|
||||
MiddlewareManager,
|
||||
@@ -26,11 +26,10 @@ fn sol_f64_to_lamports(sol: f64) -> u64 {
|
||||
(lamports.min(u64::MAX as f64)).round() as u64
|
||||
}
|
||||
|
||||
/// Build standard RPC transaction (worker hot path).
|
||||
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/rpc/middleware.
|
||||
pub async fn build_transaction(
|
||||
/// Build signed transaction (worker hot path, no RPC).
|
||||
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/middleware.
|
||||
pub fn build_transaction(
|
||||
payer: &Arc<Keypair>,
|
||||
_rpc: Option<&Arc<SolanaRpcClient>>,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
business_instructions: &[Instruction],
|
||||
@@ -74,10 +73,9 @@ pub async fn build_transaction(
|
||||
protocol_name,
|
||||
is_buy,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_versioned_transaction(
|
||||
fn build_versioned_transaction(
|
||||
payer: &Arc<Keypair>,
|
||||
instructions: Vec<Instruction>,
|
||||
address_lookup_table_account: Option<&AddressLookupTableAccount>,
|
||||
|
||||
@@ -36,7 +36,7 @@ type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
|
||||
|
||||
use crate::{
|
||||
common::nonce_cache::DurableNonceInfo,
|
||||
common::{GasFeeStrategy, SolanaRpcClient},
|
||||
common::GasFeeStrategy,
|
||||
swqos::{SwqosClient, SwqosType, TradeType},
|
||||
trading::core::params::SenderConcurrencyConfig,
|
||||
trading::{common::build_transaction, MiddlewareManager},
|
||||
@@ -51,7 +51,6 @@ const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
|
||||
struct SwqosSharedContext {
|
||||
payer: Arc<Keypair>,
|
||||
instructions: Arc<Vec<Instruction>>,
|
||||
rpc: Option<Arc<SolanaRpcClient>>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
durable_nonce: Option<DurableNonceInfo>,
|
||||
@@ -88,7 +87,6 @@ async fn run_one_swqos_job(job: SwqosJob) {
|
||||
|
||||
let transaction = match build_transaction(
|
||||
&s.payer,
|
||||
s.rpc.as_ref(),
|
||||
job.unit_limit,
|
||||
job.unit_price,
|
||||
s.instructions.as_ref(),
|
||||
@@ -101,9 +99,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
|
||||
&job.tip_account,
|
||||
tip_amount,
|
||||
s.durable_nonce.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
) {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
s.collector.submit(TaskResult {
|
||||
@@ -436,7 +432,6 @@ impl ResultCollector {
|
||||
pub async fn execute_parallel(
|
||||
swqos_clients: &[Arc<SwqosClient>],
|
||||
payer: Arc<Keypair>,
|
||||
rpc: Option<&Arc<SolanaRpcClient>>,
|
||||
instructions: Vec<Instruction>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
@@ -471,10 +466,10 @@ pub async fn execute_parallel(
|
||||
gas_fee_strategy.get_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
|
||||
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
|
||||
for (i, swqos_client) in swqos_clients.iter().enumerate() {
|
||||
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
if !with_tip && !matches!(swqos_type, SwqosType::Default) {
|
||||
continue;
|
||||
}
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
|
||||
let min_tip = if check_tip { swqos_client.min_tip_sol() } else { 0.0 };
|
||||
for config in &gas_fee_configs {
|
||||
@@ -513,7 +508,6 @@ pub async fn execute_parallel(
|
||||
let shared = Arc::new(SwqosSharedContext {
|
||||
payer,
|
||||
instructions,
|
||||
rpc: rpc.cloned(),
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
durable_nonce,
|
||||
|
||||
@@ -162,7 +162,6 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
let result = execute_parallel(
|
||||
params.swqos_clients.as_slice(),
|
||||
params.payer,
|
||||
params.rpc.as_ref(),
|
||||
final_instructions,
|
||||
params.address_lookup_table_account,
|
||||
params.recent_blockhash,
|
||||
@@ -282,7 +281,6 @@ async fn simulate_transaction(
|
||||
|
||||
let transaction = build_transaction(
|
||||
&payer,
|
||||
Some(&rpc),
|
||||
unit_limit,
|
||||
unit_price,
|
||||
&instructions,
|
||||
@@ -295,8 +293,7 @@ async fn simulate_transaction(
|
||||
&Pubkey::default(),
|
||||
tip,
|
||||
durable_nonce.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
// Simulate the transaction
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
|
||||
@@ -75,6 +75,8 @@ pub struct SwapParams {
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
/// Fixed output amount. For protocols with exact-out instructions this selects exact-out
|
||||
/// semantics and treats `input_amount` as the maximum input budget.
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
pub simulate: bool,
|
||||
|
||||
@@ -12,6 +12,11 @@ pub struct MeteoraDammV2Params {
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
pub referral_token_account: Option<Pubkey>,
|
||||
/// `swap2` mode: 0 exact-in, 1 partial-fill (recommended default), 2 exact-out.
|
||||
pub swap_mode: u8,
|
||||
/// Include the instructions sysvar remaining account when the pool's rate limiter applies.
|
||||
pub include_rate_limiter_sysvar: bool,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
@@ -32,9 +37,27 @@ impl MeteoraDammV2Params {
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
referral_token_account: None,
|
||||
swap_mode: crate::instruction::utils::meteora_damm_v2::SWAP_MODE_PARTIAL_FILL,
|
||||
include_rate_limiter_sysvar: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_referral_token_account(mut self, referral_token_account: Pubkey) -> Self {
|
||||
self.referral_token_account = Some(referral_token_account);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_swap_mode(mut self, swap_mode: u8) -> Self {
|
||||
self.swap_mode = swap_mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_rate_limiter_sysvar(mut self, include: bool) -> Self {
|
||||
self.include_rate_limiter_sysvar = include;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
@@ -61,6 +84,9 @@ impl MeteoraDammV2Params {
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
referral_token_account: None,
|
||||
swap_mode: crate::instruction::utils::meteora_damm_v2::SWAP_MODE_PARTIAL_FILL,
|
||||
include_rate_limiter_sysvar: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,26 @@ pub struct RaydiumAmmV4Params {
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// AMM open orders account
|
||||
pub amm_open_orders: Pubkey,
|
||||
/// AMM target orders account
|
||||
pub amm_target_orders: Pubkey,
|
||||
/// Serum/OpenBook program used by the AMM market
|
||||
pub serum_program: Pubkey,
|
||||
/// Serum/OpenBook market account
|
||||
pub serum_market: Pubkey,
|
||||
/// Serum/OpenBook bids account
|
||||
pub serum_bids: Pubkey,
|
||||
/// Serum/OpenBook asks account
|
||||
pub serum_asks: Pubkey,
|
||||
/// Serum/OpenBook event queue account
|
||||
pub serum_event_queue: Pubkey,
|
||||
/// Serum/OpenBook coin vault account
|
||||
pub serum_coin_vault_account: Pubkey,
|
||||
/// Serum/OpenBook pc vault account
|
||||
pub serum_pc_vault_account: Pubkey,
|
||||
/// Serum/OpenBook vault signer PDA
|
||||
pub serum_vault_signer: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
@@ -32,13 +52,68 @@ impl RaydiumAmmV4Params {
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
Self {
|
||||
amm,
|
||||
coin_mint,
|
||||
pc_mint,
|
||||
token_coin,
|
||||
token_pc,
|
||||
amm_open_orders: Pubkey::default(),
|
||||
amm_target_orders: Pubkey::default(),
|
||||
serum_program: Pubkey::default(),
|
||||
serum_market: Pubkey::default(),
|
||||
serum_bids: Pubkey::default(),
|
||||
serum_asks: Pubkey::default(),
|
||||
serum_event_queue: Pubkey::default(),
|
||||
serum_coin_vault_account: Pubkey::default(),
|
||||
serum_pc_vault_account: Pubkey::default(),
|
||||
serum_vault_signer: Pubkey::default(),
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_market_accounts(
|
||||
mut self,
|
||||
amm_open_orders: Pubkey,
|
||||
amm_target_orders: Pubkey,
|
||||
serum_program: Pubkey,
|
||||
serum_market: Pubkey,
|
||||
serum_bids: Pubkey,
|
||||
serum_asks: Pubkey,
|
||||
serum_event_queue: Pubkey,
|
||||
serum_coin_vault_account: Pubkey,
|
||||
serum_pc_vault_account: Pubkey,
|
||||
serum_vault_signer: Pubkey,
|
||||
) -> Self {
|
||||
self.amm_open_orders = amm_open_orders;
|
||||
self.amm_target_orders = amm_target_orders;
|
||||
self.serum_program = serum_program;
|
||||
self.serum_market = serum_market;
|
||||
self.serum_bids = serum_bids;
|
||||
self.serum_asks = serum_asks;
|
||||
self.serum_event_queue = serum_event_queue;
|
||||
self.serum_coin_vault_account = serum_coin_vault_account;
|
||||
self.serum_pc_vault_account = serum_pc_vault_account;
|
||||
self.serum_vault_signer = serum_vault_signer;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let market_state =
|
||||
crate::instruction::utils::raydium_amm_v4::fetch_market_state(rpc, amm_info.market)
|
||||
.await?;
|
||||
let serum_vault_signer =
|
||||
crate::instruction::utils::raydium_amm_v4::derive_serum_vault_signer(
|
||||
&amm_info.serum_dex,
|
||||
&amm_info.market,
|
||||
market_state.vault_signer_nonce,
|
||||
)?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
@@ -47,6 +122,16 @@ impl RaydiumAmmV4Params {
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
amm_open_orders: amm_info.open_orders,
|
||||
amm_target_orders: amm_info.target_orders,
|
||||
serum_program: amm_info.serum_dex,
|
||||
serum_market: amm_info.market,
|
||||
serum_bids: market_state.serum_bids,
|
||||
serum_asks: market_state.serum_asks,
|
||||
serum_event_queue: market_state.serum_event_queue,
|
||||
serum_coin_vault_account: market_state.serum_coin_vault_account,
|
||||
serum_pc_vault_account: market_state.serum_pc_vault_account,
|
||||
serum_vault_signer,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user