Compare commits

..

1 Commits

Author SHA1 Message Date
0xfnzero 05ac079d6d 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.
2026-05-16 06:42:50 +08:00
57 changed files with 32699 additions and 588 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.8"
version = "4.0.9"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+2 -2
View File
@@ -108,14 +108,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.8"
sol-trade-sdk = "4.0.9"
```
## 🛠️ Usage Examples
+2 -3
View File
@@ -108,14 +108,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.8"
sol-trade-sdk = "4.0.9"
```
## 🛠️ 使用示例
@@ -446,4 +446,3 @@ MIT 许可证
3. 注意滑点设置避免交易失败
4. 监控余额和交易费用
5. 遵循相关法律法规
+2 -2
View File
@@ -35,7 +35,7 @@ The `TradeBuyParams` struct contains all parameters required for executing buy o
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
| `create_mint_ata` | `bool` | ✅ | Whether to create token mint ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
@@ -67,7 +67,7 @@ The `TradeSellParams` struct contains all parameters required for executing sell
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee strategy instance for controlling transaction fees and priorities |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. If set, this value will be directly assigned to the output amount instead of being calculated (required for Meteora DAMM V2) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | Optional fixed output token amount. On exact-out capable DEXes, this uses the exact-out instruction and treats input_token_amount as the max input budget (required for Meteora DAMM V2) |
| `simulate` | `bool` | ✅ | Whether to simulate the transaction instead of executing it. When true, the transaction will be simulated via RPC to validate and show detailed logs, compute units consumed, and potential errors without actually submitting to the blockchain |
+2 -2
View File
@@ -35,7 +35,7 @@
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
| `create_mint_ata` | `bool` | ✅ | 是否创建代币 mint ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出Meteora DAMM V2 必需) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算Meteora DAMM V2 必需) |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
@@ -67,7 +67,7 @@
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Gas fee 策略实例,用于控制交易费用和优先级 |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。如果设置,此值将直接分配给输出数量而不是通过计算得出Meteora DAMM V2 必需) |
| `fixed_output_token_amount` | `Option<u64>` | ❌ | 可选的固定输出代币数量。对于支持 exact-out 的 DEX,会使用 exact-out 指令,并将 input_token_amount 作为最大输入预算Meteora DAMM V2 必需) |
| `simulate` | `bool` | ✅ | 是否模拟交易而不实际执行。当为 true 时,将通过 RPC 模拟交易以验证并显示详细日志、计算单元消耗和潜在错误,而不会实际提交到区块链 |
+1 -2
View File
@@ -167,7 +167,7 @@ async fn pumpfun_copy_trade_with_grpc(
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -176,7 +176,6 @@ async fn pumpfun_copy_trade_with_grpc(
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
+2 -3
View File
@@ -170,7 +170,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: true,
@@ -179,7 +179,6 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -222,7 +221,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
with_tip: false,
durable_nonce: None,
create_output_token_ata: false,
+2 -3
View File
@@ -138,7 +138,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -147,7 +147,6 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -183,7 +182,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
trade_info.global_config,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+20 -25
View File
@@ -627,7 +627,7 @@ async fn handle_buy_pumpfun(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -636,11 +636,10 @@ async fn handle_buy_pumpfun(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from PumpFun!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -684,7 +683,7 @@ async fn handle_buy_pumpswap(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -693,11 +692,10 @@ async fn handle_buy_pumpswap(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -741,7 +739,7 @@ async fn handle_buy_bonk(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -750,11 +748,10 @@ async fn handle_buy_bonk(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Bonk!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -802,7 +799,7 @@ async fn handle_buy_raydium_v4(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -811,11 +808,10 @@ async fn handle_buy_raydium_v4(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Raydium V4!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -864,7 +860,7 @@ async fn handle_buy_raydium_cpmm(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -873,11 +869,10 @@ async fn handle_buy_raydium_cpmm(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully bought tokens from Raydium CPMM!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1035,7 +1030,7 @@ async fn handle_sell_pumpfun(
with_tip: false,
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1047,7 +1042,7 @@ async fn handle_sell_pumpfun(
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from PumpFun!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1094,7 +1089,7 @@ async fn handle_sell_pumpswap(
with_tip: false,
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1105,7 +1100,7 @@ async fn handle_sell_pumpswap(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1153,7 +1148,7 @@ async fn handle_sell_bonk(
with_tip: false,
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1164,7 +1159,7 @@ async fn handle_sell_bonk(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Bonk!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1215,7 +1210,7 @@ async fn handle_sell_raydium_v4(
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1226,7 +1221,7 @@ async fn handle_sell_raydium_v4(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Raydium V4!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -1278,7 +1273,7 @@ async fn handle_sell_raydium_cpmm(
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1289,7 +1284,7 @@ async fn handle_sell_raydium_cpmm(
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
Ok((_, signature, _, _)) => {
println!(" ✅ Successfully sold tokens from Raydium CPMM!");
println!(" ✅ Transaction Signature: {:?}", signature);
}
@@ -42,7 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
close_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
create_mint_ata: true,
@@ -51,7 +51,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -84,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_mint_token_ata: false,
+1 -2
View File
@@ -103,7 +103,7 @@ async fn test_middleware() -> AnyResult<()> {
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -112,7 +112,6 @@ async fn test_middleware() -> AnyResult<()> {
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
+1 -2
View File
@@ -163,7 +163,7 @@ async fn pumpfun_copy_trade_with_grpc(
Some(trade_info.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -172,7 +172,6 @@ async fn pumpfun_copy_trade_with_grpc(
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
+2 -3
View File
@@ -160,7 +160,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -169,7 +169,6 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -211,7 +210,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: false,
close_output_token_ata: false,
close_mint_token_ata: false,
+2 -3
View File
@@ -150,7 +150,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -159,7 +159,6 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -195,7 +194,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+2 -3
View File
@@ -41,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -50,7 +50,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -81,7 +80,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+4 -5
View File
@@ -164,7 +164,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
trade_info.cashback_fee_basis_points,
0,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -195,7 +195,7 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
trade_info.protocol_fee_recipient,
pool_data.coin_creator,
pool_data.is_cashback_coin,
trade_info.cashback_fee_basis_points,
0,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -235,7 +235,7 @@ async fn pumpswap_trade_with_grpc(
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: is_sol,
close_input_token_ata: is_sol,
create_mint_ata: true,
@@ -244,7 +244,6 @@ async fn pumpswap_trade_with_grpc(
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -277,7 +276,7 @@ async fn pumpswap_trade_with_grpc(
with_tip: false,
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: is_sol,
close_output_token_ata: is_sol,
close_mint_token_ata: false,
+12 -29
View File
@@ -8,10 +8,7 @@ use sol_trade_sdk::{
},
SolanaTrade,
};
use sol_trade_sdk::{
common::TradeConfig, instruction::utils::raydium_amm_v4::fetch_amm_info,
trading::common::get_multi_token_balances, TradeTokenType,
};
use sol_trade_sdk::{common::TradeConfig, TradeTokenType};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
@@ -131,29 +128,16 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let amm_info = fetch_amm_info(&client.infrastructure.rpc, trade_info.amm).await?;
let (coin_reserve, pc_reserve) = get_multi_token_balances(
&client.infrastructure.rpc,
&amm_info.token_coin,
&amm_info.token_pc,
)
.await?;
let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
let params =
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm)
.await?;
let mint_pubkey = if params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
{
amm_info.coin_mint
params.coin_mint
} else {
amm_info.pc_mint
params.pc_mint
};
let params = RaydiumAmmV4Params::new(
trade_info.amm,
amm_info.coin_mint,
amm_info.pc_mint,
amm_info.token_coin,
amm_info.token_pc,
coin_reserve,
pc_reserve,
);
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
@@ -161,8 +145,8 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
// Buy tokens
println!("Buying tokens from Raydium_amm_v4...");
let input_token_amount = 100_000;
let is_wsol = amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_wsol = params.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.coin_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::RaydiumAmmV4,
input_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
@@ -172,7 +156,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -181,7 +165,6 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -215,7 +198,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
with_tip: false,
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
+2 -3
View File
@@ -160,7 +160,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -169,7 +169,6 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -201,7 +200,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
with_tip: false,
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
+2 -3
View File
@@ -41,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -50,7 +50,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -84,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
wait_tx_confirmed: true,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+1 -1
View File
@@ -47,7 +47,7 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
SwqosConfig::Temporal("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::FlashBlock("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::Astralane(
"your_api_token".to_string(),
SwqosRegion::Frankfurt,
+14
View File
@@ -0,0 +1,14 @@
# IDL sources
Bitquery IDLs synced from `bitquery/solana-idl-lib` commit `f804b17`.
| SDK protocol | Root IDL | Bitquery source |
| --- | --- | --- |
| Bonk / Raydium Launchpad | `bonk.json` | `bitquery/raydium/launchpad.json` |
| Raydium CPMM | `raydium_cpmm.json` | `bitquery/raydium/raydium_cp.json` |
| Raydium AMM v4 | `raydium_amm_v4.json` | `bitquery/raydium/raydium_amm.json` |
| Meteora DAMM v2 | `meteora_damm_v2.json` | `bitquery/meteora/cp_amm_016.json` |
| PumpFun | `pump.json` | `bitquery/pumpfun/pump.json` |
| PumpSwap | `pump_amm.json` | `bitquery/pumpswap/amm.json` |
The root PumpFun/PumpSwap IDLs remain the newer local copies used for cashback and v2 instruction parity. The Bitquery snapshots are kept under `idl/bitquery/` for source comparison.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5783
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -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,
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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(
&params.payer.pubkey(),
if usd1_pool {
&crate::constants::USD1_TOKEN_ACCOUNT
} else {
&crate::constants::WSOL_TOKEN_ACCOUNT
},
&quote_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, &quote_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(&params.payer.pubkey(), amount_in));
if params.create_input_mint_ata {
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&quote_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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &quote_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(
&params.payer.pubkey(),
if usd1_pool {
&crate::constants::USD1_TOKEN_ACCOUNT
} else {
&crate::constants::WSOL_TOKEN_ACCOUNT
},
&quote_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, &quote_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(&params.payer.pubkey()));
if params.create_output_mint_ata {
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&quote_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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &quote_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(&params).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);
}
}
+287 -71
View File
@@ -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(
&params.payer.pubkey(),
&params.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(
&params.payer.pubkey(),
&params.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(&params.payer.pubkey(), amount_in));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.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(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.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,
&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.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(
&params.payer.pubkey(),
&params.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(
&params.payer.pubkey(),
&params.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(&params.payer.pubkey()));
push_create_user_token_account(
&mut instructions,
&params.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(&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.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,
&params.payer.pubkey(),
&params.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(&params).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(
&params.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(
&params.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);
}
}
+1
View File
@@ -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
View File
@@ -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(&params.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(&params.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(&params.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>> {
&quote_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(
&params.payer.pubkey(),
&params.payer.pubkey(),
&quote_mint,
&quote_token_program,
params.open_seed_optimize,
),
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&quote_mint,
&quote_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, &params.payer.pubkey(), &quote_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(&params.input_mint).ok_or_else(|| {
@@ -678,17 +680,15 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
&quote_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(
&params.payer.pubkey(),
&params.payer.pubkey(),
&quote_mint,
&quote_token_program,
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.payer.pubkey(),
&quote_mint,
&quote_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, &params.payer.pubkey(), &quote_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(&params).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(&params).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(&params).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(&params).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(&params).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(&params).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);
}
}
+8 -14
View File
@@ -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` 是单字段
//! structTypeScript 传 `[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
View File
@@ -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(
&params.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(&params.payer.pubkey(), wrap_amount));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.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(
&params.payer.pubkey(),
&params.payer.pubkey(),
if quote_is_wsol_or_usdc { &base_mint } else { &quote_mint },
if quote_is_wsol_or_usdc { &base_token_program } else { &quote_token_program },
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(
&mut instructions,
&params.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(&params.payer.pubkey()));
if create_output_ata {
push_create_user_token_account(
&mut instructions,
&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(
&mut instructions,
&params.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(&params).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);
}
}
+287 -76
View File
@@ -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(
&params.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(
&params.payer.pubkey(),
&params.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(&params.payer.pubkey(), amount_in));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.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(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.output_mint,
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.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(
&params.payer.pubkey(),
&params.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(
&params.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(&params.payer.pubkey()));
push_create_user_token_account(
&mut instructions,
&params.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(&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.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(&params).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
View File
@@ -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(
&params.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(
&params.payer.pubkey(),
&params.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, &params.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(&params.payer.pubkey(), amount_in));
push_create_or_wrap_user_token_account(
&mut instructions,
&params.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(
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.output_mint,
&mint_token_program,
params.open_seed_optimize,
),
push_create_user_token_account(
&mut instructions,
&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.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(
&params.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(
&params.payer.pubkey(),
&params.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, &params.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(&params.payer.pubkey()));
push_create_user_token_account(
&mut instructions,
&params.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(&params.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(&params.payer.pubkey()));
push_close_wsol_if_needed(&mut instructions, &params.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,
&params.payer.pubkey(),
&params.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(&params).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);
}
}
+47
View File
@@ -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));
}
}
+2
View File
@@ -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,
+12
View File
@@ -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 -3
View File
@@ -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);
});
}
}
+25 -1
View File
@@ -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()
}
+4 -1
View File
@@ -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,
+11 -4
View File
@@ -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!(),
}
}
+5 -2
View File
@@ -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);
+5 -7
View File
@@ -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>,
+4 -10
View File
@@ -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,
+1 -4
View File
@@ -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;
+2
View File
@@ -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,
})
}
}
+86 -1
View File
@@ -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,
})