mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-16 18:38:05 +00:00
add nextblock, 0slot support
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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::*;
|
||||
|
||||
Executable
+93
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
Executable
+127
@@ -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))
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+128
@@ -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)
|
||||
}
|
||||
Executable
+167
@@ -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
@@ -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
@@ -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);
|
||||
},
|
||||
|
||||
Executable
+179
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
Executable
+322
@@ -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)
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
pub mod buy;
|
||||
pub mod create;
|
||||
pub mod sell;
|
||||
pub mod common;
|
||||
Executable
+229
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod buy;
|
||||
pub mod create;
|
||||
pub mod sell;
|
||||
pub mod common;
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user