mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-16 18:38:05 +00:00
update jito
This commit is contained in:
+2
-1
@@ -27,7 +27,7 @@ futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
base64 = "0.22.1"
|
||||
bs58 = "0.5.1"
|
||||
rand = "0.8.5"
|
||||
rand = "0.9.0"
|
||||
bincode = "1.3.3"
|
||||
anyhow = "1.0.90"
|
||||
reqwest = { version = "0.11.27", features = ["json"] }
|
||||
@@ -41,4 +41,5 @@ pretty_env_logger = "0.5.0"
|
||||
log = "0.4.22"
|
||||
chrono = "0.4.39"
|
||||
regex = "1"
|
||||
tracing = "0.1.41"
|
||||
|
||||
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
transaction::Transaction,
|
||||
};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JitoClient {
|
||||
base_url: String,
|
||||
uuid: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
impl JitoClient {
|
||||
pub fn new(base_url: &str, uuid: Option<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.to_string(),
|
||||
uuid,
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_request(&self, endpoint: &str, method: &str, params: Option<Value>) -> ClientResult<Value> {
|
||||
let url = format!("{}{}", self.base_url, endpoint);
|
||||
|
||||
let data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params.unwrap_or(json!([]))
|
||||
});
|
||||
|
||||
// println!("Sending request to: {}", url);
|
||||
// println!("Request body: {}", serde_json::to_string_pretty(&data).unwrap());
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ClientError::Other(format!("Request failed: {}", e)))?;
|
||||
|
||||
let status = response.status();
|
||||
// println!("Response status: {}", status);
|
||||
|
||||
let body = response.json::<Value>().await
|
||||
.map_err(|e| ClientError::Other(format!("Failed to parse response: {}", e)))?;
|
||||
// println!("Response body: {}", serde_json::to_string_pretty(&body).unwrap());
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub async fn get_tip_accounts(&self) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
self.send_request(&endpoint, "getTipAccounts", None).await
|
||||
}
|
||||
|
||||
// Get a random tip account
|
||||
pub async fn get_tip_account(&self) -> ClientResult<Pubkey> {
|
||||
let tip_accounts_response = self.get_tip_accounts().await?;
|
||||
|
||||
let tip_accounts = tip_accounts_response["result"]
|
||||
.as_array()
|
||||
.ok_or_else(|| ClientError::Other("Failed to parse tip accounts as array".to_string()))?;
|
||||
|
||||
if tip_accounts.is_empty() {
|
||||
return Err(ClientError::Other("No tip accounts available".to_string()));
|
||||
}
|
||||
|
||||
let random_account = tip_accounts
|
||||
.choose(&mut rand::thread_rng())
|
||||
.ok_or_else(|| ClientError::Other("Failed to choose random tip account".to_string()))?;
|
||||
|
||||
let address = random_account
|
||||
.as_str()
|
||||
.ok_or_else(|| ClientError::Other("Failed to parse tip account as string".to_string()))?;
|
||||
|
||||
Pubkey::from_str(address)
|
||||
.map_err(|e| ClientError::Other(format!("Failed to parse pubkey: {}", e)))
|
||||
}
|
||||
|
||||
pub async fn get_bundle_statuses(&self, bundle_uuids: Vec<String>) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
// Construct the params as a list within a list
|
||||
let params = json!([bundle_uuids]);
|
||||
|
||||
self.send_request(&endpoint, "getBundleStatuses", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
transaction: &Transaction,
|
||||
) -> ClientResult<String> {
|
||||
let wire_transaction = bincode::serialize(transaction).map_err(|e| {
|
||||
ClientError::Parse(
|
||||
"Transaction serialization failed".to_string(),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let serialized_tx = bs58::encode(&wire_transaction).into_string();
|
||||
|
||||
// Prepare bundle for submission (array of transactions)
|
||||
let bundle = json!([serialized_tx]);
|
||||
|
||||
// UUID for the bundle
|
||||
let uuid = None;
|
||||
|
||||
// Send bundle using Jito SDK
|
||||
// println!("Sending bundle with 1 transaction...");
|
||||
let response = self.send_bundle(Some(bundle), uuid).await?;
|
||||
|
||||
response["result"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| ClientError::Parse(
|
||||
"Invalid response format".to_string(),
|
||||
"Missing result field".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send_bundle(&self, params: Option<Value>, uuid: Option<&str>) -> ClientResult<Value> {
|
||||
let mut endpoint = "/bundles".to_string();
|
||||
|
||||
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() {
|
||||
return Err(ClientError::Other("Bundle must contain at least one transaction".to_string()));
|
||||
}
|
||||
if transactions.len() > 5 {
|
||||
return Err(ClientError::Other("Bundle can contain at most 5 transactions".to_string()));
|
||||
}
|
||||
transactions
|
||||
},
|
||||
_ => return Err(ClientError::Other("Invalid bundle format: expected an array of transactions".to_string())),
|
||||
};
|
||||
|
||||
// Wrap the transactions array in another array
|
||||
let params = json!([transactions]);
|
||||
|
||||
// Send the wrapped transactions array
|
||||
self.send_request(&endpoint, "sendBundle", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_txn(&self, params: Option<Value>, bundle_only: bool) -> ClientResult<Value> {
|
||||
let mut query_params = Vec::new();
|
||||
|
||||
if bundle_only {
|
||||
query_params.push("bundleOnly=true".to_string());
|
||||
}
|
||||
|
||||
let endpoint = if query_params.is_empty() {
|
||||
"/transactions".to_string()
|
||||
} else {
|
||||
format!("/transactions?{}", query_params.join("&"))
|
||||
};
|
||||
|
||||
// Construct params as an array instead of an object
|
||||
let params = match params {
|
||||
Some(Value::Object(map)) => {
|
||||
let tx = map.get("tx").and_then(Value::as_str).unwrap_or_default();
|
||||
let skip_preflight = map.get("skipPreflight").and_then(Value::as_bool).unwrap_or(false);
|
||||
json!([
|
||||
tx,
|
||||
{
|
||||
"encoding": "base64",
|
||||
"skipPreflight": skip_preflight
|
||||
}
|
||||
])
|
||||
},
|
||||
_ => json!([]),
|
||||
};
|
||||
|
||||
self.send_request(&endpoint, "sendTransaction", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn get_in_flight_bundle_statuses(&self, bundle_uuids: Vec<String>) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
// Construct the params as a list within a list
|
||||
let params = json!([bundle_uuids]);
|
||||
|
||||
self.send_request(&endpoint, "getInflightBundleStatuses", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
// Helper method to convert Value to PrettyJsonValue
|
||||
pub fn prettify(value: Value) -> PrettyJsonValue {
|
||||
PrettyJsonValue(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Proxy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::TipPercentileData;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RpcRequest {
|
||||
jsonrpc: String,
|
||||
id: u32,
|
||||
method: String,
|
||||
params: Vec<()>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct RpcResponse {
|
||||
pub jsonrpc: String,
|
||||
pub id: u32,
|
||||
pub result: serde_json::Value,
|
||||
}
|
||||
|
||||
pub async fn get_tip_accounts(block_engine_url: &str) -> Result<RpcResponse> {
|
||||
let mut client_builder = reqwest::Client::builder();
|
||||
if let Ok(http_proxy) = env::var("HTTP_PROXY") {
|
||||
let proxy = Proxy::all(http_proxy)?;
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
let client = client_builder.build()?;
|
||||
let request_body = RpcRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: 1,
|
||||
method: "getTipAccounts".to_string(),
|
||||
params: vec![],
|
||||
};
|
||||
let result = client
|
||||
.post(format!("{}/api/v1/bundles", block_engine_url))
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await?
|
||||
.json::<RpcResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
/// tip accounts
|
||||
#[derive(Debug)]
|
||||
pub struct TipAccountResult {
|
||||
pub accounts: Vec<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<RpcResponse> for TipAccountResult {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: RpcResponse) -> Result<Self, Self::Error> {
|
||||
let accounts = value
|
||||
.result
|
||||
.as_array()
|
||||
.context("expected 'result' to be an array")?
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap().to_string())
|
||||
.collect();
|
||||
Ok(TipAccountResult { accounts })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tip_amounts() -> Result<Vec<TipPercentileData>> {
|
||||
let mut client_builder = reqwest::Client::builder();
|
||||
if let Ok(http_proxy) = env::var("HTTP_PROXY") {
|
||||
let proxy = Proxy::all(http_proxy)?;
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
let client = client_builder.build()?;
|
||||
|
||||
let result = client
|
||||
.get("https://bundles.jito.wtf/api/v1/bundles/tip_floor")
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<TipPercentileData>>()
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
Executable → Regular
+210
-98
@@ -1,23 +1,45 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::{json, Value};
|
||||
use std::{convert::TryInto, future::Future, str::FromStr, time::Duration, fmt};
|
||||
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
transaction::Transaction,
|
||||
use anyhow::{anyhow, Result};
|
||||
use api::{get_tip_accounts, TipAccountResult};
|
||||
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},
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub mod api;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JitoClient {
|
||||
base_url: String,
|
||||
tip_accounts: RwLock<Vec<String>>,
|
||||
tips_percentile: RwLock<Option<TipPercentileData>>,
|
||||
tip_percentile: String,
|
||||
uuid: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrettyJsonValue(pub Value);
|
||||
|
||||
@@ -33,10 +55,41 @@ impl From<Value> for PrettyJsonValue {
|
||||
}
|
||||
}
|
||||
|
||||
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<()>,
|
||||
}
|
||||
|
||||
impl JitoClient {
|
||||
pub fn new(base_url: &str, uuid: Option<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.to_string(),
|
||||
tip_accounts: RwLock::new(vec![]),
|
||||
tips_percentile: RwLock::new(None),
|
||||
tip_percentile: DEFAULT_TIP_PERCENTILE.to_string(),
|
||||
uuid,
|
||||
client: Client::new(),
|
||||
}
|
||||
@@ -52,9 +105,6 @@ impl JitoClient {
|
||||
"params": params.unwrap_or(json!([]))
|
||||
});
|
||||
|
||||
// println!("Sending request to: {}", url);
|
||||
// println!("Request body: {}", serde_json::to_string_pretty(&data).unwrap());
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -63,62 +113,74 @@ impl JitoClient {
|
||||
.await
|
||||
.map_err(|e| ClientError::Other(format!("Request failed: {}", e)))?;
|
||||
|
||||
let status = response.status();
|
||||
// println!("Response status: {}", status);
|
||||
|
||||
let body = response.json::<Value>().await
|
||||
.map_err(|e| ClientError::Other(format!("Failed to parse response: {}", e)))?;
|
||||
// println!("Response body: {}", serde_json::to_string_pretty(&body).unwrap());
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub async fn get_tip_accounts(&self) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
pub async fn init_tip_accounts(&self) -> Result<()> {
|
||||
let accounts: TipAccountResult = get_tip_accounts(&self.base_url).await?.try_into()?;
|
||||
let mut tip_accounts = self.tip_accounts.write().await;
|
||||
|
||||
self.send_request(&endpoint, "getTipAccounts", None).await
|
||||
accounts
|
||||
.accounts
|
||||
.iter()
|
||||
.for_each(|account| tip_accounts.push(account.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Get a random tip account
|
||||
pub async fn get_tip_account(&self) -> ClientResult<Pubkey> {
|
||||
let tip_accounts_response = self.get_tip_accounts().await?;
|
||||
|
||||
let tip_accounts = tip_accounts_response["result"]
|
||||
.as_array()
|
||||
.ok_or_else(|| ClientError::Other("Failed to parse tip accounts as array".to_string()))?;
|
||||
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);
|
||||
})?);
|
||||
}
|
||||
}
|
||||
} // 这里释放读锁
|
||||
|
||||
if tip_accounts.is_empty() {
|
||||
return Err(ClientError::Other("No tip accounts available".to_string()));
|
||||
// 如果账户列表为空,初始化账户列表
|
||||
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")),
|
||||
}
|
||||
|
||||
let random_account = tip_accounts
|
||||
.choose(&mut rand::thread_rng())
|
||||
.ok_or_else(|| ClientError::Other("Failed to choose random tip account".to_string()))?;
|
||||
|
||||
let address = random_account
|
||||
.as_str()
|
||||
.ok_or_else(|| ClientError::Other("Failed to parse tip account as string".to_string()))?;
|
||||
|
||||
Pubkey::from_str(address)
|
||||
.map_err(|e| ClientError::Other(format!("Failed to parse pubkey: {}", e)))
|
||||
}
|
||||
|
||||
pub async fn get_bundle_statuses(&self, bundle_uuids: Vec<String>) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
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")),
|
||||
}
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
// Construct the params as a list within a list
|
||||
let params = json!([bundle_uuids]);
|
||||
|
||||
self.send_request(&endpoint, "getBundleStatuses", Some(params))
|
||||
.await
|
||||
Err(anyhow!("jito: failed get tip"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_transaction(
|
||||
@@ -138,10 +200,9 @@ impl JitoClient {
|
||||
let bundle = json!([serialized_tx]);
|
||||
|
||||
// UUID for the bundle
|
||||
let uuid = None;
|
||||
let uuid = self.uuid.clone();
|
||||
|
||||
// Send bundle using Jito SDK
|
||||
// println!("Sending bundle with 1 transaction...");
|
||||
let response = self.send_bundle(Some(bundle), uuid).await?;
|
||||
|
||||
response["result"]
|
||||
@@ -153,7 +214,7 @@ impl JitoClient {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send_bundle(&self, params: Option<Value>, uuid: Option<&str>) -> ClientResult<Value> {
|
||||
pub async fn send_bundle(&self, params: Option<Value>, uuid: Option<String>) -> ClientResult<Value> {
|
||||
let mut endpoint = "/bundles".to_string();
|
||||
|
||||
if let Some(uuid) = uuid {
|
||||
@@ -182,54 +243,105 @@ impl JitoClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_txn(&self, params: Option<Value>, bundle_only: bool) -> ClientResult<Value> {
|
||||
let mut query_params = Vec::new();
|
||||
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();
|
||||
|
||||
if bundle_only {
|
||||
query_params.push("bundleOnly=true".to_string());
|
||||
}
|
||||
loop {
|
||||
let statuses = fetch_statuses(bundle_id.clone()).await?;
|
||||
|
||||
let endpoint = if query_params.is_empty() {
|
||||
"/transactions".to_string()
|
||||
} else {
|
||||
format!("/transactions?{}", query_params.join("&"))
|
||||
};
|
||||
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,
|
||||
);
|
||||
})?;
|
||||
|
||||
// Construct params as an array instead of an object
|
||||
let params = match params {
|
||||
Some(Value::Object(map)) => {
|
||||
let tx = map.get("tx").and_then(Value::as_str).unwrap_or_default();
|
||||
let skip_preflight = map.get("skipPreflight").and_then(Value::as_bool).unwrap_or(false);
|
||||
json!([
|
||||
tx,
|
||||
{
|
||||
"encoding": "base64",
|
||||
"skipPreflight": skip_preflight
|
||||
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);
|
||||
}
|
||||
])
|
||||
},
|
||||
_ => json!([]),
|
||||
};
|
||||
_ => {
|
||||
debug!("bundle_status: {:?}", bundle_status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Finalizing bundle {}: {}", bundle_id, "None");
|
||||
}
|
||||
|
||||
self.send_request(&endpoint, "sendTransaction", Some(params)).await
|
||||
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}
|
||||
})]
|
||||
}
|
||||
|
||||
pub async fn get_in_flight_bundle_statuses(&self, bundle_uuids: Vec<String>) -> ClientResult<Value> {
|
||||
let endpoint = if let Some(uuid) = &self.uuid {
|
||||
format!("/bundles?uuid={}", uuid)
|
||||
} else {
|
||||
"/bundles".to_string()
|
||||
};
|
||||
|
||||
// Construct the params as a list within a list
|
||||
let params = json!([bundle_uuids]);
|
||||
|
||||
self.send_request(&endpoint, "getInflightBundleStatuses", Some(params))
|
||||
.await
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to convert Value to PrettyJsonValue
|
||||
pub fn prettify(value: Value) -> PrettyJsonValue {
|
||||
PrettyJsonValue(value)
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -7,6 +7,7 @@ pub mod jito;
|
||||
pub mod grpc;
|
||||
pub mod common;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
@@ -25,7 +26,6 @@ use spl_associated_token_account::{
|
||||
|
||||
use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe};
|
||||
use common::logs_subscribe::SubscriptionHandle;
|
||||
use common::logs_events::DexEvent;
|
||||
use spl_token::instruction::close_account;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -264,7 +264,7 @@ impl PumpFun {
|
||||
utils::calculate_with_slippage_buy(max_sol_cost, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE));
|
||||
|
||||
let mut instructions = vec![];
|
||||
let tip_account = jito_client.get_tip_account().await?;
|
||||
let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e)).unwrap();
|
||||
let ata = get_associated_token_address(&self.payer.pubkey(), mint);
|
||||
if self.rpc.get_account(&ata).is_err() {
|
||||
instructions.push(create_associated_token_account(
|
||||
@@ -452,7 +452,7 @@ impl PumpFun {
|
||||
);
|
||||
|
||||
let mut instructions = vec![];
|
||||
let tip_account = jito_client.get_tip_account().await?;
|
||||
let tip_account = jito_client.get_tip_account().await.map_err(|e| ClientError::Other(e.to_string()))?;
|
||||
instructions.push(instruction::sell(
|
||||
&self.payer.clone(),
|
||||
mint,
|
||||
|
||||
Reference in New Issue
Block a user