feat: Add address lookup, caching system and Jito GRPC integration

- Implement address lookup tables and multi-level caching

- Add GRPC stream processing and YellowStone connections

- Extend PumpFun functionality including token creation

- Integrate Jito GRPC services (auth, block engine, relayer)

- Optimize log processing and event system
This commit is contained in:
sgxiang
2025-05-29 18:53:58 +08:00
parent f261b8a66d
commit bfe76fcddc
37 changed files with 4168 additions and 694 deletions
+393
View File
@@ -0,0 +1,393 @@
use solana_program::{
address_lookup_table::{
instruction::{
create_lookup_table as create_lookup_table_instruction,
extend_lookup_table as extend_lookup_table_instruction,
freeze_lookup_table as freeze_lookup_table_instruction
},
state::AddressLookupTable,
},
instruction::Instruction,
pubkey::Pubkey,
};
use solana_sdk::{
message::{v0::Message as MessageV0, AddressLookupTableAccount, VersionedMessage},
signature::{Keypair, Signer},
transaction::{Transaction, VersionedTransaction},
};
use std::{error::Error, sync::Arc};
use crate::{common::SolanaRpcClient, constants};
/// 创建地址查找表(如果不存在)
pub async fn create_lookup_table_if_not_exists(
client: Arc<SolanaRpcClient>,
authority: &Keypair,
payer: &Keypair,
) -> Result<Pubkey, Box<dyn std::error::Error>> {
// 1. 计算预期的查找表地址
let recent_slot = client.get_slot().await?;
let (create_ix, lookup_table_address) = create_lookup_table_instruction(
authority.pubkey(),
payer.pubkey(),
recent_slot
);
// 2. 创建新表
let blockhash = client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[create_ix],
Some(&payer.pubkey()),
&[payer, authority],
blockhash,
);
client.send_and_confirm_transaction(&transaction).await?;
Ok(lookup_table_address)
}
/// 向查找表添加地址
pub async fn extend_lookup_table(
client: Arc<SolanaRpcClient>,
payer: &Keypair,
authority: &Keypair,
lookup_table_address: &Pubkey,
addresses: Vec<Pubkey>,
) -> Result<(), Box<dyn Error>> {
let extend_ix = extend_lookup_table_instruction(
*lookup_table_address,
authority.pubkey(),
Some(payer.pubkey()),
addresses.clone(),
);
let blockhash = client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[extend_ix],
Some(&payer.pubkey()),
&[payer, authority],
blockhash,
);
client.send_and_confirm_transaction(&transaction).await?;
Ok(())
}
/// 冻结查找表,防止进一步修改
pub async fn freeze_lookup_table(
client: Arc<SolanaRpcClient>,
payer: &Keypair,
authority: &Keypair,
lookup_table_address: &Pubkey,
) -> Result<(), Box<dyn Error>> {
let freeze_ix = freeze_lookup_table_instruction(
*lookup_table_address,
authority.pubkey(),
);
let blockhash = client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[freeze_ix],
Some(&payer.pubkey()),
&[payer, authority],
blockhash,
);
client.send_and_confirm_transaction(&transaction).await?;
Ok(())
}
/// 获取查找表信息
pub async fn get_address_lookup_table(
client: Arc<SolanaRpcClient>,
lookup_table_address: &Pubkey,
) -> Result<AddressLookupTableAccount, Box<dyn Error>> {
let account = client.get_account(lookup_table_address).await?;
let lookup_table = AddressLookupTable::deserialize(&account.data)?;
let address_lookup_table_account = AddressLookupTableAccount {
key: *lookup_table_address,
addresses: lookup_table.addresses.to_vec(),
};
for (i, addr) in address_lookup_table_account.addresses.iter().enumerate() {
println!("地址 {}: {}", i, addr);
}
Ok(address_lookup_table_account)
}
/// 使用查找表发送交易
pub async fn send_transaction_with_lut(
client: Arc<SolanaRpcClient>,
instructions: Vec<Instruction>,
payer: &Keypair,
signers: Vec<&Keypair>,
address_lookup_tables: Vec<AddressLookupTableAccount>,
) -> Result<(), Box<dyn Error>> {
let blockhash = client.get_latest_blockhash().await?;
let message = VersionedMessage::V0(MessageV0::try_compile(
&payer.pubkey(),
&instructions,
&address_lookup_tables,
blockhash,
)?);
let tx = VersionedTransaction::try_new(message, &signers)?;
let signature = client.send_and_confirm_transaction(&tx).await?;
println!("交易已确认: {}", signature);
Ok(())
}
/// 使用查找表的特定地址子集发送交易
pub async fn send_transaction_with_filtered_lut(
client: Arc<SolanaRpcClient>,
instructions: Vec<Instruction>,
payer: &Keypair,
signers: Vec<&Keypair>,
lookup_table: AddressLookupTableAccount,
address_indices_to_use: &[usize], // 要使用的地址索引列表
) -> Result<(), Box<dyn Error>> {
// 创建只包含选定地址的新查找表账户
let filtered_addresses: Vec<Pubkey> = address_indices_to_use
.iter()
.filter_map(|&index| lookup_table.addresses.get(index).copied())
.collect();
println!(
"从查找表中选择了 {} 个地址用于交易",
filtered_addresses.len()
);
for (i, addr) in filtered_addresses.iter().enumerate() {
println!("使用地址 {}: {}", i, addr);
}
let filtered_lookup_table = AddressLookupTableAccount {
key: lookup_table.key,
addresses: filtered_addresses,
};
let blockhash = client.get_latest_blockhash().await?;
let message = VersionedMessage::V0(MessageV0::try_compile(
&payer.pubkey(),
&instructions,
&[filtered_lookup_table],
blockhash,
)?);
let tx = VersionedTransaction::try_new(message, &signers)?;
let signature = client.send_and_confirm_transaction(&tx).await?;
println!("交易已确认: {}", signature);
Ok(())
}
/// 获取最近的区块槽位,用于创建查找表
pub async fn get_recent_slot(client: Arc<SolanaRpcClient>) -> Result<u64, Box<dyn Error>> {
let slot = client.get_slot().await?;
Ok(slot)
}
/// 使用指定的地址列表发送交易
///
/// 这个方法接受一组目标地址,自动查找它们在查找表中的索引,
/// 然后使用这些地址创建一个过滤后的查找表来发送交易
///
/// # 参数
/// * `instructions` - 交易指令
/// * `payer` - 支付交易费用的账户
/// * `signers` - 交易签名者
/// * `lookup_table` - 地址查找表
/// * `addresses_to_use` - 要使用的地址列表
///
/// # 返回值
/// 成功返回交易签名,失败返回错误
pub async fn send_transaction_with_addresses(
client: Arc<SolanaRpcClient>,
instructions: Vec<Instruction>,
payer: &Keypair,
signers: Vec<&Keypair>,
lookup_table: AddressLookupTableAccount,
addresses_to_use: &[Pubkey],
) -> Result<String, Box<dyn Error>> {
// 构建地址到索引的映射
let mut address_to_index = std::collections::HashMap::new();
for (i, addr) in lookup_table.addresses.iter().enumerate() {
address_to_index.insert(*addr, i);
}
// 查找所有存在的地址的索引
let mut indices_to_use = Vec::new();
let mut found_addresses = Vec::new();
let mut missing_addresses = Vec::new();
for addr in addresses_to_use {
if let Some(&index) = address_to_index.get(addr) {
indices_to_use.push(index);
found_addresses.push(*addr);
} else {
missing_addresses.push(*addr);
}
}
// 检查是否有地址未找到
if !missing_addresses.is_empty() {
println!("警告: {} 个地址未在查找表中找到", missing_addresses.len());
for (i, addr) in missing_addresses.iter().enumerate() {
println!("未找到的地址 {}: {}", i, addr);
}
}
// 如果没有找到任何地址,返回错误
if indices_to_use.is_empty() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"没有在查找表中找到任何指定的地址",
)));
}
// 创建只包含选定地址的新查找表账户
let filtered_addresses: Vec<Pubkey> = indices_to_use
.iter()
.filter_map(|&index| lookup_table.addresses.get(index).copied())
.collect();
println!(
"从查找表中选择了 {} 个地址用于交易",
filtered_addresses.len()
);
for (i, addr) in filtered_addresses.iter().enumerate() {
println!("使用地址 {}: {}", i, addr);
}
let filtered_lookup_table = AddressLookupTableAccount {
key: lookup_table.key,
addresses: filtered_addresses,
};
let blockhash = client.get_latest_blockhash().await?;
let message = VersionedMessage::V0(MessageV0::try_compile(
&payer.pubkey(),
&instructions,
&[filtered_lookup_table],
blockhash,
)?);
let tx = VersionedTransaction::try_new(message, &signers)?;
let signature = client.send_and_confirm_transaction(&tx).await?;
println!("交易已确认: {}", signature);
Ok(signature.to_string())
}
pub async fn create_pumpfun_lookup_table(
client: Arc<SolanaRpcClient>,
payer: &Keypair,
authority: &Keypair,
) -> Result<Pubkey, Box<dyn Error>> {
let recent_slot = client.get_slot().await?;
let (create_ix, lookup_table_address) =
create_lookup_table_instruction(authority.pubkey(), payer.pubkey(), recent_slot);
let blockhash = client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[create_ix],
Some(&payer.pubkey()),
&[payer, authority],
blockhash,
);
client.send_and_confirm_transaction(&transaction).await?;
Ok(lookup_table_address)
}
pub async fn add_pumpfun_address_to_lookup_table(
client: Arc<SolanaRpcClient>,
payer: &Keypair,
authority: &Keypair,
lookup_table_address: &Pubkey,
) -> Result<(), Box<dyn Error>> {
let addresses = get_pumpfun_addresses(payer.pubkey(), vec![]);
extend_lookup_table(
client,
payer,
authority,
lookup_table_address,
addresses
).await?;
Ok(())
}
pub async fn extend_pumpfun_address_to_lookup_table(
client: Arc<SolanaRpcClient>,
payer: &Keypair,
authority: &Keypair,
lookup_table_address: &Pubkey,
addresses: Vec<Pubkey>,
) -> Result<(), Box<dyn Error>> {
extend_lookup_table(
client,
payer,
authority,
lookup_table_address,
addresses
).await?;
Ok(())
}
pub fn get_pumpfun_addresses(payer: Pubkey, include_addresses: Vec<Pubkey>) -> Vec<Pubkey> {
let mut addresses = vec![
payer,
constants::accounts::PUMPFUN,
constants::accounts::SYSTEM_PROGRAM,
constants::accounts::TOKEN_PROGRAM,
constants::accounts::RENT,
constants::accounts::EVENT_AUTHORITY,
constants::accounts::ASSOCIATED_TOKEN_PROGRAM,
constants::global_constants::GLOBAL_ACCOUNT,
constants::global_constants::FEE_RECIPIENT,
];
addresses.extend(include_addresses);
addresses
}
pub fn get_pumpfun_filtered_addresses(payer: Pubkey, include_addresses: Vec<Pubkey>) -> Vec<Pubkey> {
let mut addresses = vec![
payer,
constants::accounts::PUMPFUN,
constants::accounts::SYSTEM_PROGRAM,
constants::accounts::TOKEN_PROGRAM,
constants::accounts::RENT,
constants::accounts::EVENT_AUTHORITY,
constants::accounts::ASSOCIATED_TOKEN_PROGRAM,
constants::global_constants::GLOBAL_ACCOUNT,
constants::global_constants::FEE_RECIPIENT,
constants::global_constants::PUMPFUN_AMM_FEE_1,
constants::global_constants::PUMPFUN_AMM_FEE_2,
constants::global_constants::PUMPFUN_AMM_FEE_3,
constants::global_constants::PUMPFUN_AMM_FEE_4,
constants::global_constants::PUMPFUN_AMM_FEE_5,
constants::global_constants::PUMPFUN_AMM_FEE_6,
constants::global_constants::PUMPFUN_AMM_FEE_7,
// constants::global_constants::PUMPFUN_AMM_FEE_8,
];
addresses.extend(include_addresses);
addresses
}
+154
View File
@@ -0,0 +1,154 @@
use solana_sdk::{message::AddressLookupTableAccount, pubkey::Pubkey};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
/// AddressLookupTableInfo 结构体,存储地址表相关信息
pub struct AddressLookupTableInfo {
/// 地址表账户地址
pub lookup_table_address: Option<Pubkey>,
/// 地址表内容
pub address_lookup_table: Option<AddressLookupTableAccount>,
/// 锁定状态
pub lock: bool,
}
/// AddressLookupTableCache 单例,用于存储和管理地址表
pub struct AddressLookupTableCache {
/// 内部存储的地址表数据,键为地址表地址
tables: Mutex<HashMap<Pubkey, AddressLookupTableInfo>>,
}
// 使用静态 OnceLock 确保单例模式的线程安全性
static ADDRESS_LOOKUP_TABLE_CACHE: OnceLock<Arc<AddressLookupTableCache>> = OnceLock::new();
impl AddressLookupTableCache {
/// 获取 AddressLookupTableCache 单例实例
pub fn get_instance() -> Arc<AddressLookupTableCache> {
ADDRESS_LOOKUP_TABLE_CACHE
.get_or_init(|| {
Arc::new(AddressLookupTableCache {
tables: Mutex::new(HashMap::new()),
})
})
.clone()
}
/// 添加或更新地址表信息
pub fn add_or_update_table(
&self,
lookup_table_address: Pubkey,
address_lookup_table: Option<AddressLookupTableAccount>,
lock: Option<bool>,
) {
let mut tables = self.tables.lock().unwrap();
if let Some(table_info) = tables.get_mut(&lookup_table_address) {
// 更新已存在的表
if let Some(table) = address_lookup_table {
table_info.address_lookup_table = Some(table);
}
if let Some(l) = lock {
table_info.lock = l;
}
} else {
// 添加新表
tables.insert(
lookup_table_address,
AddressLookupTableInfo {
lookup_table_address: Some(lookup_table_address),
address_lookup_table,
lock: lock.unwrap_or(false),
},
);
}
}
/// 移除地址表
pub fn remove_table(&self, lookup_table_address: &Pubkey) -> bool {
let mut tables = self.tables.lock().unwrap();
tables.remove(lookup_table_address).is_some()
}
/// 获取地址表信息
pub fn get_table(&self, lookup_table_address: &Pubkey) -> Option<AddressLookupTableInfo> {
let tables = self.tables.lock().unwrap();
tables.get(lookup_table_address).map(|info| AddressLookupTableInfo {
lookup_table_address: info.lookup_table_address,
address_lookup_table: info.address_lookup_table.clone(),
lock: info.lock,
})
}
/// 获取所有表地址
pub fn get_all_table_addresses(&self) -> Vec<Pubkey> {
let tables = self.tables.lock().unwrap();
tables.keys().cloned().collect()
}
/// 检查表是否存在
pub fn table_exists(&self, lookup_table_address: &Pubkey) -> bool {
let tables = self.tables.lock().unwrap();
tables.contains_key(lookup_table_address)
}
/// 锁定地址表
pub fn lock_table(&self, lookup_table_address: &Pubkey) -> bool {
let mut tables = self.tables.lock().unwrap();
if let Some(table_info) = tables.get_mut(lookup_table_address) {
table_info.lock = true;
true
} else {
false
}
}
/// 解锁地址表
pub fn unlock_table(&self, lookup_table_address: &Pubkey) -> bool {
let mut tables = self.tables.lock().unwrap();
if let Some(table_info) = tables.get_mut(lookup_table_address) {
table_info.lock = false;
true
} else {
false
}
}
/// 更新地址表内容
pub fn update_table_content(
&self,
lookup_table_address: &Pubkey,
address_lookup_table: AddressLookupTableAccount,
) -> bool {
let mut tables = self.tables.lock().unwrap();
if let Some(table_info) = tables.get_mut(lookup_table_address) {
table_info.address_lookup_table = Some(address_lookup_table);
true
} else {
false
}
}
/// 获取表的内容
pub fn get_table_content(&self, lookup_table_address: &Pubkey) -> AddressLookupTableAccount {
let tables = self.tables.lock().unwrap();
tables
.get(lookup_table_address)
.and_then(|info| info.address_lookup_table.clone())
.unwrap_or_else(|| AddressLookupTableAccount {
key: *lookup_table_address,
addresses: Vec::new(),
})
}
}
/// 获取地址表账户
pub async fn get_address_lookup_table_account(lookup_table_address: &Pubkey) -> AddressLookupTableAccount {
let cache = AddressLookupTableCache::get_instance();
return cache.get_table_content(&lookup_table_address);
}
+15 -1
View File
@@ -1,5 +1,5 @@
use borsh::{BorshDeserialize, BorshSerialize};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
use crate::error::{ClientError, ClientResult};
@@ -8,6 +8,7 @@ pub enum DexInstruction {
CreateToken(CreateTokenInfo),
UserTrade(TradeInfo),
BotTrade(TradeInfo),
Tip(TipInfo),
Other,
}
@@ -37,6 +38,12 @@ pub struct TradeInfo {
pub real_token_reserves: u64,
}
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct TipInfo {
pub slot: u64,
pub signature: String,
}
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct CompleteInfo {
pub user: Pubkey,
@@ -61,6 +68,13 @@ pub struct SwapBaseInLog {
pub out_amount: u64,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TransferInfo {
pub slot: u64,
pub signature: String,
pub tx: Option<VersionedTransaction>,
}
pub trait EventTrait: Sized + std::fmt::Debug {
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
}
+10 -2
View File
@@ -1,7 +1,7 @@
use base64::engine::general_purpose;
use base64::Engine;
use regex::Regex;
use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait};
use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo, TipInfo};
pub const PROGRAM_DATA: &str = "Program data: ";
@@ -11,6 +11,7 @@ pub enum PumpfunEvent {
NewDevTrade(TradeInfo),
NewUserTrade(TradeInfo),
NewBotTrade(TradeInfo),
// NewTip(TipInfo),
Error(String),
}
@@ -23,6 +24,12 @@ pub enum DexEvent {
Error(String),
}
#[derive(Debug)]
pub enum SystemEvent {
NewTransfer(TransferInfo),
Error(String),
}
// #[derive(Debug, Clone, Copy)]
// pub struct PumpEvent {}
@@ -67,9 +74,10 @@ impl RaydiumEvent {
if !logs.is_empty() {
let logs_iter = logs.iter().peekable();
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
for l in logs_iter.rev() {
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
if let Some(caps) = re.captures(l) {
if let Some(base64) = caps.name("base64") {
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
+66 -2
View File
@@ -1,11 +1,75 @@
use crate::common::logs_data::DexInstruction;
use crate::common::logs_parser::{parse_create_token_data, parse_trade_data};
use crate::common::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data};
use crate::error::ClientResult;
use solana_sdk::pubkey::Pubkey;
pub struct LogFilter;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use solana_sdk::transaction::VersionedTransaction;
impl LogFilter {
const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
/// Parse transaction logs and return instruction type and data
pub fn parse_compiled_instruction(
versioned_tx: VersionedTransaction,
bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
let compiled_instructions = versioned_tx.message.instructions();
let accounts = versioned_tx.message.static_account_keys();
let program_id = Pubkey::from_str(Self::PROGRAM_ID).unwrap_or_default();
let pump_index = accounts.iter().position(|key| key == &program_id);
let mut instructions: Vec<DexInstruction> = 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;
}
match instruction.data.first() {
// create
Some(&24) => {
if let Ok(token_info) = parse_instruction_create_token_data(instruction, accounts) {
instructions.push(DexInstruction::CreateToken(token_info));
};
}
// buy
Some(&102) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, true) {
if let Some(bot_wallet_pubkey) = bot_wallet {
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
instructions.push(DexInstruction::BotTrade(trade_info));
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
};
}
// sell
Some(&51) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, false) {
if let Some(bot_wallet_pubkey) = bot_wallet {
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
instructions.push(DexInstruction::BotTrade(trade_info));
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
};
}
_ => {}
}
}
}
}
Ok(instructions)
}
/// Parse transaction logs and return instruction type and data
pub fn parse_instruction(logs: &[String], bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
+62
View File
@@ -9,6 +9,8 @@ use crate::common::{
};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::instruction::CompiledInstruction;
use std::time::{SystemTime, UNIX_EPOCH};
pub async fn process_logs<F>(
signature: &str,
@@ -171,4 +173,64 @@ pub fn parse_trade_data(data: &str) -> ClientResult<TradeInfo> {
real_sol_reserves,
real_token_reserves,
})
}
fn current_timestamp_millis() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
duration.as_millis() as i64
}
pub fn parse_instruction_create_token_data(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> ClientResult<CreateTokenInfo> {
let data = instruction.data.clone();
let mut offset = 0;
offset += 8;
let len1 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let name = String::from_utf8_lossy(&data[offset..offset + len1]);
offset += len1;
let len2 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let symbol = String::from_utf8_lossy(&data[offset..offset + len2]);
offset += len2;
let _flag = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
offset += 4;
let hash_start = data.len() - 32;
let ipfs_bytes = &data[offset..hash_start];
let uri = String::from_utf8_lossy(ipfs_bytes);
let mint = accounts[instruction.accounts[0] as usize];
let user = accounts[instruction.accounts[7] as usize];
let bonding_curve= accounts[instruction.accounts[2] as usize];
Ok(CreateTokenInfo {
slot: 0,
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
mint,
bonding_curve,
user,
})
}
pub fn parse_instruction_trade_data(instruction: &CompiledInstruction, accounts: &[Pubkey], is_buy: bool) -> ClientResult<TradeInfo> {
let data = instruction.data.clone();
let amount = u64::from_le_bytes(data[8..16].try_into().unwrap());
let max_sol_cost_or_min_sol_output = u64::from_le_bytes(data[16..24].try_into().unwrap());
let user = accounts[instruction.accounts[6] as usize];
let mint = accounts[instruction.accounts[2] as usize];
Ok(TradeInfo {
slot: 0,
mint,
sol_amount: max_sol_cost_or_min_sol_output,
token_amount: amount,
is_buy,
user,
timestamp: current_timestamp_millis(),
virtual_sol_reserves: 0,
virtual_token_reserves: 0,
real_sol_reserves: 0,
real_token_reserves: 0,
})
}
+4
View File
@@ -3,6 +3,10 @@ pub mod logs_parser;
pub mod logs_filters;
pub mod logs_subscribe;
pub mod logs_events;
pub mod address_lookup;
pub mod nonce_cache;
pub mod tip_cache;
pub mod types;
pub mod address_lookup_cache;
pub use types::*;
+138
View File
@@ -0,0 +1,138 @@
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use std::sync::{Arc, Mutex, OnceLock};
use solana_hash::Hash;
/// NonceInfo 结构体,存储 nonce 相关信息
pub struct NonceInfo {
/// nonce 账户地址
pub nonce_account: Option<Pubkey>,
/// 当前 nonce 值
pub current_nonce: Hash,
/// 下次可用时间(Unix 时间戳,秒)
pub next_buy_time: i64,
/// 锁定状态
pub lock: bool,
/// 是否已使用
pub used: bool,
}
/// NonceInfoStore 单例,用于存储和管理 NonceInfo
pub struct NonceCache {
/// 内部存储的 NonceInfo 数据
nonce_info: Mutex<NonceInfo>,
}
// 使用静态 OnceLock 确保单例模式的线程安全性
static NONCE_CACHE: OnceLock<Arc<NonceCache>> = OnceLock::new();
impl NonceCache {
/// 获取 NonceInfoStore 单例实例
pub fn get_instance() -> Arc<NonceCache> {
NONCE_CACHE
.get_or_init(|| {
Arc::new(NonceCache {
nonce_info: Mutex::new(NonceInfo {
nonce_account: None,
current_nonce: Hash::default(),
next_buy_time: 0,
lock: false,
used: false,
}),
})
})
.clone()
}
/// 初始化 nonce 信息
pub fn init(&self, nonce_account_str: Option<String>) {
let nonce_account = nonce_account_str
.and_then(|s| Pubkey::from_str(&s).ok());
self.update_nonce_info_partial(
nonce_account,
None,
None,
Some(false),
Some(false),
);
}
/// 获取 NonceInfo 的副本
pub fn get_nonce_info(&self) -> NonceInfo {
let nonce_info = self.nonce_info.lock().unwrap();
NonceInfo {
nonce_account: nonce_info.nonce_account,
current_nonce: nonce_info.current_nonce,
next_buy_time: nonce_info.next_buy_time,
lock: nonce_info.lock,
used: nonce_info.used,
}
}
/// 部分更新 NonceInfo,只更新传入的字段
pub fn update_nonce_info_partial(
&self,
nonce_account: Option<Pubkey>,
current_nonce: Option<Hash>,
next_buy_time: Option<i64>,
lock: Option<bool>,
used: Option<bool>,
) {
let mut current = self.nonce_info.lock().unwrap();
// 只更新传入的字段
if let Some(account) = nonce_account {
current.nonce_account = Some(account);
}
if let Some(nonce) = current_nonce {
current.current_nonce = nonce;
}
if let Some(time) = next_buy_time {
current.next_buy_time = time;
}
if let Some(l) = lock {
current.lock = l;
}
if let Some(u) = used {
current.used = u;
}
}
/// 标记 nonce 已使用
pub fn mark_used(&self) {
self.update_nonce_info_partial(
None,
None,
None,
None,
Some(true),
);
}
/// 锁定 nonce
pub fn lock(&self) {
self.update_nonce_info_partial(
None,
None,
None,
Some(true),
None,
);
}
/// 解锁 nonce
pub fn unlock(&self) {
self.update_nonce_info_partial(
None,
None,
None,
Some(false),
None,
);
}
}
+38
View File
@@ -0,0 +1,38 @@
use std::sync::{Arc, Mutex, OnceLock};
/// TipCache 单例,用于存储和管理 tip 金额
pub struct TipCache {
/// tip 金额
tip_amount: Mutex<f64>,
}
static TIP_CACHE: OnceLock<Arc<TipCache>> = OnceLock::new();
impl TipCache {
/// 获取 TipCache 单例实例
pub fn get_instance() -> Arc<TipCache> {
TIP_CACHE
.get_or_init(|| {
Arc::new(TipCache {
tip_amount: Mutex::new(0.001),
})
})
.clone()
}
/// 初始化 tip 金额
pub fn init(&self, tip_amount: Option<f64>) {
let amount = tip_amount.unwrap_or(0.001);
self.update_tip(amount);
}
/// 获取 tip 金额
pub fn get_tip(&self) -> f64 {
*self.tip_amount.lock().unwrap()
}
/// 更新 tip 金额
pub fn update_tip(&self, amount: f64) {
*self.tip_amount.lock().unwrap() = amount;
}
}
+25 -5
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair};
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair};
use serde::Deserialize;
use crate::{constants::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, swqos::FeeClient};
@@ -19,11 +19,16 @@ pub struct Cluster {
pub nextblock_auth_token: String,
pub zeroslot_url: String,
pub zeroslot_auth_token: String,
pub nozomi_url: String,
pub nozomi_auth_token: String,
pub use_jito: bool,
pub use_nextblock: bool,
pub use_zeroslot: bool,
pub use_nozomi: bool,
pub priority_fee: PriorityFee,
pub commitment: CommitmentConfig,
pub lookup_table_key: Option<Pubkey>,
pub use_rpc: bool,
}
impl Cluster {
@@ -34,11 +39,16 @@ impl Cluster {
String, nextblock_auth_token:
String, zeroslot_url: String,
zeroslot_auth_token: String,
nozomi_url: String,
nozomi_auth_token: String,
priority_fee: PriorityFee,
commitment: CommitmentConfig,
use_jito: bool,
use_nextblock: bool,
use_zeroslot: bool
use_zeroslot: bool,
use_nozomi: bool,
lookup_table_key: Option<Pubkey>,
use_rpc: bool,
) -> Self {
Self {
rpc_url,
@@ -47,21 +57,28 @@ impl Cluster {
nextblock_auth_token,
zeroslot_url,
zeroslot_auth_token,
nozomi_url,
nozomi_auth_token,
priority_fee,
commitment,
use_jito,
use_nextblock,
use_zeroslot
use_zeroslot,
use_nozomi,
lookup_table_key,
use_rpc,
}
}
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq)]
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct PriorityFee {
pub unit_limit: u32,
pub unit_price: u64,
pub rpc_unit_limit: u32,
pub rpc_unit_price: u64,
pub buy_tip_fee: f64,
pub buy_tip_fees: Vec<f64>,
pub sell_tip_fee: f64,
}
@@ -70,7 +87,10 @@ impl Default for PriorityFee {
Self {
unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT,
unit_price: DEFAULT_COMPUTE_UNIT_PRICE,
rpc_unit_limit: 0,
rpc_unit_price: 0,
buy_tip_fee: DEFAULT_BUY_TIP_FEE,
buy_tip_fees: vec![],
sell_tip_fee: DEFAULT_SELL_TIP_FEE
}
}