use crate::constants::{
BUYBACK_FEE_RECIPIENTS, FEE_PROGRAM, PROTOCOL_FEE_RECIPIENTS, PUMP_SWAP_PROGRAM_ID,
};
use crate::state::{Pool, PoolInfo};
use anyhow::{Result, anyhow};
use base64::Engine as _;
use base64::engine::general_purpose;
use bytemuck::from_bytes;
use jito_sdk_rust::JitoJsonRpcSDK;
use log::info;
use rand::RngCore;
use rand::seq::IndexedRandom;
use serde_json::json;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::bs58;
use solana_sdk::instruction::Instruction;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use solana_sdk::transaction::Transaction;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Mutex;
use tokio::time::{Duration, Instant};
pub struct JitoPool {
clients: Vec<Arc<JitoJsonRpcSDK>>,
last: Mutex<Vec<Instant>>,
interval: Duration,
rr: AtomicUsize,
}
impl JitoPool {
pub fn new(endpoints: &[&str], uuid: Option<String>, interval: Duration) -> Result<Self> {
if endpoints.is_empty() {
anyhow::bail!("JitoPool requires at least one endpoint");
}
let clients = endpoints
.iter()
.map(|b| Arc::new(JitoJsonRpcSDK::new(&format!("{}/api/v1", b), uuid.clone())))
.collect::<Vec<_>>();
let last = Mutex::new(vec![Instant::now(); clients.len()]);
Ok(Self {
clients,
last,
interval,
rr: AtomicUsize::new(0),
})
}
pub async fn next_client(&self) -> Arc<JitoJsonRpcSDK> {
let i = self.rr.fetch_add(1, Ordering::Relaxed) % self.clients.len();
loop {
let wait_opt = {
let mut last = self.last.lock().await;
let now = Instant::now();
if now >= last[i] {
last[i] = now + self.interval;
None
} else {
Some(last[i] - now)
}
};
match wait_opt {
Some(wait) => tokio::time::sleep(wait).await,
None => break,
}
}
Arc::clone(&self.clients[i])
}
}
pub async fn load_pool(pool_pubkey: &Pubkey, rpc: &RpcClient) -> Result<PoolInfo> {
load_pool_with_token_program(pool_pubkey, rpc, None).await
}
pub async fn load_pool_with_token_program(
pool_pubkey: &Pubkey,
rpc: &RpcClient,
token_program: Option<Pubkey>,
) -> Result<PoolInfo> {
let data = rpc.get_account_data(pool_pubkey).await?;
let pool_size = size_of::<Pool>();
if data.len() < pool_size + 8 {
anyhow::bail!(
"Data size mismatch: expected at least {}, got {}",
pool_size + 8,
data.len()
);
}
let pool_data = *from_bytes::<Pool>(&data[8..pool_size + 8]);
let (base_token_program, quote_token_program) = match token_program {
Some(p) => (p, p),
None => {
let mint_accounts = rpc
.get_multiple_accounts(&[pool_data.base_mint, pool_data.quote_mint])
.await?;
let base = mint_accounts[0]
.as_ref()
.map(|a| a.owner)
.unwrap_or(spl_token::ID);
let quote = mint_accounts[1]
.as_ref()
.map(|a| a.owner)
.unwrap_or(spl_token::ID);
(base, quote)
}
};
Ok(PoolInfo {
pool: *pool_pubkey,
base_mint: pool_data.base_mint,
quote_mint: pool_data.quote_mint,
lp_mint: pool_data.lp_mint,
pool_base_token_account: pool_data.pool_base_token_account,
pool_quote_token_account: pool_data.pool_quote_token_account,
creator: pool_data.creator,
coin_creator: pool_data.coin_creator,
is_cashback_coin: pool_data.is_cashback_coin != 0,
base_token_program,
quote_token_program,
})
}
pub fn create_ata_token_or_not(
payer: &Pubkey,
mint: &Pubkey,
owner: &Pubkey,
create: bool,
) -> (Pubkey, Option<Instruction>) {
create_ata_token_or_not_with_program(payer, mint, owner, &spl_token::ID, create)
}
pub fn create_ata_token_or_not_with_program(
payer: &Pubkey,
mint: &Pubkey,
owner: &Pubkey,
token_program: &Pubkey,
create: bool,
) -> (Pubkey, Option<Instruction>) {
let token_acc = spl_associated_token_account::get_associated_token_address_with_program_id(
owner,
mint,
token_program,
);
if !create {
return (token_acc, None);
}
(
token_acc,
Some(
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
payer,
owner,
mint,
token_program,
),
),
)
}
pub fn gen_pubkey_with_seed(from_public_key: &Pubkey) -> Result<(Pubkey, String)> {
let mut rng = rand::rng();
let mut seed_bytes = [0u8; 32];
rng.fill_bytes(&mut seed_bytes);
let seed = bs58::encode(seed_bytes)
.into_string()
.chars()
.take(32)
.collect::<String>();
let public_key = Pubkey::create_with_seed(from_public_key, &seed, &spl_token::id())?;
Ok((public_key, seed))
}
pub fn calc_pool_pda(creator: &Pubkey, base_mint: &Pubkey, quote_mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
b"pool",
&[0u8, 0u8],
creator.as_ref(),
base_mint.as_ref(),
quote_mint.as_ref(),
],
&PUMP_SWAP_PROGRAM_ID,
)
}
pub fn calc_lp_mint_pda(pool: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[b"pool_lp_mint", pool.as_ref()], &PUMP_SWAP_PROGRAM_ID)
}
pub fn calc_user_pool_token_account(creator: &Pubkey, lp_mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
creator.as_ref(),
spl_token_2022::ID.as_ref(),
lp_mint.as_ref(),
],
&spl_associated_token_account::ID,
)
}
pub fn clone_keypairs(slice: &[Keypair]) -> Result<Vec<Keypair>> {
slice
.iter()
.map(|kp| {
Keypair::from_bytes(&kp.to_bytes())
.map_err(|e| anyhow!("Failed to clone keypair: {}", e))
})
.collect()
}
pub async fn send_jito_bundle(txs: Vec<Transaction>, jito_sdk: Arc<JitoJsonRpcSDK>) -> Result<()> {
let serialized_txs = txs
.iter()
.map(|tx| Ok(general_purpose::STANDARD.encode(bincode::serialize(tx)?)))
.collect::<Result<Vec<_>>>()?;
let bundle = json!(serialized_txs);
let params = json!([bundle, { "encoding": "base64" }]);
info!(
"Sending bundle with {} transaction(s)...",
serialized_txs.len()
);
let response = jito_sdk.send_bundle(Some(params), None).await?;
let bundle_uuid = response["result"]
.as_str()
.ok_or_else(|| anyhow!("Failed to get bundle UUID from response"))?;
info!("Bundle sent with UUID: {}", bundle_uuid);
Ok(())
}
pub async fn send_bundle_with_retry(
bundle: Vec<Transaction>,
jito_pool: Arc<JitoPool>,
max_retries: u64,
) -> Result<()> {
let mut attempt = 1;
loop {
match send_jito_bundle(bundle.clone(), jito_pool.next_client().await).await {
Ok(_) => {
info!("Bundle sent successfully on attempt {}", attempt);
return Ok(());
}
Err(e) => {
attempt += 1;
if attempt >= max_retries {
return Err(anyhow!(
"Failed to send after {} retries: {:?}",
max_retries,
e
));
}
info!("Send failed, retry {}/{}", attempt, max_retries);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}
pub fn find_user_vol_accumulator(user: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[b"user_volume_accumulator", user.as_ref()],
&PUMP_SWAP_PROGRAM_ID,
)
.0
}
pub fn find_coin_creator_vault_authority(coin_creator: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[b"creator_vault", coin_creator.as_ref()],
&PUMP_SWAP_PROGRAM_ID,
)
.0
}
pub fn find_coin_creator_vault_ata(
coin_creator_vault_authority: &Pubkey,
quote_token_program: &Pubkey,
quote_mint: &Pubkey,
) -> Pubkey {
spl_associated_token_account::get_associated_token_address_with_program_id(
coin_creator_vault_authority,
quote_mint,
quote_token_program,
)
}
pub fn fee_config_pda() -> Pubkey {
Pubkey::find_program_address(
&[b"fee_config", PUMP_SWAP_PROGRAM_ID.as_ref()],
&FEE_PROGRAM,
)
.0
}
pub fn pool_v2_pda(base_mint: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[b"pool-v2", base_mint.as_ref()], &PUMP_SWAP_PROGRAM_ID).0
}
pub fn user_volume_accumulator_quote_ata(
user: &Pubkey,
quote_mint: &Pubkey,
quote_token_program: &Pubkey,
) -> Pubkey {
spl_associated_token_account::get_associated_token_address_with_program_id(
&find_user_vol_accumulator(user),
quote_mint,
quote_token_program,
)
}
pub fn pick_protocol_fee_recipient() -> Pubkey {
*PROTOCOL_FEE_RECIPIENTS
.choose(&mut rand::rng())
.expect("PROTOCOL_FEE_RECIPIENTS is non-empty")
}
pub fn pick_buyback_fee_recipient() -> Pubkey {
*BUYBACK_FEE_RECIPIENTS
.choose(&mut rand::rng())
.expect("BUYBACK_FEE_RECIPIENTS is non-empty")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jito_pool_rejects_empty_endpoint_list() {
assert!(JitoPool::new(&[], None, Duration::from_millis(0)).is_err());
}
}