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:
Executable
+393
@@ -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
|
||||
}
|
||||
Executable
+154
@@ -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
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
Executable
+138
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+38
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user