From bfe76fcddc11da43a052e59726b503b511471418 Mon Sep 17 00:00:00 2001 From: sgxiang Date: Thu, 29 May 2025 18:53:58 +0800 Subject: [PATCH] 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 --- Cargo.toml | 6 +- src/accounts/bonding_curve.rs | 32 +- src/accounts/global.rs | 171 +++------- src/common/address_lookup.rs | 393 +++++++++++++++++++++++ src/common/address_lookup_cache.rs | 154 ++++++++++ src/common/logs_data.rs | 16 +- src/common/logs_events.rs | 12 +- src/common/logs_filters.rs | 68 +++- src/common/logs_parser.rs | 62 ++++ src/common/mod.rs | 4 + src/common/nonce_cache.rs | 138 +++++++++ src/common/tip_cache.rs | 38 +++ src/common/types.rs | 30 +- src/grpc/mod.rs | 254 +-------------- src/grpc/shred_stream.rs | 114 +++++++ src/grpc/yellow_stone.rs | 352 +++++++++++++++++++++ src/instruction/mod.rs | 91 +++++- src/ipfs/mod.rs | 12 +- src/lib.rs | 141 ++++++++- src/main.rs | 84 ++++- src/pumpfun/buy.rs | 372 ++++++++++++++++++---- src/pumpfun/common.rs | 102 +++++- src/pumpfun/create.rs | 238 ++++++++++++++ src/pumpfun/mod.rs | 1 + src/pumpfun/sell.rs | 205 +++++++----- src/swqos/common.rs | 9 +- src/swqos/jito_grpc/auth.rs | 260 ++++++++++++++++ src/swqos/jito_grpc/block.rs | 19 ++ src/swqos/jito_grpc/block_engine.rs | 462 ++++++++++++++++++++++++++++ src/swqos/jito_grpc/convert.rs | 10 +- src/swqos/jito_grpc/mod.rs | 8 +- src/swqos/jito_grpc/relayer.rs | 178 +++++++++++ src/swqos/jito_grpc/shredstream.rs | 279 +++++++++++++++++ src/swqos/jito_grpc/trace_shred.rs | 13 + src/swqos/mod.rs | 343 ++++++++++++++++----- src/swqos/searcher_client.rs | 24 +- src/swqos/token_authenticator.rs | 167 ++++++++++ 37 files changed, 4168 insertions(+), 694 deletions(-) create mode 100755 src/common/address_lookup.rs create mode 100755 src/common/address_lookup_cache.rs create mode 100755 src/common/nonce_cache.rs create mode 100755 src/common/tip_cache.rs create mode 100755 src/grpc/shred_stream.rs create mode 100755 src/grpc/yellow_stone.rs create mode 100755 src/pumpfun/create.rs create mode 100755 src/swqos/jito_grpc/auth.rs create mode 100755 src/swqos/jito_grpc/block.rs create mode 100755 src/swqos/jito_grpc/block_engine.rs create mode 100755 src/swqos/jito_grpc/relayer.rs create mode 100755 src/swqos/jito_grpc/shredstream.rs create mode 100755 src/swqos/jito_grpc/trace_shred.rs create mode 100755 src/swqos/token_authenticator.rs diff --git a/Cargo.toml b/Cargo.toml index 60ccbe8..8b0422a 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,10 @@ solana-rpc-client-api = "2.1.16" solana-transaction-status = "2.1.16" solana-account-decoder = "2.1.16" solana-hash = "2.1.16" -solana-perf = "2.1.16" solana-security-txt = "1.1.1" +solana-entry = "2.1.16" +solana-rpc-client-nonce-utils = "2.1.16" +solana-perf = "2.1.16" spl-token = "8.0.0" spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } @@ -48,8 +50,8 @@ tonic = { version = "0.12.3", features = ["tls", "tls-roots", "tls-webpki-roots" rustls = { version = "0.23.23", features = ["ring"] } rustls-native-certs = "0.8.1" tokio-rustls = "0.26.1" +core_affinity = "0.8" -bytes = "1.4.0" dotenvy = "0.15.7" pretty_env_logger = "0.5.0" log = "0.4.22" diff --git a/src/accounts/bonding_curve.rs b/src/accounts/bonding_curve.rs index 1b4ae53..4e3d59a 100755 --- a/src/accounts/bonding_curve.rs +++ b/src/accounts/bonding_curve.rs @@ -28,11 +28,15 @@ use serde::{Serialize, Deserialize}; use solana_sdk::pubkey::Pubkey; +use crate::{constants::global_constants::{INITIAL_REAL_TOKEN_RESERVES, INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES, TOKEN_TOTAL_SUPPLY}, pumpfun::common::{get_bonding_curve_pda, get_creator_vault_pda}}; + /// Represents the global configuration account for token pricing and fees #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BondingCurveAccount { /// Unique identifier for the bonding curve pub discriminator: u64, + /// Account address + pub account: Pubkey, /// Virtual token reserves used for price calculations pub virtual_token_reserves: u64, /// Virtual SOL reserves used for price calculations @@ -60,17 +64,23 @@ impl BondingCurveAccount { /// * `real_sol_reserves` - Actual SOL reserves available /// * `token_total_supply` - Total supply of tokens /// * `complete` - Whether the curve is complete - // pub fn new(mint: &Pubkey, dev_buy_token_amount: u64, dev_buy_sol_amount: u64) -> Self { - // Self { - // // account: get_bonding_curve_pda(mint).unwrap(), - // virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_buy_token_amount, - // virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_buy_sol_amount, - // real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_buy_token_amount, - // real_sol_reserves: dev_buy_sol_amount, - // token_total_supply: TOKEN_TOTAL_SUPPLY, - // complete: false, - // } - // } + pub fn new(mint: &Pubkey, dev_buy_token: u64, dev_cost_sol: u64, creator: Pubkey) -> Self { + Self { + discriminator: 0, + account: get_bonding_curve_pda(mint).unwrap(), + virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_buy_token, + virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_cost_sol, + real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_buy_token, + real_sol_reserves: dev_cost_sol, + token_total_supply: TOKEN_TOTAL_SUPPLY, + complete: false, + creator: creator, + } + } + + pub fn get_creator_vault_pda(&self) -> Pubkey { + get_creator_vault_pda(&self.creator).unwrap() + } /// Calculates the amount of tokens received for a given SOL amount /// diff --git a/src/accounts/global.rs b/src/accounts/global.rs index 91aa052..b424a63 100755 --- a/src/accounts/global.rs +++ b/src/accounts/global.rs @@ -17,20 +17,24 @@ //! - `initial_real_token_reserves`: Initial actual token reserves available for trading //! - `token_total_supply`: Total supply of tokens //! - `fee_basis_points`: Fee in basis points (1/100th of a percent) -//! -//! # Methods -//! -//! - `new`: Creates a new global account instance -//! - `get_initial_buy_price`: Calculates the initial amount of tokens received for a given SOL amount +//! - `withdraw_authority`: Authority that can withdraw fees +//! - `enable_migrate`: Whether migration is enabled +//! - `pool_migration_fee`: Fee for pool migration +//! - `creator_fee`: Fee for creators +//! - `fee_recipients`: Array of fee recipient accounts use solana_sdk::pubkey::Pubkey; -use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Serialize, Deserialize}; + +use crate::constants::global_constants::*; + /// Represents the global configuration account for token pricing and fees #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GlobalAccount { /// Unique identifier for the global account pub discriminator: u64, + /// Pubkey of the global account + pub account: Pubkey, /// Whether the global account has been initialized pub initialized: bool, /// Authority that can modify global settings @@ -39,7 +43,7 @@ pub struct GlobalAccount { pub fee_recipient: Pubkey, /// Initial virtual token reserves for price calculations pub initial_virtual_token_reserves: u64, - /// Initial virtual SOL reserves for price calculations + /// Initial virtual SOL reserves for price calculations pub initial_virtual_sol_reserves: u64, /// Initial actual token reserves available for trading pub initial_real_token_reserves: u64, @@ -47,43 +51,45 @@ pub struct GlobalAccount { pub token_total_supply: u64, /// Fee in basis points (1/100th of a percent) pub fee_basis_points: u64, + /// Authority that can withdraw fees + pub withdraw_authority: Pubkey, + /// Whether migration is enabled + pub enable_migrate: bool, + /// Fee for pool migration + pub pool_migration_fee: u64, + /// Fee for creators + pub creator_fee: u64, + /// Array of fee recipient accounts + pub fee_recipients: [Pubkey; 7], } impl GlobalAccount { /// Creates a new global account instance - /// - /// # Arguments - /// * `discriminator` - Unique identifier for the account - /// * `initialized` - Whether the account is initialized - /// * `authority` - Authority pubkey that can modify settings - /// * `fee_recipient` - Account that receives fees - /// * `initial_virtual_token_reserves` - Initial virtual token reserves - /// * `initial_virtual_sol_reserves` - Initial virtual SOL reserves - /// * `initial_real_token_reserves` - Initial actual token reserves - /// * `token_total_supply` - Total supply of tokens - /// * `fee_basis_points` - Fee in basis points - #[allow(clippy::too_many_arguments)] - pub fn new( - discriminator: u64, - initialized: bool, - authority: Pubkey, - fee_recipient: Pubkey, - initial_virtual_token_reserves: u64, - initial_virtual_sol_reserves: u64, - initial_real_token_reserves: u64, - token_total_supply: u64, - fee_basis_points: u64, - ) -> Self { + pub fn new() -> Self { Self { - discriminator, - initialized, - authority, - fee_recipient, - initial_virtual_token_reserves, - initial_virtual_sol_reserves, - initial_real_token_reserves, - token_total_supply, - fee_basis_points, + discriminator: 0, + account: GLOBAL_ACCOUNT, + initialized: true, + authority: AUTHORITY, + fee_recipient: FEE_RECIPIENT, + initial_virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES, + initial_virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES, + initial_real_token_reserves: INITIAL_REAL_TOKEN_RESERVES, + token_total_supply: TOKEN_TOTAL_SUPPLY, + fee_basis_points: FEE_BASIS_POINTS, + withdraw_authority: WITHDRAW_AUTHORITY, + enable_migrate: ENABLE_MIGRATE, + pool_migration_fee: POOL_MIGRATION_FEE, + creator_fee: CREATOR_FEE, + fee_recipients: [ + PUMPFUN_AMM_FEE_1, + PUMPFUN_AMM_FEE_2, + PUMPFUN_AMM_FEE_3, + PUMPFUN_AMM_FEE_4, + PUMPFUN_AMM_FEE_5, + PUMPFUN_AMM_FEE_6, + PUMPFUN_AMM_FEE_7, + ], } } @@ -112,90 +118,3 @@ impl GlobalAccount { } } } - -#[cfg(test)] -mod tests { - use super::*; - - fn get_global() -> GlobalAccount { - GlobalAccount::new( - 1, - true, - Pubkey::new_unique(), - Pubkey::new_unique(), - 1000, - 1000, - 500, - 1000, - 250, - ) - } - - fn get_large_global() -> GlobalAccount { - GlobalAccount::new( - 1, - true, - Pubkey::new_unique(), - Pubkey::new_unique(), - u64::MAX, - u64::MAX, - u64::MAX / 2, - u64::MAX, - 250, - ) - } - - #[test] - fn test_global_account() { - let global: GlobalAccount = get_global(); - - // Test initial buy price calculation - assert_eq!(global.get_initial_buy_price(0), 0); - - let price: u64 = global.get_initial_buy_price(100); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - } - - #[test] - fn test_global_account_max_reserves() { - let mut global: GlobalAccount = get_global(); - global.initial_real_token_reserves = 100; - - // Test that returned amount is capped by real_token_reserves - let price: u64 = global.get_initial_buy_price(1000); - assert_eq!(price, global.initial_real_token_reserves); - } - - #[test] - fn test_global_account_overflow() { - let global: GlobalAccount = get_large_global(); - - // Test with maximum possible SOL amount - let price: u64 = global.get_initial_buy_price(u64::MAX); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - - // Test with large but not maximum SOL amount - let price: u64 = global.get_initial_buy_price(u64::MAX / 2); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - } - - #[test] - fn test_global_account_overflow_edge_cases() { - let mut global: GlobalAccount = get_large_global(); - global.initial_virtual_sol_reserves = u64::MAX - 1000; - global.initial_virtual_token_reserves = u64::MAX - 1000; - global.initial_real_token_reserves = u64::MAX / 4; - - // Test with amounts near u64::MAX - let price: u64 = global.get_initial_buy_price(u64::MAX - 1); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - - let price: u64 = global.get_initial_buy_price(u64::MAX - 1000); - assert!(price > 0); - assert!(price <= global.initial_real_token_reserves); - } -} diff --git a/src/common/address_lookup.rs b/src/common/address_lookup.rs new file mode 100755 index 0000000..ffbc5be --- /dev/null +++ b/src/common/address_lookup.rs @@ -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, + authority: &Keypair, + payer: &Keypair, +) -> Result> { + // 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, + payer: &Keypair, + authority: &Keypair, + lookup_table_address: &Pubkey, + addresses: Vec, +) -> Result<(), Box> { + 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, + payer: &Keypair, + authority: &Keypair, + lookup_table_address: &Pubkey, +) -> Result<(), Box> { + 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, + lookup_table_address: &Pubkey, +) -> Result> { + 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, + instructions: Vec, + payer: &Keypair, + signers: Vec<&Keypair>, + address_lookup_tables: Vec, +) -> Result<(), Box> { + 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, + instructions: Vec, + payer: &Keypair, + signers: Vec<&Keypair>, + lookup_table: AddressLookupTableAccount, + address_indices_to_use: &[usize], // 要使用的地址索引列表 +) -> Result<(), Box> { + // 创建只包含选定地址的新查找表账户 + let filtered_addresses: Vec = 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) -> Result> { + 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, + instructions: Vec, + payer: &Keypair, + signers: Vec<&Keypair>, + lookup_table: AddressLookupTableAccount, + addresses_to_use: &[Pubkey], +) -> Result> { + // 构建地址到索引的映射 + 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 = 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, + payer: &Keypair, + authority: &Keypair, +) -> Result> { + 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, + payer: &Keypair, + authority: &Keypair, + lookup_table_address: &Pubkey, +) -> Result<(), Box> { + 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, + payer: &Keypair, + authority: &Keypair, + lookup_table_address: &Pubkey, + addresses: Vec, +) -> Result<(), Box> { + extend_lookup_table( + client, + payer, + authority, + lookup_table_address, + addresses + ).await?; + + Ok(()) +} + +pub fn get_pumpfun_addresses(payer: Pubkey, include_addresses: Vec) -> Vec { + 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) -> Vec { + 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 +} diff --git a/src/common/address_lookup_cache.rs b/src/common/address_lookup_cache.rs new file mode 100755 index 0000000..20e3222 --- /dev/null +++ b/src/common/address_lookup_cache.rs @@ -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, + /// 地址表内容 + pub address_lookup_table: Option, + /// 锁定状态 + pub lock: bool, +} + +/// AddressLookupTableCache 单例,用于存储和管理地址表 +pub struct AddressLookupTableCache { + /// 内部存储的地址表数据,键为地址表地址 + tables: Mutex>, +} + +// 使用静态 OnceLock 确保单例模式的线程安全性 +static ADDRESS_LOOKUP_TABLE_CACHE: OnceLock> = OnceLock::new(); + +impl AddressLookupTableCache { + /// 获取 AddressLookupTableCache 单例实例 + pub fn get_instance() -> Arc { + 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, + lock: Option, + ) { + 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 { + 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 { + 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); +} \ No newline at end of file diff --git a/src/common/logs_data.rs b/src/common/logs_data.rs index 05ae2d6..2ac8781 100755 --- a/src/common/logs_data.rs +++ b/src/common/logs_data.rs @@ -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, +} + pub trait EventTrait: Sized + std::fmt::Debug { fn from_bytes(bytes: &[u8]) -> ClientResult; } diff --git a/src/common/logs_events.rs b/src/common/logs_events.rs index a5799e5..e4c3d63 100755 --- a/src/common/logs_events.rs +++ b/src/common/logs_events.rs @@ -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[A-Za-z0-9+/=]+)").unwrap(); for l in logs_iter.rev() { + let re = Regex::new(r"ray_log: (?P[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(); diff --git a/src/common/logs_filters.rs b/src/common/logs_filters.rs index 4ac4963..d0b6c96 100755 --- a/src/common/logs_filters.rs +++ b/src/common/logs_filters.rs @@ -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) -> ClientResult> { + 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 = 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) -> ClientResult> { diff --git a/src/common/logs_parser.rs b/src/common/logs_parser.rs index 7da29d6..7832f74 100755 --- a/src/common/logs_parser.rs +++ b/src/common/logs_parser.rs @@ -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( signature: &str, @@ -171,4 +173,64 @@ pub fn parse_trade_data(data: &str) -> ClientResult { 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 { + 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 { + 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, + }) } \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs index af56e04..d4c5dfe 100755 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -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::*; diff --git a/src/common/nonce_cache.rs b/src/common/nonce_cache.rs new file mode 100755 index 0000000..4fe1026 --- /dev/null +++ b/src/common/nonce_cache.rs @@ -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, + /// 当前 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, +} + +// 使用静态 OnceLock 确保单例模式的线程安全性 +static NONCE_CACHE: OnceLock> = OnceLock::new(); + +impl NonceCache { + /// 获取 NonceInfoStore 单例实例 + pub fn get_instance() -> Arc { + 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) { + 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, + current_nonce: Option, + next_buy_time: Option, + lock: Option, + used: Option, + ) { + 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, + ); + } +} diff --git a/src/common/tip_cache.rs b/src/common/tip_cache.rs new file mode 100755 index 0000000..3f6c7b2 --- /dev/null +++ b/src/common/tip_cache.rs @@ -0,0 +1,38 @@ +use std::sync::{Arc, Mutex, OnceLock}; + +/// TipCache 单例,用于存储和管理 tip 金额 +pub struct TipCache { + /// tip 金额 + tip_amount: Mutex, +} + +static TIP_CACHE: OnceLock> = OnceLock::new(); + +impl TipCache { + /// 获取 TipCache 单例实例 + pub fn get_instance() -> Arc { + TIP_CACHE + .get_or_init(|| { + Arc::new(TipCache { + tip_amount: Mutex::new(0.001), + }) + }) + .clone() + } + + /// 初始化 tip 金额 + pub fn init(&self, tip_amount: Option) { + 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; + } +} \ No newline at end of file diff --git a/src/common/types.rs b/src/common/types.rs index 7b1de0d..4dfd3d9 100755 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -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, + 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, + 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, 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 } } diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 52067a0..217dac6 100755 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -1,251 +1,5 @@ -use std::{collections::HashMap, fmt, time::Duration}; +pub mod yellow_stone; +pub mod shred_stream; -use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; -use rustls::crypto::{ring::default_provider, CryptoProvider}; -use tonic::{transport::channel::ClientTlsConfig, Status}; -use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult}; -use yellowstone_grpc_proto::geyser::{ - CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, - SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, -}; -use log::{error, info}; -use chrono::Local; -use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature}; -use solana_transaction_status::{ - option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, -}; - -use crate::common::logs_data::DexInstruction; -use crate::common::logs_events::PumpfunEvent; -use crate::common::logs_filters::LogFilter; -use crate::error::{ClientError, ClientResult}; - -type TransactionsFilterMap = HashMap; - -const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); -const CONNECT_TIMEOUT: u64 = 10; -const REQUEST_TIMEOUT: u64 = 60; -const CHANNEL_SIZE: usize = 1000; - -#[derive(Clone)] -pub struct TransactionPretty { - pub slot: u64, - pub signature: Signature, - pub is_vote: bool, - pub tx: EncodedTransactionWithStatusMeta, - // pub transaction: Option, -} - -impl fmt::Debug for TransactionPretty { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta); - impl<'a> fmt::Debug for TxWrap<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let serialized = serde_json::to_string(self.0).expect("failed to serialize"); - fmt::Display::fmt(&serialized, f) - } - } - - f.debug_struct("TransactionPretty") - .field("slot", &self.slot) - .field("signature", &self.signature) - .field("is_vote", &self.is_vote) - .field("tx", &TxWrap(&self.tx)) - .finish() - } -} - -impl From for TransactionPretty { - fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self { - let tx = transaction.expect("should be defined"); - // let transaction_info = tx.transaction.clone().unwrap(); - Self { - slot, - signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"), - is_vote: tx.is_vote, - tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx) - .expect("valid tx with meta") - .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) - .expect("failed to encode"), - // transaction: Some(transaction_info), - } - } -} - -#[derive(Clone)] -pub struct YellowstoneGrpc { - endpoint: String, -} - -impl YellowstoneGrpc { - pub fn new(endpoint: String) -> Self { - Self { endpoint } - } - - pub async fn connect( - &self, - transactions: TransactionsFilterMap, - ) -> ClientResult< - GeyserGrpcClientResult<( - impl Sink, - impl Stream>, - )> - > { - if CryptoProvider::get_default().is_none() { - default_provider() - .install_default() - .map_err(|e| ClientError::Other(format!("Failed to install crypto provider: {:?}", e)))?; - } - - let mut client = GeyserGrpcClient::build_from_shared(self.endpoint.clone()) - .map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))? - .tls_config(ClientTlsConfig::new().with_native_roots()) - .map_err(|e| ClientError::Other(format!("Failed to build client: {:?}", e)))? - .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT)) - .timeout(Duration::from_secs(REQUEST_TIMEOUT)) - .connect() - .await - .map_err(|e| ClientError::Other(format!("Failed to connect: {:?}", e)))?; - - let subscribe_request = SubscribeRequest { - transactions, - commitment: Some(CommitmentLevel::Processed.into()), - ..Default::default() - }; - - Ok(client.subscribe_with_request(Some(subscribe_request)).await) - } - - pub fn get_subscribe_request_filter( - &self, - account_include: Vec, - account_exclude: Vec, - account_required: Vec, - ) -> TransactionsFilterMap { - let mut transactions = HashMap::new(); - transactions.insert( - "client".to_string(), - SubscribeRequestFilterTransactions { - vote: Some(false), - failed: Some(false), - signature: None, - account_include, - account_exclude, - account_required, - }, - ); - transactions - } - - async fn handle_stream_message( - msg: SubscribeUpdate, - tx: &mut mpsc::Sender, - subscribe_tx: &mut (impl Sink + Unpin), - ) -> ClientResult<()> { - match msg.update_oneof { - Some(UpdateOneof::Transaction(sut)) => { - let transaction_pretty = TransactionPretty::from(sut); - tx.try_send(transaction_pretty).map_err(|e| ClientError::Other(format!("Send error: {:?}", e)))?; - } - Some(UpdateOneof::Ping(_)) => { - subscribe_tx - .send(SubscribeRequest { - ping: Some(SubscribeRequestPing { id: 1 }), - ..Default::default() - }) - .await - .map_err(|e| ClientError::Other(format!("Ping error: {:?}", e)))?; - info!("service is ping: {}", Local::now()); - } - Some(UpdateOneof::Pong(_)) => { - info!("service is pong: {}", Local::now()); - } - _ => {} - } - Ok(()) - } - - pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> ClientResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - 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.connect(transactions).await? - .map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?; - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - - 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_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { - error!("Error processing transaction: {:?}", e); - } - } - Ok(()) - } - - async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> ClientResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync, - { - let slot = transaction_pretty.slot; - let trade_raw = transaction_pretty.tx; - let meta = trade_raw.meta.as_ref() - .ok_or_else(|| ClientError::Other("Missing transaction metadata".to_string()))?; - - if meta.err.is_some() { - return Ok(()); - } - - let logs = if let OptionSerializer::Some(logs) = &meta.log_messages { - logs - } else { - &vec![] - }; - - let mut dev_address: Option = None; - let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); - for instruction in instructions { - match instruction { - DexInstruction::CreateToken(mut token_info) => { - token_info.slot = slot; - dev_address = Some(token_info.user); - callback(PumpfunEvent::NewToken(token_info)); - } - DexInstruction::UserTrade(mut trade_info) => { - trade_info.slot = slot; - if Some(trade_info.user) == dev_address { - callback(PumpfunEvent::NewDevTrade(trade_info)); - } else { - callback(PumpfunEvent::NewUserTrade(trade_info)); - } - } - DexInstruction::BotTrade(mut trade_info) => { - trade_info.slot = slot; - callback(PumpfunEvent::NewBotTrade(trade_info)); - } - _ => {} - } - } - - Ok(()) - } -} +pub use yellow_stone::YellowstoneGrpc; +pub use shred_stream::ShredStreamGrpc; \ No newline at end of file diff --git a/src/grpc/shred_stream.rs b/src/grpc/shred_stream.rs new file mode 100755 index 0000000..1e1a52b --- /dev/null +++ b/src/grpc/shred_stream.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use futures::{channel::mpsc, StreamExt}; +use solana_entry::entry::Entry; +use tonic::transport::Channel; + +use log::error; +use solana_sdk::transaction::VersionedTransaction; + +use crate::common::AnyResult; + +use solana_sdk::pubkey::Pubkey; +use crate::common::logs_data::DexInstruction; +use crate::common::logs_events::PumpfunEvent; +use crate::common::logs_filters::LogFilter; +use crate::swqos::jito_grpc::shredstream::shredstream_proxy_client::ShredstreamProxyClient; +use crate::swqos::jito_grpc::shredstream::SubscribeEntriesRequest; + +const CHANNEL_SIZE: usize = 1000; + +pub struct ShredStreamGrpc { + shredstream_client: Arc>, +} + +struct TransactionWithSlot { + transaction: VersionedTransaction, + slot: u64, +} + + +impl ShredStreamGrpc { + pub async fn new(endpoint: String) -> AnyResult { + let shredstream_client = ShredstreamProxyClient::connect(endpoint.clone()).await?; + Ok(Self { + shredstream_client: Arc::new(shredstream_client) + }) + } + + pub async fn shredstream_subscribe(&self, callback: F, bot_wallet: Option) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + 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::(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::>(&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_pumpfun_transaction(transaction_with_slot, &*callback, bot_wallet).await { + error!("Error processing transaction: {:?}", e); + } + } + + Ok(()) + } + + async fn process_pumpfun_transaction(transaction_with_slot: TransactionWithSlot, callback: &F, bot_wallet: Option) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync, + { + let slot = transaction_with_slot.slot; + let versioned_tx = transaction_with_slot.transaction; + let mut dev_address: Option = None; + // let hash = versioned_tx.signatures[0].to_string(); + let instructions = LogFilter::parse_compiled_instruction(versioned_tx, bot_wallet).unwrap(); + for instruction in instructions { + // println!("hash: {}\ninstruction: {:?}\n\n", hash, instruction); + match instruction { + DexInstruction::CreateToken(mut token_info) => { + token_info.slot = slot; + dev_address = Some(token_info.user); + callback(PumpfunEvent::NewToken(token_info)); + } + DexInstruction::UserTrade(mut trade_info) => { + trade_info.slot = slot; + if Some(trade_info.user) == dev_address { + callback(PumpfunEvent::NewDevTrade(trade_info)); + } else { + callback(PumpfunEvent::NewUserTrade(trade_info)); + } + } + DexInstruction::BotTrade(mut trade_info) => { + trade_info.slot = slot; + callback(PumpfunEvent::NewBotTrade(trade_info)); + } + _ => {} + } + } + Ok(()) + } +} diff --git a/src/grpc/yellow_stone.rs b/src/grpc/yellow_stone.rs new file mode 100755 index 0000000..b9fb663 --- /dev/null +++ b/src/grpc/yellow_stone.rs @@ -0,0 +1,352 @@ +use std::{collections::HashMap, fmt, time::Duration}; + +use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; +use rustls::crypto::{ring::default_provider, CryptoProvider}; +use tonic::{transport::channel::ClientTlsConfig, Status}; +use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor}; +use yellowstone_grpc_proto::geyser::{ + CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, + SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, +}; +use log::{error, info}; +use chrono::Local; +use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature}; +use solana_transaction_status::{ + option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, +}; + +use crate::common::logs_data::{DexInstruction, TransferInfo}; +use crate::common::logs_events::{PumpfunEvent, SystemEvent}; +use crate::common::logs_filters::LogFilter; +use crate::common::AnyResult; + +type TransactionsFilterMap = HashMap; + +const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); +const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111"); +const CONNECT_TIMEOUT: u64 = 10; +const REQUEST_TIMEOUT: u64 = 60; +const CHANNEL_SIZE: usize = 1000; +const MAX_DECODING_MESSAGE_SIZE: usize = 1024 * 1024 * 10; + +#[derive(Clone)] +pub struct TransactionPretty { + pub slot: u64, + pub signature: Signature, + pub is_vote: bool, + pub tx: EncodedTransactionWithStatusMeta, +} + +impl fmt::Debug for TransactionPretty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta); + impl<'a> fmt::Debug for TxWrap<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let serialized = serde_json::to_string(self.0).expect("failed to serialize"); + fmt::Display::fmt(&serialized, f) + } + } + + f.debug_struct("TransactionPretty") + .field("slot", &self.slot) + .field("signature", &self.signature) + .field("is_vote", &self.is_vote) + .field("tx", &TxWrap(&self.tx)) + .finish() + } +} + +impl From for TransactionPretty { + fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self { + let tx = transaction.expect("should be defined"); + Self { + slot, + signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"), + is_vote: tx.is_vote, + tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx) + .expect("valid tx with meta") + .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) + .expect("failed to encode"), + } + } +} + +pub struct YellowstoneGrpc { + endpoint: String, + x_token: Option, +} + +impl YellowstoneGrpc { + pub fn new(endpoint: String, x_token: Option) -> AnyResult { + if CryptoProvider::get_default().is_none() { + default_provider() + .install_default() + .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?; + } + + Ok(Self { + endpoint, + x_token, + }) + } + + pub async fn connect( + &self, + ) -> AnyResult> + { + let builder = GeyserGrpcClient::build_from_shared(self.endpoint.clone())? + .x_token(self.x_token.clone())? + .tls_config(ClientTlsConfig::new().with_native_roots())? + .max_decoding_message_size(MAX_DECODING_MESSAGE_SIZE) + .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT)) + .timeout(Duration::from_secs(REQUEST_TIMEOUT)); + + Ok(builder.connect().await?) + } + + pub async fn subscribe_with_request( + &self, + transactions: TransactionsFilterMap, + ) -> AnyResult<( + impl Sink, + impl Stream>, + )> { + let subscribe_request = SubscribeRequest { + transactions, + commitment: Some(CommitmentLevel::Processed.into()), + ..Default::default() + }; + + let mut client = self.connect().await?; + let (sink, stream) = client.subscribe_with_request(Some(subscribe_request)).await?; + Ok((sink, stream)) + } + + pub fn get_subscribe_request_filter( + &self, + account_include: Vec, + account_exclude: Vec, + account_required: Vec, + ) -> TransactionsFilterMap { + let mut transactions = HashMap::new(); + transactions.insert( + "client".to_string(), + SubscribeRequestFilterTransactions { + vote: Some(false), + failed: Some(false), + signature: None, + account_include, + account_exclude, + account_required, + }, + ); + transactions + } + + async fn handle_stream_message( + msg: SubscribeUpdate, + tx: &mut mpsc::Sender, + subscribe_tx: &mut (impl Sink + Unpin), + ) -> AnyResult<()> { + match msg.update_oneof { + Some(UpdateOneof::Transaction(sut)) => { + let transaction_pretty = TransactionPretty::from(sut); + tx.try_send(transaction_pretty)?; + } + Some(UpdateOneof::Ping(_)) => { + subscribe_tx + .send(SubscribeRequest { + ping: Some(SubscribeRequestPing { id: 1 }), + ..Default::default() + }) + .await?; + info!("service is ping: {}", Local::now()); + } + Some(UpdateOneof::Pong(_)) => { + info!("service is pong: {}", Local::now()); + } + _ => {} + } + Ok(()) + } + + pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync + 'static, + { + 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::(CHANNEL_SIZE); + + 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_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + pub async fn subscribe_pumpfun_with_filter(&self, callback: F, bot_wallet: Option, account_include: Option>, account_exclude: Option>) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync + 'static, + { + 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::(CHANNEL_SIZE); + + 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_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> AnyResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync, + { + let slot = transaction_pretty.slot; + let trade_raw: 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 OptionSerializer::Some(logs) = &meta.log_messages { + logs + } else { + &vec![] + }; + + let mut dev_address: Option = None; + let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); + for instruction in instructions { + match instruction { + DexInstruction::CreateToken(mut token_info) => { + token_info.slot = slot; + dev_address = Some(token_info.user); + callback(PumpfunEvent::NewToken(token_info)); + } + DexInstruction::UserTrade(mut trade_info) => { + trade_info.slot = slot; + if Some(trade_info.user) == dev_address { + callback(PumpfunEvent::NewDevTrade(trade_info)); + } else { + callback(PumpfunEvent::NewUserTrade(trade_info)); + } + } + DexInstruction::BotTrade(mut trade_info) => { + trade_info.slot = slot; + callback(PumpfunEvent::NewBotTrade(trade_info)); + } + _ => {} + } + } + + Ok(()) + } + + pub async fn subscribe_system(&self, callback: F, account_include: Option>, account_exclude: Option>) -> AnyResult<()> + where + F: Fn(SystemEvent) + Send + Sync + 'static, + { + let addrs = vec![SYSTEM_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::(CHANNEL_SIZE); + + 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_system_transaction(transaction_pretty, &*callback).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + async fn process_system_transaction(transaction_pretty: TransactionPretty, callback: &F) -> AnyResult<()> + where + F: Fn(SystemEvent) + Send + Sync, + { + let trade_raw: 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(()); + } + + callback(SystemEvent::NewTransfer(TransferInfo { + slot: transaction_pretty.slot, + signature: transaction_pretty.signature.to_string(), + tx: trade_raw.transaction.decode(), + })); + + Ok(()) + } +} diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index 2dadbf8..135c246 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -24,6 +24,38 @@ use solana_sdk::{ signer::Signer, }; +pub struct Create { + pub _name: String, + pub _symbol: String, + pub _uri: String, + pub _creator: Pubkey, +} + +impl Create { + pub fn data(&self) -> Vec { + let mut data = Vec::with_capacity(8 + 4 + self._name.len() + 4 + self._symbol.len() + 4 + self._uri.len() + 32); + + // 追加 discriminator + data.extend_from_slice(&[24, 30, 200, 40, 5, 28, 7, 119]); // discriminator + + // 添加 name 字符串长度和内容 + data.extend_from_slice(&(self._name.len() as u32).to_le_bytes()); // 添加 name 长度 + data.extend_from_slice(self._name.as_bytes()); // 添加 name 内容 + + // 添加 symbol 字符串长度和内容 + data.extend_from_slice(&(self._symbol.len() as u32).to_le_bytes()); // 添加 symbol 长度 + data.extend_from_slice(self._symbol.as_bytes()); // 添加 symbol 内容 + + // 添加 uri 字符串长度和内容 + data.extend_from_slice(&(self._uri.len() as u32).to_le_bytes()); // 添加 uri 长度 + data.extend_from_slice(self._uri.as_bytes()); // 添加 uri 内容 + + data.extend_from_slice(&self._creator.to_bytes()); + + data + } +} + pub struct Buy { pub _amount: u64, pub _max_sol_cost: u64, @@ -54,6 +86,47 @@ impl Sell { } } + +/// Creates an instruction to create a new token with bonding curve +/// +/// Creates a new SPL token with an associated bonding curve that determines its price. +/// +/// # Arguments +/// +/// * `payer` - Keypair that will pay for account creation and transaction fees +/// * `mint` - Keypair for the new token mint account that will be created +/// * `args` - Create instruction data containing token name, symbol and metadata URI +/// +/// # Returns +/// +/// Returns a Solana instruction that when executed will create the token and its accounts +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, + &args.data(), + vec![ + AccountMeta::new(mint.pubkey(), true), + AccountMeta::new(get_mint_authority_pda(), false), + AccountMeta::new(bonding_curve, false), + AccountMeta::new( + get_associated_token_address(&bonding_curve, &mint.pubkey()), + false, + ), + AccountMeta::new_readonly(get_global_pda(), false), + AccountMeta::new_readonly(constants::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), + ], + ) +} + /// Creates an instruction to buy tokens from a bonding curve /// /// Buys tokens by providing SOL. The amount of tokens received is calculated based on @@ -73,8 +146,8 @@ impl Sell { pub fn buy( payer: &Keypair, mint: &Pubkey, - bonding_curve: &Pubkey, - creator_vault: &Pubkey, + bonding_curve_pda: &Pubkey, + creator_vault_pda: &Pubkey, fee_recipient: &Pubkey, args: Buy, ) -> Instruction { @@ -85,13 +158,13 @@ pub fn buy( AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), AccountMeta::new(*fee_recipient, false), AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*bonding_curve, false), - AccountMeta::new(get_associated_token_address(bonding_curve, 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(*creator_vault, false), + AccountMeta::new(*creator_vault_pda, false), AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), ], @@ -117,11 +190,11 @@ pub fn buy( pub fn sell( payer: &Keypair, mint: &Pubkey, - bonding_curve: &Pubkey, - creator_vault: &Pubkey, + creator_vault_pda: &Pubkey, fee_recipient: &Pubkey, args: Sell, ) -> Instruction { + let bonding_curve: Pubkey = get_bonding_curve_pda(mint).unwrap(); Instruction::new_with_bytes( constants::accounts::PUMPFUN, &args.data(), @@ -129,12 +202,12 @@ pub fn sell( AccountMeta::new_readonly(constants::global_constants::GLOBAL_ACCOUNT, false), AccountMeta::new(*fee_recipient, false), AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*bonding_curve, false), + AccountMeta::new(bonding_curve, false), AccountMeta::new(get_associated_token_address(&bonding_curve, 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(*creator_vault, false), + AccountMeta::new(*creator_vault_pda, false), AccountMeta::new_readonly(constants::accounts::TOKEN_PROGRAM, false), AccountMeta::new_readonly(constants::accounts::EVENT_AUTHORITY, false), AccountMeta::new_readonly(constants::accounts::PUMPFUN, false), diff --git a/src/ipfs/mod.rs b/src/ipfs/mod.rs index 9a5e538..fc0afb5 100755 --- a/src/ipfs/mod.rs +++ b/src/ipfs/mod.rs @@ -63,12 +63,12 @@ pub struct CreateTokenMetadata { pub metadata_uri: Option, } -pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &str) -> Result { +pub async fn create_token_metadata(metadata: CreateTokenMetadata, api_token: &str) -> Result { let ipfs_url = if metadata.file.starts_with("http") || metadata.metadata_uri.is_some() { metadata.file } else { let base64_string = file_to_base64(&metadata.file).await?; - upload_base64_file(&base64_string, jwt_token).await? + upload_base64_file(&base64_string, api_token).await? }; let token_metadata = TokenMetadata { @@ -94,7 +94,7 @@ pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &st let response = client .post("https://api.pinata.cloud/pinning/pinJSONToIPFS") .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", jwt_token)) + .header("Authorization", format!("Bearer {}", api_token)) .json(&token_metadata) .send() .await?; @@ -110,13 +110,13 @@ pub async fn create_token_metadata(metadata: CreateTokenMetadata, jwt_token: &st }; Ok(token_metadata_ipfs) } else { - eprintln!("Error: {:?}", response.status()); + eprintln!("Error: {:?}", response.text().await?); Err(anyhow::anyhow!("Failed to create token metadata")) } } } -pub async fn upload_base64_file(base64_string: &str, jwt_token: &str) -> Result { +pub async fn upload_base64_file(base64_string: &str, api_token: &str) -> Result { let decoded_bytes = general_purpose::STANDARD.decode(base64_string)?; let client = Client::builder() @@ -133,7 +133,7 @@ pub async fn upload_base64_file(base64_string: &str, jwt_token: &str) -> Result< let response = client .post("https://api.pinata.cloud/pinning/pinFileToIPFS") - .header("Authorization", format!("Bearer {}", jwt_token)) + .header("Authorization", format!("Bearer {}", api_token)) .header("Accept", "application/json") .multipart(form) .send() diff --git a/src/lib.rs b/src/lib.rs index 9f54e10..1163c08 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,9 @@ pub mod pumpfun; use std::sync::Arc; -use swqos::{FeeClient, JitoClient, NextBlockClient, ZeroSlotClient}; +use swqos::{FeeClient, JitoClient, NextBlockClient, NozomiClient, SolRpcClient, ZeroSlotClient}; use rustls::crypto::{ring::default_provider, CryptoProvider}; +use solana_hash::Hash; use solana_sdk::{ commitment_config::CommitmentConfig, pubkey::Pubkey, @@ -58,7 +59,7 @@ impl PumpFun { cluster.clone().rpc_url, cluster.clone().commitment ); - + let rpc = Arc::new(rpc); let mut fee_clients: Vec> = vec![]; if cluster.clone().use_jito { let jito_client = JitoClient::new( @@ -79,6 +80,16 @@ impl PumpFun { fee_clients.push(Arc::new(zeroslot_client)); } + if cluster.clone().use_nozomi { + let nozomi_client = NozomiClient::new( + cluster.clone().rpc_url, + cluster.clone().nozomi_url, + cluster.clone().nozomi_auth_token + ); + + fee_clients.push(Arc::new(nozomi_client)); + } + if cluster.clone().use_nextblock { let nextblock_client = NextBlockClient::new( cluster.clone().rpc_url, @@ -89,29 +100,100 @@ impl PumpFun { fee_clients.push(Arc::new(nextblock_client)); } + if cluster.clone().use_rpc { + let rpc_client = SolRpcClient::new(rpc.clone()); + fee_clients.push(Arc::new(rpc_client)); + } + Self { payer, - rpc: Arc::new(rpc), + rpc, fee_clients, priority_fee: cluster.clone().priority_fee, cluster: cluster.clone(), } } + + /// Create a new token + pub async fn create( + &self, + mint: Keypair, + ipfs: TokenMetadataIPFS, + ) -> Result<(), anyhow::Error> { + pumpfun::create::create( + self.rpc.clone(), + self.payer.clone(), + mint, + ipfs, + self.priority_fee.clone(), + ).await + } + + pub async fn create_and_buy( + &self, + mint: Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::create::create_and_buy( + self.rpc.clone(), + self.payer.clone(), + mint, + ipfs, + amount_sol, + slippage_basis_points, + self.priority_fee.clone(), + recent_blockhash, + ).await + } + + pub async fn create_and_buy_with_tip( + &self, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + buy_sol_cost: u64, + slippage_basis_points: Option, + recent_blockhash: Hash, + ) -> Result<(), anyhow::Error> { + pumpfun::create::create_and_buy_with_tip( + self.rpc.clone(), + self.fee_clients.clone(), + payer, + mint, + ipfs, + buy_sol_cost, + slippage_basis_points, + self.priority_fee.clone(), + recent_blockhash, + ).await + } /// Buy tokens pub async fn buy( &self, mint: Pubkey, - amount_sol: u64, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, slippage_basis_points: Option, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { pumpfun::buy::buy( self.rpc.clone(), self.payer.clone(), mint, - amount_sol, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, slippage_basis_points, self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, ).await } @@ -119,17 +201,25 @@ impl PumpFun { pub async fn buy_with_tip( &self, mint: Pubkey, - amount_sol: u64, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, slippage_basis_points: Option, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { pumpfun::buy::buy_with_tip( - self.rpc.clone(), self.fee_clients.clone(), self.payer.clone(), mint, - amount_sol, + creator, + dev_buy_token, + dev_sol_cost, + buy_sol_cost, slippage_basis_points, self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, ).await } @@ -137,14 +227,19 @@ impl PumpFun { pub async fn sell( &self, mint: Pubkey, - amount_token: Option, + creator: Pubkey, + amount_token: u64, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { pumpfun::sell::sell( self.rpc.clone(), self.payer.clone(), mint.clone(), + creator, amount_token, self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, ).await } @@ -152,29 +247,42 @@ impl PumpFun { pub async fn sell_by_percent( &self, mint: Pubkey, + creator: Pubkey, percent: u64, + amount_token: u64, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { pumpfun::sell::sell_by_percent( self.rpc.clone(), self.payer.clone(), mint.clone(), + creator, percent, + amount_token, self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, ).await } pub async fn sell_by_percent_with_tip( &self, mint: Pubkey, + creator: Pubkey, percent: u64, + amount_token: u64, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { pumpfun::sell::sell_by_percent_with_tip( - self.rpc.clone(), self.fee_clients.clone(), self.payer.clone(), mint, + creator, percent, + amount_token, self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, ).await } @@ -182,15 +290,19 @@ impl PumpFun { pub async fn sell_with_tip( &self, mint: Pubkey, - amount_token: Option, + creator: Pubkey, + amount_token: u64, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { pumpfun::sell::sell_with_tip( - self.rpc.clone(), self.fee_clients.clone(), self.payer.clone(), mint, + creator, amount_token, self.priority_fee.clone(), + self.cluster.clone().lookup_table_key, + recent_blockhash, ).await } @@ -258,4 +370,9 @@ impl PumpFun { pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await } + + #[inline] + pub async fn close_token_account(&self, mint: &Pubkey) -> Result<(), anyhow::Error> { + pumpfun::common::close_token_account(&self.rpc, self.payer.as_ref(), mint).await + } } diff --git a/src/main.rs b/src/main.rs index 030c06e..1c12a22 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,94 @@ -use std::sync::Arc; -use pumpfun_sdk::{common::{logs_events::PumpfunEvent, Cluster, PriorityFee}, grpc::YellowstoneGrpc, ipfs, PumpFun}; -use solana_sdk::{commitment_config::CommitmentConfig, native_token::sol_to_lamports, signature::Keypair, signer::Signer}; +use pumpfun_sdk::{common::{ + logs_events::PumpfunEvent, + logs_subscribe::{stop_subscription, tokens_subscription}, AnyResult +}, grpc::ShredStreamGrpc}; +use solana_sdk::{commitment_config::CommitmentConfig, transaction::VersionedTransaction}; #[tokio::main] async fn main() -> Result<(), Box> { - // create grpc client - let grpc_url = "http://127.0.0.1:10000"; - let client = YellowstoneGrpc::new(grpc_url.to_string()); + let grpc = ShredStreamGrpc::new( + "http://127.0.0.1:10800".to_string(), + ).await?; - // Define callback function let callback = |event: PumpfunEvent| { + + // TradeInfo 的 sol_amount 不是真实线上消费/获取的数量 + // 当 is_buy 为 true 时,sol_amount = max_sol_cost,代表用户愿意支付的最大金额 + // 当 is_buy 为 false 时,sol_amount = min_sol_output,代表用户愿意接受的最小金额 + // + // timestamp 不是真实交易发生的时间,取的值为当前系统的时间 + // + // 无法获取下面4个值 + // virtual_sol_reserves: 0, + // virtual_token_reserves: 0, + // real_sol_reserves: 0, + // real_token_reserves: 0, match event { + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, PumpfunEvent::NewToken(token_info) => { println!("Received new token event: {:?}", token_info); }, - PumpfunEvent::NewDevTrade(trade_info) => { - println!("Received dev trade event: {:?}", trade_info); - }, PumpfunEvent::NewUserTrade(trade_info) => { println!("Received new trade event: {:?}", trade_info); }, PumpfunEvent::NewBotTrade(trade_info) => { println!("Received new bot trade event: {:?}", trade_info); - } + }, PumpfunEvent::Error(err) => { println!("Received error: {}", err); } } }; - let payer_keypair = Keypair::from_base58_string("your private key"); - client.subscribe_pumpfun(callback, Some(payer_keypair.pubkey())).await?; + grpc.shredstream_subscribe(callback, None).await?; Ok(()) +} + +async fn test_wss() -> AnyResult<()> { + println!("Starting token subscription\n"); + + let ws_url = "wss://api.mainnet-beta.solana.com"; + + // Set commitment + let commitment = CommitmentConfig::confirmed(); + + // Define callback function + let callback = |event: PumpfunEvent| { + match event { + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, + PumpfunEvent::NewToken(token_info) => { + println!("Received new token event: {:?}", token_info); + }, + PumpfunEvent::NewUserTrade(trade_info) => { + println!("Received new trade event: {:?}", trade_info); + }, + PumpfunEvent::NewBotTrade(trade_info) => { + println!("Received new bot trade event: {:?}", trade_info); + }, + PumpfunEvent::Error(err) => { + println!("Received error: {}", err); + } + } + }; + + // Start subscription + let subscription = tokens_subscription( + ws_url, + commitment, + callback, + None + ).await.unwrap(); + + // Wait for a while to receive events + tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + + // Stop subscription + stop_subscription(subscription).await; + + Ok(()) } \ No newline at end of file diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs index 334db94..26335e2 100755 --- a/src/pumpfun/buy.rs +++ b/src/pumpfun/buy.rs @@ -1,73 +1,248 @@ use anyhow::anyhow; use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} + compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, AddressLookupTableAccount, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} }; use solana_hash::Hash; use spl_associated_token_account::instruction::create_associated_token_account; use tokio::task::JoinHandle; use std::{str::FromStr, time::Instant, sync::Arc}; -use crate::{common::{PriorityFee, SolanaRpcClient}, constants::{self, global_constants::FEE_RECIPIENT}, instruction, swqos::FeeClient}; +use crate::{ + common::{ + address_lookup_cache::get_address_lookup_table_account, + nonce_cache:: NonceCache, + tip_cache::TipCache, + PriorityFee, + SolanaRpcClient + }, + constants::{self, global_constants::FEE_RECIPIENT}, + instruction, + swqos::{ClientType, FeeClient, TradeType} +}; const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; -use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_buy_token_amount_from_sol_amount, get_creator_vault_pda}; +use super::common::{calculate_with_slippage_buy, get_buy_token_amount_from_sol_amount, init_bonding_curve_account}; + +/// 添加nonce消费指令到指令集合中 +/// +/// 只有提供了nonce_pubkey时才使用nonce功能 +/// 如果nonce被锁定、已使用或未准备好,将返回错误 +/// 成功时会锁定并标记nonce为已使用 +fn add_nonce_instruction(instructions: &mut Vec, payer: &Keypair) -> Result<(), anyhow::Error> { + let nonce_cache = NonceCache::get_instance(); + let nonce_info = nonce_cache.get_nonce_info(); + + // 只检查nonce_account是否存在 + if let Some(nonce_pubkey) = nonce_info.nonce_account { + // 暂不加锁 + // if nonce_info.lock { + // return Err(anyhow!("Nonce is locked")); + // } + if nonce_info.used { + return Err(anyhow!("Nonce is used")); + } + if nonce_info.current_nonce == Hash::default() { + return Err(anyhow!("Nonce is not ready")); + } + // 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(); + + // 创建Solana系统nonce推进指令 - 使用系统程序ID + let nonce_advance_ix = system_instruction::advance_nonce_account( + &nonce_pubkey, + &payer.pubkey(), + ); + + + instructions.push(nonce_advance_ix); + } + + Ok(()) +} pub async fn buy( rpc: Arc, payer: Arc, mint: Pubkey, - amount_sol: u64, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, slippage_basis_points: Option, priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { - let transaction = build_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?; + let start_time = Instant::now(); + let mint = Arc::new(mint.clone()); + let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; + println!(" 买入交易指令: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + let transaction = build_buy_transaction( + payer.clone(), + priority_fee.clone(), + instructions, + lookup_table_key, + recent_blockhash, + ).await?; + println!(" 买入交易签名: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); rpc.send_and_confirm_transaction(&transaction).await?; + println!(" 买入交易确认: {:?}", start_time.elapsed()); + Ok(()) } /// Buy tokens using Jito +// pub async fn buy_with_tip( +// fee_clients: Vec>, +// payer: Arc, +// mint: Pubkey, +// creator: Pubkey, +// dev_buy_token: u64, +// dev_sol_cost: u64, +// buy_sol_cost: u64, +// slippage_basis_points: Option, +// priority_fee: PriorityFee, +// lookup_table_key: Option, +// recent_blockhash: Hash, +// ) -> Result<(), anyhow::Error> { +// let start_time = Instant::now(); +// let mint = Arc::new(mint.clone()); +// let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; +// println!(" 买入交易指令: {:?}", start_time.elapsed()); + +// let start_time = Instant::now(); +// let mut transactions = vec![]; + +// for fee_client in fee_clients.clone() { +// if fee_client.get_client_type() == ClientType::Rpc { +// let transaction = build_buy_transaction( +// payer.clone(), +// priority_fee.clone(), +// instructions.clone(), +// lookup_table_key, +// recent_blockhash, +// ).await?; + +// transactions.push(transaction); +// } else { +// 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( +// tip_account, +// payer.clone(), +// priority_fee.clone(), +// instructions.clone(), +// lookup_table_key, +// recent_blockhash, +// ).await?; + +// transactions.push(transaction); +// } +// } + +// println!(" 买入交易签名: {:?}", start_time.elapsed()); + +// let cores = core_affinity::get_core_ids().unwrap(); +// let mut handles: Vec>> = vec![]; +// for i in 0..fee_clients.len() { +// let fee_client = fee_clients[i].clone(); +// let transactions = transactions.clone(); +// let transaction = transactions[i].clone(); + +// let core_id = cores[i % cores.len()]; +// let handle = tokio::spawn(async move { +// core_affinity::set_for_current(core_id); +// fee_client.send_transaction(TradeType::Buy, &transaction).await?; +// Ok::<(), anyhow::Error>(()) +// }); + +// handles.push(handle); +// } + +// for handle in handles { +// match handle.await { +// Ok(Ok(_)) => (), +// Ok(Err(e)) => println!("Error in task: {}", e), +// Err(e) => println!("Task join error: {}", e), +// } +// } + +// Ok(()) +// } + pub async fn buy_with_tip( - rpc: Arc, fee_clients: Vec>, payer: Arc, mint: Pubkey, - amount_sol: u64, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + buy_sol_cost: u64, slippage_basis_points: Option, priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> 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?; - - let mut transactions = vec![]; - let recent_blockhash = rpc.get_latest_blockhash().await?; - for fee_client in fee_clients.clone() { - let payer = payer.clone(); - let priority_fee = priority_fee.clone(); - let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; - let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); - - let transaction = build_buy_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; - transactions.push(transaction); - } + let instructions = build_buy_instructions(payer.clone(), mint.clone(), creator, dev_buy_token, dev_sol_cost, buy_sol_cost, slippage_basis_points).await?; + println!(" 买入交易指令: {:?}", start_time.elapsed()); + let start_time = Instant::now(); + let cores = core_affinity::get_core_ids().unwrap(); let mut handles: Vec>> = vec![]; + for i in 0..fee_clients.len() { let fee_client = fee_clients[i].clone(); - let transactions = transactions.clone(); - let start_time = start_time.clone(); - let transaction = transactions[i].clone(); + let payer = payer.clone(); + let instructions = instructions.clone(); + let mut priority_fee = priority_fee.clone(); + let core_id = cores[i % cores.len()]; + let handle = tokio::spawn(async move { - fee_client.send_transaction(&transaction).await?; - println!("index: {}, Total Jito buy operation time: {:?}ms", i, start_time.elapsed().as_millis()); + core_affinity::set_for_current(core_id); + + let transaction = if fee_client.get_client_type() == ClientType::Rpc { + build_buy_transaction( + payer.clone(), + priority_fee.clone(), + instructions.clone(), + lookup_table_key, + recent_blockhash, + ).await? + } else { + let tip_account = fee_client.get_tip_account()?; + let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); + priority_fee.buy_tip_fee = priority_fee.buy_tip_fees[i]; + // println!(" 买入交易小费: {:?}", priority_fee.buy_tip_fee); + build_buy_transaction_with_tip( + tip_account, + payer.clone(), + priority_fee.clone(), + instructions.clone(), + lookup_table_key, + recent_blockhash, + ).await? + }; + + fee_client.send_transaction(TradeType::Buy, &transaction).await?; Ok::<(), anyhow::Error>(()) }); - handles.push(handle); + handles.push(handle); } + println!(" 买入交易签名: {:?}", start_time.elapsed()); + for handle in handles { match handle.await { Ok(Ok(_)) => (), @@ -80,29 +255,44 @@ pub async fn buy_with_tip( } pub async fn build_buy_transaction( - rpc: Arc, payer: Arc, - mint: Pubkey, - amount_sol: u64, - slippage_basis_points: Option, priority_fee: PriorityFee, -) -> Result { - 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), - ]; + build_instructions: Vec, + lookup_table_key: Option, + recent_blockhash: Hash, +) -> Result { + let mut instructions = vec![]; + if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) { + return Err(e); + } - let build_instructions = build_buy_instructions(rpc.clone(), payer.clone(), Arc::new(mint), amount_sol, slippage_basis_points).await?; + // 添加计算预算指令 + instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT)); + instructions.push(ComputeBudgetInstruction::set_compute_unit_price( priority_fee.rpc_unit_price )); + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( priority_fee.rpc_unit_limit )); instructions.extend(build_instructions); - let recent_blockhash = rpc.get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); + let nonce_cache = NonceCache::get_instance(); + let nonce_info = nonce_cache.get_nonce_info(); + + let blockhash = if nonce_info.nonce_account.is_some() && instructions.len() > 0 { + nonce_info.current_nonce + } else { + recent_blockhash + }; + + 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 = + v0::Message::try_compile(&payer.pubkey(), &instructions, &address_lookup_table_accounts, blockhash)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message.clone()); + let transaction = VersionedTransaction::try_new(versioned_message, &[payer.as_ref()])?; + + // verify_lookup_table_usage(&v0_message, &address_lookup_table_accounts); Ok(transaction) } @@ -112,33 +302,66 @@ pub async fn build_buy_transaction_with_tip( payer: Arc, priority_fee: PriorityFee, build_instructions: Vec, - blockhash: Hash, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> Result { - 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, - sol_to_lamports(priority_fee.buy_tip_fee), - ), - ]; + // 从TipCache获取tip金额 + // let tip_cache = TipCache::get_instance(); + // let tip_amount = tip_cache.get_tip(); + // let tip_amount = priority_fee.buy_tip_fee; + let mut instructions = vec![]; + + // 添加nonce消费指令 + if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) { + return Err(e); + } + + // 添加计算预算指令和小费转账指令 + instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT)); + instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price)); + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit)); instructions.extend(build_instructions); + instructions.push(system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(priority_fee.buy_tip_fee), + )); + + let nonce_cache = NonceCache::get_instance(); + let nonce_info = nonce_cache.get_nonce_info(); + + // 如果使用了nonce账户,则使用nonce账户中的blockhash + let blockhash_to_use = if nonce_info.nonce_account.is_some() && instructions.len() > 0 { + nonce_info.current_nonce + } else { + recent_blockhash + }; + + 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 = - v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; - let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + v0::Message::try_compile(&payer.pubkey(), &instructions, &address_lookup_table_accounts, blockhash_to_use)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message.clone()); + let transaction = VersionedTransaction::try_new(versioned_message, &[payer.as_ref()])?; + + // nonce_cache.mark_used(); + // verify_lookup_table_usage(&v0_message, &address_lookup_table_accounts); Ok(transaction) } pub async fn build_buy_instructions( - rpc: Arc, + // rpc: Arc, payer: Arc, mint: Arc, + creator: Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, buy_sol_cost: u64, slippage_basis_points: Option, ) -> Result, anyhow::Error> { @@ -146,9 +369,9 @@ pub async fn build_buy_instructions( return Err(anyhow!("Amount cannot be zero")); } - let (bonding_curve, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?; - let creator_vault_pda = get_creator_vault_pda(&bonding_curve.creator).unwrap(); + let bonding_curve = init_bonding_curve_account(&mint, dev_buy_token, dev_sol_cost, creator).await?; let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(100)); + let creator_vault_pda = bonding_curve.get_creator_vault_pda(); let mut buy_token_amount = get_buy_token_amount_from_sol_amount(&bonding_curve, buy_sol_cost); if buy_token_amount <= 100 * 1_000_000_u64 { @@ -170,7 +393,7 @@ pub async fn build_buy_instructions( instructions.push(instruction::buy( payer.as_ref(), &mint, - &bonding_curve_pda, + &bonding_curve.account, &creator_vault_pda, &FEE_RECIPIENT, instruction::Buy { @@ -180,4 +403,27 @@ pub async fn build_buy_instructions( )); Ok(instructions) +} + +/// 验证地址表是否被成功用于编译后的消息中 +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() + ); + } + } + } } \ No newline at end of file diff --git a/src/pumpfun/common.rs b/src/pumpfun/common.rs index fa2995c..dd72456 100755 --- a/src/pumpfun/common.rs +++ b/src/pumpfun/common.rs @@ -1,13 +1,13 @@ use anyhow::anyhow; -use spl_token::state::Account; +use borsh::BorshDeserialize; +use spl_token::instruction::close_account; use tokio::sync::RwLock; use std::{collections::HashMap, sync::Arc}; use solana_sdk::{ - commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction + compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction }; use spl_associated_token_account::get_associated_token_address; use crate::{accounts::{self, BondingCurveAccount}, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::DEFAULT_SLIPPAGE}}; -use borsh::BorshDeserialize; lazy_static::lazy_static! { static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); @@ -43,6 +43,53 @@ pub async fn transfer_sol(rpc: &SolanaRpcClient, payer: &Keypair, receive_wallet Ok(()) } +/// 关闭代币账户 +/// +/// 此函数用于关闭指定代币的关联代币账户,将账户中的代币余额转移给账户所有者。 +/// +/// # 参数 +/// +/// * `rpc` - Solana RPC客户端 +/// * `payer` - 支付交易费用的账户 +/// * `mint` - 代币的Mint地址 +/// +/// # 返回值 +/// +/// 返回一个Result,成功时返回(),失败时返回错误 +pub async fn close_token_account(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(), anyhow::Error> { + // 获取关联代币账户地址 + let ata = get_associated_token_address(&payer.pubkey(), mint); + + // 检查账户是否存在 + let account_exists = rpc.get_account(&ata).await.is_ok(); + if !account_exists { + return Ok(()); // 如果账户不存在,直接返回成功 + } + + // 构建关闭账户指令 + let close_account_ix = close_account( + &spl_token::ID, + &ata, + &payer.pubkey(), + &payer.pubkey(), + &[&payer.pubkey()], + )?; + + // 构建交易 + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[close_account_ix], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + // 发送交易 + rpc.send_and_confirm_transaction(&transaction).await?; + + Ok(()) +} + #[inline] pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec { let mut instructions = Vec::with_capacity(2); @@ -137,17 +184,19 @@ pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey { } #[inline] -pub async fn get_global_account(rpc: &SolanaRpcClient) -> Result, anyhow::Error> { - let global = get_global_pda(); - if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { - return Ok(account.clone()); - } +pub async fn get_global_account(/*rpc: &SolanaRpcClient*/) -> Result, anyhow::Error> { + // let global = constants::global_constants::GLOBAL_ACCOUNT; + // if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { + // return Ok(account.clone()); + // } - let account = rpc.get_account(&global).await?; - let global_account = bincode::deserialize::(&account.data)?; + let global_account = accounts::GlobalAccount::new(); + + // let account = rpc.get_account(&global).await?; + // let global_account = bincode::deserialize::(&account.data)?; let global_account = Arc::new(global_account); - ACCOUNT_CACHE.write().await.insert(global, global_account.clone()); + // ACCOUNT_CACHE.write().await.insert(global, global_account.clone()); Ok(global_account) } @@ -175,9 +224,25 @@ pub async fn get_bonding_curve_account( Ok((bonding_curve, bonding_curve_pda)) } +// #[inline] +// pub fn get_buy_token_amount( +// mint: &Pubkey, +// dev_buy_token: u64, +// dev_cost_sol: u64, +// bot_cost_sol: u64, +// slippage_basis_points: Option, +// ) -> anyhow::Result<(u64, u64)> { +// let bonding_curve_account = BondingCurveAccount::new(mint, dev_buy_token, dev_cost_sol); +// let buy_token = bonding_curve_account.get_buy_price(bot_cost_sol).map_err(|e| anyhow!(e))?; + +// let max_sol_cost = calculate_with_slippage_buy(bot_cost_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + +// Ok((buy_token, max_sol_cost)) +// } + #[inline] pub fn get_buy_token_amount( - bonding_curve_account: &Arc, + bonding_curve_account: &BondingCurveAccount, buy_sol_cost: u64, slippage_basis_points: Option, ) -> anyhow::Result<(u64, u64)> { @@ -231,6 +296,19 @@ pub fn get_buy_token_amount_from_sol_amount( tokens_received.min(real_token_reserves) as u64 } + +#[inline] +pub async fn init_bonding_curve_account( + mint: &Pubkey, + dev_buy_token: u64, + dev_sol_cost: u64, + creator: Pubkey, +) -> Result, anyhow::Error> { + let bonding_curve = BondingCurveAccount::new(mint, dev_buy_token, dev_sol_cost, creator); + let bonding_curve = Arc::new(bonding_curve); + Ok(bonding_curve) +} + #[inline] pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Option) -> u64 { let slippage = slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); diff --git a/src/pumpfun/create.rs b/src/pumpfun/create.rs new file mode 100755 index 0000000..4f97b5a --- /dev/null +++ b/src/pumpfun/create.rs @@ -0,0 +1,238 @@ +use std::{str::FromStr, time::Instant, sync::Arc}; + +use anyhow::anyhow; +use solana_hash::Hash; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + instruction::Instruction, message::{v0, VersionedMessage}, + pubkey::Pubkey, + native_token::sol_to_lamports, + signature::Keypair, + signer::Signer, + system_instruction, + transaction::{Transaction, VersionedTransaction} +}; +use spl_associated_token_account::instruction::create_associated_token_account; + +use crate::{ + common::{PriorityFee, SolanaRpcClient}, constants, instruction, + ipfs::TokenMetadataIPFS, swqos::{FeeClient, TradeType}, +}; + +use crate::pumpfun::common::{ + create_priority_fee_instructions, + get_buy_amount_with_slippage, get_global_account +}; + +use crate::common::tip_cache::TipCache; + +use super::common::{get_bonding_curve_account, get_buy_token_amount, get_creator_vault_pda}; + +/// Create a new token +pub async fn create( + rpc: Arc, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let mut instructions = create_priority_fee_instructions(priority_fee); + + instructions.push(instruction::create( + payer.as_ref(), + &mint, + instruction::Create { + _name: ipfs.metadata.name, + _symbol: ipfs.metadata.symbol, + _uri: ipfs.metadata_uri, + _creator: payer.pubkey(), + }, + )); + + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer.as_ref(), &mint], + recent_blockhash, + ); + + rpc.send_and_confirm_transaction(&transaction).await?; + + Ok(()) +} + +/// Create and buy tokens in one transaction +pub async fn create_and_buy( + rpc: Arc, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + buy_sol_cost: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + if buy_sol_cost == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let mint = Arc::new(mint); + let transaction = build_create_and_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), ipfs, buy_sol_cost, slippage_basis_points, priority_fee.clone(), recent_blockhash).await?; + rpc.send_and_confirm_transaction(&transaction).await?; + + Ok(()) +} + +pub async fn create_and_buy_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + buy_sol_cost: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + recent_blockhash: Hash, +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + let mint = Arc::new(mint); + let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs.clone(), buy_sol_cost, slippage_basis_points).await?; + let mut handles = vec![]; + for fee_client in fee_clients { + 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_create_and_buy_transaction_with_tip(/*rpc.clone(),*/ tip_account, payer.clone(), priority_fee.clone(), build_instructions.clone(), recent_blockhash).await?; + let handle = tokio::spawn(async move { + fee_client.send_transaction(TradeType::CreateAndBuy, &transaction).await.map_err(|e| anyhow!(e.to_string()))?; + println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis()); + Ok::<(), anyhow::Error>(()) + }); + + handles.push(handle); + } + + for handle in handles { + match handle.await { + Ok(Ok(_)) => (), + Ok(Err(e)) => println!("Error in task: {}", e), + Err(e) => println!("Task join error: {}", e), + } + } + + Ok(()) +} + +pub async fn build_create_and_buy_transaction( + rpc: Arc, + payer: Arc, + mint: Arc, + ipfs: TokenMetadataIPFS, + buy_sol_cost: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, + recent_blockhash: Hash, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs, buy_sol_cost, slippage_basis_points).await?; + instructions.extend(build_instructions); + + // let recent_blockhash = rpc.get_latest_blockhash().await?; + // let recent_blockhash = Hash::default(); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer.as_ref(), mint.as_ref()], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_create_and_buy_transaction_with_tip( + // rpc: Arc, + tip_account: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + recent_blockhash: Hash, +) -> Result { + let tip_cache = TipCache::get_instance(); + let tip_amount = tip_cache.get_tip(); + + let mut instructions = vec![ + 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, + sol_to_lamports(tip_amount), + ), + ]; + instructions.extend(build_instructions); + + // let recent_blockhash = rpc.get_latest_blockhash().await?; + // let recent_blockhash = Hash::default(); + let v0_message: v0::Message = + v0::Message::try_compile(&payer.pubkey(), &instructions, &[], recent_blockhash)?; + + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + Ok(transaction) +} + +pub async fn build_create_and_buy_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + ipfs: TokenMetadataIPFS, + buy_sol_cost: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if buy_sol_cost == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint.pubkey()).await?; + let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap(); + let (buy_token_amount, max_sol_cost) = get_buy_token_amount(&bonding_curve_account, buy_sol_cost, slippage_basis_points)?; + + let mut instructions = vec![]; + + instructions.push(instruction::create( + payer.as_ref(), + mint.as_ref(), + instruction::Create { + _name: ipfs.metadata.name.clone(), + _symbol: ipfs.metadata.symbol.clone(), + _uri: ipfs.metadata_uri.clone(), + _creator: payer.pubkey(), + }, + )); + + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint.pubkey(), + &constants::accounts::TOKEN_PROGRAM, + )); + + instructions.push(instruction::buy( + payer.as_ref(), + &mint.pubkey(), + &bonding_curve_pda, + &creator_vault_pda, + &constants::global_constants::FEE_RECIPIENT, + instruction::Buy { + _amount: buy_token_amount, + _max_sol_cost: max_sol_cost, + }, + )); + + Ok(instructions) +} diff --git a/src/pumpfun/mod.rs b/src/pumpfun/mod.rs index 663177e..8126389 100755 --- a/src/pumpfun/mod.rs +++ b/src/pumpfun/mod.rs @@ -1,3 +1,4 @@ pub mod buy; +pub mod create; pub mod sell; pub mod common; \ No newline at end of file diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs index 893c775..5d17972 100755 --- a/src/pumpfun/sell.rs +++ b/src/pumpfun/sell.rs @@ -1,42 +1,38 @@ use anyhow::anyhow; use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} + compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair}, signer::Signer, system_instruction, transaction::{VersionedTransaction} }; use solana_hash::Hash; use spl_associated_token_account::get_associated_token_address; use spl_token::instruction::close_account; use tokio::task::JoinHandle; +use std::{str::FromStr, sync::Arc, time::Instant}; -use std::{str::FromStr, time::Instant, sync::Arc}; +use crate::{common::{address_lookup_cache::get_address_lookup_table_account, PriorityFee, SolanaRpcClient}, constants::{global_constants::FEE_RECIPIENT}, instruction, swqos::{FeeClient, TradeType, ClientType}}; -use crate::{common::{PriorityFee, SolanaRpcClient}, instruction, swqos::FeeClient}; - -use super::common::{get_bonding_curve_account, get_creator_vault_pda, get_global_account}; - -async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { - let ata = get_associated_token_address(&payer.pubkey(), mint); - let balance = rpc.get_token_account_balance(&ata).await?; - let balance_u64 = balance.amount.parse::() - .map_err(|_| anyhow!("Failed to parse token balance"))?; - - if balance_u64 == 0 { - return Err(anyhow!("Balance is 0")); - } - - Ok((balance_u64, ata)) -} +use super::common::get_creator_vault_pda; pub async fn sell( rpc: Arc, payer: Arc, mint: Pubkey, - amount_token: Option, + creator: Pubkey, + amount_token: u64, priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { - let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?; - let transaction = build_sell_transaction(rpc.clone(), payer.clone(), priority_fee, instructions).await?; - rpc.send_and_confirm_transaction(&transaction).await?; + let start_time = Instant::now(); + let instructions = build_sell_instructions(payer.clone(), mint.clone(), creator, amount_token).await?; + println!(" 卖出交易指令: {:?}", start_time.elapsed()); + let start_time = Instant::now(); + let transaction = build_sell_transaction(payer.clone(), priority_fee, instructions, lookup_table_key, recent_blockhash).await?; + println!(" 卖出交易签名: {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + rpc.send_and_confirm_transaction(&transaction).await?; + println!(" 卖出交易确认: {:?}", start_time.elapsed()); Ok(()) } @@ -45,73 +41,101 @@ pub async fn sell_by_percent( rpc: Arc, payer: Arc, mint: Pubkey, + creator: Pubkey, percent: u64, + amount_token: u64, priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> 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), priority_fee).await + let amount = amount_token * percent / 100; + sell(rpc, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await } pub async fn sell_by_percent_with_tip( - rpc: Arc, fee_clients: Vec>, payer: Arc, mint: Pubkey, + creator: Pubkey, percent: u64, + amount_token: u64, priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> 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), priority_fee).await + let amount = amount_token * percent / 100; + sell_with_tip(fee_clients, payer, mint, creator, amount, priority_fee, lookup_table_key, recent_blockhash).await } /// Sell tokens using Jito pub async fn sell_with_tip( - rpc: Arc, fee_clients: Vec>, payer: Arc, mint: Pubkey, - amount_token: Option, + creator: Pubkey, + amount_token: u64, priority_fee: PriorityFee, + lookup_table_key: Option, + recent_blockhash: Hash, ) -> Result<(), anyhow::Error> { let start_time = Instant::now(); + let mint = Arc::new(mint.clone()); + let instructions = build_sell_instructions(payer.clone(), *mint, creator, amount_token).await?; + println!(" 卖出交易指令: {:?}", start_time.elapsed()); - let mut transactions = vec![]; - let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token).await?; + let start_time = Instant::now(); + let cores = core_affinity::get_core_ids().unwrap(); + let mut handles: Vec>> = vec![]; - let recent_blockhash = rpc.get_latest_blockhash().await?; - for fee_client in fee_clients.clone() { - let payer = payer.clone(); - let priority_fee = priority_fee.clone(); - let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; - let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); - - let transaction = build_sell_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; - transactions.push(transaction); - } - - let mut handles = vec![]; for i in 0..fee_clients.len() { let fee_client = fee_clients[i].clone(); - let transaction = transactions[i].clone(); - let handle: JoinHandle> = tokio::spawn(async move { - fee_client.send_transaction(&transaction).await?; - println!("index: {}, Total Jito sell operation time: {:?}ms", i, start_time.elapsed().as_millis()); - Ok(()) + let payer = payer.clone(); + let instructions = instructions.clone(); + let priority_fee = priority_fee.clone(); + let core_id = cores[i % cores.len()]; + + let handle = tokio::spawn(async move { + core_affinity::set_for_current(core_id); + + let transaction = if fee_client.get_client_type() == ClientType::Rpc { + build_sell_transaction( + payer.clone(), + priority_fee.clone(), + instructions.clone(), + lookup_table_key, + recent_blockhash + ).await? + } else { + let tip_account = fee_client.get_tip_account()?; + let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); + + build_sell_transaction_with_tip( + tip_account, + payer.clone(), + priority_fee.clone(), + instructions.clone(), + lookup_table_key, + recent_blockhash + ).await? + }; + + fee_client.send_transaction(TradeType::Sell, &transaction).await?; + Ok::<(), anyhow::Error>(()) }); handles.push(handle); } + println!(" 卖出交易签名: {:?}", start_time.elapsed()); + for handle in handles { match handle.await { Ok(Ok(_)) => (), @@ -120,16 +144,16 @@ pub async fn sell_with_tip( } } - println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis()); Ok(()) } pub async fn build_sell_transaction( - rpc: Arc, payer: Arc, priority_fee: PriorityFee, - build_instructions: Vec -) -> Result { + build_instructions: Vec, + lookup_table_key: Option, + blockhash: Hash +) -> Result { let mut instructions = vec![ ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), @@ -137,13 +161,21 @@ pub async fn build_sell_transaction( instructions.extend(build_instructions); - let recent_blockhash = rpc.get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer.as_ref()], - recent_blockhash, - ); + 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 transaction = VersionedTransaction::try_new( + VersionedMessage::V0(v0::Message::try_compile( + &payer.pubkey(), + &instructions, + &address_lookup_table_accounts, + blockhash, + )?), + &[payer], + )?; Ok(transaction) } @@ -153,56 +185,65 @@ pub async fn build_sell_transaction_with_tip( payer: Arc, priority_fee: PriorityFee, build_instructions: Vec, + lookup_table_key: Option, blockhash: Hash, ) -> Result { let mut instructions = vec![ ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + instructions.extend(build_instructions); + + instructions.push( system_instruction::transfer( &payer.pubkey(), &tip_account, sol_to_lamports(priority_fee.sell_tip_fee), ), - ]; + ); - instructions.extend(build_instructions); + 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 = - v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; - let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - - let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + let transaction = VersionedTransaction::try_new( + VersionedMessage::V0(v0::Message::try_compile( + &payer.pubkey(), + &instructions, + &address_lookup_table_accounts, + blockhash, + )?), + &[payer], + )?; Ok(transaction) } pub async fn build_sell_instructions( - rpc: Arc, payer: Arc, mint: Pubkey, - amount_token: Option, + creator: Pubkey, + amount_token: u64, ) -> Result, anyhow::Error> { - let (balance_u64, ata) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; - let amount = amount_token.unwrap_or(balance_u64); - - if amount == 0 { + if amount_token == 0 { return Err(anyhow!("Amount cannot be zero")); } - - let global_account = get_global_account(rpc.as_ref()).await?; - let (bonding_curve_account, bonding_curve_pda) = get_bonding_curve_account(&rpc, &mint).await?; - let creator_vault_pda = get_creator_vault_pda(&bonding_curve_account.creator).unwrap(); + + let creator_vault_pda = get_creator_vault_pda(&creator).unwrap(); + let ata = get_associated_token_address(&payer.pubkey(), &mint); let instructions = vec![ instruction::sell( payer.as_ref(), &mint, - &bonding_curve_pda, &creator_vault_pda, - &global_account.fee_recipient, + &FEE_RECIPIENT, instruction::Sell { - _amount: amount, - _min_sol_output: 0, + _amount: amount_token, + _min_sol_output: 1, }, ), diff --git a/src/swqos/common.rs b/src/swqos/common.rs index e726be7..5aa97b8 100755 --- a/src/swqos/common.rs +++ b/src/swqos/common.rs @@ -14,10 +14,8 @@ use base64::engine::general_purpose::STANDARD; use reqwest::Client; pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result { - // 15 second timeout let timeout: Duration = Duration::from_secs(5); - // 5 second retry interval - let interval: Duration = Duration::from_millis(300); + let interval: Duration = Duration::from_millis(1000); let start: Instant = Instant::now(); loop { @@ -102,14 +100,15 @@ pub async fn serialize_and_encode( pub async fn serialize_transaction_and_encode( transaction: &impl SerializableTransaction, encoding: UiTransactionEncoding, -) -> Result { +) -> Result<(String, Signature)> { + let signature = transaction.get_signature(); let serialized_tx = serialize(transaction)?; let serialized = match encoding { UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), _ => return Err(anyhow::anyhow!("Unsupported encoding")), }; - Ok(serialized) + Ok((serialized, *signature)) } pub async fn serialize_smart_transaction_and_encode( diff --git a/src/swqos/jito_grpc/auth.rs b/src/swqos/jito_grpc/auth.rs new file mode 100755 index 0000000..127047c --- /dev/null +++ b/src/swqos/jito_grpc/auth.rs @@ -0,0 +1,260 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthChallengeRequest { + /// / Role the client is attempting to generate tokens for. + #[prost(enumeration = "Role", tag = "1")] + pub role: i32, + /// / Client's 32 byte pubkey. + #[prost(bytes = "vec", tag = "2")] + pub pubkey: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthChallengeResponse { + #[prost(string, tag = "1")] + pub challenge: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthTokensRequest { + /// / The pre-signed challenge. + #[prost(string, tag = "1")] + pub challenge: ::prost::alloc::string::String, + /// / The signing keypair's corresponding 32 byte pubkey. + #[prost(bytes = "vec", tag = "2")] + pub client_pubkey: ::prost::alloc::vec::Vec, + /// / The 64 byte signature of the challenge signed by the client's private key. The private key must correspond to + /// the pubkey passed in the \[GenerateAuthChallenge\] method. The client is expected to sign the challenge token + /// prepended with their pubkey. For example sign(pubkey, challenge). + #[prost(bytes = "vec", tag = "3")] + pub signed_challenge: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Token { + /// / The token. + #[prost(string, tag = "1")] + pub value: ::prost::alloc::string::String, + /// / When the token will expire. + #[prost(message, optional, tag = "2")] + pub expires_at_utc: ::core::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthTokensResponse { + /// / The token granting access to resources. + #[prost(message, optional, tag = "1")] + pub access_token: ::core::option::Option, + /// / The token used to refresh the access_token. This has a longer TTL than the access_token. + #[prost(message, optional, tag = "2")] + pub refresh_token: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RefreshAccessTokenRequest { + /// / Non-expired refresh token obtained from the \[GenerateAuthTokens\] method. + #[prost(string, tag = "1")] + pub refresh_token: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RefreshAccessTokenResponse { + /// / Fresh access_token. + #[prost(message, optional, tag = "1")] + pub access_token: ::core::option::Option, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Role { + Relayer = 0, + Searcher = 1, + Validator = 2, + ShredstreamSubscriber = 3, +} +impl Role { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Relayer => "RELAYER", + Self::Searcher => "SEARCHER", + Self::Validator => "VALIDATOR", + Self::ShredstreamSubscriber => "SHREDSTREAM_SUBSCRIBER", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "RELAYER" => Some(Self::Relayer), + "SEARCHER" => Some(Self::Searcher), + "VALIDATOR" => Some(Self::Validator), + "SHREDSTREAM_SUBSCRIBER" => Some(Self::ShredstreamSubscriber), + _ => None, + } + } +} +/// Generated client implementations. +pub mod auth_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / This service is responsible for issuing auth tokens to clients for API access. + #[derive(Debug, Clone)] + pub struct AuthServiceClient { + inner: tonic::client::Grpc, + } + impl AuthServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl AuthServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> AuthServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + AuthServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// / Returns a challenge, client is expected to sign this challenge with an appropriate keypair in order to obtain access tokens. + pub async fn generate_auth_challenge( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/auth.AuthService/GenerateAuthChallenge", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("auth.AuthService", "GenerateAuthChallenge")); + self.inner.unary(req, path, codec).await + } + /// / Provides the client with the initial pair of auth tokens for API access. + pub async fn generate_auth_tokens( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/auth.AuthService/GenerateAuthTokens", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("auth.AuthService", "GenerateAuthTokens")); + self.inner.unary(req, path, codec).await + } + /// / Call this method with a non-expired refresh token to obtain a new access token. + pub async fn refresh_access_token( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/auth.AuthService/RefreshAccessToken", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("auth.AuthService", "RefreshAccessToken")); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/src/swqos/jito_grpc/block.rs b/src/swqos/jito_grpc/block.rs new file mode 100755 index 0000000..0fc0434 --- /dev/null +++ b/src/swqos/jito_grpc/block.rs @@ -0,0 +1,19 @@ +// This file is @generated by prost-build. +/// Condensed block helpful for getting data around efficiently internal to our system. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CondensedBlock { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(string, tag = "2")] + pub previous_blockhash: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub blockhash: ::prost::alloc::string::String, + #[prost(uint64, tag = "4")] + pub parent_slot: u64, + #[prost(bytes = "vec", repeated, tag = "5")] + pub versioned_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + #[prost(uint64, tag = "6")] + pub slot: u64, + #[prost(string, tag = "7")] + pub commitment: ::prost::alloc::string::String, +} diff --git a/src/swqos/jito_grpc/block_engine.rs b/src/swqos/jito_grpc/block_engine.rs new file mode 100755 index 0000000..69c22eb --- /dev/null +++ b/src/swqos/jito_grpc/block_engine.rs @@ -0,0 +1,462 @@ +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribePacketsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribePacketsResponse { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub batch: ::core::option::Option, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribeBundlesRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeBundlesResponse { + #[prost(message, repeated, tag = "1")] + pub bundles: ::prost::alloc::vec::Vec, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct BlockBuilderFeeInfoRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockBuilderFeeInfoResponse { + #[prost(string, tag = "1")] + pub pubkey: ::prost::alloc::string::String, + /// commission (0-100) + #[prost(uint64, tag = "2")] + pub commission: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountsOfInterest { + /// use * for all accounts + #[prost(string, repeated, tag = "1")] + pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct AccountsOfInterestRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountsOfInterestUpdate { + #[prost(string, repeated, tag = "1")] + pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ProgramsOfInterestRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProgramsOfInterestUpdate { + #[prost(string, repeated, tag = "1")] + pub programs: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// A series of packets with an expiration attached to them. +/// The header contains a timestamp for when this packet was generated. +/// The expiry is how long the packet batches have before they expire and are forwarded to the validator. +/// This provides a more censorship resistant method to MEV than block engines receiving packets directly. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExpiringPacketBatch { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub batch: ::core::option::Option, + #[prost(uint32, tag = "3")] + pub expiry_ms: u32, +} +/// Packets and heartbeats are sent over the same stream. +/// ExpiringPacketBatches have an expiration attached to them so the block engine can track +/// how long it has until the relayer forwards the packets to the validator. +/// Heartbeats contain a timestamp from the system and is used as a simple and naive time-sync mechanism +/// so the block engine has some idea on how far their clocks are apart. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketBatchUpdate { + #[prost(oneof = "packet_batch_update::Msg", tags = "1, 2")] + pub msg: ::core::option::Option, +} +/// Nested message and enum types in `PacketBatchUpdate`. +pub mod packet_batch_update { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Msg { + #[prost(message, tag = "1")] + Batches(super::ExpiringPacketBatch), + #[prost(message, tag = "2")] + Heartbeat(super::super::shared::Heartbeat), + } +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct StartExpiringPacketStreamResponse { + #[prost(message, optional, tag = "1")] + pub heartbeat: ::core::option::Option, +} +/// Generated client implementations. +pub mod block_engine_validator_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / Validators can connect to Block Engines to receive packets and bundles. + #[derive(Debug, Clone)] + pub struct BlockEngineValidatorClient { + inner: tonic::client::Grpc, + } + impl BlockEngineValidatorClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl BlockEngineValidatorClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> BlockEngineValidatorClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + BlockEngineValidatorClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// / Validators can subscribe to the block engine to receive a stream of packets + pub async fn subscribe_packets( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineValidator/SubscribePackets", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineValidator", + "SubscribePackets", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// / Validators can subscribe to the block engine to receive a stream of simulated and profitable bundles + pub async fn subscribe_bundles( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineValidator/SubscribeBundles", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineValidator", + "SubscribeBundles", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Block builders can optionally collect fees. This returns fee information if a block builder wants to + /// collect one. + pub async fn get_block_builder_fee_info( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineValidator/GetBlockBuilderFeeInfo", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineValidator", + "GetBlockBuilderFeeInfo", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated client implementations. +pub mod block_engine_relayer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / Relayers can forward packets to Block Engines. + /// / Block Engines provide an AccountsOfInterest field to only send transactions that are of interest. + #[derive(Debug, Clone)] + pub struct BlockEngineRelayerClient { + inner: tonic::client::Grpc, + } + impl BlockEngineRelayerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl BlockEngineRelayerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> BlockEngineRelayerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + BlockEngineRelayerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// / The block engine feeds accounts of interest (AOI) updates to the relayer periodically. + /// / For all transactions the relayer receives, it forwards transactions to the block engine which write-lock + /// / any of the accounts in the AOI. + pub async fn subscribe_accounts_of_interest( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineRelayer/SubscribeAccountsOfInterest", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineRelayer", + "SubscribeAccountsOfInterest", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + pub async fn subscribe_programs_of_interest( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineRelayer/SubscribeProgramsOfInterest", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineRelayer", + "SubscribeProgramsOfInterest", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture + /// of packets and heartbeats. + /// NOTE: This is a bi-directional stream due to a bug with how Envoy handles half closed client-side streams. + /// The issue is being tracked here: https://github.com/envoyproxy/envoy/issues/22748. In the meantime, the + /// server will stream heartbeats to clients at some reasonable cadence. + pub async fn start_expiring_packet_stream( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineRelayer/StartExpiringPacketStream", + ); + let mut req = request.into_streaming_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineRelayer", + "StartExpiringPacketStream", + ), + ); + self.inner.streaming(req, path, codec).await + } + } +} diff --git a/src/swqos/jito_grpc/convert.rs b/src/swqos/jito_grpc/convert.rs index 500d989..9ea2332 100755 --- a/src/swqos/jito_grpc/convert.rs +++ b/src/swqos/jito_grpc/convert.rs @@ -11,11 +11,13 @@ use solana_sdk::{ transaction::VersionedTransaction, }; -use crate::swqos::jito_grpc::packet::{ - Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch, - PacketFlags as ProtoPacketFlags, +use crate::swqos::jito_grpc::{ + packet::{ + Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch, + PacketFlags as ProtoPacketFlags, + }, + shared::Socket, }; -use crate::swqos::jito_grpc::shared::Socket; /// Converts a Solana packet to a protobuf packet /// NOTE: the packet.data() function will filter packets marked for discard diff --git a/src/swqos/jito_grpc/mod.rs b/src/swqos/jito_grpc/mod.rs index 6bad46f..ab4a948 100755 --- a/src/swqos/jito_grpc/mod.rs +++ b/src/swqos/jito_grpc/mod.rs @@ -1,5 +1,11 @@ +pub mod auth; +pub mod block; +pub mod block_engine; pub mod bundle; pub mod packet; +pub mod relayer; pub mod searcher; pub mod shared; -pub mod convert; \ No newline at end of file +pub mod shredstream; +pub mod trace_shred; +pub mod convert; diff --git a/src/swqos/jito_grpc/relayer.rs b/src/swqos/jito_grpc/relayer.rs new file mode 100755 index 0000000..c771fc5 --- /dev/null +++ b/src/swqos/jito_grpc/relayer.rs @@ -0,0 +1,178 @@ +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct GetTpuConfigsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTpuConfigsResponse { + #[prost(message, optional, tag = "1")] + pub tpu: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub tpu_forward: ::core::option::Option, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribePacketsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribePacketsResponse { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(oneof = "subscribe_packets_response::Msg", tags = "2, 3")] + pub msg: ::core::option::Option, +} +/// Nested message and enum types in `SubscribePacketsResponse`. +pub mod subscribe_packets_response { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Msg { + #[prost(message, tag = "2")] + Heartbeat(super::super::shared::Heartbeat), + #[prost(message, tag = "3")] + Batch(super::super::packet::PacketBatch), + } +} +/// Generated client implementations. +pub mod relayer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / Relayers offer a TPU and TPU forward proxy for Solana validators. + /// / Validators can connect and fetch the TPU configuration for the relayer and start to advertise the + /// / relayer's information in gossip. + /// / They can also subscribe to packets which arrived on the TPU ports at the relayer + #[derive(Debug, Clone)] + pub struct RelayerClient { + inner: tonic::client::Grpc, + } + impl RelayerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl RelayerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> RelayerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + RelayerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// The relayer has TPU and TPU forward sockets that validators can leverage. + /// A validator can fetch this config and change its TPU and TPU forward port in gossip. + pub async fn get_tpu_configs( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/relayer.Relayer/GetTpuConfigs", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("relayer.Relayer", "GetTpuConfigs")); + self.inner.unary(req, path, codec).await + } + /// Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture + /// of packets and heartbeats + pub async fn subscribe_packets( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/relayer.Relayer/SubscribePackets", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("relayer.Relayer", "SubscribePackets")); + self.inner.server_streaming(req, path, codec).await + } + } +} diff --git a/src/swqos/jito_grpc/shredstream.rs b/src/swqos/jito_grpc/shredstream.rs new file mode 100755 index 0000000..95a36f5 --- /dev/null +++ b/src/swqos/jito_grpc/shredstream.rs @@ -0,0 +1,279 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Heartbeat { + /// don't trust IP:PORT from tcp header since it can be tampered over the wire + /// `socket.ip` must match incoming packet's ip. this prevents spamming an unwitting destination + #[prost(message, optional, tag = "1")] + pub socket: ::core::option::Option, + /// regions for shredstream proxy to receive shreds from + /// list of valid regions: + #[prost(string, repeated, tag = "2")] + pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct HeartbeatResponse { + /// client must respond within `ttl_ms` to keep stream alive + #[prost(uint32, tag = "1")] + pub ttl_ms: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TraceShred { + /// source region, one of: + #[prost(string, tag = "1")] + pub region: ::prost::alloc::string::String, + /// timestamp of creation + #[prost(message, optional, tag = "2")] + pub created_at: ::core::option::Option<::prost_types::Timestamp>, + /// monotonically increases, resets upon service restart + #[prost(uint32, tag = "3")] + pub seq_num: u32, +} +/// tbd: we may want to add filters here +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribeEntriesRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Entry { + /// the slot that the entry is from + #[prost(uint64, tag = "1")] + pub slot: u64, + /// Serialized bytes of Vec: + #[prost(bytes = "vec", tag = "2")] + pub entries: ::prost::alloc::vec::Vec, +} +/// Generated client implementations. +pub mod shredstream_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ShredstreamClient { + inner: tonic::client::Grpc, + } + impl ShredstreamClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ShredstreamClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ShredstreamClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ShredstreamClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// RPC endpoint to send heartbeats to keep shreds flowing + pub async fn send_heartbeat( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/shredstream.Shredstream/SendHeartbeat", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("shredstream.Shredstream", "SendHeartbeat")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated client implementations. +pub mod shredstream_proxy_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ShredstreamProxyClient { + inner: tonic::client::Grpc, + } + impl ShredstreamProxyClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ShredstreamProxyClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ShredstreamProxyClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ShredstreamProxyClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn subscribe_entries( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/shredstream.ShredstreamProxy/SubscribeEntries", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("shredstream.ShredstreamProxy", "SubscribeEntries"), + ); + self.inner.server_streaming(req, path, codec).await + } + } +} diff --git a/src/swqos/jito_grpc/trace_shred.rs b/src/swqos/jito_grpc/trace_shred.rs new file mode 100755 index 0000000..35c5558 --- /dev/null +++ b/src/swqos/jito_grpc/trace_shred.rs @@ -0,0 +1,13 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TraceShred { + /// source region, one of: + #[prost(string, tag = "1")] + pub region: ::prost::alloc::string::String, + /// timestamp of creation + #[prost(message, optional, tag = "2")] + pub created_at: ::core::option::Option<::prost_types::Timestamp>, + /// monotonically increases, resets upon service restart + #[prost(uint32, tag = "3")] + pub seq_num: u32, +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index eca4b4c..ff40e7d 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -1,5 +1,6 @@ use api::api_client::ApiClient; -use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode}; +use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode, serialize_transaction_and_encode}; +use solana_client::rpc_config::RpcSendTransactionConfig; use crate::swqos::jito_grpc::searcher::searcher_service_client::SearcherServiceClient; use reqwest::Client; use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation}; @@ -9,7 +10,7 @@ use yellowstone_grpc_client::Interceptor; use std::{sync::Arc, time::Instant}; use tokio::sync::{Mutex, RwLock}; -use solana_sdk::signature::Signature; +use solana_sdk::{commitment_config::CommitmentLevel, signature::Signature}; use std::str::FromStr; use rustls::crypto::{ring::default_provider, CryptoProvider}; @@ -23,11 +24,11 @@ use anyhow::{anyhow, Result}; use rand::{rng, seq::{IndexedRandom, IteratorRandom}}; use solana_sdk::transaction::VersionedTransaction; -use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS}}; +use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS, NOZOMI_TIP_ACCOUNTS}}; +pub mod api; pub mod common; pub mod searcher_client; -pub mod api; pub mod jito_grpc; lazy_static::lazy_static! { @@ -35,20 +36,92 @@ lazy_static::lazy_static! { } #[derive(Debug, Clone, Copy)] +pub enum TradeType { + Create, + CreateAndBuy, + Buy, + Sell, +} + +impl std::fmt::Display for TradeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + TradeType::Create => "创建", + TradeType::CreateAndBuy => "创建并买入", + TradeType::Buy => "买入", + TradeType::Sell => "卖出", + }; + write!(f, "{}", s) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] pub enum ClientType { Jito, NextBlock, ZeroSlot, + Nozomi, + Rpc, } pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static; #[async_trait::async_trait] pub trait FeeClientTrait { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result; - async fn send_transactions(&self, transactions: &Vec) -> Result>; - async fn get_tip_account(&self) -> Result; - async fn get_client_type(&self) -> ClientType; + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result; + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result>; + fn get_tip_account(&self) -> Result; + fn get_client_type(&self) -> ClientType; +} + +#[derive(Clone)] +pub struct SolRpcClient { + pub rpc_client: Arc, +} + +#[async_trait::async_trait] +impl FeeClientTrait for SolRpcClient { + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + let signature = self.rpc_client.send_transaction_with_config(transaction, RpcSendTransactionConfig{ + skip_preflight: true, + preflight_commitment: Some(CommitmentLevel::Processed), + encoding: Some(UiTransactionEncoding::Base64), + max_retries: Some(3), + min_context_slot: Some(0), + }).await?; + + let start_time = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(_) => (), + Err(_) => (), + } + println!(" rpc{}确认: {:?}", trade_type, start_time.elapsed()); + + Ok(signature) + } + + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result> { + let mut signatures = Vec::new(); + for transaction in transactions { + let signature = self.send_transaction(trade_type, transaction).await?; + signatures.push(signature); + } + Ok(signatures) + } + + fn get_tip_account(&self) -> Result { + Ok("".to_string()) + } + + fn get_client_type(&self) -> ClientType { + ClientType::Rpc + } +} + +impl SolRpcClient { + pub fn new(rpc_client: Arc) -> Self { + Self { rpc_client } + } } pub struct JitoClient { @@ -58,15 +131,15 @@ pub struct JitoClient { #[async_trait::async_trait] impl FeeClientTrait for JitoClient { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction")) + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + self.send_bundle_with_confirmation(trade_type, &vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction")) } - async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - self.send_bundle_with_confirmation(transactions).await + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { + self.send_bundle_with_confirmation(trade_type, transactions).await } - async fn get_tip_account(&self) -> Result { + fn get_tip_account(&self) -> Result { if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) { Ok(acc.to_string()) } else { @@ -74,7 +147,7 @@ impl FeeClientTrait for JitoClient { } } - async fn get_client_type(&self) -> ClientType { + fn get_client_type(&self) -> ClientType { ClientType::Jito } } @@ -88,9 +161,10 @@ impl JitoClient { pub async fn send_bundle_with_confirmation( &self, + trade_type: TradeType, transactions: &Vec, ) -> Result, anyhow::Error> { - send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await + send_bundle_with_confirmation(self.rpc_client.clone(), trade_type, &transactions, self.searcher_client.clone()).await } pub async fn send_bundle_no_wait( @@ -131,20 +205,20 @@ pub struct NextBlockClient { #[async_trait::async_trait] impl FeeClientTrait for NextBlockClient { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - self.send_transaction(transaction).await + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + self.send_transaction(trade_type, transaction).await } - async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - self.send_transactions(transactions).await + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { + self.send_transactions(trade_type, transactions).await } - async fn get_tip_account(&self) -> Result { - let tip_account = self.get_tip_account().await?; - Ok(tip_account) + fn get_tip_account(&self) -> Result { + let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) } - async fn get_client_type(&self) -> ClientType { + fn get_client_type(&self) -> ClientType { ClientType::NextBlock } } @@ -173,7 +247,8 @@ impl NextBlockClient { Self { rpc_client: Arc::new(rpc_client), client } } - pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { + pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + let start_time = Instant::now(); let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; self.client.clone().post_submit_v2(api::PostSubmitRequest { @@ -187,19 +262,23 @@ impl NextBlockClient { snipe_transaction: Some(true), }).await?; - let timeout: Duration = Duration::from_secs(10); + println!(" nextblock{}提交: {:?}", trade_type, start_time.elapsed()); + let start_time: Instant = Instant::now(); + let timeout: Duration = Duration::from_secs(10); while Instant::now().duration_since(start_time) < timeout { match poll_transaction_confirmation(&self.rpc_client, signature).await { - Ok(sig) => return Ok(sig), + Ok(_) => break, Err(_) => continue, } } + println!(" nextblock{}确认: {:?}", trade_type, start_time.elapsed()); + Ok(signature) } - pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { let mut entries = Vec::new(); let encoding = UiTransactionEncoding::Base64; @@ -223,24 +302,18 @@ impl NextBlockClient { front_running_protection: Some(true), }).await?; - let timeout: Duration = Duration::from_secs(10); let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - for signature in signatures.clone() { - match poll_transaction_confirmation(&self.rpc_client, signature).await { - Ok(sig) => signatures.push(sig), - Err(_) => continue, - } + for signature in signatures.clone() { + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(_) => continue, + Err(_) => continue, } } + println!(" nextblock{}确认: {:?}", trade_type, start_time.elapsed()); + Ok(signatures) } - - async fn get_tip_account(&self) -> Result { - let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); - Ok(tip_account.to_string()) - } } #[derive(Clone)] @@ -248,24 +321,25 @@ pub struct ZeroSlotClient { pub endpoint: String, pub auth_token: String, pub rpc_client: Arc, + pub http_client: Client, } #[async_trait::async_trait] impl FeeClientTrait for ZeroSlotClient { - async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - self.send_transaction(transaction).await + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + self.send_transaction(trade_type, transaction).await } - async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { - self.send_transactions(transactions).await + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { + self.send_transactions(trade_type, transactions).await } - async fn get_tip_account(&self) -> Result { - let tip_account = self.get_tip_account().await?; - Ok(tip_account) + fn get_tip_account(&self) -> Result { + let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ZEROSLOT_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) } - async fn get_client_type(&self) -> ClientType { + fn get_client_type(&self) -> ClientType { ClientType::ZeroSlot } } @@ -273,63 +347,174 @@ impl FeeClientTrait for ZeroSlotClient { impl ZeroSlotClient { pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { let rpc_client = SolanaRpcClient::new(rpc_url); - Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token } + let http_client = Client::builder() + .pool_idle_timeout(Duration::from_secs(60)) + .pool_max_idle_per_host(64) + .tcp_keepalive(Some(Duration::from_secs(1200))) + .http2_keep_alive_interval(Duration::from_secs(15)) + .timeout(Duration::from_secs(10)) + .connect_timeout(Duration::from_secs(5)) + .build() + .unwrap(); + Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client } } - pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { - let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; - - let client = Client::new(); - let request_body = json!({ + pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + let start_time = Instant::now(); + let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; + println!(" 交易编码base64: {:?}", start_time.elapsed()); + + let request_body = serde_json::to_string(&json!({ "jsonrpc": "2.0", "id": 1, "method": "sendTransaction", "params": [ content, - { - "encoding": "base64", - "skipPreflight": true, - } + { "encoding": "base64", "skipPreflight": true } ] - }); + }))?; - // Send the request - let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token)) - .json(&request_body) + let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20); + url.push_str(&self.endpoint); + url.push_str("/?api-key="); + url.push_str(&self.auth_token); + + // 4. 直接使用 `text().await?`,避免 `json().await?` 的异步 JSON 解析 + let response_text = self.http_client.post(&url) + .body(request_body) // 直接传字符串,避免 `json()` 开销 + .header("Content-Type", "application/json") // 显式指定 JSON 头 .send() + .await? + .text() .await?; - // Parse the response - let response_json: serde_json::Value = response.json().await?; - if let Some(result) = response_json.get("result") { - println!("Transaction sent successfully: {}", result); - } else if let Some(error) = response_json.get("error") { - eprintln!("Failed to send transaction: {}", error); - } - - let timeout: Duration = Duration::from_secs(10); - let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - match poll_transaction_confirmation(&self.rpc_client, signature).await { - Ok(sig) => return Ok(sig), - Err(_) => continue, + // 5. 用 `serde_json::from_str()` 解析 JSON,减少 `.json().await?` 额外等待 + if let Ok(response_json) = serde_json::from_str::(&response_text) { + if response_json.get("result").is_some() { + println!(" 0slot{}提交: {:?}", trade_type, start_time.elapsed()); + } else if let Some(_error) = response_json.get("error") { + eprintln!(" 0slot{}提交失败: {:?}", trade_type, _error); } } + let start_time: Instant = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(_) => (), + Err(_) => (), + } + + println!(" 0slot{}确认: {:?}", trade_type, start_time.elapsed()); + Ok(signature) } - pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { let mut signatures = Vec::new(); for transaction in transactions { - let signature = self.send_transaction(transaction).await?; + let signature = self.send_transaction(trade_type, transaction).await?; signatures.push(signature); } Ok(signatures) } +} - async fn get_tip_account(&self) -> Result { - let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); +#[derive(Clone)] +pub struct NozomiClient { + pub rpc_client: Arc, + pub endpoint: String, + pub auth_token: String, + pub http_client: Client, +} + +#[async_trait::async_trait] +impl FeeClientTrait for NozomiClient { + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + self.send_transaction(trade_type, transaction).await + } + + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { + self.send_transactions(trade_type, transactions).await + } + + fn get_tip_account(&self) -> Result { + let tip_account = *NOZOMI_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NOZOMI_TIP_ACCOUNTS.first()).unwrap(); Ok(tip_account.to_string()) } + + fn get_client_type(&self) -> ClientType { + ClientType::Nozomi + } +} + +impl NozomiClient { + pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + let http_client = Client::builder() + .pool_idle_timeout(Duration::from_secs(60)) + .pool_max_idle_per_host(64) + .tcp_keepalive(Some(Duration::from_secs(1200))) + .http2_keep_alive_interval(Duration::from_secs(15)) + .timeout(Duration::from_secs(10)) + .connect_timeout(Duration::from_secs(5)) + .build() + .unwrap(); + Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client } + } + + pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result { + let start_time = Instant::now(); + let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; + println!(" 交易编码base64: {:?}", start_time.elapsed()); + + // 按照 Nozomi 文档要求构建请求体 + let request_body = serde_json::to_string(&json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": [ + content, + { "encoding": "base64" } + ] + }))?; + + let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20); + url.push_str(&self.endpoint); + url.push_str("/?c="); + url.push_str(&self.auth_token); + + let response_text = self.http_client.post(&url) + .body(request_body) + .header("Content-Type", "application/json") + .send() + .await? + .text() + .await?; + + if let Ok(response_json) = serde_json::from_str::(&response_text) { + if response_json.get("result").is_some() { + println!(" nozomi{}提交: {:?}", trade_type, start_time.elapsed()); + } else if let Some(_error) = response_json.get("error") { + // eprintln!("nozomi交易提交失败: {:?}", _error); + } + } + + let start_time: Instant = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(_) => (), + Err(_) => (), + } + + println!(" nozomi{}确认: {:?}", trade_type, start_time.elapsed()); + + Ok(signature) + } + + pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result, anyhow::Error> { + let mut signatures = Vec::new(); + for transaction in transactions { + let signature = self.send_transaction(trade_type, transaction).await?; + signatures.push(signature); + } + Ok(signatures) + } } \ No newline at end of file diff --git a/src/swqos/searcher_client.rs b/src/swqos/searcher_client.rs index a6eb3c4..3dc78fa 100755 --- a/src/swqos/searcher_client.rs +++ b/src/swqos/searcher_client.rs @@ -18,12 +18,16 @@ use solana_sdk::{ }; use thiserror::Error; use tokio::sync::Mutex; -use tonic::{transport::{self, Channel, Endpoint}, Status}; +use tonic::{ + transport::{self, Channel, Endpoint}, Status +}; use yellowstone_grpc_client::ClientTlsConfig; use crate::swqos::common::poll_transaction_confirmation; use crate::common::SolanaRpcClient; +use super::TradeType; + #[derive(Debug, Error)] pub enum BlockEngineConnectionError { #[error("transport error {0}")] @@ -81,21 +85,23 @@ pub async fn subscribe_bundle_results( pub async fn send_bundle_with_confirmation( rpc: Arc, + trade_type: TradeType, transactions: &Vec, searcher_client: Arc>>, ) -> Result, anyhow::Error> { - let mut signatures = send_bundle_no_wait(transactions, searcher_client).await?; + let start_time = Instant::now(); + let signatures = send_bundle_no_wait(transactions, searcher_client).await?; + println!(" Jito{}提交: {:?}", trade_type, start_time.elapsed()); - let timeout: Duration = Duration::from_secs(10); let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { - for signature in signatures.clone() { - match poll_transaction_confirmation(&rpc, signature).await { - Ok(sig) => signatures.push(sig), - Err(_) => continue, - } + for signature in signatures.clone() { + match poll_transaction_confirmation(&rpc, signature).await { + Ok(_) => continue, + Err(_) => continue, } } + + println!(" Jito{}确认: {:?}", trade_type, start_time.elapsed()); Ok(signatures) } diff --git a/src/swqos/token_authenticator.rs b/src/swqos/token_authenticator.rs new file mode 100755 index 0000000..917efb9 --- /dev/null +++ b/src/swqos/token_authenticator.rs @@ -0,0 +1,167 @@ +use std::{ + sync::{Arc, RwLock}, + time::{Duration, SystemTime}, +}; + +use jito_protos::auth::{ + auth_service_client::AuthServiceClient, GenerateAuthChallengeRequest, + GenerateAuthTokensRequest, RefreshAccessTokenRequest, Role, Token, +}; +use prost_types::Timestamp; +use solana_metrics::datapoint_info; +use solana_sdk::signature::{Keypair, Signer}; +use tokio::{task::JoinHandle, time::sleep}; +use tonic::{service::Interceptor, transport::Channel, Request, Status}; + +use super::searcher_client::BlockEngineConnectionResult; + +const AUTHORIZATION_HEADER: &str = "authorization"; +const BEARER: &str = "Bearer "; + +/// Adds the token to each requests' authorization header. +/// Manages refreshing the token in a separate thread. +#[derive(Clone)] +pub struct ClientInterceptor { + /// The token added to each request header. + bearer_token: Arc>, +} + +impl ClientInterceptor { + pub async fn new( + mut auth_service_client: AuthServiceClient, + keypair: &Arc, + role: Role, + ) -> BlockEngineConnectionResult { + let (access_token, refresh_token) = + Self::auth(&mut auth_service_client, keypair, role).await?; + + let bearer_token = Arc::new(RwLock::new(access_token.value.clone())); + + let _refresh_token_thread = Self::spawn_token_refresh_thread( + auth_service_client, + bearer_token.clone(), + refresh_token, + access_token.expires_at_utc.unwrap(), + keypair.clone(), + role, + ); + + Ok(Self { bearer_token }) + } + + async fn auth( + auth_service_client: &mut AuthServiceClient, + keypair: &Keypair, + role: Role, + ) -> BlockEngineConnectionResult<(Token, Token)> { + let challenge_resp = auth_service_client + .generate_auth_challenge(GenerateAuthChallengeRequest { + role: role as i32, + pubkey: keypair.pubkey().as_ref().to_vec(), + }) + .await? + .into_inner(); + let challenge = format!("{}-{}", keypair.pubkey(), challenge_resp.challenge); + let signed_challenge = keypair.sign_message(challenge.as_bytes()).as_ref().to_vec(); + + let tokens = auth_service_client + .generate_auth_tokens(GenerateAuthTokensRequest { + challenge, + client_pubkey: keypair.pubkey().as_ref().to_vec(), + signed_challenge, + }) + .await? + .into_inner(); + + Ok((tokens.access_token.unwrap(), tokens.refresh_token.unwrap())) + } + + fn spawn_token_refresh_thread( + mut auth_service_client: AuthServiceClient, + bearer_token: Arc>, + refresh_token: Token, + access_token_expiration: Timestamp, + keypair: Arc, + role: Role, + ) -> JoinHandle> { + tokio::spawn(async move { + let mut refresh_token = refresh_token; + let mut access_token_expiration = access_token_expiration; + + loop { + let access_token_ttl = SystemTime::try_from(access_token_expiration.clone()) + .unwrap() + .duration_since(SystemTime::now()) + .unwrap_or_else(|_| Duration::from_secs(0)); + let refresh_token_ttl = + SystemTime::try_from(refresh_token.expires_at_utc.as_ref().unwrap().clone()) + .unwrap() + .duration_since(SystemTime::now()) + .unwrap_or_else(|_| Duration::from_secs(0)); + + let does_access_token_expire_soon = access_token_ttl < Duration::from_secs(5 * 60); + let does_refresh_token_expire_soon = + refresh_token_ttl < Duration::from_secs(5 * 60); + + match ( + does_refresh_token_expire_soon, + does_access_token_expire_soon, + ) { + // re-run entire auth workflow is refresh token expiring soon + (true, _) => { + let is_error = { + if let Ok((new_access_token, new_refresh_token)) = + Self::auth(&mut auth_service_client, &keypair, role).await + { + *bearer_token.write().unwrap() = new_access_token.value.clone(); + access_token_expiration = new_access_token.expires_at_utc.unwrap(); + refresh_token = new_refresh_token; + false + } else { + true + } + }; + datapoint_info!("searcher-full-auth", ("is_error", is_error, bool)); + } + // re-up the access token if it expires soon + (_, true) => { + let is_error = { + if let Ok(refresh_resp) = auth_service_client + .refresh_access_token(RefreshAccessTokenRequest { + refresh_token: refresh_token.value.clone(), + }) + .await + { + let access_token = refresh_resp.into_inner().access_token.unwrap(); + *bearer_token.write().unwrap() = access_token.value.clone(); + access_token_expiration = access_token.expires_at_utc.unwrap(); + false + } else { + true + } + }; + + datapoint_info!("searcher-refresh-auth", ("is_error", is_error, bool)); + } + _ => { + sleep(Duration::from_secs(60)).await; + } + } + } + }) + } +} + +impl Interceptor for ClientInterceptor { + fn call(&mut self, mut request: Request<()>) -> Result, Status> { + let l_token = self.bearer_token.read().unwrap(); + if !l_token.is_empty() { + request.metadata_mut().insert( + AUTHORIZATION_HEADER, + format!("{BEARER}{l_token}").parse().unwrap(), + ); + } + + Ok(request) + } +}