use std::{str::FromStr, sync::Arc, time::Duration};
#[cfg(feature = "tpu")]
use {
indicatif::{MultiProgress, ProgressStyle},
solana_client::{
rpc_client::RpcClient as BlockingRpcClient,
rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS,
tpu_client::{TpuClient, TpuClientConfig, TpuSenderError},
},
solana_quic_client::{QuicConfig, QuicConnectionManager, QuicPool},
solana_sdk::signers::Signers,
std::{collections::HashMap, thread::sleep, time::Instant},
};
use futures_util::{lock::Mutex, stream::FuturesOrdered, StreamExt};
use indicatif::ProgressBar;
use ratelimit::Ratelimiter;
use serde::{Deserialize, Serialize};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction,
instruction::Instruction, message::Message, signature::Keypair, signature::Signature,
signer::Signer, transaction::Transaction,
};
use tracing::debug;
mod error;
use error::JibError;
const MAX_TX_LEN: usize = 1232;
const TX_BATCH_SIZE: usize = 10;
#[cfg(feature = "tpu")]
const SEND_TRANSACTION_INTERVAL: Duration = Duration::from_millis(10);
#[cfg(feature = "tpu")]
const TRANSACTION_RESEND_INTERVAL: Duration = Duration::from_secs(4);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Network {
#[default]
Devnet,
MainnetBeta,
Testnet,
Localnet,
}
impl FromStr for Network {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"devnet" => Ok(Network::Devnet),
"mainnet" => Ok(Network::MainnetBeta),
"testnet" => Ok(Network::Testnet),
"localnet" => Ok(Network::Localnet),
_ => Err(()),
}
}
}
impl std::fmt::Display for Network {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Network::Devnet => write!(f, "devnet"),
Network::MainnetBeta => write!(f, "mainnet"),
Network::Testnet => write!(f, "testnet"),
Network::Localnet => write!(f, "localnet"),
}
}
}
impl Network {
pub fn url(&self) -> &'static str {
match self {
Network::Devnet => "https://api.devnet.solana.com",
Network::MainnetBeta => "https://api.mainnet-beta.solana.com",
Network::Testnet => "https://api.testnet.solana.com",
Network::Localnet => "http://127.0.0.1:8899",
}
}
}
pub struct Jib {
#[cfg(feature = "tpu")]
tpu_client: Option<TpuClient<QuicPool, QuicConnectionManager, QuicConfig>>,
client: Arc<RpcClient>,
signers: Vec<Keypair>,
ixes: Vec<Instruction>,
compute_budget: u32,
priority_fee: u64,
batch_size: usize,
rate_limit: u64,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum JibResult {
Success(String),
Failure(JibFailedTransaction),
}
impl JibResult {
pub fn is_success(&self) -> bool {
match self {
JibResult::Success(_) => true,
JibResult::Failure(_) => false,
}
}
pub fn is_failure(&self) -> bool {
!self.is_success()
}
pub fn get_failure(self) -> Option<JibFailedTransaction> {
match self {
JibResult::Success(_) => None,
JibResult::Failure(f) => Some(f),
}
}
pub fn message(&self) -> Option<Message> {
match self {
JibResult::Success(_) => None,
JibResult::Failure(f) => Some(f.message.clone()),
}
}
pub fn error(&self) -> Option<String> {
match self {
JibResult::Success(_) => None,
JibResult::Failure(f) => Some(f.error.clone()),
}
}
pub fn signature(&self) -> Option<String> {
match self {
JibResult::Success(s) => Some(s.clone()),
JibResult::Failure(_) => None,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JibFailedTransaction {
pub signature: Signature,
pub message: Message,
pub error: String,
}
impl Jib {
pub fn new(signers: Vec<Keypair>, rpc_url: String) -> Result<Self, JibError> {
let client = Arc::new(RpcClient::new_with_commitment(
rpc_url,
CommitmentConfig::confirmed(),
));
Ok(Self {
#[cfg(feature = "tpu")]
tpu_client: None,
client,
signers,
ixes: Vec::new(),
compute_budget: 200_000,
priority_fee: 0,
batch_size: 10,
rate_limit: 10,
})
}
#[cfg(feature = "tpu")]
pub fn create_tpu_client(&mut self, url: &str) -> Result<(), JibError> {
let rpc_client = Arc::new(BlockingRpcClient::new_with_commitment(
url.to_string(),
CommitmentConfig::confirmed(),
));
let wss = url.replace("http", "ws");
let tpu_config = TpuClientConfig { fanout_slots: 1 };
let tpu_client = TpuClient::new(rpc_client, &wss, tpu_config)
.map_err(|e| JibError::FailedToCreateTpuClient(e.to_string()))?;
self.tpu_client = Some(tpu_client);
Ok(())
}
pub fn set_instructions(&mut self, ixes: Vec<Instruction>) {
self.ixes = ixes;
}
pub fn set_signers(&mut self, signers: Vec<Keypair>) {
self.signers = signers;
}
pub fn set_compute_budget(&mut self, compute_budget: u32) {
self.compute_budget = compute_budget;
}
pub fn set_priority_fee(&mut self, priority_fee: u64) {
self.priority_fee = priority_fee;
}
pub fn set_batch_size(&mut self, batch_size: usize) {
self.batch_size = batch_size;
}
pub fn set_rate_limit(&mut self, rate_limit: u64) {
self.rate_limit = rate_limit;
}
pub fn payer(&self) -> &Keypair {
self.signers.first().unwrap()
}
pub async fn retry_failed(
&mut self,
failed_transactions: Vec<JibFailedTransaction>,
) -> Result<Vec<JibResult>, JibError> {
let mut status_tasks = FuturesOrdered::new();
let retries = Arc::new(Mutex::new(Vec::new()));
let ratelimiter = Ratelimiter::builder(self.rate_limit, Duration::from_secs(1))
.max_tokens(self.rate_limit)
.initial_available(self.rate_limit)
.build()
.unwrap();
let pb = ProgressBar::new(failed_transactions.len() as u64);
pb.set_message("Checking failed transaction statuses...");
for tx in failed_transactions {
let pb = pb.clone();
let client = Arc::clone(&self.client);
let retries = Arc::clone(&retries);
if let Err(sleep) = ratelimiter.try_wait() {
tokio::time::sleep(sleep).await;
continue;
}
let task = tokio::spawn(async move {
let res = client.confirm_transaction(&tx.signature).await;
if res.is_err() || !res.unwrap() {
let mut retries = retries.lock().await;
retries.push(tx);
}
});
status_tasks.push_back(task);
pb.inc(1);
}
pb.finish();
while let Some(result) = status_tasks.next().await {
match result {
Ok(_) => {}
Err(e) => {
return Err(JibError::TransactionError(e.to_string()));
}
}
}
let retries_vec = Arc::try_unwrap(retries).unwrap().into_inner();
let mut send_tasks = FuturesOrdered::new();
let ratelimiter = Ratelimiter::builder(self.rate_limit, Duration::from_secs(1))
.max_tokens(self.rate_limit)
.initial_available(self.rate_limit)
.build()
.unwrap();
println!("Found {} failed transactions to retry", retries_vec.len());
let pb = ProgressBar::new(retries_vec.len() as u64);
pb.set_message("Resending transactions...");
for tx in retries_vec.into_iter() {
let signers: Vec<Keypair> = self.signers.iter().map(|k| k.insecure_clone()).collect();
let pb = pb.clone();
let client = Arc::clone(&self.client);
if let Err(sleep) = ratelimiter.try_wait() {
tokio::time::sleep(sleep).await;
continue;
}
let task = tokio::spawn(async move {
let signers_ref: Vec<&Keypair> = signers.iter().collect();
let mut tx = Transaction::new_unsigned(tx.message.clone());
let blockhash = client.get_latest_blockhash().await.unwrap();
tx.sign(&signers_ref, blockhash);
pb.inc(1);
let res = client.send_and_confirm_transaction(&tx.clone()).await;
match res {
Ok(signature) => JibResult::Success(signature.to_string()),
Err(e) => JibResult::Failure(JibFailedTransaction {
signature: tx.signatures[0],
message: tx.message.clone(),
error: e.to_string(),
}),
}
});
send_tasks.push_back(task);
}
pb.finish();
let mut results = Vec::new();
while let Some(result) = send_tasks.next().await {
match result {
Ok(result) => results.push(result),
Err(e) => {
return Err(JibError::TransactionError(e.to_string()));
}
}
}
Ok(results)
}
pub async fn repack_failed(
&mut self,
failed_transactions: Vec<JibFailedTransaction>,
) -> Result<Vec<Transaction>, JibError> {
let mut packed_transactions = Vec::new();
let mut tasks = FuturesOrdered::new();
let retries = Arc::new(Mutex::new(Vec::new()));
let signers: Vec<&Keypair> = self.signers.iter().map(|k| k as &Keypair).collect();
let ratelimiter = Ratelimiter::builder(self.rate_limit, Duration::from_secs(1))
.max_tokens(self.rate_limit)
.initial_available(self.rate_limit)
.build()
.unwrap();
let pb = ProgressBar::new(failed_transactions.len() as u64);
pb.set_message("Checking failed transaction statuses...");
for tx in failed_transactions {
let pb = pb.clone();
let client = Arc::clone(&self.client);
let retries = Arc::clone(&retries);
if let Err(sleep) = ratelimiter.try_wait() {
tokio::time::sleep(sleep).await;
continue;
}
let task = tokio::spawn(async move {
let res = client.confirm_transaction(&tx.signature).await;
if res.is_err() || !res.unwrap() {
let mut retries = retries.lock().await;
retries.push(tx);
}
});
tasks.push_back(task);
pb.inc(1);
}
pb.finish();
while let Some(result) = tasks.next().await {
match result {
Ok(_) => {}
Err(e) => {
return Err(JibError::TransactionError(e.to_string()));
}
}
}
let retries = retries.lock().await;
println!("Found {} failed transactions to retry", retries.len());
let pb = ProgressBar::new(retries.len() as u64);
pb.set_message("Repacking transactions...");
for tx in retries.iter() {
let mut tx = Transaction::new_unsigned(tx.message.clone());
let blockhash = self
.client
.get_latest_blockhash()
.await
.map_err(|_| JibError::NoRecentBlockhash)?;
tx.sign(&signers, blockhash);
packed_transactions.push(tx);
pb.inc(1);
}
pb.finish();
Ok(packed_transactions)
}
pub async fn hoist(&mut self) -> Result<Vec<JibResult>, JibError> {
if self.ixes.is_empty() {
return Err(JibError::NoInstructions);
}
let mut packed_transactions = Vec::new();
let mut results = Vec::new();
let mut instructions = Vec::new();
let payer_pubkey = self.signers.first().ok_or(JibError::NoSigners)?.pubkey();
if self.compute_budget != 200_000 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
self.compute_budget,
));
}
if self.priority_fee != 0 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
self.priority_fee,
));
}
let mut current_transaction =
Transaction::new_with_payer(&instructions, Some(&payer_pubkey));
let signers: Vec<&Keypair> = self.signers.iter().map(|k| k as &Keypair).collect();
let mut latest_blockhash = self
.client
.get_latest_blockhash()
.await
.map_err(|_| JibError::NoRecentBlockhash)?;
let mut ixes = self.ixes.clone();
for ix in ixes.iter_mut() {
instructions.push(ix.clone());
let mut tx = Transaction::new_with_payer(&instructions, Some(&payer_pubkey));
tx.sign(&signers, latest_blockhash);
let tx_len = bincode::serialize(&tx).unwrap().len();
debug!("tx_len: {}", tx_len);
if tx_len > MAX_TX_LEN || tx.message.account_keys.len() > 64 {
packed_transactions.push(current_transaction.clone());
debug!("Packed instructions: {}", instructions.len());
instructions = vec![];
if self.compute_budget != 200_000 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
self.compute_budget,
));
}
if self.priority_fee != 0 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
self.priority_fee,
));
}
instructions.push(ix.clone());
} else {
current_transaction = tx;
}
if packed_transactions.len() == TX_BATCH_SIZE {
results.extend(self._hoist(packed_transactions.clone()).await?);
packed_transactions.clear();
latest_blockhash = self
.client
.get_latest_blockhash()
.await
.map_err(|_| JibError::NoRecentBlockhash)?;
}
}
packed_transactions.push(current_transaction);
results.extend(self._hoist(packed_transactions.clone()).await?);
Ok(results)
}
pub async fn hoist_with_transactions(
&mut self,
packed_transactions: Vec<Transaction>,
) -> Result<Vec<JibResult>, JibError> {
let results = self._hoist(packed_transactions).await?;
Ok(results)
}
async fn _hoist(
&self,
packed_transactions: Vec<Transaction>,
) -> Result<Vec<JibResult>, JibError> {
let mut tasks = FuturesOrdered::new();
let ratelimiter = Ratelimiter::builder(self.rate_limit, Duration::from_secs(1))
.max_tokens(self.rate_limit)
.initial_available(self.rate_limit)
.build()
.unwrap();
let pb = ProgressBar::new(packed_transactions.len() as u64);
pb.set_message("Sending batch of transactions...");
for tx in packed_transactions {
let pb = pb.clone();
let client = Arc::clone(&self.client);
if let Err(sleep) = ratelimiter.try_wait() {
tokio::time::sleep(sleep).await;
continue;
}
let task = tokio::spawn(async move {
let res = client.send_and_confirm_transaction(&tx.clone()).await;
pb.inc(1);
match res {
Ok(signature) => JibResult::Success(signature.to_string()),
Err(e) => JibResult::Failure(JibFailedTransaction {
signature: tx.signatures[0],
message: tx.message.clone(),
error: e.to_string(),
}),
}
});
tasks.push_back(task);
}
let mut results = Vec::new();
while let Some(result) = tasks.next().await {
match result {
Ok(result) => results.push(result),
Err(e) => {
return Err(JibError::TransactionError(e.to_string()));
}
}
}
pb.finish_and_clear();
Ok(results)
}
pub async fn pack(&mut self) -> Result<Vec<Transaction>, JibError> {
if self.ixes.is_empty() {
return Err(JibError::NoInstructions);
}
let mut packed_transactions = Vec::new();
let mut instructions = Vec::new();
let payer_pubkey = self.signers.first().ok_or(JibError::NoSigners)?.pubkey();
if self.compute_budget != 200_000 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
self.compute_budget,
));
}
if self.priority_fee != 0 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
self.priority_fee,
));
}
let mut current_transaction =
Transaction::new_with_payer(&instructions, Some(&payer_pubkey));
let signers: Vec<&Keypair> = self.signers.iter().map(|k| k as &Keypair).collect();
let latest_blockhash = self
.client
.get_latest_blockhash()
.await
.map_err(|_| JibError::NoRecentBlockhash)?;
for ix in self.ixes.iter_mut() {
instructions.push(ix.clone());
let mut tx = Transaction::new_with_payer(&instructions, Some(&payer_pubkey));
tx.sign(&signers, latest_blockhash);
let tx_len = bincode::serialize(&tx).unwrap().len();
debug!("tx_len: {}", tx_len);
if tx_len > MAX_TX_LEN || tx.message.account_keys.len() > 64 {
packed_transactions.push(current_transaction.clone());
debug!("Packed instructions: {}", instructions.len());
instructions = vec![];
if self.compute_budget != 200_000 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
self.compute_budget,
));
}
if self.priority_fee != 0 {
instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
self.priority_fee,
));
}
instructions.push(ix.clone());
} else {
current_transaction = tx;
}
}
packed_transactions.push(current_transaction);
Ok(packed_transactions)
}
#[cfg(feature = "tpu")]
pub async fn hoist_via_tpu(&mut self) -> Result<Vec<JibResult>, JibError> {
let packed_transactions = self.pack().await?;
let results = self.submit_packed_transactions(packed_transactions).await?;
Ok(results)
}
#[cfg(feature = "tpu")]
pub async fn submit_packed_transactions(
&mut self,
transactions: Vec<Transaction>,
) -> Result<Vec<JibResult>, JibError> {
let signers: Vec<&Keypair> = self.signers.iter().map(|k| k as &Keypair).collect();
let messages = transactions
.as_slice()
.iter()
.map(|tx| tx.message.clone())
.collect::<Vec<_>>();
let mut results = vec![];
let mpb = MultiProgress::new();
let pb = mpb.add(ProgressBar::new(messages.len() as u64));
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.blue} {msg} {wide_bar:.cyan/blue} {pos:>7}/{len:7} {eta_precise}",
)
.unwrap(),
);
pb.set_message("Sending transactions");
for chunk in messages.chunks(self.batch_size) {
let res = self
.send_and_confirm_messages_with_spinner(chunk, &signers, &mpb)
.await
.map_err(|e| JibError::TransactionError(e.to_string()))?;
results.extend(res);
pb.inc(chunk.len() as u64);
}
Ok(results)
}
#[cfg(feature = "tpu")]
async fn send_and_confirm_messages_with_spinner<T: Signers>(
&self,
messages: &[Message],
signers: &T,
mpb: &MultiProgress,
) -> Result<Vec<JibResult>, TpuSenderError> {
let tpu_client = self.tpu_client.as_ref().ok_or(TpuSenderError::Custom(
"TPU client not initialized".to_string(),
))?;
let mut expired_blockhash_retries = 5;
let spinner = mpb.add(ProgressBar::new(42));
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {wide_msg}")
.unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(100));
let mut jib_results = Vec::with_capacity(messages.len());
let mut transactions = messages
.iter()
.enumerate()
.map(|(i, message)| (i, Transaction::new_unsigned(message.clone())))
.collect::<Vec<_>>();
let total_transactions = transactions.len();
let mut transaction_errors = vec![None; transactions.len()];
let mut confirmed_transactions = 0;
let mut block_height = self.client.get_block_height().await?;
while expired_blockhash_retries > 0 {
let (blockhash, last_valid_block_height) = self
.client
.get_latest_blockhash_with_commitment(self.client.commitment())
.await?;
let mut pending_transactions: HashMap<Signature, (usize, Transaction)> = HashMap::new();
for (i, ref mut transaction) in &mut transactions {
transaction.try_sign(signers, blockhash)?;
pending_transactions.insert(transaction.signatures[0], (*i, transaction.clone()));
}
let mut last_resend = Instant::now() - TRANSACTION_RESEND_INTERVAL;
while block_height <= last_valid_block_height {
let num_transactions = pending_transactions.len();
if Instant::now().duration_since(last_resend) > TRANSACTION_RESEND_INTERVAL {
for (index, (_i, transaction)) in pending_transactions.values().enumerate() {
if !tpu_client.send_transaction(transaction) {
let _result = self.client.send_transaction(transaction).await.ok();
}
set_message_for_confirmed_transactions(
&spinner,
confirmed_transactions,
total_transactions,
None, last_valid_block_height,
&format!("Sending {}/{} transactions", index + 1, num_transactions,),
);
sleep(SEND_TRANSACTION_INTERVAL);
}
last_resend = Instant::now();
}
let mut block_height_refreshes = 10;
set_message_for_confirmed_transactions(
&spinner,
confirmed_transactions,
total_transactions,
Some(block_height),
last_valid_block_height,
&format!("Waiting for next block, {} pending...", num_transactions),
);
let mut new_block_height = block_height;
while block_height == new_block_height && block_height_refreshes > 0 {
sleep(Duration::from_millis(500));
new_block_height = self.client.get_block_height().await?;
block_height_refreshes -= 1;
}
block_height = new_block_height;
let pending_signatures = pending_transactions.keys().cloned().collect::<Vec<_>>();
for pending_signatures_chunk in
pending_signatures.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS)
{
if let Ok(result) = self
.client
.get_signature_statuses(pending_signatures_chunk)
.await
{
let statuses = result.value;
for (signature, status) in
pending_signatures_chunk.iter().zip(statuses.into_iter())
{
if let Some(status) = status {
if status.satisfies_commitment(self.client.commitment()) {
if let Some((i, _)) = pending_transactions.remove(signature) {
confirmed_transactions += 1;
if status.err.is_some() {
spinner.println(format!(
"Failed transaction: {:?}",
status
));
jib_results.push(JibResult::Failure(
JibFailedTransaction {
signature: *signature,
message: transactions[i].1.message.clone(),
error: status.err.clone().unwrap().to_string(),
},
));
} else {
jib_results
.push(JibResult::Success(signature.to_string()));
}
transaction_errors[i] = status.err;
}
}
}
}
}
set_message_for_confirmed_transactions(
&spinner,
confirmed_transactions,
total_transactions,
Some(block_height),
last_valid_block_height,
"Checking transaction status...",
);
}
if pending_transactions.is_empty() {
return Ok(jib_results);
}
}
transactions = pending_transactions.into_values().collect();
spinner.println(format!(
"Blockhash expired. {} retries remaining",
expired_blockhash_retries
));
expired_blockhash_retries -= 1;
}
Err(TpuSenderError::Custom("Max retries exceeded".into()))
}
}
#[cfg(feature = "tpu")]
fn set_message_for_confirmed_transactions(
progress_bar: &ProgressBar,
confirmed_transactions: u32,
total_transactions: usize,
block_height: Option<u64>,
last_valid_block_height: u64,
status: &str,
) {
progress_bar.set_message(format!(
"{:>5.1}% | {:<40}{}",
confirmed_transactions as f64 * 100. / total_transactions as f64,
status,
match block_height {
Some(block_height) => format!(
" [block height {}; re-sign in {} blocks]",
block_height,
last_valid_block_height.saturating_sub(block_height),
),
None => String::new(),
},
));
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::signature::{Keypair, Signature};
use solana_sdk::signer::Signer;
use std::str::FromStr;
#[test]
fn test_network_from_str_valid() {
assert_eq!(Network::from_str("devnet"), Ok(Network::Devnet));
assert_eq!(Network::from_str("mainnet"), Ok(Network::MainnetBeta));
assert_eq!(Network::from_str("testnet"), Ok(Network::Testnet));
assert_eq!(Network::from_str("localnet"), Ok(Network::Localnet));
}
#[test]
fn test_network_from_str_invalid() {
assert!(Network::from_str("invalid").is_err());
assert!(Network::from_str("Devnet").is_err());
assert!(Network::from_str("").is_err());
}
#[test]
fn test_network_display_roundtrip() {
for variant in &[
Network::Devnet,
Network::MainnetBeta,
Network::Testnet,
Network::Localnet,
] {
let display = variant.to_string();
let parsed = Network::from_str(&display).expect("round-trip should succeed");
assert_eq!(*variant, parsed);
}
}
#[test]
fn test_network_url() {
assert_eq!(Network::Devnet.url(), "https://api.devnet.solana.com");
assert_eq!(
Network::MainnetBeta.url(),
"https://api.mainnet-beta.solana.com"
);
assert_eq!(Network::Testnet.url(), "https://api.testnet.solana.com");
assert_eq!(Network::Localnet.url(), "http://127.0.0.1:8899");
}
#[test]
fn test_network_default_is_devnet() {
assert_eq!(Network::default(), Network::Devnet);
}
fn make_success() -> JibResult {
JibResult::Success("somesig123".to_string())
}
fn make_failure() -> JibResult {
JibResult::Failure(JibFailedTransaction {
signature: Signature::default(),
message: Message::new(&[], None),
error: "something went wrong".to_string(),
})
}
#[test]
fn test_jib_result_is_success() {
assert!(make_success().is_success());
assert!(!make_failure().is_success());
}
#[test]
fn test_jib_result_is_failure() {
assert!(!make_success().is_failure());
assert!(make_failure().is_failure());
}
#[test]
fn test_jib_result_signature() {
assert_eq!(make_success().signature(), Some("somesig123".to_string()));
assert_eq!(make_failure().signature(), None);
}
#[test]
fn test_jib_result_error() {
assert_eq!(make_success().error(), None);
assert_eq!(
make_failure().error(),
Some("something went wrong".to_string())
);
}
#[test]
fn test_jib_result_message() {
assert!(make_success().message().is_none());
let msg = make_failure().message();
assert!(msg.is_some());
}
#[test]
fn test_jib_result_get_failure() {
assert!(make_success().get_failure().is_none());
let failed = make_failure().get_failure();
assert!(failed.is_some());
let failed = failed.unwrap();
assert_eq!(failed.error, "something went wrong");
}
#[test]
fn test_jib_new_defaults() {
let kp = Keypair::new();
let pubkey = kp.pubkey();
let jib = Jib::new(vec![kp], "http://localhost:8899".to_string())
.expect("Jib::new should succeed");
assert_eq!(jib.compute_budget, 200_000);
assert_eq!(jib.priority_fee, 0);
assert_eq!(jib.batch_size, 10);
assert_eq!(jib.rate_limit, 10);
assert_eq!(jib.payer().pubkey(), pubkey);
}
#[test]
fn test_jib_payer_returns_first_signer() {
let kp1 = Keypair::new();
let kp2 = Keypair::new();
let expected = kp1.pubkey();
let jib = Jib::new(vec![kp1, kp2], "http://localhost:8899".to_string()).unwrap();
assert_eq!(jib.payer().pubkey(), expected);
}
#[test]
fn test_jib_set_compute_budget() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
jib.set_compute_budget(400_000);
assert_eq!(jib.compute_budget, 400_000);
}
#[test]
fn test_jib_set_priority_fee() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
jib.set_priority_fee(5000);
assert_eq!(jib.priority_fee, 5000);
}
#[test]
fn test_jib_set_batch_size() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
jib.set_batch_size(25);
assert_eq!(jib.batch_size, 25);
}
#[test]
fn test_jib_set_rate_limit() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
jib.set_rate_limit(50);
assert_eq!(jib.rate_limit, 50);
}
#[test]
fn test_jib_set_instructions() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
assert!(jib.ixes.is_empty());
let ix = Instruction::new_with_bytes(solana_sdk::system_program::id(), &[], vec![]);
jib.set_instructions(vec![ix.clone(), ix]);
assert_eq!(jib.ixes.len(), 2);
}
#[test]
fn test_jib_set_signers() {
let kp1 = Keypair::new();
let mut jib = Jib::new(vec![kp1], "http://localhost:8899".to_string()).unwrap();
assert_eq!(jib.signers.len(), 1);
let kp2 = Keypair::new();
let kp3 = Keypair::new();
jib.set_signers(vec![kp2, kp3]);
assert_eq!(jib.signers.len(), 2);
}
#[test]
fn test_jib_new_empty_signers() {
let jib = Jib::new(vec![], "http://localhost:8899".to_string());
assert!(jib.is_ok());
}
#[test]
#[should_panic]
fn test_jib_payer_panics_with_no_signers() {
let jib = Jib::new(vec![], "http://localhost:8899".to_string()).unwrap();
let _ = jib.payer(); }
#[test]
fn test_jib_error_display_no_instructions() {
let err = JibError::NoInstructions;
assert_eq!(err.to_string(), "No instructions to hoist");
}
#[test]
fn test_jib_error_display_no_recent_blockhash() {
let err = JibError::NoRecentBlockhash;
assert_eq!(err.to_string(), "No recent blockhash");
}
#[test]
fn test_jib_error_display_no_signers() {
let err = JibError::NoSigners;
assert_eq!(err.to_string(), "No signers found");
}
#[test]
fn test_jib_error_display_transaction_error() {
let err = JibError::TransactionError("timeout".to_string());
assert_eq!(err.to_string(), "Transaction Error");
}
#[test]
fn test_jib_error_display_batch_transaction_error() {
let err = JibError::BatchTransactionError("batch fail".to_string());
assert_eq!(err.to_string(), "Send batch transaction failed: batch fail");
}
#[test]
fn test_jib_error_display_failed_to_create_tpu_client() {
let err = JibError::FailedToCreateTpuClient("connection refused".to_string());
assert_eq!(
err.to_string(),
"Failed to create the TPU client: connection refused"
);
}
#[tokio::test]
async fn test_hoist_no_instructions_returns_error() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
let result = jib.hoist().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), JibError::NoInstructions));
}
#[tokio::test]
async fn test_pack_no_instructions_returns_error() {
let kp = Keypair::new();
let mut jib = Jib::new(vec![kp], "http://localhost:8899".to_string()).unwrap();
let result = jib.pack().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), JibError::NoInstructions));
}
}