add nextblock, 0slot support

This commit is contained in:
William
2025-04-02 13:39:59 +08:00
parent 2dc15b9020
commit ec03ed8edc
59 changed files with 5315 additions and 1855 deletions
+2 -2
View File
@@ -25,9 +25,9 @@
use solana_sdk::pubkey::Pubkey;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Serialize, Deserialize};
/// Represents the global configuration account for token pricing and fees
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalAccount {
/// Unique identifier for the global account
pub discriminator: u64,
+2
View File
@@ -13,6 +13,7 @@ pub enum DexInstruction {
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct CreateTokenInfo {
pub slot: u64,
pub name: String,
pub symbol: String,
pub uri: String,
@@ -23,6 +24,7 @@ pub struct CreateTokenInfo {
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct TradeInfo {
pub slot: u64,
pub mint: Pubkey,
pub sol_amount: u64,
pub token_amount: u64,
+1
View File
@@ -8,6 +8,7 @@ pub const PROGRAM_DATA: &str = "Program data: ";
#[derive(Debug)]
pub enum PumpfunEvent {
NewToken(CreateTokenInfo),
NewDevTrade(TradeInfo),
NewUserTrade(TradeInfo),
NewBotTrade(TradeInfo),
Error(String),
+2
View File
@@ -94,6 +94,7 @@ pub fn parse_create_token_data(data: &str) -> ClientResult<CreateTokenInfo> {
let user = bs58::encode(&decoded[cursor..cursor+32]).into_string();
Ok(CreateTokenInfo {
slot: 0,
name,
symbol,
uri,
@@ -158,6 +159,7 @@ pub fn parse_trade_data(data: &str) -> ClientResult<TradeInfo> {
let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
Ok(TradeInfo {
slot: 0,
mint: Pubkey::from_str(&mint).unwrap(),
sol_amount,
token_amount,
+3
View File
@@ -3,3 +3,6 @@ pub mod logs_parser;
pub mod logs_filters;
pub mod logs_subscribe;
pub mod logs_events;
pub mod types;
pub use types::*;
+93
View File
@@ -0,0 +1,93 @@
use std::sync::Arc;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{commitment_config::CommitmentConfig, 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}, jito::FeeClient};
#[derive(Debug, Clone, PartialEq)]
pub enum FeeType {
Jito,
NextBlock,
}
#[derive(Debug, Clone)]
pub struct Cluster {
pub rpc_url: String,
pub block_engine_url: String,
pub nextblock_url: String,
pub nextblock_auth_token: String,
pub zeroslot_url: String,
pub zeroslot_auth_token: String,
pub use_jito: bool,
pub use_nextblock: bool,
pub use_zeroslot: bool,
pub priority_fee: PriorityFee,
pub commitment: CommitmentConfig,
}
impl Cluster {
pub fn new(
rpc_url: String,
block_engine_url:
String, nextblock_url:
String, nextblock_auth_token:
String, zeroslot_url: String,
zeroslot_auth_token: String,
priority_fee: PriorityFee,
commitment: CommitmentConfig,
use_jito: bool,
use_nextblock: bool,
use_zeroslot: bool
) -> Self {
Self {
rpc_url,
block_engine_url,
nextblock_url,
nextblock_auth_token,
zeroslot_url,
zeroslot_auth_token,
priority_fee,
commitment,
use_jito,
use_nextblock,
use_zeroslot
}
}
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq)]
pub struct PriorityFee {
pub unit_limit: u32,
pub unit_price: u64,
pub buy_tip_fee: f64,
pub sell_tip_fee: f64,
}
impl Default for PriorityFee {
fn default() -> Self {
Self {
unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT,
unit_price: DEFAULT_COMPUTE_UNIT_PRICE,
buy_tip_fee: DEFAULT_BUY_TIP_FEE,
sell_tip_fee: DEFAULT_SELL_TIP_FEE
}
}
}
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
pub struct MethodArgs {
pub payer: Arc<Keypair>,
pub rpc: Arc<RpcClient>,
pub nonblocking_rpc: Arc<SolanaRpcClient>,
pub jito_client: Arc<FeeClient>,
}
impl MethodArgs {
pub fn new(payer: Arc<Keypair>, rpc: Arc<RpcClient>, nonblocking_rpc: Arc<SolanaRpcClient>, jito_client: Arc<FeeClient>) -> Self {
Self { payer, rpc, nonblocking_rpc, jito_client }
}
}
+45 -3
View File
@@ -27,6 +27,8 @@ pub mod seeds {
/// Constants related to program accounts and authorities
pub mod accounts {
use std::collections::{HashMap, HashSet};
use solana_sdk::{pubkey, pubkey::Pubkey};
/// Public key for the Pump.fun program
@@ -50,13 +52,53 @@ pub mod accounts {
/// Rent Sysvar ID
pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111");
pub const JITO_TIP_ACCOUNTS: [&str; 8] = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
];
/// Tip accounts
pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[
"NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
"NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
"NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
"NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
"neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
"nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
"NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
"nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"
];
pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[
"Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3",
"FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe",
"ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13",
"6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK",
"Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr",
];
pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
}
pub mod trade {
pub const JITO_TIP_AMOUNT: f64 = 0.0001;
pub const TRADER_TIP_AMOUNT: f64 = 0.0001;
pub const DEFAULT_SLIPPAGE: u64 = 3000; // 30%
pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000;
pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000;
pub const DEFAULT_BUY_JITO_FEE: f64 = 0.0006;
pub const DEFAULT_SELL_JITO_FEE: f64 = 0.00006;
pub const DEFAULT_BUY_TIP_FEE: f64 = 0.0006;
pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001;
}
pub struct Symbol;
impl Symbol {
pub const SOLANA: &'static str = "solana";
}
+121 -50
View File
@@ -2,8 +2,10 @@ 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::codec::CompressionEncoding;
use tonic::{transport::channel::ClientTlsConfig, Status};
use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult};
use yellowstone_grpc_proto::geyser::SubscribeUpdateSlot;
use yellowstone_grpc_proto::geyser::{
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate,
SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing,
@@ -15,7 +17,9 @@ 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<String, SubscribeRequestFilterTransactions>;
@@ -25,11 +29,13 @@ 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<Transaction>,
}
impl fmt::Debug for TransactionPretty {
@@ -54,6 +60,7 @@ impl fmt::Debug for TransactionPretty {
impl From<SubscribeUpdateTransaction> 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"),
@@ -62,10 +69,12 @@ impl From<SubscribeUpdateTransaction> for TransactionPretty {
.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,
}
@@ -109,43 +118,6 @@ impl YellowstoneGrpc {
Ok(client.subscribe_with_request(Some(subscribe_request)).await)
}
pub async fn subscribe_pumpfun<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> 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::<TransactionPretty>(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_transaction(transaction_pretty, &*callback, bot_wallet).await {
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
pub fn get_subscribe_request_filter(
&self,
account_include: Vec<String>,
@@ -167,6 +139,43 @@ impl YellowstoneGrpc {
transactions
}
// pub fn get_subscribe_account_updater_request_filter(
// &self,
// account_include: Vec<String>,
// account_exclude: Vec<String>,
// account_required: Vec<String>,
// ) -> TransactionsFilterMap {
// let mut transactions = HashMap::new();
// transactions.insert(
// "client".to_string(),
// SubscribeUpdateAccount {
// account: account_include,
// slot: None,
// is_startup: None,
// },
// );
// transactions
// }
pub fn get_subscribe_update_slot_request_filter(
&self,
account_include: Vec<String>,
account_exclude: Vec<String>,
account_required: Vec<String>,
) -> TransactionsFilterMap {
let mut transactions = HashMap::new();
transactions.insert(
"client".to_string(),
SubscribeUpdateSlot {
slot: 0,
parent: None,
status: None,
dead_error: None,
},
);
transactions
}
async fn handle_stream_message(
msg: SubscribeUpdate,
tx: &mut mpsc::Sender<TransactionPretty>,
@@ -195,10 +204,62 @@ impl YellowstoneGrpc {
Ok(())
}
async fn process_transaction<F>(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option<Pubkey>) -> ClientResult<()>
// pub async fn subscribe_account_updater<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> 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::<TransactionPretty>(CHANNEL_SIZE);
// let callback = Box::new(callback);
// }
pub async fn subscribe_pumpfun<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> 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::<TransactionPretty>(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<F>(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option<Pubkey>) -> 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()))?;
@@ -213,21 +274,31 @@ impl YellowstoneGrpc {
&vec![]
};
let (create_event, trade_event) = PumpfunEvent::parse_logs(logs);
if let Some(create_event) = create_event {
callback(PumpfunEvent::NewToken(create_event));
}
if let Some(trade_event) = trade_event {
if let Some(bot_wallet_pubkey) = bot_wallet {
if trade_event.user == bot_wallet_pubkey {
callback(PumpfunEvent::NewBotTrade(trade_event));
} else {
callback(PumpfunEvent::NewUserTrade(trade_event));
let mut dev_address: Option<Pubkey> = 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));
}
} else {
callback(PumpfunEvent::NewUserTrade(trade_event));
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(())
}
}
+169 -1
View File
@@ -10,7 +10,20 @@
//! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL.
//! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL.
use crate::{constants, trade::common::{get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda}, PumpFun};
use std::sync::Arc;
use spl_associated_token_account::instruction::create_associated_token_account;
use spl_token::instruction::close_account;
use crate::common::SolanaRpcClient;
use crate::constants::trade::DEFAULT_SLIPPAGE;
use crate::ipfs::TokenMetadataIPFS;
use crate::pumpfun::common::{calculate_with_slippage_buy, calculate_with_slippage_sell, get_bonding_curve_account, get_buy_amount_with_slippage, get_global_account, get_initial_buy_price, get_token_balance, get_token_balance_and_ata};
use crate::{
constants,
pumpfun::common::{
get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda
},
};
use spl_associated_token_account::get_associated_token_address;
use solana_sdk::{
@@ -20,6 +33,7 @@ use solana_sdk::{
signer::Signer,
};
use anyhow::{anyhow, Result};
pub struct Create {
pub _name: String,
pub _symbol: String,
@@ -205,3 +219,157 @@ pub fn sell(
],
)
}
pub async fn build_create_and_buy_instructions(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Arc<Keypair>,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("build_create_and_buy_instructions: Amount cannot be zero"));
}
let rpc = rpc.as_ref();
let global_account = get_global_account(&rpc).await?;
let buy_amount = global_account.get_initial_buy_price(amount_sol);
let buy_amount_with_slippage =
get_buy_amount_with_slippage(amount_sol, slippage_basis_points);
let mut instructions = vec![];
instructions.push(create(
payer.as_ref(),
mint.as_ref(),
Create {
_name: ipfs.metadata.name.clone(),
_symbol: ipfs.metadata.symbol.clone(),
_uri: ipfs.metadata_uri.clone(),
},
));
let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey());
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&constants::accounts::TOKEN_PROGRAM,
));
instructions.push(buy(
payer.as_ref(),
&mint.pubkey(),
&global_account.fee_recipient,
Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
Ok(instructions)
}
pub async fn build_buy_instructions(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Arc<Pubkey>,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("build_buy_instructions:Amount cannot be zero"));
}
let global_account = get_global_account(&rpc).await?;
let buy_amount = match get_bonding_curve_account(&rpc, mint.as_ref()).await {
Ok(account) => {
account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))?
},
Err(_e) => {
let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?;
initial_buy_amount * 80 / 100
}
};
let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
let mut instructions = vec![];
// let ata = get_associated_token_address(&payer.pubkey(), &mint);
// match rpc.get_account(&ata).await {
// Ok(_) => {},
// Err(_) => {
// instructions.push(create_associated_token_account(
// &payer.pubkey(),
// &payer.pubkey(),
// &mint,
// &constants::accounts::TOKEN_PROGRAM,
// ));
// }
// }
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint,
&constants::accounts::TOKEN_PROGRAM,
));
instructions.push(buy(
payer.as_ref(),
&mint,
&global_account.fee_recipient,
Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
Ok(instructions)
}
pub async fn build_sell_instructions(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Arc<Pubkey>,
amount_token: u64,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_token == 0 {
return Err(anyhow!("build_sell_instructions: Amount cannot be zero"));
}
let ata = get_associated_token_address(&payer.pubkey(), mint.as_ref());
let global_account = get_global_account(&rpc).await?;
let bonding_curve_account = get_bonding_curve_account(&rpc, mint.as_ref()).await?;
let min_sol_output = bonding_curve_account
.get_sell_price(amount_token, global_account.fee_basis_points)
.map_err(|e| anyhow!(e))?;
let min_sol_output_with_slippage = calculate_with_slippage_sell(
min_sol_output,
slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let mut instructions = vec![];
instructions.push(sell(
payer.as_ref(),
&mint,
&global_account.fee_recipient,
Sell {
_amount: amount_token,
_min_sol_output: min_sol_output_with_slippage,
},
));
instructions.push(close_account(
&spl_token::ID,
&ata,
&payer.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
)?);
Ok(instructions)
}
+459 -15
View File
@@ -1,18 +1,462 @@
use std::env;
use anyhow::{Context, Result};
use reqwest::Proxy;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::convert::TryFrom;
#[derive(Debug)]
pub struct TipAccountResult {
pub accounts: Vec<String>,
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostSubmitRequest {
#[prost(message, optional, tag = "1")]
pub transaction: ::core::option::Option<TransactionMessage>,
#[prost(bool, tag = "2")]
pub skip_pre_flight: bool,
#[prost(bool, optional, tag = "3")]
pub front_running_protection: ::core::option::Option<bool>,
#[prost(bool, optional, tag = "8")]
pub experimental_front_running_protection: ::core::option::Option<bool>,
#[prost(bool, optional, tag = "9")]
pub snipe_transaction: ::core::option::Option<bool>,
}
impl TipAccountResult {
pub fn from(accounts: Vec<String>) -> Result<Self> {
Ok(TipAccountResult { accounts })
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostSubmitRequestEntry {
#[prost(message, optional, tag = "1")]
pub transaction: ::core::option::Option<TransactionMessage>,
#[prost(bool, tag = "2")]
pub skip_pre_flight: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostSubmitBatchRequest {
#[prost(message, repeated, tag = "1")]
pub entries: ::prost::alloc::vec::Vec<PostSubmitRequestEntry>,
#[prost(enumeration = "SubmitStrategy", tag = "2")]
pub submit_strategy: i32,
#[prost(bool, optional, tag = "3")]
pub use_bundle: ::core::option::Option<bool>,
#[prost(bool, optional, tag = "4")]
pub front_running_protection: ::core::option::Option<bool>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostSubmitBatchResponseEntry {
#[prost(string, tag = "1")]
pub signature: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub error: ::prost::alloc::string::String,
#[prost(bool, tag = "3")]
pub submitted: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostSubmitBatchResponse {
#[prost(message, repeated, tag = "1")]
pub transactions: ::prost::alloc::vec::Vec<PostSubmitBatchResponseEntry>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostSubmitResponse {
#[prost(string, tag = "1")]
pub signature: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TransactionMessage {
#[prost(string, tag = "1")]
pub content: ::prost::alloc::string::String,
#[prost(bool, tag = "2")]
pub is_cleanup: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TransactionMessageV2 {
#[prost(string, tag = "1")]
pub content: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum SubmitStrategy {
PUknown = 0,
PSubmitAll = 1,
PAbortOnFirstError = 2,
PWaitForConfirmation = 3,
}
impl SubmitStrategy {
/// 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::PUknown => "P_UKNOWN",
Self::PSubmitAll => "P_SUBMIT_ALL",
Self::PAbortOnFirstError => "P_ABORT_ON_FIRST_ERROR",
Self::PWaitForConfirmation => "P_WAIT_FOR_CONFIRMATION",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"P_UKNOWN" => Some(Self::PUknown),
"P_SUBMIT_ALL" => Some(Self::PSubmitAll),
"P_ABORT_ON_FIRST_ERROR" => Some(Self::PAbortOnFirstError),
"P_WAIT_FOR_CONFIRMATION" => Some(Self::PWaitForConfirmation),
_ => None,
}
}
}
/// Generated client implementations.
pub mod api_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 ApiClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ApiClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: std::convert::TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> ApiClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + 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<F>(
inner: T,
interceptor: F,
) -> ApiClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
ApiClient::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 post_submit_v2(
&mut self,
request: impl tonic::IntoRequest<super::PostSubmitRequest>,
) -> std::result::Result<
tonic::Response<super::PostSubmitResponse>,
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("/api.Api/PostSubmitV2");
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitV2"));
self.inner.unary(req, path, codec).await
}
pub async fn post_submit_batch_v2(
&mut self,
request: impl tonic::IntoRequest<super::PostSubmitBatchRequest>,
) -> std::result::Result<
tonic::Response<super::PostSubmitBatchResponse>,
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(
"/api.Api/PostSubmitBatchV2",
);
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitBatchV2"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod api_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with ApiServer.
#[async_trait]
pub trait Api: std::marker::Send + std::marker::Sync + 'static {
async fn post_submit_v2(
&self,
request: tonic::Request<super::PostSubmitRequest>,
) -> std::result::Result<
tonic::Response<super::PostSubmitResponse>,
tonic::Status,
>;
async fn post_submit_batch_v2(
&self,
request: tonic::Request<super::PostSubmitBatchRequest>,
) -> std::result::Result<
tonic::Response<super::PostSubmitBatchResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct ApiServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> ApiServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(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.max_decoding_message_size = Some(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.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for ApiServer<T>
where
T: Api,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/api.Api/PostSubmitV2" => {
#[allow(non_camel_case_types)]
struct PostSubmitV2Svc<T: Api>(pub Arc<T>);
impl<T: Api> tonic::server::UnaryService<super::PostSubmitRequest>
for PostSubmitV2Svc<T> {
type Response = super::PostSubmitResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostSubmitRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Api>::post_submit_v2(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostSubmitV2Svc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/api.Api/PostSubmitBatchV2" => {
#[allow(non_camel_case_types)]
struct PostSubmitBatchV2Svc<T: Api>(pub Arc<T>);
impl<
T: Api,
> tonic::server::UnaryService<super::PostSubmitBatchRequest>
for PostSubmitBatchV2Svc<T> {
type Response = super::PostSubmitBatchResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostSubmitBatchRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Api>::post_submit_batch_v2(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostSubmitBatchV2Svc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for ApiServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "api.Api";
impl<T> tonic::server::NamedService for ApiServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}
-125
View File
@@ -1,125 +0,0 @@
pub use reqwest;
use solana_rpc_client_api::{client_error::ErrorKind, request};
use solana_sdk::{
signature::SignerError, transaction::TransactionError, transport::TransportError,
};
use thiserror::Error as ThisError;
use crate::jito::request::RpcRequest;
#[derive(ThisError, Debug)]
#[error("{kind}")]
pub struct Error {
pub request: Option<RpcRequest>,
#[source]
pub kind: ErrorKind,
}
impl Error {
pub fn new_with_request(kind: ErrorKind, request: RpcRequest) -> Self {
Self {
request: Some(request),
kind,
}
}
pub fn into_with_request(self, request: RpcRequest) -> Self {
Self {
request: Some(request),
..self
}
}
pub fn request(&self) -> Option<&RpcRequest> {
self.request.as_ref()
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
pub fn get_transaction_error(&self) -> Option<TransactionError> {
self.kind.get_transaction_error()
}
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Self {
Self {
request: None,
kind,
}
}
}
impl From<TransportError> for Error {
fn from(err: TransportError) -> Self {
Self {
request: None,
kind: err.into(),
}
}
}
impl From<Error> for TransportError {
fn from(client_error: Error) -> Self {
client_error.kind.into()
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self {
request: None,
kind: err.into(),
}
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Self {
request: None,
kind: ErrorKind::Custom(format!("Reqwest error: {}", err)),
}
}
}
impl From<request::RpcError> for Error {
fn from(err: request::RpcError) -> Self {
Self {
request: None,
kind: err.into(),
}
}
}
impl From<serde_json::error::Error> for Error {
fn from(err: serde_json::error::Error) -> Self {
Self {
request: None,
kind: err.into(),
}
}
}
impl From<SignerError> for Error {
fn from(err: SignerError) -> Self {
Self {
request: None,
kind: err.into(),
}
}
}
impl From<TransactionError> for Error {
fn from(err: TransactionError) -> Self {
Self {
request: None,
kind: err.into(),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
+127
View File
@@ -0,0 +1,127 @@
use bincode::serialize;
use serde_json::json;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::signature::Signature;
use solana_sdk::transaction::Transaction;
use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding};
use std::str::FromStr;
use std::time::{Duration, Instant};
use tokio::time::sleep;
use crate::common::types::SolanaRpcClient;
use anyhow::Result;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use reqwest::Client;
pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result<Signature> {
// 15 second timeout
let timeout: Duration = Duration::from_secs(15);
// 5 second retry interval
let interval: Duration = Duration::from_secs(5);
let start: Instant = Instant::now();
loop {
if start.elapsed() >= timeout {
return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig));
}
let status = rpc.get_signature_statuses(&[txt_sig]).await?;
match status.value[0].clone() {
Some(status) => {
if status.err.is_none()
&& (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|| status.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
{
return Ok(txt_sig);
}
if status.err.is_some() {
return Err(anyhow::anyhow!(status.err.unwrap()));
}
}
None => {
sleep(interval).await;
}
}
}
}
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
// 序列化交易
let serialized = bincode::serialize(transaction)
.map_err(|e| anyhow::anyhow!("序列化交易失败: {}", e))?;
// Base64编码
let encoded = STANDARD.encode(serialized);
let request_data = json!({
"transaction": {
"content": encoded
},
"frontRunningProtection": true
});
let url = format!("{}/api/v2/submit", endpoint);
let response = client
.post(url)
.header("Authorization", auth_token)
.header("Content-Type", "application/json")
.json(&request_data)
.send()
.await
.map_err(|e| anyhow::anyhow!("请求失败: {}", e))?;
let resp = response.json::<serde_json::Value>().await
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
if let Some(reason) = resp["reason"].as_str() {
return Err(anyhow::anyhow!(reason.to_string()));
}
let signature = resp["signature"].as_str()
.ok_or_else(|| anyhow::anyhow!("响应中缺少signature字段"))?;
let signature = Signature::from_str(signature)
.map_err(|e| anyhow::anyhow!("无效的签名: {}", e))?;
Ok(signature)
}
pub async fn serialize_and_encode(
transaction: &Vec<u8>,
encoding: UiTransactionEncoding,
) -> Result<String> {
let serialized = match encoding {
UiTransactionEncoding::Base58 => bs58::encode(transaction).into_string(),
UiTransactionEncoding::Base64 => STANDARD.encode(transaction),
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
};
Ok(serialized)
}
pub async fn serialize_transaction_and_encode(
transaction: &impl SerializableTransaction,
encoding: UiTransactionEncoding,
) -> Result<String> {
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)
}
pub async fn serialize_smart_transaction_and_encode(
transaction: &impl SerializableTransaction,
encoding: UiTransactionEncoding,
) -> 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, *signature))
}
-218
View File
@@ -1,218 +0,0 @@
use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc, RwLock,
},
time::{Duration, Instant},
};
use async_trait::async_trait;
use log::debug;
use reqwest::{
self,
header::{CONTENT_TYPE, RETRY_AFTER},
StatusCode,
};
use solana_rpc_client_api::{
custom_error,
error_object::RpcErrorObject,
request::{RpcError, RpcResponseErrorData},
response::RpcSimulateTransactionResult,
};
use tokio::time::sleep;
use crate::jito::{client_error::Result, request::RpcRequest, rpc_sender::RpcSender};
pub struct HttpSender {
client: Arc<reqwest::Client>,
url: String,
request_id: AtomicU64,
stats: RwLock<solana_rpc_client::rpc_sender::RpcTransportStats>,
}
/// Nonblocking [`RpcSender`] over HTTP.
impl HttpSender {
/// Create an HTTP RPC sender.
///
/// The URL is an HTTP URL, usually for port 8899, as in
/// "http://localhost:8899". The sender has a default timeout of 30 seconds.
pub fn new<U: ToString>(url: U) -> Self {
Self::new_with_timeout(url, Duration::from_secs(30))
}
/// Create an HTTP RPC sender.
///
/// The URL is an HTTP URL, usually for port 8899.
pub fn new_with_timeout<U: ToString>(url: U, timeout: Duration) -> Self {
let client = Arc::new(
reqwest::Client::builder()
.timeout(timeout)
.pool_idle_timeout(timeout)
.build()
.expect("build rpc client"),
);
Self {
client,
url: url.to_string(),
request_id: AtomicU64::new(0),
stats: RwLock::new(solana_rpc_client::rpc_sender::RpcTransportStats::default()),
}
}
}
struct StatsUpdater<'a> {
stats: &'a RwLock<solana_rpc_client::rpc_sender::RpcTransportStats>,
request_start_time: Instant,
rate_limited_time: Duration,
}
impl<'a> StatsUpdater<'a> {
fn new(stats: &'a RwLock<solana_rpc_client::rpc_sender::RpcTransportStats>) -> Self {
Self {
stats,
request_start_time: Instant::now(),
rate_limited_time: Duration::default(),
}
}
fn add_rate_limited_time(&mut self, duration: Duration) {
self.rate_limited_time += duration;
}
}
impl<'a> Drop for StatsUpdater<'a> {
fn drop(&mut self) {
let mut stats = self.stats.write().unwrap();
stats.request_count += 1;
stats.elapsed_time += Instant::now().duration_since(self.request_start_time);
stats.rate_limited_time += self.rate_limited_time;
}
}
#[async_trait]
impl RpcSender for HttpSender {
fn get_transport_stats(&self) -> solana_rpc_client::rpc_sender::RpcTransportStats {
self.stats.read().unwrap().clone()
}
async fn send(
&self,
request: RpcRequest,
params: serde_json::Value,
) -> Result<serde_json::Value> {
let mut stats_updater = StatsUpdater::new(&self.stats);
let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
let request_json = request.build_request_json(request_id, params).to_string();
let mut too_many_requests_retries = 5;
loop {
let response = {
let client = self.client.clone();
let request_json = request_json.clone();
client
.post(&self.url)
.header(CONTENT_TYPE, "application/json")
.body(request_json)
.send()
.await
}?;
if !response.status().is_success() {
if response.status() == StatusCode::TOO_MANY_REQUESTS
&& too_many_requests_retries > 0
{
let mut duration = Duration::from_millis(500);
if let Some(retry_after) = response.headers().get(RETRY_AFTER) {
if let Ok(retry_after) = retry_after.to_str() {
if let Ok(retry_after) = retry_after.parse::<u64>() {
if retry_after < 120 {
duration = Duration::from_secs(retry_after);
}
}
}
}
too_many_requests_retries -= 1;
debug!(
"Too many requests: server responded with {:?}, {} retries left, pausing for {:?}",
response, too_many_requests_retries, duration
);
sleep(duration).await;
stats_updater.add_rate_limited_time(duration);
continue;
}
return Err(response.error_for_status().unwrap_err().into());
}
let mut json = response.json::<serde_json::Value>().await?;
if json["error"].is_object() {
return match serde_json::from_value::<RpcErrorObject>(json["error"].clone()) {
Ok(rpc_error_object) => {
let data = match rpc_error_object.code {
solana_rpc_client_api::custom_error::JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE => {
match serde_json::from_value::<RpcSimulateTransactionResult>(json["error"]["data"].clone()) {
Ok(data) => RpcResponseErrorData::SendTransactionPreflightFailure(data),
Err(err) => {
debug!("Failed to deserialize RpcSimulateTransactionResult: {:?}", err);
RpcResponseErrorData::Empty
}
}
},
custom_error::JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY => {
match serde_json::from_value::<custom_error::NodeUnhealthyErrorData>(json["error"]["data"].clone()) {
Ok(custom_error::NodeUnhealthyErrorData {num_slots_behind}) => RpcResponseErrorData::NodeUnhealthy {num_slots_behind},
Err(_err) => {
RpcResponseErrorData::Empty
}
}
},
_ => RpcResponseErrorData::Empty
};
Err(RpcError::RpcResponseError {
code: rpc_error_object.code,
message: rpc_error_object.message,
data,
}
.into())
}
Err(err) => Err(RpcError::RpcRequestError(format!(
"Failed to deserialize RPC error response: {} [{}]",
serde_json::to_string(&json["error"]).unwrap(),
err
))
.into()),
};
}
return Ok(json["result"].take());
}
}
fn url(&self) -> String {
self.url.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn http_sender_on_tokio_multi_thread() {
let http_sender = HttpSender::new("http://localhost:1234".to_string());
let _ = http_sender
.send(RpcRequest::GetTipAccounts, serde_json::Value::Null)
.await;
}
#[tokio::test(flavor = "current_thread")]
async fn http_sender_on_tokio_current_thread() {
let http_sender = HttpSender::new("http://localhost:1234".to_string());
let _ = http_sender
.send(RpcRequest::GetTipAccounts, serde_json::Value::Null)
.await;
}
}
+322 -78
View File
@@ -1,105 +1,349 @@
use api::api_client::ApiClient;
use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode};
use jito_protos::{searcher::searcher_service_client::SearcherServiceClient, shredstream::shredstream_client::ShredstreamClient};
use reqwest::Client;
use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation};
use serde_json::json;
use tonic::transport::Channel;
use tracing::instrument::WithSubscriber;
use yellowstone_grpc_client::Interceptor;
use std::{sync::Arc, time::Instant};
use tokio::sync::{Mutex, RwLock};
use solana_sdk::signature::Signature;
use std::str::FromStr;
use rustls::crypto::{ring::default_provider, CryptoProvider};
use tonic::{service::interceptor::InterceptedService, transport::Uri, Status};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use tonic::transport::ClientTlsConfig;
use anyhow::{anyhow, Result};
use api::TipAccountResult;
use rand::seq::IteratorRandom;
use solana_sdk::{
pubkey::Pubkey,
transaction::{Transaction, VersionedTransaction},
};
use tokio::sync::RwLock;
use tracing::error;
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}};
pub mod common;
pub mod searcher_client;
pub mod api;
pub mod client_error;
pub mod http_sender;
pub mod request;
pub mod rpc_client;
pub mod rpc_sender;
use crate::jito::rpc_client::RpcClient;
pub struct JitoClient {
base_url: String,
tip_accounts: RwLock<Vec<String>>,
client: RpcClient,
lazy_static::lazy_static! {
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
}
impl Clone for JitoClient {
fn clone(&self) -> Self {
Self {
base_url: self.base_url.clone(),
tip_accounts: RwLock::new(Vec::new()),
client: RpcClient::new(self.base_url.clone()),
#[derive(Debug, Clone, Copy)]
pub enum ClientType {
Jito,
NextBlock,
ZeroSlot,
}
pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static;
#[async_trait::async_trait]
pub trait FeeClientTrait {
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature>;
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>>;
async fn get_tip_account(&self) -> Result<String>;
async fn get_client_type(&self) -> ClientType;
}
pub struct JitoClient {
pub rpc_client: Arc<SolanaRpcClient>,
pub searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
}
#[async_trait::async_trait]
impl FeeClientTrait for JitoClient {
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction"))
}
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_bundle_with_confirmation(transactions).await
}
async fn get_tip_account(&self) -> Result<String, anyhow::Error> {
if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) {
Ok(acc.to_string())
} else {
Err(anyhow!("no valid tip accounts found"))
}
}
async fn get_client_type(&self) -> ClientType {
ClientType::Jito
}
}
impl JitoClient {
pub fn new(jito_url: &str, _uuid: Option<String>) -> Self {
Self {
base_url: jito_url.to_string(),
tip_accounts: RwLock::new(vec![]),
client: RpcClient::new(jito_url.to_string()),
pub async fn new(rpc_url: String, block_engine_url: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
let searcher_client = get_searcher_client_no_auth(block_engine_url.as_str()).await?;
Ok(Self { rpc_client: Arc::new(rpc_client), searcher_client: Arc::new(Mutex::new(searcher_client)) })
}
pub async fn send_bundle_with_confirmation(
&self,
transactions: &Vec<VersionedTransaction>,
) -> Result<Vec<Signature>, anyhow::Error> {
send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await
}
pub async fn send_bundle_no_wait(
&self,
transactions: &Vec<VersionedTransaction>,
) -> Result<Vec<Signature>, anyhow::Error> {
searcher_client::send_bundle_no_wait(&transactions, self.searcher_client.clone()).await
}
// pub async fn get_tip_accounts(&self) -> Result<Vec<String>, anyhow::Error> {
// let client = ShredstreamClient::connect("dst").await?;
// // let subscriber = Dispatch::new(tracing_subscriber::fmt::Subscriber::builder().finish());
// let subscriber = tracing::subscriber::set_global_default(tracing_subscriber::fmt::Subscriber::builder().finish()).unwrap();
// let aaa = client.with_subscriber(subscriber);
// let mut stream = client.subscribe_accounts_of_interest(tonic::Request::new(()));
// let mut accounts = Vec::new();
// while let Some(Ok(response)) = stream.next().await {
// accounts.extend(response.accounts);
// }
// Ok(accounts)
// }
}
#[derive(Clone)]
pub struct MyInterceptor {
auth_token: String,
}
impl MyInterceptor {
pub fn new(auth_token: String) -> Self {
Self { auth_token }
}
}
impl Interceptor for MyInterceptor {
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
request.metadata_mut().insert(
"authorization",
tonic::metadata::MetadataValue::from_str(&self.auth_token)
.map_err(|_| Status::invalid_argument("Invalid auth token"))?
);
Ok(request)
}
}
#[derive(Clone)]
pub struct NextBlockClient {
pub rpc_client: Arc<SolanaRpcClient>,
pub client: ApiClient<InterceptedService<Channel, MyInterceptor>>,
}
#[async_trait::async_trait]
impl FeeClientTrait for NextBlockClient {
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_transaction(transaction).await
}
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(transactions).await
}
async fn get_tip_account(&self) -> Result<String> {
let tip_account = self.get_tip_account().await?;
Ok(tip_account)
}
async fn get_client_type(&self) -> ClientType {
ClientType::NextBlock
}
}
impl NextBlockClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
if CryptoProvider::get_default().is_none() {
let _ = default_provider()
.install_default()
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
}
let endpoint = endpoint.parse::<Uri>().unwrap();
let tls = ClientTlsConfig::new().with_native_roots();
let channel = Channel::builder(endpoint)
.tls_config(tls).expect("Failed to create TLS config")
.tcp_keepalive(Some(Duration::from_secs(60)))
.http2_keep_alive_interval(Duration::from_secs(30))
.keep_alive_while_idle(true)
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.connect_lazy();
let client = ApiClient::with_interceptor(channel, MyInterceptor::new(auth_token));
let rpc_client = SolanaRpcClient::new(rpc_url);
Self { rpc_client: Arc::new(rpc_client), client }
}
pub async fn get_tip_accounts(&self) -> Result<TipAccountResult> {
let result = self.client.get_tip_accounts().await?;
TipAccountResult::from(result).map_err(|e| anyhow!(e))
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
self.client.clone().post_submit_v2(api::PostSubmitRequest {
transaction: Some(api::TransactionMessage {
content,
is_cleanup: false,
}),
skip_pre_flight: true,
front_running_protection: Some(true),
experimental_front_running_protection: Some(true),
snipe_transaction: Some(true),
}).await?;
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,
}
}
Ok(signature)
}
pub async fn init_tip_accounts(&self) -> Result<()> {
let accounts = self.get_tip_accounts().await?;
let mut tip_accounts = self.tip_accounts.write().await;
*tip_accounts = accounts.accounts.iter().map(|a| a.to_string()).collect();
Ok(())
}
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
let mut entries = Vec::new();
let encoding = UiTransactionEncoding::Base64;
let mut signatures = Vec::new();
for transaction in transactions {
let (content, signature) = serialize_smart_transaction_and_encode(transaction, encoding).await?;
entries.push(api::PostSubmitRequestEntry {
transaction: Some(api::TransactionMessage {
content,
is_cleanup: false,
}),
skip_pre_flight: true,
});
signatures.push(signature);
}
pub async fn get_tip_account(&self) -> Result<Pubkey> {
{
let accounts = self.tip_accounts.read().await;
if !accounts.is_empty() {
if let Some(acc) = accounts.iter().choose(&mut rand::rng()) {
return Pubkey::from_str(acc)
.map_err(|err| {
error!("jito: failed to parse Pubkey: {:?}", err);
anyhow!("Invalid pubkey format")
});
self.client.clone().post_submit_batch_v2(api::PostSubmitBatchRequest {
entries,
submit_strategy: api::SubmitStrategy::PSubmitAll as i32,
use_bundle: Some(true),
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,
}
}
}
self.init_tip_accounts().await?;
let accounts = self.tip_accounts.read().await;
accounts
.iter()
.choose(&mut rand::rng())
.ok_or_else(|| anyhow!("jito: no tip accounts available"))
.and_then(|acc| {
Pubkey::from_str(acc).map_err(|err| {
error!("jito: failed to parse Pubkey: {:?}", err);
anyhow!("Invalid pubkey format")
})
})
Ok(signatures)
}
pub async fn send_transaction(
&self,
transaction: &Transaction,
) -> Result<String, anyhow::Error> {
let bundles = vec![VersionedTransaction::from(transaction.clone())];
Ok(self.client.send_bundle(&bundles).await?)
}
pub async fn send_transactions(
&self,
transactions: &Vec<Transaction>,
) -> Result<String, anyhow::Error> {
let bundles: Vec<VersionedTransaction> = transactions.iter()
.map(|t| VersionedTransaction::from(t.clone()))
.collect(); // 显式指定类型
Ok(self.client.send_bundle(&bundles).await?)
async fn get_tip_account(&self) -> Result<String> {
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
Ok(tip_account.to_string())
}
}
#[derive(Clone)]
pub struct ZeroSlotClient {
pub endpoint: String,
pub auth_token: String,
pub rpc_client: Arc<SolanaRpcClient>,
}
#[async_trait::async_trait]
impl FeeClientTrait for ZeroSlotClient {
async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
self.send_transaction(transaction).await
}
async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
self.send_transactions(transactions).await
}
async fn get_tip_account(&self) -> Result<String> {
let tip_account = self.get_tip_account().await?;
Ok(tip_account)
}
async fn get_client_type(&self) -> ClientType {
ClientType::ZeroSlot
}
}
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 }
}
pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result<Signature, anyhow::Error> {
let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
let client = Client::new();
let request_body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
content,
{
"encoding": "base64",
"skipPreflight": true,
}
]
});
// Send the request
let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token))
.json(&request_body)
.send()
.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,
}
}
Ok(signature)
}
pub async fn send_transactions(&self, transactions: &Vec<VersionedTransaction>) -> Result<Vec<Signature>, anyhow::Error> {
let mut signatures = Vec::new();
for transaction in transactions {
let signature = self.send_transaction(transaction).await?;
signatures.push(signature);
}
Ok(signatures)
}
async fn get_tip_account(&self) -> Result<String> {
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
Ok(tip_account.to_string())
}
}
-55
View File
@@ -1,55 +0,0 @@
use std::fmt;
use serde_json::{json, Value};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum RpcRequest {
Custom { method: &'static str },
GetBundlesStatuses,
GetTipAccounts,
SendBundle,
}
impl fmt::Display for RpcRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let method = match self {
RpcRequest::Custom { method } => method,
RpcRequest::GetBundlesStatuses => "getBundleStatuses",
RpcRequest::GetTipAccounts => "getTipAccounts",
RpcRequest::SendBundle => "sendBundle",
};
write!(f, "{method}")
}
}
impl RpcRequest {
pub fn build_request_json(self, id: u64, params: Value) -> Value {
let jsonrpc = "2.0";
json!({
"jsonrpc": jsonrpc,
"id": id,
"method": format!("{self}"),
"params": params,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_request_json() {
let test_request = RpcRequest::GetTipAccounts;
let request = test_request.build_request_json(1, json!([]));
assert_eq!(request["method"], "getTipAccounts");
assert_eq!(request["params"], json!([]));
let test_request = RpcRequest::GetBundlesStatuses;
let addr = json!("deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx");
let request = test_request.build_request_json(1, json!([addr]));
assert_eq!(request["method"], "getBundleStatuses");
assert_eq!(request["params"], json!([addr]));
}
}
-201
View File
@@ -1,201 +0,0 @@
use std::time::Duration;
use bincode::serialize;
use log::*;
use serde_json::{json, Value};
use solana_rpc_client::{
rpc_client::{RpcClientConfig, SerializableTransaction},
rpc_sender::RpcTransportStats,
};
use solana_rpc_client_api::{
client_error::ErrorKind as ClientErrorKind, request::RpcError, response::Response,
};
use solana_sdk::{bs58, commitment_config::CommitmentConfig};
use solana_transaction_status::UiTransactionEncoding;
use crate::jito::{
client_error,
client_error::{Error as ClientError, Result as ClientResult},
http_sender::HttpSender,
request::RpcRequest,
rpc_sender::*,
};
pub type RpcResult<T> = client_error::Result<Response<T>>;
pub struct RpcClient {
sender: Box<dyn RpcSender + Send + Sync + 'static>,
config: RpcClientConfig,
}
impl RpcClient {
pub fn new_sender<T: RpcSender + Send + Sync + 'static>(
sender: T,
config: RpcClientConfig,
) -> Self {
Self {
sender: Box::new(sender),
config,
}
}
pub fn new(url: String) -> Self {
Self::new_with_commitment(url, CommitmentConfig::default())
}
fn new_with_commitment(url: String, commitment_config: CommitmentConfig) -> Self {
Self::new_sender(
HttpSender::new(url),
RpcClientConfig::with_commitment(commitment_config),
)
}
pub fn new_with_timeout(url: String, timeout: Duration) -> Self {
Self::new_sender(
HttpSender::new_with_timeout(url, timeout),
RpcClientConfig::with_commitment(CommitmentConfig::default()),
)
}
pub fn url(&self) -> String {
self.sender.url()
}
pub fn commitment(&self) -> CommitmentConfig {
self.config.commitment_config
}
pub async fn send_bundle(
&self,
transactions: &[impl SerializableTransaction],
) -> ClientResult<String> {
let mut serialized_encoded: Vec<String> = Vec::with_capacity(transactions.len());
for transaction in transactions {
let encoding = self.default_cluster_transaction_encoding().await?;
serialized_encoded.push(serialize_and_encode(transaction, encoding)?);
}
match self
.send(RpcRequest::SendBundle, json!([serialized_encoded]))
.await
{
Ok(signature_base58_str) => ClientResult::Ok(signature_base58_str),
Err(err) => {
if let ClientErrorKind::RpcError(RpcError::RpcResponseError {
code, message, ..
}) = &err.kind
{
debug!("{} {}", code, message);
}
Err(err)
}
}
}
async fn default_cluster_transaction_encoding(
&self,
) -> Result<UiTransactionEncoding, RpcError> {
Ok(UiTransactionEncoding::Base58)
}
pub async fn get_bundle_statuses(
&self,
signatures: &[String],
) -> RpcResult<Vec<serde_json::Value>> {
self.send(RpcRequest::GetBundlesStatuses, json!([signatures]))
.await
}
pub async fn get_tip_accounts(&self) -> ClientResult<Vec<String>> {
self.send(RpcRequest::GetTipAccounts, Value::Null).await
}
pub async fn send<T>(&self, request: RpcRequest, params: Value) -> ClientResult<T>
where
T: serde::de::DeserializeOwned,
{
assert!(params.is_array() || params.is_null());
let response = self
.sender
.send(request, params)
.await
.map_err(|err| err.into_with_request(request))?;
serde_json::from_value(response)
.map_err(|err| ClientError::new_with_request(err.into(), request))
}
pub fn get_transport_stats(&self) -> RpcTransportStats {
self.sender.get_transport_stats()
}
}
fn serialize_and_encode<T>(input: &T, encoding: UiTransactionEncoding) -> ClientResult<String>
where
T: serde::ser::Serialize,
{
let serialized = serialize(input)
.map_err(|e| ClientErrorKind::Custom(format!("Serialization failed: {e}")))?;
let encoded = match encoding {
UiTransactionEncoding::Base58 => bs58::encode(serialized).into_string(),
_ => {
return Err(ClientErrorKind::Custom(format!(
"unsupported encoding: {encoding}. Supported encodings: base58"
))
.into())
}
};
Ok(encoded)
}
#[cfg(test)]
mod rpc_client_tests {
use solana_program::hash::Hash;
use solana_sdk::{
pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, system_transaction,
transaction::VersionedTransaction,
};
use crate::jito::rpc_client::RpcClient;
const SERVER_URL: &str = "http://0.0.0.0:8080/api/v1/bundles";
#[tokio::test]
pub async fn get_tip_accounts() {
let rpc_client = RpcClient::new(SERVER_URL.to_owned());
let tip_accounts = rpc_client.get_tip_accounts().await;
println!("{:?}", tip_accounts);
}
#[tokio::test]
pub async fn send_bundle() {
let rpc_client = RpcClient::new(SERVER_URL.to_owned());
let signer_keypair = Keypair::new();
let recent_blockhash = Hash::new_unique();
let tip_account = Pubkey::try_from("DCN82qDxJAQuSqHhv2BJuAgi41SPeKZB5ioBCTMNDrCC").unwrap();
let mut bundle: Vec<_> = vec![VersionedTransaction::from(system_transaction::transfer(
&signer_keypair,
&signer_keypair.pubkey(),
10000,
recent_blockhash,
))];
bundle.push(VersionedTransaction::from(system_transaction::transfer(
&signer_keypair,
&tip_account,
10000,
recent_blockhash,
)));
let response = rpc_client.send_bundle(&bundle).await;
println!("{:?}", response);
}
#[tokio::test]
pub async fn get_bundle_statuses() {
let rpc_client = RpcClient::new(SERVER_URL.to_owned());
let bundle_id =
"6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_owned();
let response = rpc_client.get_bundle_statuses(&[bundle_id]).await;
println!("{:?}", response);
}
}
-21
View File
@@ -1,21 +0,0 @@
use async_trait::async_trait;
use solana_rpc_client::rpc_sender::RpcTransportStats;
use crate::jito::{client_error::Result, request::RpcRequest};
/// A transport for RPC calls.
///
/// `RpcSender` implements the underlying transport of requests to, and
/// responses from, a Solana node, and is used primarily by [`RpcClient`].
///
/// [`RpcClient`]: crate::rpc_client::RpcClient
#[async_trait]
pub trait RpcSender {
async fn send(
&self,
request: RpcRequest,
params: serde_json::Value,
) -> Result<serde_json::Value>;
fn get_transport_stats(&self) -> RpcTransportStats;
fn url(&self) -> String;
}
+128
View File
@@ -0,0 +1,128 @@
use std::{
sync::Arc,
time::{Duration, Instant},
};
use jito_protos::{
bundle::{
Bundle, BundleResult,
},
convert::proto_packet_from_versioned_tx,
searcher::{
searcher_service_client::SearcherServiceClient, SendBundleRequest, SubscribeBundleResultsRequest,
},
};
use solana_sdk::{
signature::Signature,
transaction::VersionedTransaction,
};
use thiserror::Error;
use tokio::sync::Mutex;
use tonic::{
codec::CompressionEncoding, transport::{self, Channel, Endpoint}, Status
};
use yellowstone_grpc_client::ClientTlsConfig;
use crate::jito::common::poll_transaction_confirmation;
use crate::common::SolanaRpcClient;
#[derive(Debug, Error)]
pub enum BlockEngineConnectionError {
#[error("transport error {0}")]
TransportError(#[from] transport::Error),
#[error("client error {0}")]
ClientError(#[from] Status),
}
#[derive(Debug, Error)]
pub enum BundleRejectionError {
#[error("bundle lost state auction, auction: {0}, tip {1} lamports")]
StateAuctionBidRejected(String, u64),
#[error("bundle won state auction but failed global auction, auction {0}, tip {1} lamports")]
WinningBatchBidRejected(String, u64),
#[error("bundle simulation failure on tx {0}, message: {1:?}")]
SimulationFailure(String, Option<String>),
#[error("internal error {0}")]
InternalError(String),
}
pub type BlockEngineConnectionResult<T> = Result<T, BlockEngineConnectionError>;
pub async fn get_searcher_client_no_auth(
block_engine_url: &str,
) -> BlockEngineConnectionResult<SearcherServiceClient<Channel>> {
let searcher_channel = create_grpc_channel(block_engine_url).await?;
let searcher_client = SearcherServiceClient::new(searcher_channel);
Ok(searcher_client)
}
pub async fn create_grpc_channel(url: &str) -> BlockEngineConnectionResult<Channel> {
let mut endpoint = Endpoint::from_shared(url.to_string()).expect("invalid url");
if url.starts_with("https") {
endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?;
}
endpoint = endpoint.tcp_nodelay(true);
endpoint = endpoint.tcp_keepalive(Some(Duration::from_secs(10)));
endpoint = endpoint.connect_timeout(Duration::from_secs(20));
endpoint = endpoint.http2_keep_alive_interval(Duration::from_secs(10));
Ok(endpoint.connect().await?)
}
pub async fn subscribe_bundle_results(
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
request: impl tonic::IntoRequest<SubscribeBundleResultsRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<BundleResult>>,
tonic::Status,
> {
let mut searcher = searcher_client.lock().await;
searcher.subscribe_bundle_results(request).await
}
pub async fn send_bundle_with_confirmation(
rpc: Arc<SolanaRpcClient>,
transactions: &Vec<VersionedTransaction>,
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
) -> Result<Vec<Signature>, anyhow::Error> {
let mut signatures = send_bundle_no_wait(transactions, searcher_client).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(&rpc, signature).await {
Ok(sig) => signatures.push(sig),
Err(_) => continue,
}
}
}
Ok(signatures)
}
pub async fn send_bundle_no_wait(
transactions: &Vec<VersionedTransaction>,
searcher_client: Arc<Mutex<SearcherServiceClient<Channel>>>,
) -> Result<Vec<Signature>, anyhow::Error> {
let mut packets = vec![];
let mut signatures = vec![];
for transaction in transactions {
let packet = proto_packet_from_versioned_tx(transaction);
packets.push(packet);
signatures.push(transaction.signatures[0]);
}
let mut searcher = searcher_client.lock().await;
searcher
.send_bundle(SendBundleRequest {
bundle: Some(Bundle {
header: None,
packets,
}),
})
.await?;
Ok(signatures)
}
+167
View File
@@ -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<RwLock<String>>,
}
impl ClientInterceptor {
pub async fn new(
mut auth_service_client: AuthServiceClient<Channel>,
keypair: &Arc<Keypair>,
role: Role,
) -> BlockEngineConnectionResult<Self> {
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<Channel>,
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<Channel>,
bearer_token: Arc<RwLock<String>>,
refresh_token: Token,
access_token_expiration: Timestamp,
keypair: Arc<Keypair>,
role: Role,
) -> JoinHandle<BlockEngineConnectionResult<()>> {
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<Request<()>, 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)
}
}
+141 -137
View File
@@ -2,254 +2,257 @@ pub mod accounts;
pub mod constants;
pub mod error;
pub mod instruction;
pub mod jito;
pub mod grpc;
pub mod common;
pub mod ipfs;
pub mod trade;
pub mod jito;
pub mod pumpfun;
use std::sync::Arc;
use anyhow::anyhow;
use solana_client::rpc_client::RpcClient;
use jito::{FeeClient, JitoClient, NextBlockClient, ZeroSlotClient};
use rustls::crypto::{ring::default_provider, CryptoProvider};
use solana_sdk::{
commitment_config::CommitmentConfig,
pubkey::Pubkey,
signature::{Keypair, Signer, Signature},
signature::{Keypair, Signer},
};
use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe};
use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe, Cluster, PriorityFee, SolanaRpcClient};
use common::logs_subscribe::SubscriptionHandle;
use ipfs::TokenMetadataIPFS;
use crate::jito::JitoClient;
use crate::trade::common::PriorityFee;
#[derive(Clone)]
pub struct PumpFun {
pub payer: Arc<Keypair>,
pub rpc: Arc<RpcClient>,
pub jito_client: Arc<JitoClient>,
pub rpc: Arc<SolanaRpcClient>,
pub fee_clients: Vec<Arc<FeeClient>>,
pub priority_fee: PriorityFee,
pub cluster: Cluster,
}
impl Clone for PumpFun {
fn clone(&self) -> Self {
Self {
payer: self.payer.clone(),
rpc: self.rpc.clone(),
fee_clients: self.fee_clients.clone(),
priority_fee: self.priority_fee.clone(),
cluster: self.cluster.clone(),
}
}
}
impl PumpFun {
#[inline]
pub fn new(
pub async fn new(
payer: Arc<Keypair>,
rpc_url: String,
jito_url: String,
commitment: CommitmentConfig,
priority_fee: PriorityFee,
cluster: &Cluster,
) -> Self {
let rpc = Arc::new(RpcClient::new_with_commitment(
rpc_url,
commitment
));
if CryptoProvider::get_default().is_none() {
let _ = default_provider()
.install_default()
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
}
let jito_client = Arc::new(JitoClient::new(&jito_url, None));
let rpc = SolanaRpcClient::new_with_commitment(
cluster.clone().rpc_url,
cluster.clone().commitment
);
let mut fee_clients: Vec<Arc<FeeClient>> = vec![];
if cluster.clone().use_jito {
let jito_client = JitoClient::new(
cluster.clone().rpc_url,
cluster.clone().block_engine_url
).await.expect("Failed to create Jito client");
fee_clients.push(Arc::new(jito_client));
}
if cluster.clone().use_zeroslot {
let zeroslot_client = ZeroSlotClient::new(
cluster.clone().rpc_url,
cluster.clone().zeroslot_url,
cluster.clone().zeroslot_auth_token
);
fee_clients.push(Arc::new(zeroslot_client));
}
if cluster.clone().use_nextblock {
let nextblock_client = NextBlockClient::new(
cluster.clone().rpc_url,
cluster.clone().nextblock_url,
cluster.clone().nextblock_auth_token
);
fee_clients.push(Arc::new(nextblock_client));
}
Self {
payer,
rpc,
jito_client,
priority_fee,
rpc: Arc::new(rpc),
fee_clients,
priority_fee: cluster.clone().priority_fee,
cluster: cluster.clone(),
}
}
/// Create a new token
pub async fn create(
&self,
mint: &Keypair,
mint: Keypair,
ipfs: TokenMetadataIPFS,
) -> Result<Signature, anyhow::Error> {
trade::create::create(
&self.rpc,
&self.payer,
) -> Result<(), anyhow::Error> {
pumpfun::create::create(
self.rpc.clone(),
self.payer.clone(),
mint,
ipfs,
self.priority_fee,
self.priority_fee.clone(),
).await
}
pub async fn create_and_buy(
&self,
mint: &Keypair,
mint: Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<Signature, anyhow::Error> {
trade::create::create_and_buy(
&self.rpc,
&self.payer,
) -> Result<(), anyhow::Error> {
pumpfun::create::create_and_buy(
self.rpc.clone(),
self.payer.clone(),
mint,
ipfs,
amount_sol,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
pub async fn create_and_buy_list_with_jito(
pub async fn create_and_buy_with_tip(
&self,
payers: Vec<&Keypair>,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sols: Vec<u64>,
slippage_basis_points: Option<u64>,
) -> Result<String, anyhow::Error> {
trade::create::create_and_buy_list_with_jito(
&self.rpc,
&self.jito_client,
payers,
mint,
ipfs,
amount_sols,
slippage_basis_points,
self.priority_fee,
).await
}
pub async fn create_and_buy_with_jito(
&self,
payer: &Keypair,
mint: &Keypair,
payer: Arc<Keypair>,
mint: Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<String, anyhow::Error> {
trade::create::create_and_buy_with_jito(
&self.rpc,
&self.jito_client,
) -> Result<(), anyhow::Error> {
pumpfun::create::create_and_buy_with_tip(
self.rpc.clone(),
self.fee_clients.clone(),
payer,
mint,
ipfs,
amount_sol,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
/// Buy tokens
pub async fn buy(
&self,
mint: &Pubkey,
mint: Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<Signature, anyhow::Error> {
trade::buy::buy(
&self.rpc,
&self.payer,
) -> Result<(), anyhow::Error> {
pumpfun::buy::buy(
self.rpc.clone(),
self.payer.clone(),
mint,
amount_sol,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
/// Buy tokens using Jito
pub async fn buy_with_jito(
pub async fn buy_with_tip(
&self,
mint: &Pubkey,
mint: Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<String, anyhow::Error> {
trade::buy::buy_with_jito(
&self.rpc,
&self.jito_client,
&self.payer,
) -> Result<(), anyhow::Error> {
pumpfun::buy::buy_with_tip(
self.rpc.clone(),
self.fee_clients.clone(),
self.payer.clone(),
mint,
amount_sol,
slippage_basis_points,
self.priority_fee,
).await
}
pub async fn buy_list_with_jito(
&self,
payers: Vec<&Keypair>,
mint: &Pubkey,
amount_sols: Vec<u64>,
slippage_basis_points: Option<u64>,
) -> Result<String, anyhow::Error> {
trade::buy::buy_list_with_jito(
&self.rpc,
&self.jito_client,
payers,
mint,
amount_sols,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
/// Sell tokens
pub async fn sell(
&self,
mint: &Pubkey,
mint: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
) -> Result<Signature, anyhow::Error> {
trade::sell::sell(
&self.rpc,
&self.payer,
mint,
) -> Result<(), anyhow::Error> {
pumpfun::sell::sell(
self.rpc.clone(),
self.payer.clone(),
mint.clone(),
amount_token,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
/// Sell tokens by percentage
pub async fn sell_by_percent(
&self,
mint: &Pubkey,
mint: Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
) -> Result<Signature, anyhow::Error> {
trade::sell::sell_by_percent(
&self.rpc,
&self.payer,
mint,
) -> Result<(), anyhow::Error> {
pumpfun::sell::sell_by_percent(
self.rpc.clone(),
self.payer.clone(),
mint.clone(),
percent,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
pub async fn sell_by_percent_with_jito(
&self,
mint: &Pubkey,
mint: Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
) -> Result<String, anyhow::Error> {
trade::sell::sell_by_percent_with_jito(
&self.rpc,
&self.payer,
&self.jito_client,
) -> Result<(), anyhow::Error> {
pumpfun::sell::sell_by_percent_with_jito(
self.rpc.clone(),
self.fee_clients.clone(),
self.payer.clone(),
mint,
percent,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
/// Sell tokens using Jito
pub async fn sell_with_jito(
&self,
mint: &Pubkey,
mint: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
) -> Result<String, anyhow::Error> {
let jito_client = self.jito_client.as_ref();
trade::sell::sell_with_jito(
&self.rpc,
&self.payer,
jito_client,
) -> Result<(), anyhow::Error> {
pumpfun::sell::sell_with_jito(
self.rpc.clone(),
self.fee_clients.clone(),
self.payer.clone(),
mint,
amount_token,
slippage_basis_points,
self.priority_fee,
self.priority_fee.clone(),
).await
}
@@ -273,23 +276,24 @@ impl PumpFun {
}
#[inline]
pub fn get_sol_balance(&self, payer: &Pubkey) -> Result<u64, anyhow::Error> {
trade::common::get_sol_balance(&self.rpc, payer)
pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result<u64, anyhow::Error> {
pumpfun::common::get_sol_balance(&self.rpc, payer).await
}
#[inline]
pub fn get_payer_sol_balance(&self) -> Result<u64, anyhow::Error> {
trade::common::get_sol_balance(&self.rpc, &self.payer.pubkey())
pub async fn get_payer_sol_balance(&self) -> Result<u64, anyhow::Error> {
pumpfun::common::get_sol_balance(&self.rpc, &self.payer.pubkey()).await
}
#[inline]
pub fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result<u64, anyhow::Error> {
trade::common::get_token_balance(&self.rpc, payer, mint)
pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result<u64, anyhow::Error> {
println!("get_token_balance payer: {}, mint: {}, cluster: {}", payer, mint, self.cluster.rpc_url);
pumpfun::common::get_token_balance(&self.rpc, payer, mint).await
}
#[inline]
pub fn get_payer_token_balance(&self, mint: &Pubkey) -> Result<u64, anyhow::Error> {
trade::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint)
pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result<u64, anyhow::Error> {
pumpfun::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint).await
}
#[inline]
@@ -304,16 +308,16 @@ impl PumpFun {
#[inline]
pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 {
trade::common::get_token_price(virtual_sol_reserves, virtual_token_reserves)
pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves)
}
#[inline]
pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 {
trade::common::get_buy_price(amount, trade_info)
pumpfun::common::get_buy_price(amount, trade_info)
}
#[inline]
pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> {
trade::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await
pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await
}
}
+4 -1
View File
@@ -1,4 +1,4 @@
use mai3_pumpfun_sdk::common::{
use pumpfun_sdk::common::{
logs_events::PumpfunEvent,
logs_subscribe::{tokens_subscription, stop_subscription}
};
@@ -16,6 +16,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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);
},
+179
View File
@@ -0,0 +1,179 @@
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}
};
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, trade::DEFAULT_SLIPPAGE}, instruction, jito::FeeClient};
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000;
use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_global_account, get_initial_buy_price};
pub async fn buy(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<(), anyhow::Error> {
let transaction = build_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?;
rpc.send_and_confirm_transaction(&transaction).await?;
Ok(())
}
/// Buy tokens using Jito
pub async fn buy_with_tip(
rpc: Arc<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> 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 mut handles: Vec<JoinHandle<Result<(), anyhow::Error>>> = 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 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());
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_buy_transaction(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let mut instructions = vec![
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
];
let build_instructions = build_buy_instructions(rpc.clone(), payer.clone(), Arc::new(mint), amount_sol, slippage_basis_points).await?;
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,
);
Ok(transaction)
}
pub async fn build_buy_transaction_with_tip(
tip_account: Arc<Pubkey>,
payer: Arc<Keypair>,
priority_fee: PriorityFee,
build_instructions: Vec<Instruction>,
blockhash: Hash,
) -> Result<VersionedTransaction, anyhow::Error> {
let mut instructions = vec![
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT),
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
system_instruction::transfer(
&payer.pubkey(),
&tip_account,
sol_to_lamports(priority_fee.buy_tip_fee),
),
];
instructions.extend(build_instructions);
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])?;
Ok(transaction)
}
pub async fn build_buy_instructions(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Arc<Pubkey>,
amount_sol: u64,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let rpc = rpc.as_ref();
let global_account = get_global_account(rpc).await?;
let buy_amount = match get_bonding_curve_account(rpc, mint.as_ref()).await {
Ok(account) => account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))?,
Err(_e) => {
println!("Bonding curve account not found, using initial buy price: {}", _e);
let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?;
initial_buy_amount * 80 / 100
}
};
let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
let mut instructions = vec![];
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint,
&constants::accounts::TOKEN_PROGRAM,
));
instructions.push(instruction::buy(
payer.as_ref(),
&mint,
&global_account.fee_recipient,
instruction::Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
Ok(instructions)
}
+56 -52
View File
@@ -1,45 +1,24 @@
use anyhow::anyhow;
use serde::Deserialize;
use spl_token::state::Account;
use tokio::sync::RwLock;
use std::{collections::HashMap, sync::Arc};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction
};
use spl_associated_token_account::get_associated_token_address;
use crate::{accounts, common::logs_data::TradeInfo, constants::{self, trade::{DEFAULT_BUY_JITO_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_JITO_FEE, DEFAULT_SLIPPAGE}}};
use crate::{accounts, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, trade::DEFAULT_SLIPPAGE}};
use borsh::BorshDeserialize;
lazy_static::lazy_static! {
static ref ACCOUNT_CACHE: RwLock<HashMap<Pubkey, Arc<accounts::GlobalAccount>>> = RwLock::new(HashMap::new());
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq)]
pub struct PriorityFee {
pub unit_limit: u32,
pub unit_price: u64,
pub buy_jito_fee: f64,
pub sell_jito_fee: f64,
}
impl Default for PriorityFee {
fn default() -> Self {
Self {
unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT,
unit_price: DEFAULT_COMPUTE_UNIT_PRICE,
buy_jito_fee: DEFAULT_BUY_JITO_FEE,
sell_jito_fee: DEFAULT_SELL_JITO_FEE
}
}
}
pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> {
pub async fn transfer_sol(rpc: &SolanaRpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> {
if amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
return Err(anyhow!("transfer_sol: Amount cannot be zero"));
}
let balance = get_sol_balance(rpc, &payer.pubkey())?;
let balance = get_sol_balance(rpc, &payer.pubkey()).await?;
if balance < amount {
return Err(anyhow!("Insufficient balance"));
}
@@ -50,7 +29,7 @@ pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pub
amount,
);
let recent_blockhash = rpc.get_latest_blockhash()?;
let recent_blockhash = rpc.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[transfer_instruction],
@@ -59,7 +38,7 @@ pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pub
recent_blockhash,
);
rpc.send_and_confirm_transaction(&transaction)?;
rpc.send_and_confirm_transaction(&transaction).await?;
Ok(())
}
@@ -73,19 +52,46 @@ pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec<Instru
instructions
}
pub fn get_token_balance(rpc: &RpcClient, account: &Pubkey, mint: &Pubkey) -> Result<u64, anyhow::Error> {
let ata = get_associated_token_address(account, mint);
if rpc.get_account(&ata).is_err() {
return Ok(0);
}
// #[inline]
pub async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Pubkey, mint: &Pubkey) -> Result<u64, anyhow::Error> {
let ata = get_associated_token_address(payer, mint);
// let account_data = rpc.get_account_data(&ata).await?;
// let token_account = Account::unpack(&account_data.as_slice())?;
let balance = rpc.get_token_account_balance(&ata)?;
balance.amount.parse::<u64>()
.map_err(|_| anyhow!("Failed to parse token balance"))
// Ok(token_account.amount)
// println!("get_token_balance ata: {}", ata);
let balance = rpc.get_token_account_balance(&ata).await?;
let balance_u64 = balance.amount.parse::<u64>()
.map_err(|_| anyhow!("Failed to parse token balance"))?;
Ok(balance_u64)
}
pub fn get_sol_balance(rpc: &RpcClient, account: &Pubkey) -> Result<u64, anyhow::Error> {
rpc.get_balance(account).map_err(|_| anyhow!("Failed to get SOL balance"))
#[inline]
pub async fn get_token_balance_and_ata(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> {
let ata = get_associated_token_address(&payer.pubkey(), mint);
// let account_data = rpc.get_account_data(&ata).await?;
// let token_account = Account::unpack(&account_data)?;
// Ok((token_account.amount, ata))
let balance = rpc.get_token_account_balance(&ata).await?;
let balance_u64 = balance.amount.parse::<u64>()
.map_err(|_| anyhow!("Failed to parse token balance"))?;
if balance_u64 == 0 {
return Err(anyhow!("Balance is 0"));
}
Ok((balance_u64, ata))
}
#[inline]
pub async fn get_sol_balance(rpc: &SolanaRpcClient, account: &Pubkey) -> Result<u64, anyhow::Error> {
println!("get_sol_balance account: {}", account);
let balance = rpc.get_balance(account).await?;
println!("get_sol_balance balance: {}", balance);
Ok(balance)
}
#[inline]
@@ -125,21 +131,17 @@ pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey {
}
#[inline]
pub async fn get_global_account(rpc: &RpcClient) -> Result<Arc<accounts::GlobalAccount>, anyhow::Error> {
pub async fn get_global_account(rpc: &SolanaRpcClient) -> Result<Arc<accounts::GlobalAccount>, anyhow::Error> {
let global = get_global_pda();
// Try cache first
if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) {
return Ok(account.clone());
}
// Cache miss, fetch from RPC
let account = rpc.get_account(&global)?;
let global_account = Arc::new(accounts::GlobalAccount::try_from_slice(&account.data)?);
// Update cache
let account = rpc.get_account(&global).await?;
let global_account = bincode::deserialize::<accounts::GlobalAccount>(&account.data)?;
let global_account = Arc::new(global_account);
ACCOUNT_CACHE.write().await.insert(global, global_account.clone());
Ok(global_account)
}
@@ -151,17 +153,17 @@ pub async fn get_initial_buy_price(global_account: &Arc<accounts::GlobalAccount>
#[inline]
pub async fn get_bonding_curve_account(
rpc: &RpcClient,
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Arc<accounts::BondingCurveAccount>, anyhow::Error> {
let bonding_curve_pda = get_bonding_curve_pda(mint)
.ok_or(anyhow!("Bonding curve not found"))?;
if rpc.get_account(&bonding_curve_pda).is_err() {
let account = rpc.get_account(&bonding_curve_pda).await?;
if account.data.is_empty() {
return Err(anyhow!("Bonding curve not found"));
}
let account = rpc.get_account(&bonding_curve_pda)?;
let bonding_curve = Arc::new(accounts::BondingCurveAccount::try_from_slice(&account.data)?);
Ok(bonding_curve)
}
@@ -172,12 +174,14 @@ pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Opti
amount_sol + (amount_sol * slippage / 10000)
}
#[inline]
pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 {
let v_sol = virtual_sol_reserves as f64 / 100_000_000.0;
let v_tokens = virtual_token_reserves as f64 / 100_000.0;
v_sol / v_tokens
}
#[inline]
pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 {
if amount == 0 {
return 0;
+322
View File
@@ -0,0 +1,322 @@
use std::{str::FromStr, time::Instant, sync::Arc};
use anyhow::anyhow;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_sdk::{
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, 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, jito::FeeClient,
pumpfun::buy::build_buy_transaction_with_tip
};
use crate::pumpfun::common::{
create_priority_fee_instructions,
get_buy_amount_with_slippage, get_global_account
};
/// Create a new token
pub async fn create(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
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,
},
));
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<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<(), anyhow::Error> {
if amount_sol == 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, amount_sol, slippage_basis_points, priority_fee.clone()).await?;
rpc.send_and_confirm_transaction(&transaction).await?;
Ok(())
}
pub async fn create_and_buy_with_tip(
rpc: Arc<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> 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(), amount_sol, slippage_basis_points, priority_fee.clone()).await?;
let mut handles = vec![];
for fee_client in fee_clients {
let rpc = rpc.clone();
let payer = payer.clone();
let mint = mint.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 build_instructions = build_instructions.clone();
let handle = tokio::spawn(async move {
let transaction = build_create_and_buy_transaction_with_tip(rpc, tip_account, payer, mint, priority_fee, build_instructions).await?;
fee_client.send_transaction(&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<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Arc<Keypair>,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
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, amount_sol, slippage_basis_points, priority_fee.clone()).await?;
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(), mint.as_ref()],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_create_and_buy_transaction_with_tip(
rpc: Arc<SolanaRpcClient>,
tip_account: Arc<Pubkey>,
payer: Arc<Keypair>,
mint: Arc<Keypair>,
priority_fee: PriorityFee,
build_instructions: Vec<Instruction>,
) -> Result<VersionedTransaction, anyhow::Error> {
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(priority_fee.buy_tip_fee),
),
];
instructions.extend(build_instructions);
let recent_blockhash = rpc.get_latest_blockhash().await?;
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<SolanaRpcClient>,
// payer: Arc<Keypair>,
// mint: Arc<Keypair>,
// ipfs: TokenMetadataIPFS,
// amount_sol: u64,
// slippage_basis_points: Option<u64>,
// priority_fee: PriorityFee,
// ) -> Result<Vec<Instruction>, anyhow::Error> {
// if amount_sol == 0 {
// return Err(anyhow!("Amount cannot be zero"));
// }
// let rpc = rpc.as_ref();
// let global_account = get_global_account(rpc).await?;
// let buy_amount = global_account.get_initial_buy_price(amount_sol);
// let buy_amount_with_slippage =
// get_buy_amount_with_slippage(amount_sol, slippage_basis_points);
// let mut instructions = vec![
// ComputeBudgetInstruction::set_compute_unit_limit(1_400_000),
// ComputeBudgetInstruction::set_compute_unit_price(0),
// ];
// instructions.push(instruction::create(
// payer.as_ref(),
// mint.as_ref(),
// instruction::Create {
// _name: ipfs.metadata.name,
// _symbol: ipfs.metadata.symbol,
// _uri: ipfs.metadata_uri,
// },
// ));
// 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(),
// &global_account.fee_recipient,
// instruction::Buy {
// _amount: buy_amount,
// _max_sol_cost: buy_amount_with_slippage,
// },
// ));
// let commitment_config = CommitmentConfig::confirmed();
// let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config).await?.0;
// let simulate_tx = Transaction::new_signed_with_payer(
// &instructions,
// Some(&payer.pubkey()),
// &[payer.as_ref(), mint.as_ref()],
// recent_blockhash,
// );
// let config = RpcSimulateTransactionConfig {
// sig_verify: true,
// commitment: Some(commitment_config),
// ..RpcSimulateTransactionConfig::default()
// };
// let result = rpc.simulate_transaction_with_config(&simulate_tx, config).await?.value;
// if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) {
// return Err(anyhow!("Simulation failed: {:?}", result.err));
// }
// let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?;
// let fees = rpc.get_recent_prioritization_fees(&[]).await?;
// let average_fees = if fees.is_empty() {
// priority_fee.unit_price
// } else {
// fees.iter()
// .map(|fee| fee.prioritization_fee)
// .sum::<u64>() / fees.len() as u64
// };
// let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees };
// instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32);
// instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price);
// Ok(instructions)
// }
pub async fn build_create_and_buy_instructions(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Arc<Keypair>,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let rpc = rpc.as_ref();
let global_account = get_global_account(rpc).await?;
let buy_amount = global_account.get_initial_buy_price(amount_sol);
let buy_amount_with_slippage =
get_buy_amount_with_slippage(amount_sol, slippage_basis_points);
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
];
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(),
},
));
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(),
&global_account.fee_recipient,
instruction::Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
Ok(instructions)
}
+4
View File
@@ -0,0 +1,4 @@
pub mod buy;
pub mod create;
pub mod sell;
pub mod common;
+229
View File
@@ -0,0 +1,229 @@
use anyhow::anyhow;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_sdk::{
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::{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, time::Instant, sync::Arc};
use crate::{common::{PriorityFee, SolanaRpcClient}, constants::trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE}, instruction, jito::FeeClient};
use super::common::{calculate_with_slippage_sell, get_bonding_curve_account, 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::<u64>()
.map_err(|_| anyhow!("Failed to parse token balance"))?;
if balance_u64 == 0 {
return Err(anyhow!("Balance is 0"));
}
Ok((balance_u64, ata))
}
pub async fn sell(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<(), anyhow::Error> {
let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?;
let transaction = build_sell_transaction(rpc.clone(), payer.clone(), priority_fee, instructions).await?;
rpc.send_and_confirm_transaction(&transaction).await?;
Ok(())
}
/// Sell tokens by percentage
pub async fn sell_by_percent(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<(), anyhow::Error> {
if percent == 0 || percent > 100 {
return Err(anyhow!("Percentage must be between 1 and 100"));
}
let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
let amount = balance_u64 * percent / 100;
sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee).await
}
pub async fn sell_by_percent_with_jito(
rpc: Arc<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> 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_jito(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee).await
}
/// Sell tokens using Jito
pub async fn sell_with_jito(
rpc: Arc<SolanaRpcClient>,
fee_clients: Vec<Arc<FeeClient>>,
payer: Arc<Keypair>,
mint: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<(), anyhow::Error> {
let start_time = Instant::now();
let mut transactions = vec![];
let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?;
let recent_blockhash = rpc.get_latest_blockhash().await?;
for fee_client in fee_clients.clone() {
let 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<Result<(), anyhow::Error>> = 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(())
});
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),
}
}
println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis());
Ok(())
}
pub async fn build_sell_transaction(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
priority_fee: PriorityFee,
build_instructions: Vec<Instruction>
) -> Result<Transaction, anyhow::Error> {
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);
let recent_blockhash = rpc.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer.as_ref()],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_sell_transaction_with_tip(
tip_account: Arc<Pubkey>,
payer: Arc<Keypair>,
priority_fee: PriorityFee,
build_instructions: Vec<Instruction>,
blockhash: Hash,
) -> Result<VersionedTransaction, anyhow::Error> {
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(priority_fee.sell_tip_fee),
),
];
instructions.extend(build_instructions);
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])?;
Ok(transaction)
}
pub async fn build_sell_instructions(
rpc: Arc<SolanaRpcClient>,
payer: Arc<Keypair>,
mint: Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
) -> Result<Vec<Instruction>, anyhow::Error> {
let (balance_u64, ata) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?;
let amount = amount_token.unwrap_or(balance_u64);
if amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc.as_ref()).await?;
let bonding_curve_account = get_bonding_curve_account(rpc.as_ref(), &mint).await?;
let min_sol_output = bonding_curve_account
.get_sell_price(amount, global_account.fee_basis_points)
.map_err(|e| anyhow!(e))?;
let min_sol_output_with_slippage = calculate_with_slippage_sell(
min_sol_output,
slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let instructions = vec![
instruction::sell(
payer.as_ref(),
&mint,
&global_account.fee_recipient,
instruction::Sell {
_amount: amount,
_min_sol_output: min_sol_output_with_slippage,
},
),
close_account(
&spl_token::ID,
&ata,
&payer.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
)?
];
Ok(instructions)
}
-263
View File
@@ -1,263 +0,0 @@
use anyhow::anyhow;
use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
use solana_sdk::{
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction
};
use spl_associated_token_account::{
get_associated_token_address,
instruction::create_associated_token_account,
};
use std::time::Instant;
use crate::{constants::{self, trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE, JITO_TIP_AMOUNT}}, instruction, jito::JitoClient};
use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_global_account, get_initial_buy_price, PriorityFee};
pub async fn buy(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Signature, anyhow::Error> {
let transaction = build_buy_transaction(rpc, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?;
let signature = rpc.send_transaction(&transaction)?;
Ok(signature)
}
/// Buy tokens using Jito
pub async fn buy_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<String, anyhow::Error> {
let start_time = Instant::now();
let transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?;
let signature = jito_client.send_transaction(&transaction).await?;
println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis());
Ok(signature)
}
pub async fn buy_list_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payers: Vec<&Keypair>,
mint: &Pubkey,
amount_sols: Vec<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<String, anyhow::Error> {
let start_time = Instant::now();
let mut transactions = vec![];
for (i, payer) in payers.iter().enumerate() {
let transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, mint, amount_sols[i], slippage_basis_points, priority_fee).await?;
transactions.push(transaction);
}
let signature = jito_client.send_transactions(&transactions).await?;
println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis());
Ok(signature)
}
pub async fn build_buy_transaction(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let instructions = build_buy_instructions(rpc, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?;
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_buy_transaction_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let instructions = build_buy_instructions_with_jito(rpc, jito_client, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?;
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_buy_instructions(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc).await?;
let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?;
let buy_amount = bonding_curve_account
.get_buy_price(amount_sol)
.map_err(|e| anyhow!(e))?;
let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_limit(1_400_000),
ComputeBudgetInstruction::set_compute_unit_price(0),
];
let ata = get_associated_token_address(&payer.pubkey(), mint);
if rpc.get_account(&ata).is_err() {
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
mint,
&constants::accounts::TOKEN_PROGRAM,
));
}
instructions.push(instruction::buy(
payer,
mint,
&global_account.fee_recipient,
instruction::Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
let commitment_config = CommitmentConfig::confirmed();
let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)?
.0;
let simulate_tx = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);
let config = RpcSimulateTransactionConfig {
sig_verify: true,
commitment: Some(commitment_config),
..RpcSimulateTransactionConfig::default()
};
let result = rpc.simulate_transaction_with_config(&simulate_tx, config)?
.value;
if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) {
return Err(anyhow!("Simulation failed: {:?}", result.err));
}
let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?;
let fees = rpc.get_recent_prioritization_fees(&[])?;
let average_fees = if fees.is_empty() {
DEFAULT_COMPUTE_UNIT_PRICE
} else {
fees.iter()
.map(|fee| fee.prioritization_fee)
.sum::<u64>() / fees.len() as u64
};
let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees };
instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32);
instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price);
Ok(instructions)
}
pub async fn build_buy_instructions_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Pubkey,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc).await?;
let buy_amount = match get_bonding_curve_account(rpc, mint).await {
Ok(account) => account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))?,
Err(_e) => {
let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?;
initial_buy_amount * 80 / 100
}
};
let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
];
let ata = get_associated_token_address(&payer.pubkey(), mint);
if rpc.get_account(&ata).is_err() {
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
mint,
&constants::accounts::TOKEN_PROGRAM,
));
}
instructions.push(instruction::buy(
payer,
mint,
&global_account.fee_recipient,
instruction::Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?;
let jito_fee = priority_fee.buy_jito_fee;
instructions.push(
system_instruction::transfer(
&payer.pubkey(),
&tip_account,
sol_to_lamports(jito_fee),
),
);
Ok(instructions)
}
-326
View File
@@ -1,326 +0,0 @@
use std::time::Instant;
use anyhow::anyhow;
use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
use solana_sdk::{
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction
};
use spl_associated_token_account::{
get_associated_token_address,
instruction::create_associated_token_account,
};
use crate::{constants, instruction, ipfs::TokenMetadataIPFS, jito::JitoClient, trade::buy::build_buy_transaction_with_jito};
use super::common::{create_priority_fee_instructions, get_buy_amount_with_slippage, get_global_account, PriorityFee};
/// Create a new token
pub async fn create(
rpc: &RpcClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
priority_fee: PriorityFee,
) -> Result<Signature, anyhow::Error> {
let mut instructions = create_priority_fee_instructions(priority_fee);
instructions.push(instruction::create(
payer,
mint,
instruction::Create {
_name: ipfs.metadata.name,
_symbol: ipfs.metadata.symbol,
_uri: ipfs.metadata_uri,
},
));
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer, mint],
recent_blockhash,
);
let signature = rpc.send_and_confirm_transaction(&transaction)?;
Ok(signature)
}
/// Create and buy tokens in one transaction
pub async fn create_and_buy(
rpc: &RpcClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Signature, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let transaction = build_create_and_buy_transaction(rpc, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?;
let signature = rpc.send_and_confirm_transaction(&transaction)?;
Ok(signature)
}
pub async fn create_and_buy_list_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payers: Vec<&Keypair>,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sols: Vec<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<String, anyhow::Error> {
let start_time = Instant::now();
let mut transactions = Vec::new();
let transaction = build_create_and_buy_transaction_with_jito(rpc, jito_client, payers[0], mint, ipfs, amount_sols[0], slippage_basis_points, priority_fee).await?;
transactions.push(transaction);
for (i, payer) in payers.iter().skip(1).enumerate() {
println!("Creating and buying token index: {}", i);
let buy_transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, &mint.pubkey(), amount_sols[i], slippage_basis_points, priority_fee).await?;
transactions.push(buy_transaction);
}
let signatures = jito_client.send_transactions(&transactions).await?;
println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis());
Ok(signatures)
}
pub async fn create_and_buy_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<String, anyhow::Error> {
let start_time = Instant::now();
let transaction = build_create_and_buy_transaction_with_jito(rpc, jito_client, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?;
let signature = jito_client.send_transaction(&transaction).await?;
println!("Total Jito create and buy operation time: {:?}ms, signature: {}", start_time.elapsed().as_millis(), signature);
Ok(signature)
}
pub async fn build_create_and_buy_transaction(
rpc: &RpcClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let instructions = build_create_and_buy_instructions(rpc, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?;
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer, mint],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_create_and_buy_transaction_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let instructions = build_create_and_buy_instructions_with_jito(rpc, jito_client, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?;
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer, mint],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_create_and_buy_instructions(
rpc: &RpcClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc).await?;
let buy_amount = global_account.get_initial_buy_price(amount_sol);
let buy_amount_with_slippage =
get_buy_amount_with_slippage(amount_sol, slippage_basis_points);
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_limit(1_400_000),
ComputeBudgetInstruction::set_compute_unit_price(0),
];
instructions.push(instruction::create(
payer,
mint,
instruction::Create {
_name: ipfs.metadata.name,
_symbol: ipfs.metadata.symbol,
_uri: ipfs.metadata_uri,
},
));
let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey());
if rpc.get_account(&ata).is_err() {
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&constants::accounts::TOKEN_PROGRAM,
));
}
instructions.push(instruction::buy(
payer,
&mint.pubkey(),
&global_account.fee_recipient,
instruction::Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
let commitment_config = CommitmentConfig::confirmed();
let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)?
.0;
let simulate_tx = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer, mint],
recent_blockhash,
);
let config = RpcSimulateTransactionConfig {
sig_verify: true,
commitment: Some(commitment_config),
..RpcSimulateTransactionConfig::default()
};
let result = rpc.simulate_transaction_with_config(&simulate_tx, config)?
.value;
if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) {
return Err(anyhow!("Simulation failed: {:?}", result.err));
}
let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?;
let fees = rpc.get_recent_prioritization_fees(&[])?;
let average_fees = if fees.is_empty() {
priority_fee.unit_price
} else {
fees.iter()
.map(|fee| fee.prioritization_fee)
.sum::<u64>() / fees.len() as u64
};
let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees };
instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32);
instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price);
Ok(instructions)
}
pub async fn build_create_and_buy_instructions_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Keypair,
ipfs: TokenMetadataIPFS,
amount_sol: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
if amount_sol == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc).await?;
let buy_amount = global_account.get_initial_buy_price(amount_sol);
let buy_amount_with_slippage =
get_buy_amount_with_slippage(amount_sol, slippage_basis_points);
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
];
instructions.push(instruction::create(
payer,
mint,
instruction::Create {
_name: ipfs.metadata.name,
_symbol: ipfs.metadata.symbol,
_uri: ipfs.metadata_uri,
},
));
let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey());
if rpc.get_account(&ata).is_err() {
instructions.push(create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&constants::accounts::TOKEN_PROGRAM,
));
}
instructions.push(instruction::buy(
payer,
&mint.pubkey(),
&global_account.fee_recipient,
instruction::Buy {
_amount: buy_amount,
_max_sol_cost: buy_amount_with_slippage,
},
));
let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?;
let jito_fee = priority_fee.buy_jito_fee;
instructions.push(
system_instruction::transfer(
&payer.pubkey(),
&tip_account,
sol_to_lamports(jito_fee * 2.0),
),
);
Ok(instructions)
}
-4
View File
@@ -1,4 +0,0 @@
pub mod buy;
pub mod create;
pub mod sell;
pub mod common;
-289
View File
@@ -1,289 +0,0 @@
use anyhow::anyhow;
use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
use solana_sdk::{
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction
};
use spl_associated_token_account::get_associated_token_address;
use spl_token::instruction::close_account;
use std::time::Instant;
use crate::{constants::trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE}, instruction, jito::JitoClient};
use super::common::{calculate_with_slippage_sell, get_bonding_curve_account, get_global_account, PriorityFee};
async fn get_token_balance(rpc: &RpcClient, 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)?;
let balance_u64 = balance.amount.parse::<u64>()
.map_err(|_| anyhow!("Failed to parse token balance"))?;
if balance_u64 == 0 {
return Err(anyhow!("Balance is 0"));
}
Ok((balance_u64, ata))
}
pub async fn sell(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Signature, anyhow::Error> {
let transaction = build_sell_transaction(rpc, payer, mint, amount_token, slippage_basis_points, priority_fee).await?;
let signature = rpc.send_and_confirm_transaction(&transaction)?;
Ok(signature)
}
/// Sell tokens by percentage
pub async fn sell_by_percent(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Signature, anyhow::Error> {
if percent == 0 || percent > 100 {
return Err(anyhow!("Percentage must be between 1 and 100"));
}
let (balance_u64, _) = get_token_balance(rpc, payer, mint).await?;
let amount = balance_u64 * percent / 100;
sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee).await
}
pub async fn sell_by_percent_with_jito(
rpc: &RpcClient,
payer: &Keypair,
jito_client: &JitoClient,
mint: &Pubkey,
percent: u64,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<String, anyhow::Error> {
if percent == 0 || percent > 100 {
return Err(anyhow!("Percentage must be between 1 and 100"));
}
let (balance_u64, _) = get_token_balance(rpc, payer, mint).await?;
let amount = balance_u64 * percent / 100;
sell_with_jito(rpc, payer, jito_client, mint, Some(amount), slippage_basis_points, priority_fee).await
}
/// Sell tokens using Jito
pub async fn sell_with_jito(
rpc: &RpcClient,
payer: &Keypair,
jito_client: &JitoClient,
mint: &Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<String, anyhow::Error> {
let start_time = Instant::now();
let transaction = build_sell_transaction_with_jito(rpc, jito_client, payer, mint, amount_token, slippage_basis_points, priority_fee).await?;
let signature = jito_client.send_transaction(&transaction).await?;
println!("Total Jito sell operation time: {:?}ms, signature: {}", start_time.elapsed().as_millis(), signature);
Ok(signature)
}
pub async fn build_sell_transaction(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let instructions = build_sell_instructions(rpc, payer, mint, amount_token, slippage_basis_points, priority_fee).await?;
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_sell_transaction_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Transaction, anyhow::Error> {
let instructions = build_sell_instructions_with_jito(rpc, jito_client, payer, mint, amount_token, slippage_basis_points, priority_fee).await?;
let recent_blockhash = rpc.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);
Ok(transaction)
}
pub async fn build_sell_instructions(
rpc: &RpcClient,
payer: &Keypair,
mint: &Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
let (balance_u64, ata) = get_token_balance(rpc, payer, mint).await?;
let amount = amount_token.unwrap_or(balance_u64);
if amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc).await?;
let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?;
let min_sol_output = bonding_curve_account
.get_sell_price(amount, global_account.fee_basis_points)
.map_err(|e| anyhow!(e))?;
let min_sol_output_with_slippage = calculate_with_slippage_sell(
min_sol_output,
slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_limit(1_400_000),
ComputeBudgetInstruction::set_compute_unit_price(0),
];
instructions.push(instruction::sell(
payer,
mint,
&global_account.fee_recipient,
instruction::Sell {
_amount: amount,
_min_sol_output: min_sol_output_with_slippage,
},
));
instructions.push(close_account(
&spl_token::ID,
&ata,
&payer.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
)?);
let commitment_config = CommitmentConfig::confirmed();
let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)?
.0;
let simulate_tx = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);
let config = RpcSimulateTransactionConfig {
sig_verify: true,
commitment: Some(commitment_config),
..RpcSimulateTransactionConfig::default()
};
let result = rpc.simulate_transaction_with_config(&simulate_tx, config)?
.value;
if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) {
return Err(anyhow!("Simulation failed: {:?}", result.err));
}
let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?;
let fees = rpc.get_recent_prioritization_fees(&[])?;
let average_fees = if fees.is_empty() {
DEFAULT_COMPUTE_UNIT_PRICE
} else {
fees.iter()
.map(|fee| fee.prioritization_fee)
.sum::<u64>() / fees.len() as u64
};
let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees };
instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32);
instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price);
Ok(instructions)
}
pub async fn build_sell_instructions_with_jito(
rpc: &RpcClient,
jito_client: &JitoClient,
payer: &Keypair,
mint: &Pubkey,
amount_token: Option<u64>,
slippage_basis_points: Option<u64>,
priority_fee: PriorityFee,
) -> Result<Vec<Instruction>, anyhow::Error> {
let (balance_u64, ata) = get_token_balance(rpc, payer, mint).await?;
let amount = amount_token.unwrap_or(balance_u64);
if amount == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
let global_account = get_global_account(rpc).await?;
let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?;
let min_sol_output = bonding_curve_account
.get_sell_price(amount, global_account.fee_basis_points)
.map_err(|e| anyhow!(e))?;
let min_sol_output_with_slippage = calculate_with_slippage_sell(
min_sol_output,
slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let mut instructions = vec![
ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price),
ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit),
];
instructions.push(instruction::sell(
payer,
mint,
&global_account.fee_recipient,
instruction::Sell {
_amount: amount,
_min_sol_output: min_sol_output_with_slippage,
},
));
instructions.push(close_account(
&spl_token::ID,
&ata,
&payer.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
)?);
let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?;
let jito_fee = priority_fee.sell_jito_fee;
instructions.push(
system_instruction::transfer(
&payer.pubkey(),
&tip_account,
sol_to_lamports(jito_fee),
),
);
Ok(instructions)
}