Files
sol-trade-sdk/src/jito/mod.rs
T

357 lines
11 KiB
Rust
Raw Normal View History

2025-02-12 20:12:14 +08:00
use std::{convert::TryInto, future::Future, 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-12 20:12:14 +08:00
use rand::{rng, seq::IteratorRandom};
use reqwest::Client;
use serde::Deserialize;
use serde_json::{json, Value};
use solana_sdk::{pubkey::Pubkey, transaction::Transaction};
use tokio::{
sync::RwLock,
time::{sleep, Instant},
2025-01-03 14:43:26 +08:00
};
2025-02-12 20:12:14 +08:00
use tracing::{debug, error, info, warn};
2025-01-03 14:43:26 +08:00
2025-01-14 19:18:48 +08:00
use crate::error::{ClientError, ClientResult};
2025-01-08 16:33:43 +08:00
2025-02-12 20:12:14 +08:00
pub mod api;
#[derive(Debug)]
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-01-14 19:18:48 +08:00
uuid: Option<String>,
client: Client,
}
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(),
uuid: self.uuid.clone(),
client: Client::new(),
}
}
}
2025-01-14 19:18:48 +08:00
#[derive(Debug)]
pub struct PrettyJsonValue(pub Value);
impl fmt::Display for PrettyJsonValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", serde_json::to_string_pretty(&self.0).unwrap())
}
}
impl From<Value> for PrettyJsonValue {
fn from(value: Value) -> Self {
PrettyJsonValue(value)
}
2025-01-03 14:43:26 +08:00
}
2025-02-12 20:12:14 +08:00
const DEFAULT_TIP_PERCENTILE: &str = "25";
#[derive(Debug, Deserialize, Clone)]
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,
}
#[derive(Deserialize, Debug)]
pub struct BundleStatus {
pub bundle_id: String,
pub transactions: Vec<String>,
pub slot: u64,
pub confirmation_status: String,
pub err: ErrorStatus,
}
#[derive(Deserialize, Debug)]
pub struct ErrorStatus {
#[serde(rename = "Ok")]
pub ok: Option<()>,
}
2025-01-03 14:43:26 +08:00
impl JitoClient {
2025-01-14 19:18:48 +08:00
pub fn new(base_url: &str, uuid: Option<String>) -> Self {
2025-01-03 14:43:26 +08:00
Self {
2025-01-14 19:18:48 +08:00
base_url: base_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-01-14 19:18:48 +08:00
uuid,
client: Client::new(),
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> {
let endpoint = if let Some(uuid) = &self.uuid {
2025-02-12 22:56:44 +08:00
format!("/api/v1/bundles?uuid={}", uuid)
2025-02-12 22:46:09 +08:00
} else {
2025-02-12 22:56:44 +08:00
"/api/v1/bundles".to_string()
2025-02-12 22:46:09 +08:00
};
let result = self.send_request(&endpoint, "getTipAccounts", None).await?;
let tip_accounts = TipAccountResult::from(result).map_err(|e| anyhow!(e))?;
Ok(tip_accounts)
}
async fn send_request(&self, endpoint: &str, method: &str, params: Option<Value>) -> Result<Value> {
2025-02-13 00:08:14 +08:00
let url = format!("{}{}", self.base_url, endpoint);
2025-01-14 19:18:48 +08:00
let data = json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params.unwrap_or(json!([]))
2025-01-03 14:43:26 +08:00
});
2025-01-14 19:18:48 +08:00
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.json(&data)
.send()
.await
2025-02-12 22:46:09 +08:00
.map_err(|e| anyhow!(format!("Request failed: {}", e)))?;
2025-01-09 00:12:43 +08:00
2025-01-14 19:18:48 +08:00
let body = response.json::<Value>().await
2025-02-12 22:46:09 +08:00
.map_err(|e| anyhow!(format!("Failed to parse response: {}", e)))?;
2025-01-14 19:18:48 +08:00
Ok(body)
}
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() {
let mut rng = rng();
if let Some(acc) = accounts.iter().choose(&mut rng) {
return Ok(Pubkey::from_str(acc).inspect_err(|err| {
error!("jito: failed to parse Pubkey: {:?}", err);
})?);
}
}
} // 这里释放读锁
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;
let mut rng = rng();
match accounts.iter().choose(&mut rng) {
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-02-12 20:12:14 +08:00
pub async fn init_tip_amounts(&self) -> Result<()> {
let tip_percentiles = api::get_tip_amounts().await?;
*self.tips_percentile.write().await = tip_percentiles.first().cloned();
Ok(())
}
// unit sol
pub async fn get_tip_value(&self) -> Result<f64> {
let tips = self.tips_percentile.read().await;
if let Some(ref data) = *tips {
match self.tip_percentile.as_str() {
"25" => Ok(data.landed_tips_25th_percentile),
"50" => Ok(data.landed_tips_50th_percentile),
"75" => Ok(data.landed_tips_75th_percentile),
"95" => Ok(data.landed_tips_95th_percentile),
"99" => Ok(data.landed_tips_99th_percentile),
_ => Err(anyhow!("jito: invalid TIP_PERCENTILE value")),
}
2025-01-14 19:18:48 +08:00
} else {
2025-02-12 20:12:14 +08:00
Err(anyhow!("jito: failed get tip"))
}
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-01-09 00:12:43 +08:00
let wire_transaction = bincode::serialize(transaction).map_err(|e| {
2025-02-12 22:46:09 +08:00
anyhow!(
"Transaction serialization failed: {}",
2025-01-09 00:12:43 +08:00
e.to_string(),
)
})?;
2025-01-14 19:18:48 +08:00
let serialized_tx = bs58::encode(&wire_transaction).into_string();
2025-01-03 14:43:26 +08:00
2025-01-14 19:18:48 +08:00
// Prepare bundle for submission (array of transactions)
let bundle = json!([serialized_tx]);
2025-01-09 00:12:43 +08:00
2025-01-14 19:18:48 +08:00
// UUID for the bundle
2025-02-12 20:12:14 +08:00
let uuid = self.uuid.clone();
2025-01-03 14:43:26 +08:00
2025-01-14 19:18:48 +08:00
// Send bundle using Jito SDK
let response = self.send_bundle(Some(bundle), uuid).await?;
2025-01-03 14:43:26 +08:00
response["result"]
.as_str()
.map(|s| s.to_string())
2025-02-12 22:46:09 +08:00
.ok_or_else(|| anyhow!("Invalid response format: missing result field"))
2025-01-03 14:43:26 +08:00
}
2025-02-12 22:46:09 +08:00
pub async fn send_bundle(&self, params: Option<Value>, uuid: Option<String>) -> Result<Value> {
2025-02-12 22:56:44 +08:00
let mut endpoint = "/api/v1/bundles".to_string();
2025-01-14 19:18:48 +08:00
if let Some(uuid) = uuid {
endpoint = format!("{}?uuid={}", endpoint, uuid);
}
// Ensure params is an array of transactions
let transactions = match params {
Some(Value::Array(transactions)) => {
if transactions.is_empty() {
2025-02-12 22:46:09 +08:00
return Err(anyhow!("Bundle must contain at least one transaction"));
2025-01-14 19:18:48 +08:00
}
if transactions.len() > 5 {
2025-02-12 22:46:09 +08:00
return Err(anyhow!("Bundle can contain at most 5 transactions"));
2025-01-14 19:18:48 +08:00
}
transactions
},
2025-02-12 22:46:09 +08:00
_ => return Err(anyhow!("Invalid bundle format: expected an array of transactions")),
2025-01-14 19:18:48 +08:00
};
// Wrap the transactions array in another array
let params = json!([transactions]);
// Send the wrapped transactions array
self.send_request(&endpoint, "sendBundle", Some(params))
2025-01-03 14:43:26 +08:00
.await
2025-02-12 22:46:09 +08:00
.map_err(|e| anyhow!(e))
2025-01-14 19:18:48 +08:00
}
2025-01-03 14:43:26 +08:00
2025-02-12 20:12:14 +08:00
pub async fn wait_for_bundle_confirmation<F, Fut>(
&self,
fetch_statuses: F,
bundle_id: String,
interval: Duration,
timeout: Duration,
) -> Result<Vec<String>>
where
F: Fn(String) -> Fut,
Fut: Future<Output = Result<Vec<Value>>>,
{
let start_time = Instant::now();
2025-01-09 00:12:43 +08:00
2025-02-12 20:12:14 +08:00
loop {
let statuses = fetch_statuses(bundle_id.clone()).await?;
2025-01-03 14:43:26 +08:00
2025-02-12 20:12:14 +08:00
if let Some(status) = statuses.first() {
let bundle_status: BundleStatus =
serde_json::from_value(status.clone()).inspect_err(|err| {
error!(
"Failed to parse JSON when get_bundle_statuses, err: {}",
err,
);
})?;
2025-01-14 19:18:48 +08:00
2025-02-12 20:12:14 +08:00
debug!("{:?}", bundle_status);
match bundle_status.confirmation_status.as_str() {
"finalized" | "confirmed" => {
info!(
"Finalized bundle {}: {}",
bundle_id, bundle_status.confirmation_status
);
bundle_status
.transactions
.iter()
.for_each(|tx| info!("https://solscan.io/tx/{}", tx));
return Ok(bundle_status.transactions);
2025-01-14 19:18:48 +08:00
}
2025-02-12 20:12:14 +08:00
_ => {
debug!("bundle_status: {:?}", bundle_status);
}
}
} else {
debug!("Finalizing bundle {}: {}", bundle_id, "None");
}
2025-01-14 19:18:48 +08:00
2025-02-12 20:12:14 +08:00
if start_time.elapsed() > timeout {
warn!("Loop exceeded {:?}, breaking out.", timeout);
return Err(anyhow!("Bundle status get timeout"));
}
sleep(interval).await;
}
}
}
#[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
}