2025-02-13 20:44:20 +08:00
|
|
|
use std::{str::FromStr, time::Duration, fmt};
|
2025-01-14 19:18:48 +08:00
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
use anyhow::{anyhow, Result};
|
2025-02-12 22:46:09 +08:00
|
|
|
use api::TipAccountResult;
|
2025-02-13 20:44:20 +08:00
|
|
|
use rand::{seq::IteratorRandom};
|
2025-02-12 20:12:14 +08:00
|
|
|
use serde::Deserialize;
|
|
|
|
|
use serde_json::{json, Value};
|
2025-02-13 20:44:20 +08:00
|
|
|
use solana_sdk::{
|
|
|
|
|
pubkey::Pubkey,
|
|
|
|
|
transaction::{Transaction, VersionedTransaction},
|
2025-01-03 14:43:26 +08:00
|
|
|
};
|
2025-02-13 20:44:20 +08:00
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
use tracing::error;
|
2025-01-08 16:33:43 +08:00
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
pub mod api;
|
2025-02-13 20:44:20 +08:00
|
|
|
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;
|
2025-02-12 20:12:14 +08:00
|
|
|
|
2025-01-03 14:43:26 +08:00
|
|
|
pub struct JitoClient {
|
2025-01-14 19:18:48 +08:00
|
|
|
base_url: String,
|
2025-02-12 20:12:14 +08:00
|
|
|
tip_accounts: RwLock<Vec<String>>,
|
|
|
|
|
tips_percentile: RwLock<Option<TipPercentileData>>,
|
|
|
|
|
tip_percentile: String,
|
2025-02-13 20:44:20 +08:00
|
|
|
client: RpcClient,
|
2025-01-14 19:18:48 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
impl Clone for JitoClient {
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
base_url: self.base_url.clone(),
|
|
|
|
|
tip_accounts: RwLock::new(Vec::new()),
|
|
|
|
|
tips_percentile: RwLock::new(None),
|
|
|
|
|
tip_percentile: self.tip_percentile.clone(),
|
2025-02-13 20:44:20 +08:00
|
|
|
client: RpcClient::new(self.base_url.clone()),
|
2025-02-12 20:12:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_TIP_PERCENTILE: &str = "25";
|
|
|
|
|
pub struct TipPercentileData {
|
|
|
|
|
pub time: String,
|
|
|
|
|
pub landed_tips_25th_percentile: f64,
|
|
|
|
|
pub landed_tips_50th_percentile: f64,
|
|
|
|
|
pub landed_tips_75th_percentile: f64,
|
|
|
|
|
pub landed_tips_95th_percentile: f64,
|
|
|
|
|
pub landed_tips_99th_percentile: f64,
|
|
|
|
|
pub ema_landed_tips_50th_percentile: f64,
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-03 14:43:26 +08:00
|
|
|
impl JitoClient {
|
2025-02-13 20:44:20 +08:00
|
|
|
pub fn new(jito_url: &str, uuid: Option<String>) -> Self {
|
2025-01-03 14:43:26 +08:00
|
|
|
Self {
|
2025-02-13 20:44:20 +08:00
|
|
|
base_url: jito_url.to_string(),
|
2025-02-12 20:12:14 +08:00
|
|
|
tip_accounts: RwLock::new(vec![]),
|
|
|
|
|
tips_percentile: RwLock::new(None),
|
|
|
|
|
tip_percentile: DEFAULT_TIP_PERCENTILE.to_string(),
|
2025-02-13 20:44:20 +08:00
|
|
|
client: RpcClient::new(jito_url.to_string()),
|
2025-01-03 14:43:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 22:46:09 +08:00
|
|
|
pub async fn get_tip_accounts(&self) -> Result<TipAccountResult> {
|
2025-02-13 20:44:20 +08:00
|
|
|
let result = self.client.get_tip_accounts().await?;
|
2025-02-12 22:46:09 +08:00
|
|
|
let tip_accounts = TipAccountResult::from(result).map_err(|e| anyhow!(e))?;
|
|
|
|
|
Ok(tip_accounts)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
pub async fn init_tip_accounts(&self) -> Result<()> {
|
2025-02-12 22:46:09 +08:00
|
|
|
let accounts = self.get_tip_accounts().await?;
|
2025-02-12 20:12:14 +08:00
|
|
|
let mut tip_accounts = self.tip_accounts.write().await;
|
2025-01-14 19:18:48 +08:00
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
accounts
|
|
|
|
|
.accounts
|
|
|
|
|
.iter()
|
|
|
|
|
.for_each(|account| tip_accounts.push(account.to_string()));
|
|
|
|
|
Ok(())
|
2025-01-14 19:18:48 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
pub async fn get_tip_account(&self) -> Result<Pubkey> {
|
|
|
|
|
{
|
|
|
|
|
let accounts = self.tip_accounts.read().await;
|
|
|
|
|
if !accounts.is_empty() {
|
2025-02-13 20:44:20 +08:00
|
|
|
if let Some(acc) = accounts.iter().choose(&mut rand::thread_rng()) {
|
2025-02-12 20:12:14 +08:00
|
|
|
return Ok(Pubkey::from_str(acc).inspect_err(|err| {
|
|
|
|
|
error!("jito: failed to parse Pubkey: {:?}", err);
|
|
|
|
|
})?);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-13 20:44:20 +08:00
|
|
|
}
|
2025-01-14 19:18:48 +08:00
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
self.init_tip_accounts().await?;
|
|
|
|
|
|
|
|
|
|
let accounts = self.tip_accounts.read().await;
|
2025-02-13 20:44:20 +08:00
|
|
|
match accounts.iter().choose(&mut rand::thread_rng()) {
|
2025-02-12 20:12:14 +08:00
|
|
|
Some(acc) => Ok(Pubkey::from_str(acc).inspect_err(|err| {
|
|
|
|
|
error!("jito: failed to parse Pubkey: {:?}", err);
|
|
|
|
|
})?),
|
|
|
|
|
None => Err(anyhow!("jito: no tip accounts available")),
|
2025-01-03 14:43:26 +08:00
|
|
|
}
|
2025-01-14 19:18:48 +08:00
|
|
|
}
|
|
|
|
|
|
2025-01-03 14:43:26 +08:00
|
|
|
pub async fn send_transaction(
|
|
|
|
|
&self,
|
|
|
|
|
transaction: &Transaction,
|
2025-02-12 22:46:09 +08:00
|
|
|
) -> Result<String, anyhow::Error> {
|
2025-02-13 20:44:20 +08:00
|
|
|
let bundles = vec![VersionedTransaction::from(transaction.clone())];
|
|
|
|
|
Ok(self.client.send_bundle(&bundles).await?)
|
2025-02-12 20:12:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn generate_statuses(bundle_id: String, confirmation_status: &str) -> Vec<Value> {
|
|
|
|
|
vec![json!({
|
|
|
|
|
"bundle_id": bundle_id,
|
|
|
|
|
"transactions": ["tx1", "tx2"],
|
|
|
|
|
"slot": 12345,
|
|
|
|
|
"confirmation_status": confirmation_status,
|
|
|
|
|
"err": {"Ok": null}
|
|
|
|
|
})]
|
2025-01-03 14:43:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_success_confirmation() {
|
|
|
|
|
let client = JitoClient::new("http://localhost:8899", None);
|
|
|
|
|
for &status in &["finalized", "confirmed"] {
|
|
|
|
|
let wait_result = client.wait_for_bundle_confirmation(
|
|
|
|
|
|id| async { Ok(generate_statuses(id, status)) },
|
|
|
|
|
"6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_string(),
|
|
|
|
|
Duration::from_secs(1),
|
|
|
|
|
Duration::from_secs(1),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
assert!(wait_result.is_ok());
|
|
|
|
|
}
|
2025-01-14 19:18:48 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 20:12:14 +08:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_error_confirmation() {
|
|
|
|
|
let client = JitoClient::new("http://localhost:8899", None);
|
|
|
|
|
let wait_result = client.wait_for_bundle_confirmation(
|
|
|
|
|
|id| async { Ok(generate_statuses(id, "processed")) },
|
|
|
|
|
"6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_string(),
|
|
|
|
|
Duration::from_secs(1),
|
|
|
|
|
Duration::from_secs(2),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
assert!(wait_result.is_err());
|
2025-01-14 19:18:48 +08:00
|
|
|
}
|
2025-02-12 20:12:14 +08:00
|
|
|
}
|