Refactor the code to optimize performance.

This commit is contained in:
William
2025-02-15 18:53:17 +08:00
parent 7cf908ae5d
commit 38f1ce3fad
5 changed files with 189 additions and 580 deletions
+20 -83
View File
@@ -1,10 +1,8 @@
use std::{str::FromStr, time::Duration, fmt};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use api::TipAccountResult;
use rand::{seq::IteratorRandom};
use serde::Deserialize;
use serde_json::{json, Value};
use rand::seq::IteratorRandom;
use solana_sdk::{
pubkey::Pubkey,
transaction::{Transaction, VersionedTransaction},
@@ -24,8 +22,6 @@ use crate::jito::rpc_client::RpcClient;
pub struct JitoClient {
base_url: String,
tip_accounts: RwLock<Vec<String>>,
tips_percentile: RwLock<Option<TipPercentileData>>,
tip_percentile: String,
client: RpcClient,
}
@@ -34,49 +30,29 @@ impl Clone for JitoClient {
Self {
base_url: self.base_url.clone(),
tip_accounts: RwLock::new(Vec::new()),
tips_percentile: RwLock::new(None),
tip_percentile: self.tip_percentile.clone(),
client: RpcClient::new(self.base_url.clone()),
}
}
}
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,
}
impl JitoClient {
pub fn new(jito_url: &str, uuid: Option<String>) -> Self {
pub fn new(jito_url: &str, _uuid: Option<String>) -> Self {
Self {
base_url: jito_url.to_string(),
tip_accounts: RwLock::new(vec![]),
tips_percentile: RwLock::new(None),
tip_percentile: DEFAULT_TIP_PERCENTILE.to_string(),
client: RpcClient::new(jito_url.to_string()),
}
}
pub async fn get_tip_accounts(&self) -> Result<TipAccountResult> {
let result = self.client.get_tip_accounts().await?;
let tip_accounts = TipAccountResult::from(result).map_err(|e| anyhow!(e))?;
Ok(tip_accounts)
TipAccountResult::from(result).map_err(|e| anyhow!(e))
}
pub async fn init_tip_accounts(&self) -> Result<()> {
let accounts = self.get_tip_accounts().await?;
let mut tip_accounts = self.tip_accounts.write().await;
accounts
.accounts
.iter()
.for_each(|account| tip_accounts.push(account.to_string()));
*tip_accounts = accounts.accounts.iter().map(|a| a.to_string()).collect();
Ok(())
}
@@ -85,9 +61,11 @@ impl JitoClient {
let accounts = self.tip_accounts.read().await;
if !accounts.is_empty() {
if let Some(acc) = accounts.iter().choose(&mut rand::thread_rng()) {
return Ok(Pubkey::from_str(acc).inspect_err(|err| {
error!("jito: failed to parse Pubkey: {:?}", err);
})?);
return Pubkey::from_str(acc)
.map_err(|err| {
error!("jito: failed to parse Pubkey: {:?}", err);
anyhow!("Invalid pubkey format")
});
}
}
}
@@ -95,12 +73,16 @@ impl JitoClient {
self.init_tip_accounts().await?;
let accounts = self.tip_accounts.read().await;
match accounts.iter().choose(&mut rand::thread_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")),
}
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")
})
})
}
pub async fn send_transaction(
@@ -111,48 +93,3 @@ impl JitoClient {
Ok(self.client.send_bundle(&bundles).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}
})]
}
#[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());
}
}
#[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());
}
}
+2 -207
View File
@@ -22,33 +22,13 @@ use crate::jito::{
};
pub type RpcResult<T> = client_error::Result<Response<T>>;
/// A client of a remote Jito block engine node.
///
/// `RpcClient` communicates with a block engine node over [JSON-RPC]
/// It is the primary Rust interface for querying and transacting with the network
/// from external programs.
///
/// This is modeled very closely with the solan RpcClient with similar error types.
/// You can treat the client similar to the Solana RpcClient with the difference being
/// the RpcClient supports the block engine apis.
///
/// The client can be used with jito block engine proxy server for authentication
/// The client can be used as is to send requests unauthenticated to the jito block engine as well
///
/// Please note that the commitment level is not used at the moment. Support will be added
/// later on to specify and use the commitment levels.
pub struct RpcClient {
sender: Box<dyn RpcSender + Send + Sync + 'static>,
config: RpcClientConfig,
}
impl RpcClient {
/// Create an `RpcClient` from an [`RpcSender`] and an [`RpcClientConfig`].
///
/// This is the basic constructor, allowing construction with any type of
/// `RpcSender`. Most applications should use one of the other constructors,
/// such as [`RpcClient::new`], [`RpcClient::new_with_commitment`] or
/// [`RpcClient::new_with_timeout`].
pub fn new_sender<T: RpcSender + Send + Sync + 'static>(
sender: T,
config: RpcClientConfig,
@@ -59,46 +39,10 @@ impl RpcClient {
}
}
/// Create an HTTP `RpcClient`.
///
/// The URL is an HTTP URL, usually for port 8899, as in
/// "http://localhost:8899".
///
/// The client has a default timeout of 30 seconds, and a default [commitment
/// level][cl] of [`Finalized`](CommitmentLevel::Finalized).
///
/// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment
///
/// # Examples
///
/// ```
/// # use jito_block_engine_json_rpc_client::jsonrpc_client::rpc_client::RpcClient;
/// let url = "http://localhost:8899".to_string();
/// let client = RpcClient::new(url);
/// ```
pub fn new(url: String) -> Self {
Self::new_with_commitment(url, CommitmentConfig::default())
}
/// Create an HTTP `RpcClient` with specified [commitment level][cl].
///
/// Please note the client is not currently implemented to support commitment level configs
///
/// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment
///
/// The URL is an HTTP URL, usually for port 8899, as in
/// "http://localhost:8899".
///
/// The client has a default timeout of 30 seconds, and a user-specified
/// [`CommitmentLevel`] via [`CommitmentConfig`].
///
/// # Examples
///
/// # use solana_sdk::commitment_config::CommitmentConfig;
/// # use jito_block_engine_json_rpc_client::jsonrpc_client::rpc_client::RpcClient;
/// let url = "http://localhost:8899".to_string();
/// let commitment_config = CommitmentConfig::processed();
/// let client = RpcClient::new_with_commitment(url, commitment_config);
fn new_with_commitment(url: String, commitment_config: CommitmentConfig) -> Self {
Self::new_sender(
HttpSender::new(url),
@@ -106,25 +50,6 @@ impl RpcClient {
)
}
/// Create an HTTP `RpcClient` with specified timeout.
///
/// The URL is an HTTP URL, usually for port 8899, as in
/// "http://localhost:8899".
///
/// The client has and a default [commitment level][cl] of
/// [`Finalized`](CommitmentLevel::Finalized).
///
/// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment
///
/// # Examples
///
/// ```
/// # use std::time::Duration;
/// # use jito_block_engine_json_rpc_client::jsonrpc_client::rpc_client::RpcClient;
/// let url = "http://localhost::8899".to_string();
/// let timeout = Duration::from_secs(1);
/// let client = RpcClient::new_with_timeout(url, timeout);
/// ```
pub fn new_with_timeout(url: String, timeout: Duration) -> Self {
Self::new_sender(
HttpSender::new_with_timeout(url, timeout),
@@ -132,60 +57,14 @@ impl RpcClient {
)
}
/// Get the configured url of the client's sender
pub fn url(&self) -> String {
self.sender.url()
}
/// Get the configured default [commitment level][cl].
///
/// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment
///
/// The commitment config may be specified during construction, and
/// determines how thoroughly committed a transaction must be when waiting
/// for its confirmation or otherwise checking for confirmation. If not
/// specified, the default commitment level is
/// [`Finalized`](CommitmentLevel::Finalized).
///
/// The default commitment level is overridden when calling methods that
/// explicitly provide a [`CommitmentConfig`], like
/// [`RpcClient::confirm_transaction_with_commitment`].
pub fn commitment(&self) -> CommitmentConfig {
self.config.commitment_config
}
/// Submits a bundle of signed transactions to the network.
///
/// This returns a bundle_id on success and will return an error
/// code on failure. The error code will only be on the basic
/// validation of the bundle and publishing the bundle to the mempool
/// For the bundle status regarding whether it landed or not, get_bundle_statuses
/// should be used with the bundle id.
///
/// Example excerpt can be as below
///
/// use jito_block_engine_json_rpc_client::jsonrpc_client::rpc_client::RpcClient;
/// use solana_program::hash::Hash;
/// use solana_sdk::{pubkey::Pubkey, signer::keypair::Keypair};
///
/// let base = 0;
/// let MAX_BUNDLE_LEN = 5;
/// let searcher_keypair = Keypair::new();
/// let recent_blockhash = Hash::new_unique();
///
/// let mut bundle: Vec<_> = (0..(MAX_BUNDLE_LEN) as u64)
/// .map(|amount| {
/// VersionedTransaction::from(system_transaction::transfer(
/// &searcher_keypair,
/// &searcher_keypair.pubkey(),
/// base + amount,
/// recent_blockhash,
/// ))
/// })
/// .collect();
///
/// let rpc_client = RpcClient::new(SERVER_URL.to_owned());
/// let response = rpc_client.send_bundle(&bundle).await;
pub async fn send_bundle(
&self,
transactions: &[impl SerializableTransaction],
@@ -195,9 +74,6 @@ impl RpcClient {
let encoding = self.default_cluster_transaction_encoding().await?;
serialized_encoded.push(serialize_and_encode(transaction, encoding)?);
}
//The bundle may or may
// not have been submitted to the cluster, so callers should verify the success of
// the correct transaction signature independently.
match self
.send(RpcRequest::SendBundle, json!([serialized_encoded]))
.await
@@ -221,35 +97,6 @@ impl RpcClient {
Ok(UiTransactionEncoding::Base58)
}
/// Gets the statuses of a list of bundle ids.
///
/// Returns the statuses of a list of signatures. Each signature must be a bundle_id.
/// bundle ids are sha256 hashes of their tx signatures (we get it after a sendBundle)
/// This method currently will provide information regarding whether the bundle
/// landed or not.
/// The behavior is similar to the solana rpc method getSignatureStatuses
/// https://docs.solana.com/api/http#getsignaturestatuses
///
/// If the bundle_id is not found or the all of the transactions in the bundle has not landed,
/// we return null. If found and landed, we return the context information including the slot
/// at which the request was made and result with the bundle_id(s) and the transactions with the
/// slot and confirmation status. At this point, its assumed that all transactions within a bundle
/// will have the same slot number and confirmation status.
///
/// The confirmation status of a bundle is the confirmation status of the transactions.
/// This api does not provide a commitment level to configure, but will return the commitment level
/// as returned by the rpc. The rpc used to fetch bulk transaction status does not provide a commitment
/// level configuration option either.
///
/// Example excerpt can be as below
///
/// use jito_block_engine_json_rpc_client::jsonrpc_client::rpc_client::RpcClient;
///
/// let SERVER_URL = "http://localhost:8899";
/// let bundle_id = "bundle_id".to_owned();
///
/// let rpc_client = RpcClient::new(SERVER_URL.to_owned());
/// let response = rpc_client.get_bundle_statuses(&[bundle_id.clone()]).await;
pub async fn get_bundle_statuses(
&self,
signatures: &[String],
@@ -258,7 +105,6 @@ impl RpcClient {
.await
}
/// Returns the tip accounts to be used for tip payments.
pub async fn get_tip_accounts(&self) -> ClientResult<Vec<String>> {
self.send(RpcRequest::GetTipAccounts, Value::Null).await
}
@@ -301,7 +147,6 @@ where
Ok(encoded)
}
// Sample tests. Can use these as a reference point on how to use the api and what to expect
#[cfg(test)]
mod rpc_client_tests {
use solana_program::hash::Hash;
@@ -310,41 +155,22 @@ mod rpc_client_tests {
transaction::VersionedTransaction,
};
use crate::jsonrpc_client::rpc_client::RpcClient;
use crate::jito::rpc_client::RpcClient;
// Use the proxy server url here.
const SERVER_URL: &str = "http://0.0.0.0:8080/api/v1/bundles";
#[tokio::test]
pub async fn get_tip_accounts() {
// Let's try the same with the rpc client
let rpc_client = RpcClient::new(SERVER_URL.to_owned());
let tip_accounts = rpc_client.get_tip_accounts().await;
// Sample output. Pick only randomly to not have contention
// ["9ttgPBBhRYFuQccdR1DSnb7hydsWANoDsV3P9kaGMCEh",
// "EoW3SUQap7ZeynXQ2QJ847aerhxbPVr843uMeTfc9dxM",
// "4xgEmT58RwTNsF5xm2RMYCnR1EVukdK8a1i2qFjnJFu3",
// "B1mrQSpdeMU9gCvkJ6VsXVVoYjRGkNA7TtjMyqxrhecH",
// "aTtUk2DHgLhKZRDjePq6eiHRKC1XXFMBiSUfQ2JNDbN",
// "9n3d1K5YD2vECAbRFhFFGYNNjiXtHXJWn9F31t89vsAV",
// "ARTtviJkLLt6cHGQDydfo1Wyk6M4VGZdKZ2ZhdnJL336",
// "E2eSqe33tuhAHKTrwky5uEjaVqnb2T9ns6nHHUrN8588"]
println!("{:?}", tip_accounts);
}
#[tokio::test]
pub async fn send_bundle() {
let rpc_client = RpcClient::new(SERVER_URL.to_owned());
// Use your own keypair to sign
let signer_keypair = Keypair::new();
// Get the latest blockhash from solana cluster. Can use https://docs.solana.com/api/http#getlatestblockhash
let recent_blockhash = Hash::new_unique();
// Use the get_tip_accounts to randomly select a tip account to send tips to
let tip_account = Pubkey::try_from("DCN82qDxJAQuSqHhv2BJuAgi41SPeKZB5ioBCTMNDrCC").unwrap();
let mut bundle: Vec<_> = vec![VersionedTransaction::from(system_transaction::transfer(
@@ -354,7 +180,6 @@ mod rpc_client_tests {
recent_blockhash,
))];
// Add the tip
bundle.push(VersionedTransaction::from(system_transaction::transfer(
&signer_keypair,
&tip_account,
@@ -362,45 +187,15 @@ mod rpc_client_tests {
recent_blockhash,
)));
let response = rpc_client.send_bundle(&bundle).await;
// If successful, the bundle_id can be retrieved. Else, an error code will be provided
println!("{:?}", response);
}
#[tokio::test]
pub async fn get_bundle_statuses() {
let rpc_client = RpcClient::new(SERVER_URL.to_owned());
// Use the bundle id you got from send_bundle
let bundle_id =
"6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_owned();
let response = rpc_client.get_bundle_statuses(&[bundle_id]).await;
// Sample success output:
// Response {
// context: RpcResponseContext {
// slot: 0, api_version: None },
// value: [Object {
// "bundle_id": String("6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d"),
// "transactions": Array [String("4DGCuaKc2oue4Z8YC6mBwyg3oPAFG64BfxDtMbqDU3Du9zr26oVSuZcjSnJqTnHnKYFJ4AdPuq5kUrWKwTFLKtW6"),
// String("srrgfKABYeaKazZjBmpuPKySJ8qgqezYaCdDnB9nhED5CFhviZ1wgcs5vEKnAK9L2ytRauWG9czGoKRxajpZ1YR")],
// "slot": Number(240632575),
// "confirmation_status": String("finalized"),
// "err": Object {"Ok": Null}}] }
//
// Sample retryable error output:
// Response {
// context: RpcResponseContext {
// slot: 0, api_version: None },
// value: [Object {
// "bundle_id": String("6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d"),
// "transactions": Array [String("4DGCuaKc2oue4Z8YC6mBwyg3oPAFG64BfxDtMbqDU3Du9zr26oVSuZcjSnJqTnHnKYFJ4AdPuq5kUrWKwTFLKtW6"),
// String("srrgfKABYeaKazZjBmpuPKySJ8qgqezYaCdDnB9nhED5CFhviZ1wgcs5vEKnAK9L2ytRauWG9czGoKRxajpZ1YR")],
// "slot": Number(612529),
// "confirmation_status": Null,
// "err": Object {"Err": Object {"Retryable": String("Failed to retrieve information from solana cluster")}}}] }
// If unknown bundle, the response would be null
println!("{:?}", response);
}
}