feat: Add PumpSwap trading functionality module
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod address_lookup;
|
||||
pub mod nonce_cache;
|
||||
pub mod tip_cache;
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
/// PumpSwap指令类型
|
||||
#[derive(Debug)]
|
||||
pub enum PumpSwapInstruction {
|
||||
Buy(BuyEvent),
|
||||
Sell(SellEvent),
|
||||
CreatePool(CreatePoolEvent),
|
||||
Deposit(DepositEvent),
|
||||
Withdraw(WithdrawEvent),
|
||||
Disable(DisableEvent),
|
||||
UpdateAdmin(UpdateAdminEvent),
|
||||
UpdateFeeConfig(UpdateFeeConfigEvent),
|
||||
Other,
|
||||
}
|
||||
|
||||
/// 买入事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BuyEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub base_amount_out: u64,
|
||||
pub max_quote_amount_in: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub lp_fee: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub quote_amount_in_with_lp_fee: u64,
|
||||
pub user_quote_amount_in: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub protocol_fee_recipient: Pubkey,
|
||||
pub protocol_fee_recipient_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 卖出事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SellEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub base_amount_in: u64,
|
||||
pub min_quote_amount_out: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub quote_amount_out: u64,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub lp_fee: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub protocol_fee: u64,
|
||||
pub quote_amount_out_without_lp_fee: u64,
|
||||
pub user_quote_amount_out: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub protocol_fee_recipient: Pubkey,
|
||||
pub protocol_fee_recipient_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 创建池子事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct CreatePoolEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub index: u16,
|
||||
pub creator: Pubkey,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub base_mint_decimals: u8,
|
||||
pub quote_mint_decimals: u8,
|
||||
pub base_amount_in: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub pool_base_amount: u64,
|
||||
pub pool_quote_amount: u64,
|
||||
pub minimum_liquidity: u64,
|
||||
pub initial_liquidity: u64,
|
||||
pub lp_token_amount_out: u64,
|
||||
pub pool_bump: u8,
|
||||
pub pool: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 存款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct DepositEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub lp_token_amount_out: u64,
|
||||
pub max_base_amount_in: u64,
|
||||
pub max_quote_amount_in: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub base_amount_in: u64,
|
||||
pub quote_amount_in: u64,
|
||||
pub lp_mint_supply: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 提款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct WithdrawEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub lp_token_amount_in: u64,
|
||||
pub min_base_amount_out: u64,
|
||||
pub min_quote_amount_out: u64,
|
||||
pub user_base_token_reserves: u64,
|
||||
pub user_quote_token_reserves: u64,
|
||||
pub pool_base_token_reserves: u64,
|
||||
pub pool_quote_token_reserves: u64,
|
||||
pub base_amount_out: u64,
|
||||
pub quote_amount_out: u64,
|
||||
pub lp_mint_supply: u64,
|
||||
pub pool: Pubkey,
|
||||
pub user: Pubkey,
|
||||
pub user_base_token_account: Pubkey,
|
||||
pub user_quote_token_account: Pubkey,
|
||||
pub user_pool_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 禁用事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct DisableEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub admin: Pubkey,
|
||||
pub disable_create_pool: bool,
|
||||
pub disable_deposit: bool,
|
||||
pub disable_withdraw: bool,
|
||||
pub disable_buy: bool,
|
||||
pub disable_sell: bool,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 更新管理员事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct UpdateAdminEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub old_admin: Pubkey,
|
||||
pub new_admin: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 更新费用配置事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct UpdateFeeConfigEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub admin: Pubkey,
|
||||
pub old_lp_fee_basis_points: u64,
|
||||
pub new_lp_fee_basis_points: u64,
|
||||
pub old_protocol_fee_basis_points: u64,
|
||||
pub new_protocol_fee_basis_points: u64,
|
||||
pub old_protocol_fee_recipients: [Pubkey; 8],
|
||||
pub new_protocol_fee_recipients: [Pubkey; 8],
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 全局配置
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct GlobalConfig {
|
||||
pub admin: Pubkey,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub disable_flags: u8,
|
||||
pub protocol_fee_recipients: [Pubkey; 8],
|
||||
}
|
||||
|
||||
/// 池子信息
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct Pool {
|
||||
pub index: u16,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub base_mint_decimals: u8,
|
||||
pub quote_mint_decimals: u8,
|
||||
pub lp_mint_decimals: u8,
|
||||
pub base_token_account: Pubkey,
|
||||
pub quote_token_account: Pubkey,
|
||||
pub bump: u8,
|
||||
pub is_disabled: bool,
|
||||
}
|
||||
|
||||
/// 事件特性
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
fn discriminator() -> &'static [u8];
|
||||
}
|
||||
|
||||
/// 从字节中提取鉴别器
|
||||
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if data.len() < length {
|
||||
return None;
|
||||
}
|
||||
Some((&data[..length], &data[length..]))
|
||||
}
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
pub const BUY_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x67, 0xf4, 0x52, 0x1f, 0x2c, 0xf5, 0x77, 0x77];
|
||||
pub const SELL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x3e, 0x2f, 0x37, 0x0a, 0xa5, 0x03, 0xdc, 0x2a];
|
||||
pub const CREATE_POOL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xb1, 0x31, 0x0c, 0xd2, 0xa0, 0x76, 0xa7, 0x74];
|
||||
pub const DEPOSIT_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x78, 0xf8, 0x3d, 0x53, 0x1f, 0x8e, 0x6b, 0x90];
|
||||
pub const WITHDRAW_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x16, 0x09, 0x85, 0x1a, 0xa0, 0x2c, 0x47, 0xc0];
|
||||
pub const DISABLE_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x6b, 0xfd, 0xc1, 0x4c, 0xe4, 0xca, 0x1b, 0x68];
|
||||
pub const UPDATE_ADMIN_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xe1, 0x98, 0xab, 0x57, 0xf6, 0x3f, 0x42, 0xea];
|
||||
pub const UPDATE_FEE_CONFIG_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x5a, 0x17, 0x41, 0x23, 0x3e, 0xf4, 0xbc, 0xd0];
|
||||
|
||||
// 指令鉴别器
|
||||
pub const BUY_IX: &[u8] = &[102,
|
||||
6,
|
||||
61,
|
||||
18,
|
||||
1,
|
||||
218,
|
||||
235,
|
||||
234];
|
||||
pub const SELL_IX: &[u8] = &[51,
|
||||
230,
|
||||
133,
|
||||
164,
|
||||
1,
|
||||
127,
|
||||
131,
|
||||
173];
|
||||
pub const CREATE_POOL_IX: &[u8] = &[233,
|
||||
146,
|
||||
209,
|
||||
142,
|
||||
207,
|
||||
104,
|
||||
64,
|
||||
188];
|
||||
pub const DEPOSIT_IX: &[u8] = &[242,
|
||||
35,
|
||||
198,
|
||||
137,
|
||||
82,
|
||||
225,
|
||||
242,
|
||||
182];
|
||||
pub const WITHDRAW_IX: &[u8] = &[183,
|
||||
18,
|
||||
70,
|
||||
156,
|
||||
148,
|
||||
109,
|
||||
161,
|
||||
34];
|
||||
pub const DISABLE_IX: &[u8] = &[107,
|
||||
253,
|
||||
193,
|
||||
76,
|
||||
228,
|
||||
202,
|
||||
27,
|
||||
104];
|
||||
pub const UPDATE_ADMIN_IX: &[u8] = &[225,
|
||||
152,
|
||||
171,
|
||||
87,
|
||||
246,
|
||||
63,
|
||||
66,
|
||||
234];
|
||||
pub const UPDATE_FEE_CONFIG_IX: &[u8] = &[90,
|
||||
23,
|
||||
65,
|
||||
35,
|
||||
62,
|
||||
244,
|
||||
188,
|
||||
208];
|
||||
}
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use crate::common::pumpswap::logs_data::{
|
||||
BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent,
|
||||
DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators
|
||||
};
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
pub const PROGRAM_LOG_PREFIX: &str = "Program log: PumpSwap: ";
|
||||
|
||||
/// PumpSwap事件枚举
|
||||
#[derive(Debug)]
|
||||
pub enum PumpSwapEvent {
|
||||
Buy(BuyEvent),
|
||||
Sell(SellEvent),
|
||||
CreatePool(CreatePoolEvent),
|
||||
Deposit(DepositEvent),
|
||||
Withdraw(WithdrawEvent),
|
||||
Disable(DisableEvent),
|
||||
UpdateAdmin(UpdateAdminEvent),
|
||||
UpdateFeeConfig(UpdateFeeConfigEvent),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl PumpSwapEvent {
|
||||
/// 解析日志并提取PumpSwap事件
|
||||
pub fn parse_logs(logs: &[String]) -> Vec<PumpSwapEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
if logs.is_empty() {
|
||||
return events;
|
||||
}
|
||||
|
||||
for log in logs {
|
||||
// 检查是否是事件日志
|
||||
if let Some(event_data) = log.strip_prefix(PROGRAM_DATA) {
|
||||
let borsh_bytes = match general_purpose::STANDARD.decode(event_data) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// 检查鉴别器
|
||||
if borsh_bytes.len() < 16 {
|
||||
continue;
|
||||
}
|
||||
let prefix = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d];
|
||||
let discriminator = &[&prefix[..], &borsh_bytes[..8]].concat();
|
||||
let data = &borsh_bytes[8..];
|
||||
// 根据鉴别器解析不同类型的事件
|
||||
if discriminator == discriminators::BUY_EVENT {
|
||||
if let Ok(mut event) = BuyEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Buy(event));
|
||||
}
|
||||
} else if discriminator == discriminators::SELL_EVENT {
|
||||
if let Ok(mut event) = SellEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Sell(event));
|
||||
}
|
||||
} else if discriminator == discriminators::CREATE_POOL_EVENT {
|
||||
if let Ok(mut event) = CreatePoolEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::CreatePool(event));
|
||||
}
|
||||
} else if discriminator == discriminators::DEPOSIT_EVENT {
|
||||
if let Ok(mut event) = DepositEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Deposit(event));
|
||||
}
|
||||
} else if discriminator == discriminators::WITHDRAW_EVENT {
|
||||
if let Ok(mut event) = WithdrawEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Withdraw(event));
|
||||
}
|
||||
} else if discriminator == discriminators::DISABLE_EVENT {
|
||||
if let Ok(mut event) = DisableEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Disable(event));
|
||||
}
|
||||
} else if discriminator == discriminators::UPDATE_ADMIN_EVENT {
|
||||
if let Ok(mut event) = UpdateAdminEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::UpdateAdmin(event));
|
||||
}
|
||||
} else if discriminator == discriminators::UPDATE_FEE_CONFIG_EVENT {
|
||||
if let Ok(mut event) = UpdateFeeConfigEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::UpdateFeeConfig(event));
|
||||
}
|
||||
}
|
||||
} else if let Some(event_log) = log.strip_prefix(PROGRAM_LOG_PREFIX) {
|
||||
// 处理程序日志中的事件信息
|
||||
if event_log.contains("BuyEvent") {
|
||||
// 这里可以添加从日志文本中解析事件的逻辑
|
||||
// 例如使用正则表达式提取关键信息
|
||||
} else if event_log.contains("SellEvent") {
|
||||
// 同上
|
||||
}
|
||||
// 其他事件类型...
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
}
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
use crate::common::pumpswap::logs_data::PumpSwapInstruction;
|
||||
use crate::common::pumpswap::logs_parser::parse_pumpswap_instruction;
|
||||
use crate::common::pumpswap::logs_events::PumpSwapEvent;
|
||||
use crate::constants::pumpswap::accounts;
|
||||
use crate::error::ClientResult;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
pub struct LogFilter;
|
||||
|
||||
impl LogFilter {
|
||||
/// 解析PumpSwap编译后的指令并返回指令类型和数据
|
||||
pub fn parse_pumpswap_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction) -> ClientResult<Vec<PumpSwapInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
let pump_index = accounts.iter().position(|key| key == &program_id);
|
||||
let mut instructions: Vec<PumpSwapInstruction> = Vec::new();
|
||||
|
||||
if let Some(index) = pump_index {
|
||||
for instruction in compiled_instructions {
|
||||
if instruction.program_id_index as usize == index {
|
||||
let all_accounts_valid = instruction.accounts.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
if !all_accounts_valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) = parse_pumpswap_instruction(instruction, accounts) {
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
/// 解析PumpSwap交易日志并返回事件
|
||||
pub fn parse_pumpswap_logs(logs: &[String]) -> Vec<PumpSwapEvent> {
|
||||
PumpSwapEvent::parse_logs(logs)
|
||||
}
|
||||
}
|
||||
Executable
+292
@@ -0,0 +1,292 @@
|
||||
use crate::error::ClientResult;
|
||||
use crate::common::pumpswap::{
|
||||
logs_data::{
|
||||
PumpSwapInstruction,
|
||||
BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent,
|
||||
DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators
|
||||
},
|
||||
logs_events::PumpSwapEvent
|
||||
};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 处理PumpSwap日志并调用回调函数
|
||||
pub async fn process_pumpswap_logs<F>(
|
||||
signature: &str,
|
||||
logs: Vec<String>,
|
||||
slot: Option<u64>,
|
||||
callback: F,
|
||||
) -> ClientResult<()>
|
||||
where
|
||||
F: Fn(&str, PumpSwapEvent) + Send + Sync,
|
||||
{
|
||||
let events = PumpSwapEvent::parse_logs(&logs);
|
||||
for mut event in events {
|
||||
// 设置签名和slot
|
||||
match &mut event {
|
||||
PumpSwapEvent::Buy(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Sell(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::CreatePool(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Deposit(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Withdraw(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Disable(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::UpdateAdmin(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::UpdateFeeConfig(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
callback(signature, event);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前时间戳
|
||||
fn current_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// 从指令中解析PumpSwap指令
|
||||
pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> Option<PumpSwapInstruction> {
|
||||
if instruction.data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let discriminator = &instruction.data[..8];
|
||||
let data = &instruction.data[8..];
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::BUY_IX => {
|
||||
// buy指令参数: base_amount_out: u64, max_quote_amount_in: u64
|
||||
// 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account,
|
||||
// user_quote_token_account, pool_base_token_account, pool_quote_token_account,
|
||||
// protocol_fee_recipient, protocol_fee_recipient_token_account, ...
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let base_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let max_quote_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Buy(BuyEvent {
|
||||
base_amount_out,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::SELL_IX => {
|
||||
// sell指令参数: base_amount_in: u64, min_quote_amount_out: u64
|
||||
// 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account,
|
||||
// user_quote_token_account, pool_base_token_account, pool_quote_token_account,
|
||||
// protocol_fee_recipient, protocol_fee_recipient_token_account, ...
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let base_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let min_quote_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Sell(SellEvent {
|
||||
base_amount_in,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::CREATE_POOL_IX => {
|
||||
// create_pool指令参数: index: u16, base_amount_in: u64, quote_amount_in: u64
|
||||
// 账户顺序:pool, global_config, creator, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
if data.len() < 18 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let index = u16::from_le_bytes(data[0..2].try_into().ok()?);
|
||||
let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?);
|
||||
let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::CreatePool(CreatePoolEvent {
|
||||
index,
|
||||
base_amount_in,
|
||||
quote_amount_in,
|
||||
pool: accounts[0],
|
||||
creator: accounts[2],
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
lp_mint: accounts[5],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::DEPOSIT_IX => {
|
||||
// deposit指令参数: lp_token_amount_out: u64, max_base_amount_in: u64, max_quote_amount_in: u64
|
||||
// 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
if data.len() < 24 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Deposit(DepositEvent {
|
||||
lp_token_amount_out,
|
||||
max_base_amount_in,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::WITHDRAW_IX => {
|
||||
// withdraw指令参数: lp_token_amount_in: u64, min_base_amount_out: u64, min_quote_amount_out: u64
|
||||
// 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
if data.len() < 24 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Withdraw(WithdrawEvent {
|
||||
lp_token_amount_in,
|
||||
min_base_amount_out,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::DISABLE_IX => {
|
||||
// disable指令参数: disable_create_pool: bool, disable_deposit: bool, disable_withdraw: bool, disable_buy: bool, disable_sell: bool
|
||||
// 账户顺序:admin, global_config, event_authority, program
|
||||
if data.len() < 5 || accounts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let disable_create_pool = data[0] != 0;
|
||||
let disable_deposit = data[1] != 0;
|
||||
let disable_withdraw = data[2] != 0;
|
||||
let disable_buy = data[3] != 0;
|
||||
let disable_sell = data[4] != 0;
|
||||
|
||||
Some(PumpSwapInstruction::Disable(DisableEvent {
|
||||
disable_create_pool,
|
||||
disable_deposit,
|
||||
disable_withdraw,
|
||||
disable_buy,
|
||||
disable_sell,
|
||||
admin: accounts[0],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::UPDATE_ADMIN_IX => {
|
||||
// update_admin指令参数: 无
|
||||
// 账户顺序:admin, global_config, new_admin, event_authority, program
|
||||
if accounts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
Some(PumpSwapInstruction::UpdateAdmin(UpdateAdminEvent {
|
||||
old_admin: accounts[0],
|
||||
new_admin: accounts[2],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::UPDATE_FEE_CONFIG_IX => {
|
||||
// update_fee_config指令参数: lp_fee_basis_points: u64, protocol_fee_basis_points: u64, protocol_fee_recipients: [pubkey; 8]
|
||||
// 账户顺序:admin, global_config, event_authority, program
|
||||
if data.len() < 272 || accounts.len() < 2 { // 8 + 8 + 32*8 = 272 bytes
|
||||
return None;
|
||||
}
|
||||
let lp_fee_basis_points = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let protocol_fee_basis_points = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
let mut protocol_fee_recipients = [Pubkey::default(); 8];
|
||||
for i in 0..8 {
|
||||
let start = 16 + i * 32;
|
||||
let end = start + 32;
|
||||
if let Ok(pubkey_bytes) = data[start..end].try_into() {
|
||||
protocol_fee_recipients[i] = Pubkey::new_from_array(pubkey_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PumpSwapInstruction::UpdateFeeConfig(UpdateFeeConfigEvent {
|
||||
admin: accounts[0],
|
||||
new_lp_fee_basis_points: lp_fee_basis_points,
|
||||
new_protocol_fee_basis_points: protocol_fee_basis_points,
|
||||
new_protocol_fee_recipients: protocol_fee_recipients,
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
_ => Some(PumpSwapInstruction::Other),
|
||||
}
|
||||
}
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
use solana_client::{
|
||||
nonblocking::pubsub_client::PubsubClient,
|
||||
rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}
|
||||
};
|
||||
|
||||
use solana_sdk::commitment_config::CommitmentConfig;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use futures::StreamExt;
|
||||
use crate::common::pumpswap::{
|
||||
logs_events::PumpSwapEvent,
|
||||
logs_filters::LogFilter
|
||||
};
|
||||
use crate::constants::pumpswap::accounts;
|
||||
|
||||
/// 订阅句柄,包含任务和取消订阅逻辑
|
||||
pub struct SubscriptionHandle {
|
||||
pub task: JoinHandle<()>,
|
||||
pub unsub_fn: Box<dyn Fn() + Send>,
|
||||
}
|
||||
|
||||
impl SubscriptionHandle {
|
||||
pub async fn shutdown(self) {
|
||||
(self.unsub_fn)();
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建PubSub客户端
|
||||
pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient {
|
||||
PubsubClient::new(ws_url).await.unwrap()
|
||||
}
|
||||
|
||||
/// 启动PumpSwap代币订阅
|
||||
pub async fn tokens_subscription<F>(
|
||||
ws_url: &str,
|
||||
commitment: CommitmentConfig,
|
||||
callback: F,
|
||||
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
|
||||
where
|
||||
F: Fn(PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
// 使用constants中定义的AMM_PROGRAM
|
||||
let program_address = accounts::AMM_PROGRAM.to_string();
|
||||
let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]);
|
||||
|
||||
let logs_config = RpcTransactionLogsConfig {
|
||||
commitment: Some(commitment),
|
||||
};
|
||||
|
||||
// 创建PubsubClient
|
||||
let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap());
|
||||
|
||||
let sub_client_clone = Arc::clone(&sub_client);
|
||||
|
||||
// 创建用于取消订阅的通道
|
||||
let (unsub_tx, _) = mpsc::channel(1);
|
||||
|
||||
// 启动订阅任务
|
||||
let task = tokio::spawn(async move {
|
||||
let (mut stream, _) = sub_client_clone.logs_subscribe(logs_filter, logs_config).await.unwrap();
|
||||
|
||||
loop {
|
||||
let msg = stream.next().await;
|
||||
match msg {
|
||||
Some(msg) => {
|
||||
if let Some(_err) = msg.value.err {
|
||||
continue;
|
||||
}
|
||||
|
||||
let events = LogFilter::parse_pumpswap_logs(&msg.value.logs);
|
||||
for mut event in events {
|
||||
// 设置签名和slot
|
||||
match &mut event {
|
||||
PumpSwapEvent::Buy(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::Sell(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::CreatePool(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::Deposit(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::Withdraw(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::Disable(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::UpdateAdmin(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
PumpSwapEvent::UpdateFeeConfig(e) => {
|
||||
e.signature = msg.value.signature.clone();
|
||||
e.slot = msg.context.slot;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("PumpSwap subscription stream ended");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 返回订阅句柄和取消订阅逻辑
|
||||
Ok(SubscriptionHandle {
|
||||
task,
|
||||
unsub_fn: Box::new(move || {
|
||||
let _ = unsub_tx.try_send(());
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 停止订阅
|
||||
pub async fn stop_subscription(handle: SubscriptionHandle) {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_subscribe;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_subscribe::*;
|
||||
pub use logs_events::*;
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod pumpfun;
|
||||
pub use pumpfun::*;
|
||||
pub mod pumpswap;
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
//! Constants used by the crate.
|
||||
//!
|
||||
//! This module contains various constants used throughout the crate, including:
|
||||
//!
|
||||
//! - Seeds for deriving Program Derived Addresses (PDAs)
|
||||
//! - Program account addresses and public keys
|
||||
//!
|
||||
//! The constants are organized into submodules for better organization:
|
||||
//!
|
||||
//! - `seeds`: Contains seed values used for PDA derivation
|
||||
//! - `accounts`: Contains important program account addresses
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
/// Seed for the global state PDA
|
||||
pub const GLOBAL_SEED: &[u8] = b"global";
|
||||
|
||||
/// Seed for the mint authority PDA
|
||||
pub const MINT_AUTHORITY_SEED: &[u8] = b"mint-authority";
|
||||
|
||||
/// Seed for bonding curve PDAs
|
||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||
|
||||
/// Seed for metadata PDAs
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Public key for the fee recipient
|
||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
|
||||
pub const FEE_RECIPIENT_ATA:Pubkey = pubkey!("94qWNrtmfn42h3ZjUZwWvK1MEo9uVmmrBPd2hpNjYDjb");
|
||||
|
||||
/// Public key for the global PDA
|
||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw");
|
||||
|
||||
/// Authority for program events
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR");
|
||||
|
||||
pub const WSOL_TOKEN_ACCOUNT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
|
||||
|
||||
/// System Program ID
|
||||
pub const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111");
|
||||
|
||||
/// Token Program ID
|
||||
pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
||||
|
||||
/// Associated Token Program ID
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
|
||||
// PumpSwap 协议费用接收者
|
||||
pub const PROTOCOL_FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
|
||||
/// Rent Sysvar ID
|
||||
pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111");
|
||||
|
||||
pub const JITO_TIP_ACCOUNTS: &[&str] = &[
|
||||
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
|
||||
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
|
||||
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
|
||||
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
|
||||
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
|
||||
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
|
||||
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
|
||||
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
|
||||
];
|
||||
|
||||
|
||||
/// Tip accounts
|
||||
pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[
|
||||
"NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
|
||||
"NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
|
||||
"NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
|
||||
"NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
|
||||
"neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
|
||||
"nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
|
||||
"NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
|
||||
"nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"
|
||||
];
|
||||
|
||||
pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[
|
||||
"Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3",
|
||||
"FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe",
|
||||
"ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13",
|
||||
"6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK",
|
||||
"Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr",
|
||||
];
|
||||
|
||||
pub const NOZOMI_TIP_ACCOUNTS: &[&str] = &[
|
||||
"TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq",
|
||||
"noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4",
|
||||
"noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE",
|
||||
"noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo",
|
||||
"noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ",
|
||||
"nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L",
|
||||
"nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z",
|
||||
"nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu",
|
||||
"noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7",
|
||||
"nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP",
|
||||
"nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P",
|
||||
"nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge",
|
||||
"nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3",
|
||||
"nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ",
|
||||
"nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk",
|
||||
"nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne",
|
||||
"nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb"
|
||||
];
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
|
||||
|
||||
}
|
||||
|
||||
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
pub mod trade {
|
||||
pub const TRADER_TIP_AMOUNT: u64 = 100000; // 0.0001 SOL in lamports
|
||||
pub const DEFAULT_SLIPPAGE: u64 = 1000; // 10%
|
||||
pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000;
|
||||
pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000;
|
||||
pub const DEFAULT_BUY_TIP_FEE: u64 = 600000; // 0.0006 SOL in lamports
|
||||
pub const DEFAULT_SELL_TIP_FEE: u64 = 100000; // 0.0001 SOL in lamports
|
||||
}
|
||||
@@ -7,12 +7,15 @@ use tonic::transport::Channel;
|
||||
use log::error;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
use crate::common::pumpswap::PumpSwapInstruction;
|
||||
use crate::common::AnyResult;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use crate::common::pumpfun::logs_data::DexInstruction;
|
||||
use crate::common::pumpfun::logs_events::PumpfunEvent;
|
||||
use crate::common::pumpswap::logs_events::PumpSwapEvent;
|
||||
use crate::common::pumpfun::logs_filters::LogFilter;
|
||||
use crate::common::pumpswap::logs_filters::LogFilter as PumpswapLogFilter;
|
||||
use crate::swqos::jito_grpc::shredstream::shredstream_proxy_client::ShredstreamProxyClient;
|
||||
use crate::swqos::jito_grpc::shredstream::SubscribeEntriesRequest;
|
||||
|
||||
@@ -77,6 +80,47 @@ impl ShredStreamGrpc {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shredstream_subscribe_pumpswap<F>(&self, callback: F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let request = tonic::Request::new(SubscribeEntriesRequest {});
|
||||
let mut client = (*self.shredstream_client).clone();
|
||||
let mut stream = client.subscribe_entries(request).await?.into_inner();
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionWithSlot>(CHANNEL_SIZE);
|
||||
let callback = Box::new(callback);
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Ok(entries) = bincode::deserialize::<Vec<Entry>>(&msg.entries) {
|
||||
for entry in entries {
|
||||
for transaction in entry.transactions {
|
||||
let _ = tx.try_send(TransactionWithSlot {
|
||||
transaction: transaction.clone(),
|
||||
slot: msg.slot,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(transaction_with_slot) = rx.next().await {
|
||||
if let Err(e) = Self::process_pumpswap_transaction(transaction_with_slot, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_pumpfun_transaction<F>(transaction_with_slot: TransactionWithSlot, callback: &F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync,
|
||||
@@ -111,4 +155,51 @@ impl ShredStreamGrpc {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_pumpswap_transaction<F>(transaction_with_slot: TransactionWithSlot, callback: &F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(PumpSwapEvent) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_with_slot.slot;
|
||||
let versioned_tx = transaction_with_slot.transaction;
|
||||
let instructions = PumpswapLogFilter::parse_pumpswap_compiled_instruction(versioned_tx).unwrap();
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
PumpSwapInstruction::CreatePool(mut create_event) => {
|
||||
create_event.slot = slot;
|
||||
callback(PumpSwapEvent::CreatePool(create_event));
|
||||
}
|
||||
PumpSwapInstruction::Deposit(mut deposit_event) => {
|
||||
deposit_event.slot = slot;
|
||||
callback(PumpSwapEvent::Deposit(deposit_event));
|
||||
}
|
||||
PumpSwapInstruction::Withdraw(mut withdraw_event) => {
|
||||
withdraw_event.slot = slot;
|
||||
callback(PumpSwapEvent::Withdraw(withdraw_event));
|
||||
}
|
||||
PumpSwapInstruction::Buy(mut buy_event) => {
|
||||
buy_event.slot = slot;
|
||||
callback(PumpSwapEvent::Buy(buy_event));
|
||||
}
|
||||
PumpSwapInstruction::Sell(mut sell_event) => {
|
||||
sell_event.slot = slot;
|
||||
callback(PumpSwapEvent::Sell(sell_event));
|
||||
}
|
||||
PumpSwapInstruction::UpdateFeeConfig(mut update_fee_event) => {
|
||||
update_fee_event.slot = slot;
|
||||
callback(PumpSwapEvent::UpdateFeeConfig(update_fee_event));
|
||||
}
|
||||
PumpSwapInstruction::UpdateAdmin(mut update_admin_event) => {
|
||||
update_admin_event.slot = slot;
|
||||
callback(PumpSwapEvent::UpdateAdmin(update_admin_event));
|
||||
}
|
||||
PumpSwapInstruction::Disable(mut disable_event) => {
|
||||
disable_event.slot = slot;
|
||||
callback(PumpSwapEvent::Disable(disable_event));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,4 +349,190 @@ impl YellowstoneGrpc {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// PumpSwap
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// 订阅PumpSwap事件
|
||||
pub async fn subscribe_pumpswap<F>(&self, callback: F) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
// 使用constants中定义的AMM_PROGRAM
|
||||
let pump_program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM;
|
||||
let addrs = vec![pump_program_id.to_string()];
|
||||
|
||||
// 创建过滤器
|
||||
let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]);
|
||||
|
||||
// 订阅事件
|
||||
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
|
||||
|
||||
// 创建通道
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
|
||||
// 创建回调函数
|
||||
let callback = Box::new(callback);
|
||||
|
||||
// 启动处理流的任务
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
|
||||
error!("Error handling message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 处理交易
|
||||
while let Some(transaction_pretty) = rx.next().await {
|
||||
if let Err(e) = Self::process_pumpswap_transaction(transaction_pretty, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 使用过滤器订阅PumpSwap事件
|
||||
pub async fn subscribe_pumpswap_with_filter<F>(
|
||||
&self,
|
||||
callback: F,
|
||||
account_include: Option<Vec<String>>,
|
||||
account_exclude: Option<Vec<String>>
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync + 'static,
|
||||
{
|
||||
// 使用constants中定义的AMM_PROGRAM
|
||||
let pump_program_id = crate::constants::pumpswap::accounts::AMM_PROGRAM;
|
||||
let addrs = vec![pump_program_id.to_string()];
|
||||
|
||||
// 创建过滤器
|
||||
let account_include = account_include.unwrap_or_default();
|
||||
let account_exclude = account_exclude.unwrap_or_default();
|
||||
let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs);
|
||||
|
||||
// 订阅事件
|
||||
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
|
||||
|
||||
// 创建通道
|
||||
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(1000);
|
||||
|
||||
// 创建回调函数
|
||||
let callback = Box::new(callback);
|
||||
|
||||
// 启动处理流的任务
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = stream.next().await {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
|
||||
error!("Error handling message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Stream error: {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 处理交易
|
||||
while let Some(transaction_pretty) = rx.next().await {
|
||||
if let Err(e) = Self::process_pumpswap_transaction(transaction_pretty, &*callback).await {
|
||||
error!("Error processing transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理PumpSwap交易
|
||||
async fn process_pumpswap_transaction<F>(
|
||||
transaction_pretty: TransactionPretty,
|
||||
callback: &F
|
||||
) -> AnyResult<()>
|
||||
where
|
||||
F: Fn(crate::common::pumpswap::logs_events::PumpSwapEvent) + Send + Sync,
|
||||
{
|
||||
let slot = transaction_pretty.slot;
|
||||
let trade_raw: solana_transaction_status::EncodedTransactionWithStatusMeta = transaction_pretty.tx;
|
||||
|
||||
// 检查交易元数据
|
||||
let meta = trade_raw.meta.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
|
||||
|
||||
// 检查交易是否成功
|
||||
if meta.err.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 获取日志
|
||||
let logs = if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = &meta.log_messages {
|
||||
logs
|
||||
} else {
|
||||
&vec![]
|
||||
};
|
||||
|
||||
// 解析PumpSwap事件
|
||||
let events = crate::common::pumpswap::logs_filters::LogFilter::parse_pumpswap_logs(logs);
|
||||
|
||||
// 处理事件
|
||||
for mut event in events {
|
||||
// 设置签名和slot
|
||||
match &mut event {
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Buy(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Sell(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::CreatePool(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Deposit(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Withdraw(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::Disable(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::UpdateAdmin(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
crate::common::pumpswap::logs_events::PumpSwapEvent::UpdateFeeConfig(e) => {
|
||||
e.signature = transaction_pretty.signature.to_string();
|
||||
e.slot = slot;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 调用回调函数
|
||||
callback(event);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -103,7 +103,7 @@ impl Sell {
|
||||
pub fn create(payer: &Keypair, mint: &Keypair, args: Create) -> Instruction {
|
||||
let bonding_curve: Pubkey = get_bonding_curve_pda(&mint.pubkey()).unwrap();
|
||||
Instruction::new_with_bytes(
|
||||
constants::accounts::PUMPFUN,
|
||||
constants::pumpfun::accounts::PUMPFUN,
|
||||
&args.data(),
|
||||
vec![
|
||||
AccountMeta::new(mint.pubkey(), true),
|
||||
@@ -114,15 +114,15 @@ pub fn create(payer: &Keypair, mint: &Keypair, args: Create) -> Instruction {
|
||||
false,
|
||||
),
|
||||
AccountMeta::new_readonly(get_global_pda(), false),
|
||||
AccountMeta::new_readonly(constants::accounts::MPL_TOKEN_METADATA, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::MPL_TOKEN_METADATA, false),
|
||||
AccountMeta::new(get_metadata_pda(&mint.pubkey()), false),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::RENT, false),
|
||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::ASSOCIATED_TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::RENT, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::PUMPFUN, false),
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -152,21 +152,21 @@ pub fn buy(
|
||||
args: Buy,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
constants::accounts::PUMPFUN,
|
||||
constants::pumpfun::accounts::PUMPFUN,
|
||||
&args.data(),
|
||||
vec![
|
||||
AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::global_constants::GLOBAL_ACCOUNT, false),
|
||||
AccountMeta::new(*fee_recipient, false),
|
||||
AccountMeta::new_readonly(*mint, false),
|
||||
AccountMeta::new(*bonding_curve_pda, false),
|
||||
AccountMeta::new(get_associated_token_address(bonding_curve_pda, mint), false),
|
||||
AccountMeta::new(get_associated_token_address(&payer.pubkey(), mint), false),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new_readonly(constants::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::SYSTEM_PROGRAM, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::TOKEN_PROGRAM, false),
|
||||
AccountMeta::new(*creator_vault_pda, false),
|
||||
AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::accounts::PUMPFUN, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::EVENT_AUTHORITY, false),
|
||||
AccountMeta::new_readonly(constants::pumpfun::accounts::PUMPFUN, false),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
+58
-4
@@ -1,11 +1,16 @@
|
||||
use pumpfun_sdk::{common::{
|
||||
pumpfun::logs_events::PumpfunEvent,
|
||||
pumpfun::logs_subscribe::{stop_subscription, tokens_subscription}, AnyResult
|
||||
}, grpc::ShredStreamGrpc};
|
||||
pumpfun::{logs_events::PumpfunEvent, logs_subscribe::{stop_subscription, tokens_subscription}}, pumpswap::{self, PumpSwapEvent}, AnyResult
|
||||
}, grpc::{ShredStreamGrpc, YellowstoneGrpc}};
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTransaction};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// test_pumpfun().await?;
|
||||
test_pumpswap().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpfun() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let grpc = ShredStreamGrpc::new(
|
||||
"http://127.0.0.1:10800".to_string(),
|
||||
).await?;
|
||||
@@ -44,9 +49,58 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
grpc.shredstream_subscribe(callback, None).await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_pumpswap() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 使用 GRPC 客户端订阅 PumpSwap 事件
|
||||
println!("正在订阅 PumpSwap GRPC 事件...");
|
||||
|
||||
let grpc_client = ShredStreamGrpc::new(
|
||||
"http://127.0.0.1:10800".to_string(),
|
||||
).await?;
|
||||
|
||||
// 定义回调函数处理 PumpSwap 事件
|
||||
let callback = |event: PumpSwapEvent| {
|
||||
match event {
|
||||
PumpSwapEvent::Buy(buy_event) => {
|
||||
println!("buy_event: {:?}", buy_event);
|
||||
},
|
||||
PumpSwapEvent::Sell(sell_event) => {
|
||||
println!("sell_event: {:?}", sell_event);
|
||||
},
|
||||
PumpSwapEvent::CreatePool(create_event) => {
|
||||
println!("create_event: {:?}", create_event);
|
||||
},
|
||||
PumpSwapEvent::Deposit(deposit_event) => {
|
||||
println!("deposit_event: {:?}", deposit_event);
|
||||
},
|
||||
PumpSwapEvent::Withdraw(withdraw_event) => {
|
||||
println!("withdraw_event: {:?}", withdraw_event);
|
||||
},
|
||||
PumpSwapEvent::Disable(disable_event) => {
|
||||
println!("disable_event: {:?}", disable_event);
|
||||
},
|
||||
PumpSwapEvent::UpdateAdmin(update_admin_event) => {
|
||||
println!("update_admin_event: {:?}", update_admin_event);
|
||||
},
|
||||
PumpSwapEvent::UpdateFeeConfig(update_fee_event) => {
|
||||
println!("update_fee_event: {:?}", update_fee_event);
|
||||
},
|
||||
PumpSwapEvent::Error(err) => {
|
||||
println!("error: {}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 订阅 PumpSwap 事件
|
||||
println!("开始监听 PumpSwap 事件,按 Ctrl+C 停止...");
|
||||
|
||||
grpc_client.shredstream_subscribe_pumpswap(callback).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
async fn test_wss() -> AnyResult<()> {
|
||||
println!("Starting token subscription\n");
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ pub async fn build_create_and_buy_instructions(
|
||||
&payer.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&mint.pubkey(),
|
||||
&constants::accounts::TOKEN_PROGRAM,
|
||||
&constants::pumpfun::accounts::TOKEN_PROGRAM,
|
||||
));
|
||||
|
||||
instructions.push(instruction::buy(
|
||||
@@ -227,7 +227,7 @@ pub async fn build_create_and_buy_instructions(
|
||||
&mint.pubkey(),
|
||||
&bonding_curve_pda,
|
||||
&creator_vault_pda,
|
||||
&constants::global_constants::FEE_RECIPIENT,
|
||||
&constants::pumpfun::global_constants::FEE_RECIPIENT,
|
||||
instruction::Buy {
|
||||
_amount: buy_token_amount,
|
||||
_max_sol_cost: max_sol_cost,
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::str::FromStr;
|
||||
use anyhow::anyhow;
|
||||
use chrono;
|
||||
use solana_sdk::{
|
||||
compute_budget::ComputeBudgetInstruction,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::{v0, AddressLookupTableAccount, VersionedMessage},
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction,
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
use spl_associated_token_account::instruction::create_associated_token_account_idempotent;
|
||||
|
||||
use crate::common::{address_lookup_cache::get_address_lookup_table_account, nonce_cache::{self, NonceCache}, PriorityFee, SolanaRpcClient};
|
||||
use crate::pumpswap::common::{calculate_with_slippage_buy, find_pool, get_buy_token_amount};
|
||||
use crate::constants::{accounts, trade::DEFAULT_SLIPPAGE, BUY_DISCRIMINATOR};
|
||||
use crate::swqos::FeeClient;
|
||||
|
||||
// Constants for compute budget
|
||||
// Increased from 64KB to 256KB to handle larger transactions
|
||||
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024;
|
||||
|
||||
/// 添加nonce消费指令到指令集合中
|
||||
///
|
||||
/// 只有当同时提供了nonce_pubkey和nonce_program_id时才使用nonce功能
|
||||
/// 如果nonce被锁定、已使用或未准备好,将返回错误
|
||||
/// 成功时会锁定并标记nonce为已使用
|
||||
fn add_nonce_instruction(instructions: &mut Vec<Instruction>, payer: &Keypair) -> Result<(), anyhow::Error> {
|
||||
let nonce_cache = NonceCache::get_instance();
|
||||
let nonce_info = nonce_cache.get_nonce_info();
|
||||
if let (Some(nonce_pubkey), Some(program_id)) = (nonce_info.nonce_account, nonce_info.program_id) {
|
||||
let nonce_value = nonce_info.current_nonce;
|
||||
// 暂不加锁
|
||||
// if nonce_info.lock {
|
||||
// return Err(anyhow!("Nonce is locked"));
|
||||
// }
|
||||
if nonce_info.used {
|
||||
return Err(anyhow!("Nonce is used"));
|
||||
}
|
||||
if nonce_info.next_buy_time == 0 || chrono::Utc::now().timestamp() < nonce_info.next_buy_time {
|
||||
return Err(anyhow!("Nonce is not ready"));
|
||||
}
|
||||
// 加锁 - 暂不加锁
|
||||
// nonce_cache.lock();
|
||||
// 创建自定义nonce消费指令
|
||||
let nonce_consume_ix = Instruction {
|
||||
program_id,
|
||||
accounts: vec![
|
||||
AccountMeta::new(nonce_pubkey, false),
|
||||
AccountMeta::new_readonly(payer.pubkey(), true),
|
||||
],
|
||||
// INSTR_CONSUME = 1, 使用传入的nonce值
|
||||
data: {
|
||||
let mut data = vec![1]; // INSTR_CONSUME = 1
|
||||
data.extend_from_slice(&nonce_value.to_le_bytes()); // 添加nonce值
|
||||
data
|
||||
},
|
||||
};
|
||||
instructions.push(nonce_consume_ix);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 验证地址表是否被成功用于编译后的消息中
|
||||
fn verify_lookup_table_usage(
|
||||
v0_message: &v0::Message,
|
||||
address_lookup_table_accounts: &[AddressLookupTableAccount],
|
||||
) {
|
||||
if !address_lookup_table_accounts.is_empty() {
|
||||
println!("消息已编译,使用了地址表引用");
|
||||
// 如果地址表有地址,但没有被使用,给出警告
|
||||
if v0_message.address_table_lookups.is_empty() {
|
||||
println!("警告:编译后的消息没有使用地址表引用!");
|
||||
} else {
|
||||
for (i, lookup) in v0_message.address_table_lookups.iter().enumerate() {
|
||||
println!(
|
||||
"使用地址表 {}: 可写索引 {} 个, 只读索引 {} 个",
|
||||
i,
|
||||
lookup.writable_indexes.len(),
|
||||
lookup.readonly_indexes.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buy tokens from a Pumpswap pool
|
||||
pub async fn buy(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let start_time = Instant::now();
|
||||
let mint = Arc::new(mint.clone());
|
||||
let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?;
|
||||
println!(" Buy transaction instructions: {:?}", start_time.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
let transaction = build_buy_transaction(
|
||||
rpc.clone(),
|
||||
payer.clone(),
|
||||
priority_fee.clone(),
|
||||
instructions,
|
||||
lookup_table_key,
|
||||
).await?;
|
||||
println!(" Buy transaction signature: {:?}", start_time.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
||||
println!(" Buy transaction confirmation: {:?}", start_time.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Buy tokens using a MEV service
|
||||
pub async fn buy_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
fee_clients: Vec<Arc<FeeClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let start_time = Instant::now();
|
||||
let mint = Arc::new(mint.clone());
|
||||
let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?;
|
||||
println!(" Buy transaction instructions: {:?}", start_time.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut transactions = vec![];
|
||||
|
||||
for fee_client in fee_clients.clone() {
|
||||
let tip_account = fee_client.get_tip_account()?;
|
||||
let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?);
|
||||
|
||||
let transaction = build_buy_transaction_with_tip(
|
||||
rpc.clone(),
|
||||
tip_account,
|
||||
payer.clone(),
|
||||
priority_fee.clone(),
|
||||
instructions.clone(),
|
||||
lookup_table_key,
|
||||
).await?;
|
||||
|
||||
transactions.push(transaction);
|
||||
}
|
||||
|
||||
println!(" Buy transaction signature: {:?}", start_time.elapsed());
|
||||
|
||||
let mut handles = vec![];
|
||||
for (i, fee_client) in fee_clients.iter().enumerate() {
|
||||
let transaction = transactions[i].clone();
|
||||
let fee_client = fee_client.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
fee_client.send_transaction(crate::swqos::TradeType::Buy, &transaction).await
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await?;
|
||||
}
|
||||
|
||||
println!(" Buy transaction confirmation: {:?}", start_time.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Build a transaction for buying tokens
|
||||
pub async fn build_buy_transaction(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
priority_fee: PriorityFee,
|
||||
build_instructions: Vec<Instruction>,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let mut instructions = vec![
|
||||
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
|
||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
||||
];
|
||||
|
||||
// 添加nonce消费指令
|
||||
if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
instructions.extend(build_instructions);
|
||||
|
||||
let blockhash = rpc.get_latest_blockhash().await?;
|
||||
|
||||
// 确保所有需要签名的账户都被正确标记
|
||||
for instruction in &instructions {
|
||||
for account_meta in &instruction.accounts {
|
||||
if account_meta.is_signer && account_meta.pubkey != payer.pubkey() {
|
||||
return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut address_lookup_table_accounts = vec![];
|
||||
if let Some(lookup_table_key) = lookup_table_key {
|
||||
let account = get_address_lookup_table_account(&lookup_table_key).await;
|
||||
address_lookup_table_accounts.push(account);
|
||||
}
|
||||
|
||||
let v0_message = v0::Message::try_compile(
|
||||
&payer.pubkey(),
|
||||
&instructions,
|
||||
&address_lookup_table_accounts,
|
||||
blockhash,
|
||||
).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let versioned_message = VersionedMessage::V0(v0_message.clone());
|
||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
||||
|
||||
// 验证地址表使用情况
|
||||
verify_lookup_table_usage(&v0_message, &address_lookup_table_accounts);
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
// Build a transaction with tip for buying tokens
|
||||
pub async fn build_buy_transaction_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
tip_account: Arc<Pubkey>,
|
||||
payer: Arc<Keypair>,
|
||||
priority_fee: PriorityFee,
|
||||
build_instructions: Vec<Instruction>,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let mut instructions = vec![
|
||||
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
|
||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
||||
system_instruction::transfer(
|
||||
&payer.pubkey(),
|
||||
&tip_account,
|
||||
priority_fee.buy_tip_fee,
|
||||
),
|
||||
];
|
||||
|
||||
// 添加nonce消费指令
|
||||
if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
instructions.extend(build_instructions);
|
||||
|
||||
let blockhash = rpc.get_latest_blockhash().await?;
|
||||
|
||||
// 确保所有需要签名的账户都被正确标记
|
||||
for instruction in &instructions {
|
||||
for account_meta in &instruction.accounts {
|
||||
if account_meta.is_signer && account_meta.pubkey != payer.pubkey() {
|
||||
return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut address_lookup_table_accounts = vec![];
|
||||
if let Some(lookup_table_key) = lookup_table_key {
|
||||
let account = get_address_lookup_table_account(&lookup_table_key).await;
|
||||
address_lookup_table_accounts.push(account);
|
||||
}
|
||||
|
||||
let v0_message = v0::Message::try_compile(
|
||||
&payer.pubkey(),
|
||||
&instructions,
|
||||
&address_lookup_table_accounts,
|
||||
blockhash,
|
||||
).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let versioned_message = VersionedMessage::V0(v0_message.clone());
|
||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
||||
|
||||
// 验证地址表使用情况
|
||||
verify_lookup_table_usage(&v0_message, &address_lookup_table_accounts);
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
// Build instructions for buying tokens
|
||||
pub async fn build_buy_instructions(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Arc<Pubkey>,
|
||||
amount_sol: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<Vec<Instruction>, anyhow::Error> {
|
||||
if amount_sol == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
// Find the pool for this mint
|
||||
let pool = find_pool(rpc.as_ref(), mint.as_ref()).await?;
|
||||
|
||||
// Calculate the expected token amount
|
||||
let token_amount = get_buy_token_amount(rpc.as_ref(), &pool, amount_sol).await?;
|
||||
|
||||
// Calculate the maximum SOL amount with slippage
|
||||
let max_sol_amount = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
|
||||
|
||||
// Create the user's token account if it doesn't exist
|
||||
let user_base_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), mint.as_ref());
|
||||
let user_quote_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT);
|
||||
|
||||
// Get pool token accounts
|
||||
let pool_base_token_account = spl_associated_token_account::get_associated_token_address_with_program_id(
|
||||
&pool,
|
||||
mint.as_ref(),
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
let pool_quote_token_account = spl_associated_token_account::get_associated_token_address_with_program_id(
|
||||
&pool,
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
// Create the user's base token account if it doesn't exist
|
||||
instructions.push(
|
||||
create_associated_token_account_idempotent(
|
||||
&payer.pubkey(),
|
||||
&payer.pubkey(),
|
||||
mint.as_ref(),
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
)
|
||||
);
|
||||
|
||||
// Create the buy instruction
|
||||
// 注意:账户顺序必须与JavaScript SDK匹配
|
||||
let accounts = vec![
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(pool, false), // pool_id (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_ACCOUNT, false), // global (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(*mint, false), // mint (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // WSOL_TOKEN_ACCOUNT (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // user_base_token_account
|
||||
solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // user_quote_token_account
|
||||
solana_sdk::instruction::AccountMeta::new(pool_base_token_account, false), // pool_base_token_account
|
||||
solana_sdk::instruction::AccountMeta::new(pool_quote_token_account, false), // pool_quote_token_account
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::FEE_RECIPIENT, false), // fee_recipient (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(accounts::FEE_RECIPIENT_ATA, false), // fee_recipient_ata
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly, duplicated as in JS)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::SYSTEM_PROGRAM, false), // System Program (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // event_authority (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_PROGRAM, false), // PUMP_AMM_PROGRAM_ID (readonly)
|
||||
];
|
||||
|
||||
// Create the instruction data
|
||||
let mut data = vec![];
|
||||
data.extend_from_slice(&BUY_DISCRIMINATOR);
|
||||
data.extend_from_slice(&token_amount.to_le_bytes());
|
||||
data.extend_from_slice(&max_sol_amount.to_le_bytes());
|
||||
|
||||
instructions.push(
|
||||
Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts,
|
||||
data,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
};
|
||||
use crate::common::SolanaRpcClient;
|
||||
|
||||
// Calculate slippage for buy operations
|
||||
pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 {
|
||||
amount + (amount * basis_points / 10000)
|
||||
}
|
||||
|
||||
// Calculate slippage for sell operations
|
||||
pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
||||
if amount <= basis_points / 10000 {
|
||||
1
|
||||
} else {
|
||||
amount - (amount * basis_points / 10000)
|
||||
}
|
||||
}
|
||||
|
||||
// Get token balance for a specific mint and owner
|
||||
pub async fn get_token_balance(
|
||||
rpc: &SolanaRpcClient,
|
||||
owner: &Keypair,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(u64, Pubkey), anyhow::Error> {
|
||||
let ata = spl_associated_token_account::get_associated_token_address(&owner.pubkey(), mint);
|
||||
|
||||
match rpc.get_token_account_balance(&ata).await {
|
||||
Ok(balance) => {
|
||||
let amount = balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
Ok((amount, ata))
|
||||
}
|
||||
Err(_) => Ok((0, ata)),
|
||||
}
|
||||
}
|
||||
|
||||
// Find a pool for a specific mint
|
||||
pub async fn find_pool(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Pubkey, anyhow::Error> {
|
||||
let (pool_address, _) = crate::pumpswap::pool::Pool::find_by_mint(rpc, mint).await?;
|
||||
Ok(pool_address)
|
||||
}
|
||||
|
||||
// Calculate the amount of tokens to receive for a given SOL amount
|
||||
pub async fn get_buy_token_amount(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool: &Pubkey,
|
||||
sol_amount: u64,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let pool_data = crate::pumpswap::pool::Pool::fetch(rpc, pool).await?;
|
||||
pool_data.calculate_buy_amount(rpc, sol_amount).await
|
||||
}
|
||||
|
||||
// Calculate the amount of SOL to receive for a given token amount
|
||||
pub async fn get_sell_sol_amount(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool: &Pubkey,
|
||||
token_amount: u64,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let pool_data = crate::pumpswap::pool::Pool::fetch(rpc, pool).await?;
|
||||
pool_data.calculate_sell_amount(rpc, token_amount).await
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod buy;
|
||||
pub mod sell;
|
||||
pub mod common;
|
||||
pub mod pool;
|
||||
@@ -0,0 +1,163 @@
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use anyhow::anyhow;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use crate::{common::SolanaRpcClient, constants::accounts};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pool {
|
||||
pub pool_bump: u8,
|
||||
pub index: u16,
|
||||
pub creator: Pubkey,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub pool_base_token_account: Pubkey,
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
pub lp_supply: u64,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, anyhow::Error> {
|
||||
if data.len() < 211 {
|
||||
return Err(anyhow!("Data too short for Pool account"));
|
||||
}
|
||||
|
||||
// 跳过discriminator (8字节)
|
||||
let data = &data[8..];
|
||||
|
||||
let pool_bump = data[0];
|
||||
let index = u16::from_le_bytes([data[1], data[2]]);
|
||||
|
||||
let creator = Pubkey::new_from_array(data[3..35].try_into().map_err(|e| anyhow!("Failed to convert creator: {:?}", e))?);
|
||||
let base_mint = Pubkey::new_from_array(data[35..67].try_into().map_err(|e| anyhow!("Failed to convert base_mint: {:?}", e))?);
|
||||
let quote_mint = Pubkey::new_from_array(data[67..99].try_into().map_err(|e| anyhow!("Failed to convert quote_mint: {:?}", e))?);
|
||||
let lp_mint = Pubkey::new_from_array(data[99..131].try_into().map_err(|e| anyhow!("Failed to convert lp_mint: {:?}", e))?);
|
||||
let pool_base_token_account = Pubkey::new_from_array(data[131..163].try_into().map_err(|e| anyhow!("Failed to convert pool_base_token_account: {:?}", e))?);
|
||||
let pool_quote_token_account = Pubkey::new_from_array(data[163..195].try_into().map_err(|e| anyhow!("Failed to convert pool_quote_token_account: {:?}", e))?);
|
||||
|
||||
let lp_supply = u64::from_le_bytes([
|
||||
data[195], data[196], data[197], data[198],
|
||||
data[199], data[200], data[201], data[202],
|
||||
]);
|
||||
|
||||
Ok(Self {
|
||||
pool_bump,
|
||||
index,
|
||||
creator,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
lp_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
lp_supply,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account = rpc.get_account(pool_address).await?;
|
||||
|
||||
if account.owner != accounts::AMM_PROGRAM {
|
||||
return Err(anyhow!("Account is not owned by PumpSwap program"));
|
||||
}
|
||||
|
||||
Self::from_bytes(&account.data)
|
||||
}
|
||||
|
||||
pub async fn find_by_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Self), anyhow::Error> {
|
||||
// 使用getProgramAccounts查找给定mint的池子
|
||||
let filters = vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool账户的大小
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &mint.to_bytes()),
|
||||
),
|
||||
];
|
||||
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(filters),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
commitment: None,
|
||||
min_context_slot: None,
|
||||
},
|
||||
with_context: None,
|
||||
sort_results: None,
|
||||
};
|
||||
|
||||
let program_id = crate::constants::accounts::AMM_PROGRAM;
|
||||
println!("program_id: {:?}", program_id);
|
||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", mint));
|
||||
}
|
||||
|
||||
let mut pools: Vec<_> = accounts.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
Self::from_bytes(&acc.data)
|
||||
.map(|pool| (addr, pool))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
|
||||
|
||||
let (address, pool) = pools[0].clone();
|
||||
println!("pool: {:?}", pool);
|
||||
println!("address: {:?}", address);
|
||||
Ok((address, pool))
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
&self,
|
||||
rpc: &SolanaRpcClient,
|
||||
) -> Result<(u64, u64), anyhow::Error> {
|
||||
let base_balance = rpc.get_token_account_balance(&self.pool_base_token_account).await?;
|
||||
let quote_balance = rpc.get_token_account_balance(&self.pool_quote_token_account).await?;
|
||||
|
||||
let base_amount = base_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
let quote_amount = quote_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok((base_amount, quote_amount))
|
||||
}
|
||||
|
||||
pub async fn calculate_buy_amount(
|
||||
&self,
|
||||
rpc: &SolanaRpcClient,
|
||||
sol_amount: u64,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let (base_amount, quote_amount) = self.get_token_balances(rpc).await?;
|
||||
|
||||
// 使用常数乘积公式 (x * y = k) 计算
|
||||
let product = base_amount as u128 * quote_amount as u128;
|
||||
let new_quote_amount = quote_amount as u128 + sol_amount as u128;
|
||||
let new_base_amount = product / new_quote_amount;
|
||||
|
||||
let token_amount = base_amount as u128 - new_base_amount;
|
||||
|
||||
Ok(token_amount as u64)
|
||||
}
|
||||
|
||||
pub async fn calculate_sell_amount(
|
||||
&self,
|
||||
rpc: &SolanaRpcClient,
|
||||
token_amount: u64,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let (base_amount, quote_amount) = self.get_token_balances(rpc).await?;
|
||||
|
||||
// 使用常数乘积公式 (x * y = k) 计算
|
||||
let product = base_amount as u128 * quote_amount as u128;
|
||||
let new_base_amount = base_amount as u128 + token_amount as u128;
|
||||
let new_quote_amount = product / new_base_amount;
|
||||
|
||||
let sol_amount = quote_amount as u128 - new_quote_amount;
|
||||
|
||||
Ok(sol_amount as u64)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::str::FromStr;
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::{
|
||||
compute_budget::ComputeBudgetInstruction,
|
||||
instruction::Instruction,
|
||||
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction,
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
use spl_associated_token_account::instruction::create_associated_token_account_idempotent;
|
||||
|
||||
use crate::common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient};
|
||||
use crate::pumpswap::common::{calculate_with_slippage_sell, find_pool, get_sell_sol_amount, get_token_balance};
|
||||
use crate::constants::{accounts, trade::DEFAULT_SLIPPAGE, SELL_DISCRIMINATOR};
|
||||
use crate::swqos::FeeClient;
|
||||
|
||||
// Constants for compute budget
|
||||
// Increased from 64KB to 256KB to handle larger transactions
|
||||
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 256 * 1024;
|
||||
|
||||
// Sell tokens to a Pumpswap pool
|
||||
pub async fn sell(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let start_time = Instant::now();
|
||||
let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?;
|
||||
println!(" Sell transaction instructions: {:?}", start_time.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
let transaction = build_sell_transaction(
|
||||
rpc.clone(),
|
||||
payer.clone(),
|
||||
priority_fee,
|
||||
instructions,
|
||||
lookup_table_key,
|
||||
recent_blockhash
|
||||
).await?;
|
||||
println!(" Sell transaction signature: {:?}", start_time.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
rpc.send_and_confirm_transaction(&transaction).await?;
|
||||
println!(" Sell transaction confirmation: {:?}", start_time.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sell tokens by percentage
|
||||
pub async fn sell_by_percent(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
percent: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
|
||||
let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
|
||||
let amount = balance_u64 * percent / 100;
|
||||
sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await
|
||||
}
|
||||
|
||||
// Sell tokens using a MEV service
|
||||
pub async fn sell_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
fee_clients: Vec<Arc<FeeClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut transactions = vec![];
|
||||
let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?;
|
||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
|
||||
for fee_client in fee_clients.clone() {
|
||||
let tip_account = fee_client.get_tip_account()?;
|
||||
let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?);
|
||||
|
||||
let transaction = build_sell_transaction_with_tip(
|
||||
rpc.clone(),
|
||||
tip_account,
|
||||
payer.clone(),
|
||||
priority_fee.clone(),
|
||||
instructions.clone(),
|
||||
lookup_table_key,
|
||||
recent_blockhash,
|
||||
).await?;
|
||||
|
||||
transactions.push(transaction);
|
||||
}
|
||||
|
||||
let mut handles = vec![];
|
||||
for (i, fee_client) in fee_clients.iter().enumerate() {
|
||||
let transaction = transactions[i].clone();
|
||||
let fee_client = fee_client.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
fee_client.send_transaction(crate::swqos::TradeType::Sell, &transaction).await
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sell tokens by percentage using a MEV service
|
||||
pub async fn sell_by_percent_with_tip(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
fee_clients: Vec<Arc<FeeClient>>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
percent: u64,
|
||||
slippage_basis_points: Option<u64>,
|
||||
priority_fee: PriorityFee,
|
||||
lookup_table_key: Option<Pubkey>
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
|
||||
let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
|
||||
let amount = balance_u64 * percent / 100;
|
||||
sell_with_tip(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee, lookup_table_key).await
|
||||
}
|
||||
|
||||
// Build a transaction for selling tokens
|
||||
pub async fn build_sell_transaction(
|
||||
_rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
priority_fee: PriorityFee,
|
||||
build_instructions: Vec<Instruction>,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: solana_sdk::hash::Hash,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let mut instructions = vec![
|
||||
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
|
||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
||||
];
|
||||
|
||||
instructions.extend(build_instructions);
|
||||
|
||||
// 确保所有需要签名的账户都被正确标记
|
||||
for instruction in &instructions {
|
||||
for account_meta in &instruction.accounts {
|
||||
if account_meta.is_signer && account_meta.pubkey != payer.pubkey() {
|
||||
return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut address_lookup_table_accounts = vec![];
|
||||
if let Some(lookup_table_key) = lookup_table_key {
|
||||
let account = get_address_lookup_table_account(&lookup_table_key).await;
|
||||
address_lookup_table_accounts.push(account);
|
||||
}
|
||||
|
||||
let v0_message = solana_sdk::message::v0::Message::try_compile(
|
||||
&payer.pubkey(),
|
||||
&instructions,
|
||||
&address_lookup_table_accounts,
|
||||
recent_blockhash,
|
||||
).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message);
|
||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
// Build a transaction with tip for selling tokens
|
||||
pub async fn build_sell_transaction_with_tip(
|
||||
_rpc: Arc<SolanaRpcClient>,
|
||||
tip_account: Arc<Pubkey>,
|
||||
payer: Arc<Keypair>,
|
||||
priority_fee: PriorityFee,
|
||||
build_instructions: Vec<Instruction>,
|
||||
lookup_table_key: Option<Pubkey>,
|
||||
recent_blockhash: solana_sdk::hash::Hash,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let mut instructions = vec![
|
||||
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
|
||||
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
|
||||
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
|
||||
system_instruction::transfer(
|
||||
&payer.pubkey(),
|
||||
&tip_account,
|
||||
priority_fee.sell_tip_fee,
|
||||
),
|
||||
];
|
||||
|
||||
instructions.extend(build_instructions);
|
||||
|
||||
// 确保所有需要签名的账户都被正确标记
|
||||
for instruction in &instructions {
|
||||
for account_meta in &instruction.accounts {
|
||||
if account_meta.is_signer && account_meta.pubkey != payer.pubkey() {
|
||||
return Err(anyhow!("Transaction requires a signature from an account other than the payer: {}", account_meta.pubkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut address_lookup_table_accounts = vec![];
|
||||
if let Some(lookup_table_key) = lookup_table_key {
|
||||
let account = get_address_lookup_table_account(&lookup_table_key).await;
|
||||
address_lookup_table_accounts.push(account);
|
||||
}
|
||||
|
||||
let v0_message = solana_sdk::message::v0::Message::try_compile(
|
||||
&payer.pubkey(),
|
||||
&instructions,
|
||||
&address_lookup_table_accounts,
|
||||
recent_blockhash,
|
||||
).map_err(|e| anyhow!(e))?;
|
||||
|
||||
let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message);
|
||||
let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?;
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
// Build instructions for selling tokens
|
||||
pub async fn build_sell_instructions(
|
||||
rpc: Arc<SolanaRpcClient>,
|
||||
payer: Arc<Keypair>,
|
||||
mint: Pubkey,
|
||||
amount_token: Option<u64>,
|
||||
slippage_basis_points: Option<u64>,
|
||||
) -> Result<Vec<Instruction>, anyhow::Error> {
|
||||
let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
|
||||
let amount = amount_token.unwrap_or(balance_u64);
|
||||
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
// Find the pool for this mint
|
||||
let pool = find_pool(rpc.as_ref(), &mint).await?;
|
||||
|
||||
// Calculate the expected SOL amount
|
||||
let sol_amount = get_sell_sol_amount(rpc.as_ref(), &pool, amount).await?;
|
||||
|
||||
// Calculate the minimum SOL amount with slippage
|
||||
let min_sol_amount = calculate_with_slippage_sell(sol_amount, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
|
||||
|
||||
// Get token accounts
|
||||
let user_base_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &mint);
|
||||
let user_quote_token_account = spl_associated_token_account::get_associated_token_address(&payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT);
|
||||
|
||||
// Get pool token accounts
|
||||
let pool_base_token_account = spl_associated_token_account::get_associated_token_address_with_program_id(
|
||||
&pool,
|
||||
&mint,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
let pool_quote_token_account = spl_associated_token_account::get_associated_token_address_with_program_id(
|
||||
&pool,
|
||||
&accounts::WSOL_TOKEN_ACCOUNT,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
let mut instructions = vec![];
|
||||
|
||||
// Create the user's token account if it doesn't exist
|
||||
instructions.push(
|
||||
create_associated_token_account_idempotent(
|
||||
&payer.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&mint,
|
||||
&accounts::TOKEN_PROGRAM,
|
||||
)
|
||||
);
|
||||
|
||||
// Create the sell instruction
|
||||
// 注意:账户顺序必须与JavaScript SDK匹配
|
||||
let accounts = vec![
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(pool, false), // pool_id (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(payer.pubkey(), true), // user (signer)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::GLOBAL_ACCOUNT, false), // global (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(mint, false), // mint (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // WSOL_TOKEN_ACCOUNT (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(user_base_token_account, false), // user_base_token_account
|
||||
solana_sdk::instruction::AccountMeta::new(user_quote_token_account, false), // user_quote_token_account
|
||||
solana_sdk::instruction::AccountMeta::new(pool_base_token_account, false), // pool_base_token_account
|
||||
solana_sdk::instruction::AccountMeta::new(pool_quote_token_account, false), // pool_quote_token_account
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::FEE_RECIPIENT, false), // fee_recipient (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new(accounts::FEE_RECIPIENT_ATA, false), // fee_recipient_ata
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // TOKEN_PROGRAM_ID (readonly, duplicated as in JS)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::SYSTEM_PROGRAM, false), // System Program (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), // ASSOCIATED_TOKEN_PROGRAM_ID (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // event_authority (readonly)
|
||||
solana_sdk::instruction::AccountMeta::new_readonly(accounts::AMM_PROGRAM, false), // PUMP_AMM_PROGRAM_ID (readonly)
|
||||
];
|
||||
|
||||
// Create the instruction data
|
||||
let mut data = vec![];
|
||||
data.extend_from_slice(&SELL_DISCRIMINATOR);
|
||||
data.extend_from_slice(&amount.to_le_bytes());
|
||||
data.extend_from_slice(&min_sol_amount.to_le_bytes());
|
||||
|
||||
instructions.push(
|
||||
Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts,
|
||||
data,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
Reference in New Issue
Block a user