diff --git a/Cargo.lock b/Cargo.lock index 19e3756..bca7e73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2323,9 +2323,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -2666,6 +2666,7 @@ dependencies = [ "core_affinity", "drillx", "futures", + "indicatif", "num_cpus", "ore-api", "ore-utils", diff --git a/Cargo.toml b/Cargo.toml index 1f5fda9..8aa2f22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ core_affinity = "0.8.1" drillx = "2.0.0" # drillx = { git = "https://github.com/regolith-labs/drillx", branch = "master", features = ["solana"] } futures = "0.3.30" +indicatif = "0.17.8" num_cpus = "1.16.0" ore-api = "2.1.0" ore-utils = "2.1.0" @@ -53,6 +54,8 @@ spl-associated-token-account = { version = "^2.3", features = [ ] } tokio = "1.35.1" url = "2.5" +# tokio-tungstenite = "0.16" +# serde = { version = "1.0", features = ["derive"] } # [patch.crates-io] # drillx = { path = "../drillx/drillx" } diff --git a/src/args.rs b/src/args.rs index 24c3be6..8af4ff2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -105,6 +105,24 @@ pub struct MineArgs { default_value = "0" )] pub risk_time: u64, + + #[arg( + long, + short, + value_name = "EXTRA_FEE_DIFFICULTY", + help = "The min difficulty that the miner thinks deserves to pay more priority fee.", + default_value = "27" + )] + pub extra_fee_difficulty: u32, + + #[arg( + long, + short, + value_name = "EXTRA_FEE_PERCENT", + help = "The extra percentage that the miner thinks deserves to pay more priority fee. Integer range 0..100 inclusive and the final priority fee cannot exceed the priority fee cap.", + default_value = "0" + )] + pub extra_fee_percent: u64, } #[derive(Parser, Debug)] diff --git a/src/claim.rs b/src/claim.rs index c81b997..3f0cf28 100644 --- a/src/claim.rs +++ b/src/claim.rs @@ -99,7 +99,7 @@ impl Miner { &ore_api::consts::MINT_ADDRESS, &spl_token::id(), ); - self.send_and_confirm(&[ix], ComputeBudget::Dynamic, false, None) + self.send_and_confirm(&[ix], ComputeBudget::Fixed(400_000), false, None) .await .ok(); diff --git a/src/close.rs b/src/close.rs index 9af5f38..25f1ab2 100644 --- a/src/close.rs +++ b/src/close.rs @@ -37,7 +37,7 @@ impl Miner { // Submit close transaction let ix = ore_api::instruction::close(signer.pubkey()); - self.send_and_confirm(&[ix], ComputeBudget::Dynamic, false, None) + self.send_and_confirm(&[ix], ComputeBudget::Fixed(500_000), false, None) .await .ok(); } diff --git a/src/dynamic_fee.rs b/src/dynamic_fee.rs index f3ecb64..47a36a1 100644 --- a/src/dynamic_fee.rs +++ b/src/dynamic_fee.rs @@ -3,12 +3,20 @@ use crate::Miner; use ore_api::consts::BUS_ADDRESSES; use reqwest::Client; use serde_json::{json, Value}; + +use solana_sdk::pubkey::Pubkey; +use std::{collections::HashMap, str::FromStr}; + use solana_client::rpc_response::RpcPrioritizationFee; + use url::Url; +pub const DEFAULT_PRIORITY_FEE: u64 = 10_000; + enum FeeStrategy { Helius, Triton, + LOCAL, Alchemy, Quiknode, } @@ -36,7 +44,7 @@ impl Miner { } else if host.contains("rpcpool.com") { FeeStrategy::Triton } else { - return Err("Dynamic fees not supported by this RPC.".to_string()); + FeeStrategy::LOCAL }; // Build fee estimate request @@ -45,53 +53,46 @@ impl Miner { .chain(BUS_ADDRESSES.iter().map(|pubkey| pubkey.to_string())) .collect(); let body = match strategy { - FeeStrategy::Helius => { - json!({ - "jsonrpc": "2.0", - "id": "priority-fee-estimate", - "method": "getPriorityFeeEstimate", - "params": [{ - "accountKeys": ore_addresses, - "options": { - "recommended": true - } - }] - }) - } - FeeStrategy::Alchemy => { - json!({ - "jsonrpc": "2.0", - "id": "priority-fee-estimate", - "method": "getRecentPrioritizationFees", - "params": [ - ore_addresses - ] - }) - } - FeeStrategy::Quiknode => { - json!({ - "jsonrpc": "2.0", - "id": "1", - "method": "qn_estimatePriorityFees", - "params": { - "account": BUS_ADDRESSES[0].to_string(), - "last_n_blocks": 100 + FeeStrategy::Helius => Some(json!({ + "jsonrpc": "2.0", + "id": "priority-fee-estimate", + "method": "getPriorityFeeEstimate", + "params": [{ + "accountKeys": ore_addresses, + "options": { + "recommended": true } - }) - } - FeeStrategy::Triton => { - json!({ - "jsonrpc": "2.0", - "id": "priority-fee-estimate", - "method": "getRecentPrioritizationFees", - "params": [ - ore_addresses, - { - "percentile": 5000, - } - ] - }) - } + }] + })), + FeeStrategy::Alchemy => Some(json!({ + "jsonrpc": "2.0", + "id": "priority-fee-estimate", + "method": "getRecentPrioritizationFees", + "params": [ + ore_addresses + ] + })), + FeeStrategy::Quiknode => Some(json!({ + "jsonrpc": "2.0", + "id": "1", + "method": "qn_estimatePriorityFees", + "params": { + "account": "oreV2ZymfyeXgNgBdqMkumTqqAprVqgBWQfoYkrtKWQ", + "last_n_blocks": 100 + } + })), + FeeStrategy::Triton => Some(json!({ + "jsonrpc": "2.0", + "id": "priority-fee-estimate", + "method": "getRecentPrioritizationFees", + "params": [ + ore_addresses, + { + "percentile": 5000, + } + ] + })), + FeeStrategy::LOCAL => None, }; // // Send request in one step @@ -105,22 +106,25 @@ impl Miner { // .await // .unwrap(); - // MI, Send request in two steps - // split json from send - // 1) handle response - let Ok(resp) = client - .post(rpc_url) - .json(&body) - .send() - .await else { + // Send rpc request + let response = if let Some(body) = body { + // MI, Send request in two steps + // split json from send + // 1) handle response + let Ok(resp) = client.post(rpc_url).json(&body).send().await else { eprintln!("didn't get dynamic fee estimate, use default instead."); - return Ok(5000); + return Ok(DEFAULT_PRIORITY_FEE); }; - - // 2) handle json - let Ok(response) = resp.json::().await else { - eprintln!("didn't get json data from fee estimate response, use default instead."); - return Ok(5000); + + // 2) handle json + let Ok(response) = resp.json::().await else { + eprintln!("didn't get json data from fee estimate response, use default instead."); + return Ok(DEFAULT_PRIORITY_FEE); + }; + + response + } else { + Value::Null }; // Parse response @@ -158,7 +162,12 @@ impl Miner { "Failed to parse priority fee response: {response:?}, error: {error}" )) }) - } + }, + FeeStrategy::LOCAL => { + self.local_dynamic_fee().await.or_else(|err| { + Err(format!("Failed to parse priority fee response: {err}")) + }) + }, }; // Check if the calculated fee is higher than max @@ -173,6 +182,55 @@ impl Miner { } } } + + pub async fn local_dynamic_fee(&self) -> Result> { + let client = self.rpc_client.clone(); + let pubkey = [ + "oreV2ZymfyeXgNgBdqMkumTqqAprVqgBWQfoYkrtKWQ", + "5HngGmYzvSuh3XyU11brHDpMTHXQQRQQT4udGFtQSjgR", + "2oLNTQKRb4a2117kFi6BYTUDu3RPrMVAHFhCfPKMosxX", + ]; + let address_strings = pubkey; + + // Convert strings to Pubkey + let addresses: Vec = address_strings + .into_iter() + .map(|addr_str| Pubkey::from_str(addr_str).expect("Invalid address")) + .collect(); + + // Get recent prioritization fees + let recent_prioritization_fees = client.get_recent_prioritization_fees(&addresses).await?; + if recent_prioritization_fees.is_empty() { + return Err("No recent prioritization fees".into()); + } + let mut sorted_fees: Vec<_> = recent_prioritization_fees.into_iter().collect(); + sorted_fees.sort_by(|a, b| b.slot.cmp(&a.slot)); + let chunk_size = 150; + let chunks: Vec<_> = sorted_fees.chunks(chunk_size).take(3).collect(); + let mut percentiles: HashMap = HashMap::new(); + for (_, chunk) in chunks.iter().enumerate() { + let fees: Vec = chunk.iter().map(|fee| fee.prioritization_fee).collect(); + percentiles = Self::calculate_percentiles(&fees); + } + + // Default to 75 percentile + let fee = *percentiles.get(&75).unwrap_or(&0); + Ok(fee) + } + + fn calculate_percentiles(fees: &[u64]) -> HashMap { + let mut sorted_fees = fees.to_vec(); + sorted_fees.sort_unstable(); + let len = sorted_fees.len(); + let percentiles = vec![10, 25, 50, 60, 70, 75, 80, 85, 90, 100]; + percentiles + .into_iter() + .map(|p| { + let index = (p as f64 / 100.0 * len as f64).round() as usize; + (p, sorted_fees[index.saturating_sub(1)]) + }) + .collect() + } } /// Our estimate is the average over the last 20 slots diff --git a/src/mine.rs b/src/mine.rs index ee7157e..42a6403 100644 --- a/src/mine.rs +++ b/src/mine.rs @@ -37,6 +37,13 @@ enum ParallelStrategy { Threads(u64), } +pub struct DifficultyPayload { + pub solution_difficulty: u32, + pub expected_min_difficulty: u32, + pub extra_fee_difficulty: u32, + pub extra_fee_percent: u64, +} + impl Miner { pub async fn mine(&self, args: MineArgs) { // Open account, if needed. @@ -64,6 +71,8 @@ impl Miner { let nonce_checkpoint_step: u64 = args.nonce_checkpoint_step; let expected_min_difficulty: u32 = args.expected_min_difficulty; + let extra_fee_difficulty: u32 = args.extra_fee_difficulty; + let extra_fee_percent: u64 = args.extra_fee_percent; let risk_time: u64 = args.risk_time; // Start mining loop @@ -122,6 +131,13 @@ impl Miner { } }; + let difficulty_payload = DifficultyPayload { + solution_difficulty: solution.to_hash().difficulty(), + expected_min_difficulty, + extra_fee_difficulty, + extra_fee_percent, + }; + // Build instruction set let mut ixs = vec![ore_api::instruction::auth(proof_pubkey(signer.pubkey()))]; let mut compute_budget = 500_000; @@ -149,7 +165,8 @@ impl Miner { &ixs, ComputeBudget::Fixed(compute_budget), false, - Some(solution.to_hash().difficulty()), + // Some(solution.to_hash().difficulty()), + Some(difficulty_payload), ) .await .is_ok() diff --git a/src/open.rs b/src/open.rs index 8761975..78538e7 100644 --- a/src/open.rs +++ b/src/open.rs @@ -15,7 +15,7 @@ impl Miner { // Sign and send transaction. println!("Generating challenge..."); let ix = ore_api::instruction::open(signer.pubkey(), signer.pubkey(), fee_payer.pubkey()); - self.send_and_confirm(&[ix], ComputeBudget::Dynamic, false, None) + self.send_and_confirm(&[ix], ComputeBudget::Fixed(400_000), false, None) .await .ok(); } diff --git a/src/send_and_confirm.rs b/src/send_and_confirm.rs index e295264..95cb826 100644 --- a/src/send_and_confirm.rs +++ b/src/send_and_confirm.rs @@ -2,6 +2,8 @@ use std::time::Duration; use chrono::Local; use colored::*; +use indicatif::ProgressBar; +use ore_api::error::OreError; use solana_client::{ client_error::{ClientError, ClientErrorKind, Result as ClientResult}, rpc_config::RpcSendTransactionConfig, @@ -9,6 +11,7 @@ use solana_client::{ use solana_program::{ instruction::Instruction, native_token::{lamports_to_sol, sol_to_lamports}, + // system_instruction::transfer, pubkey::Pubkey, }; use solana_rpc_client::spinner; use solana_sdk::{ @@ -20,6 +23,7 @@ use solana_sdk::{ use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding}; use crate::Miner; +use crate::{mine::DifficultyPayload, utils::get_latest_blockhash_with_retries}; const MIN_SOL_BALANCE: f64 = 0.005; @@ -32,6 +36,7 @@ const CONFIRM_DELAY: u64 = 500; // MI, 0 in version 1 const GATEWAY_DELAY: u64 = 0; // MI, 300 in version 1 pub enum ComputeBudget { + #[allow(dead_code)] Dynamic, Fixed(u32), } @@ -42,11 +47,13 @@ impl Miner { ixs: &[Instruction], compute_budget: ComputeBudget, skip_confirm: bool, - difficulty: Option, // MI + difficulty_payload: Option, // MI ) -> ClientResult { + let progress_bar = spinner::new_progress_bar(); let signer = self.signer(); let client = self.rpc_client.clone(); let fee_payer = self.fee_payer(); + let send_client = self.rpc_client.clone(); // Return error, if balance is zero self.check_balance().await; @@ -55,8 +62,8 @@ impl Miner { let mut final_ixs = vec![]; match compute_budget { ComputeBudget::Dynamic => { - // TODO simulate - final_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)) + // final_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)) + todo!("simulate tx") } ComputeBudget::Fixed(cus) => { final_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(cus)) @@ -71,6 +78,36 @@ impl Miner { // Add in user instructions final_ixs.extend_from_slice(ixs); + // // Add jito tip + // let jito_tip = *self.tip.read().unwrap(); + // if jito_tip > 0 { + // send_client = self.jito_client.clone(); + // } + // if jito_tip > 0 { + // let tip_accounts = [ + // "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", + // "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", + // "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", + // "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", + // "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", + // "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", + // "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", + // "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", + // ]; + // final_ixs.push(transfer( + // &signer.pubkey(), + // &Pubkey::from_str( + // &tip_accounts + // .choose(&mut rand::thread_rng()) + // .unwrap() + // .to_string(), + // ) + // .unwrap(), + // jito_tip, + // )); + // progress_bar.println(format!(" Jito tip: {} SOL", lamports_to_sol(jito_tip))); + // } + // Build tx let send_cfg = RpcSendTransactionConfig { skip_preflight: true, @@ -82,7 +119,6 @@ impl Miner { let mut tx = Transaction::new_with_payer(&final_ixs, Some(&fee_payer.pubkey())); // Submit tx - let progress_bar = spinner::new_progress_bar(); let mut attempts = 0; loop { progress_bar.set_message(format!("Submitting transaction... (attempt {})", attempts,)); @@ -94,27 +130,48 @@ impl Miner { let fee = match self.dynamic_fee().await { Ok(fee) => { let mut prio_fee = fee; - // MI: upbound 300K for diff > 21 - if let Some(difficulty) = difficulty { - if difficulty > 21 { - prio_fee = - 300_000.min(prio_fee.saturating_mul(15).saturating_div(10)); - } else if difficulty < 18 { - // prio_fee = 5000.max(prio_fee.saturating_mul(2).saturating_div(3)); - // keep priority fee recommendation + // MI: calc uplimit of priority fee for precious fee difficulty, eg. diff > 27 + if let Some(DifficultyPayload { + solution_difficulty, + extra_fee_difficulty, + extra_fee_percent, + .. + }) = difficulty_payload + { + if solution_difficulty > extra_fee_difficulty { + prio_fee = if let Some(priority_fee) = self.priority_fee { + priority_fee.min( + prio_fee + .saturating_mul( + 100u64.saturating_add(extra_fee_percent), + ) + .saturating_div(100), + ) + } else { + // MI: not exceed 300K + 300_000.min( + prio_fee + .saturating_mul( + 100u64.saturating_add(extra_fee_percent), + ) + .saturating_div(100), + ) + } } } - progress_bar.println(format!(" Priority fee: {} microlamports", prio_fee)); + progress_bar + .println(format!(" Priority fee: {} microlamports", prio_fee)); prio_fee } Err(err) => { let fee = self.priority_fee.unwrap_or(0); - progress_bar.println(format!( - " {} {} Falling back to static value: {} microlamports", - "WARNING".bold().yellow(), - err, - fee - )); + log_warning( + &progress_bar, + &format!( + "{} Falling back to static value: {} microlamports", + err, fee + ), + ); fee } }; @@ -125,16 +182,19 @@ impl Miner { } // Resign the tx - // MI: use loop to retry, otherwise program stops with .await.unwrap() when failure - let (hash, _slot) = loop { - match client - .get_latest_blockhash_with_commitment(self.rpc_client.commitment()) - .await - { - Ok((hash, _slot)) => break (hash, _slot), - Err(_) => {} - } - }; + // MI: next line was born in ore-cli 2.2.1, later than loop section below + let (hash, _slot) = get_latest_blockhash_with_retries(&client).await?; + + // // MI: use loop to retry, otherwise program stops when .await.unwrap() failure + // let (hash, _slot) = loop { + // match client + // .get_latest_blockhash_with_commitment(self.rpc_client.commitment()) + // .await + // { + // Ok((hash, _slot)) => break (hash, _slot), + // Err(_) => std::thread::sleep(Duration::from_millis(500)), + // } + // }; if signer.pubkey() == fee_payer.pubkey() { tx.sign(&[&signer], hash); } else { @@ -143,7 +203,11 @@ impl Miner { } // Send transaction - match client.send_transaction_with_config(&tx, send_cfg).await { + attempts += 1; + match send_client + .send_transaction_with_config(&tx, send_cfg) + .await + { Ok(sig) => { // Skip confirmation if skip_confirm { @@ -152,24 +216,58 @@ impl Miner { } // Confirm transaction - for _ in 0..CONFIRM_RETRIES { + 'confirm: for _ in 0..CONFIRM_RETRIES { std::thread::sleep(Duration::from_millis(CONFIRM_DELAY)); match client.get_signature_statuses(&[sig]).await { Ok(signature_statuses) => { for status in signature_statuses.value { if let Some(status) = status { if let Some(err) = status.err { - progress_bar.finish_with_message(format!( - "{}: {}", - "ERROR".bold().red(), - err - )); - return Err(ClientError { - request: None, - kind: ClientErrorKind::Custom(err.to_string()), - }); - } - if let Some(confirmation) = status.confirmation_status { + match err { + // Instruction error + solana_sdk::transaction::TransactionError::InstructionError(_, err) => { + match err { + // Custom instruction error, parse into OreError + solana_program::instruction::InstructionError::Custom(err_code) => { + match err_code { + e if e == OreError::NeedsReset as u32 => { + attempts = 0; + log_error(&progress_bar, "Needs reset. Retrying...", false); + break 'confirm; + }, + _ => { + log_error(&progress_bar, &err.to_string(), true); + return Err(ClientError { + request: None, + kind: ClientErrorKind::Custom(err.to_string()), + }); + } + } + }, + + // Non custom instruction error, return + _ => { + log_error(&progress_bar, &err.to_string(), true); + return Err(ClientError { + request: None, + kind: ClientErrorKind::Custom(err.to_string()), + }); + } + } + }, + + // Non instruction error, return + _ => { + log_error(&progress_bar, &err.to_string(), true); + return Err(ClientError { + request: None, + kind: ClientErrorKind::Custom(err.to_string()), + }); + } + } + } else if let Some(confirmation) = + status.confirmation_status + { match confirmation { TransactionConfirmationStatus::Processed => {} TransactionConfirmationStatus::Confirmed @@ -199,11 +297,7 @@ impl Miner { // Handle confirmation errors Err(err) => { - progress_bar.set_message(format!( - "{}: {}", - "ERROR".bold().red(), - err.kind().to_string() - )); + log_error(&progress_bar, &err.kind().to_string(), false); } } } @@ -211,19 +305,14 @@ impl Miner { // Handle submit errors Err(err) => { - progress_bar.set_message(format!( - "{}: {}", - "ERROR".bold().red(), - err.kind().to_string() - )); + log_error(&progress_bar, &err.kind().to_string(), false); } } // Retry std::thread::sleep(Duration::from_millis(GATEWAY_DELAY)); - attempts += 1; - if attempts >= GATEWAY_RETRIES { - progress_bar.finish_with_message(format!("{}: Max retries", "ERROR".bold().red())); + if attempts > GATEWAY_RETRIES { + log_error(&progress_bar, "Max retries", true); return Err(ClientError { request: None, kind: ClientErrorKind::Custom("Max retries".into()), @@ -307,3 +396,15 @@ impl Miner { // } } } + +fn log_error(progress_bar: &ProgressBar, err: &str, finish: bool) { + if finish { + progress_bar.finish_with_message(format!("{} {}", "ERROR".bold().red(), err)); + } else { + progress_bar.println(format!(" {} {}", "ERROR".bold().red(), err)); + } +} + +fn log_warning(progress_bar: &ProgressBar, msg: &str) { + progress_bar.println(format!(" {} {}", "WARNING".bold().yellow(), msg)); +} diff --git a/src/upgrade.rs b/src/upgrade.rs index f40c0a8..6f35b48 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -105,7 +105,7 @@ impl Miner { &ore_api::consts::MINT_ADDRESS, &spl_token::id(), ); - self.send_and_confirm(&[ix], ComputeBudget::Dynamic, false, None) + self.send_and_confirm(&[ix], ComputeBudget::Fixed(500_000), false, None) .await .ok(); } diff --git a/src/utils.rs b/src/utils.rs index d2b09d5..e0e4b1d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,10 +11,16 @@ use ore_api::{ state::{Config, Proof, Treasury}, }; use ore_utils::AccountDeserialize; +// use serde::Deserialize; +use solana_client::client_error::{ClientError, ClientErrorKind}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::{pubkey::Pubkey, sysvar}; -use solana_sdk::clock::Clock; +use solana_sdk::{clock::Clock, hash::Hash}; use spl_associated_token_account::get_associated_token_address; +use tokio::time::sleep; + +pub const BLOCKHASH_QUERY_RETRIES: usize = 5; +pub const BLOCKHASH_QUERY_DELAY: u64 = 500; pub async fn _get_treasury(client: &RpcClient) -> Treasury { let data = client @@ -128,6 +134,33 @@ pub fn ask_confirm(question: &str) -> bool { } } +pub async fn get_latest_blockhash_with_retries( + client: &RpcClient, +) -> Result<(Hash, u64), ClientError> { + let mut attempts = 0; + + loop { + if let Ok((hash, slot)) = client + .get_latest_blockhash_with_commitment(client.commitment()) + .await + { + return Ok((hash, slot)); + } + + // Retry + sleep(Duration::from_millis(BLOCKHASH_QUERY_DELAY)).await; + attempts += 1; + if attempts >= BLOCKHASH_QUERY_RETRIES { + return Err(ClientError { + request: None, + kind: ClientErrorKind::Custom( + "Max retries reached for latest blockhash query".into(), + ), + }); + } + } +} + // MI pub fn play_sound() { match rodio::OutputStream::try_default() { @@ -151,3 +184,14 @@ pub fn proof_pubkey(authority: Pubkey) -> Pubkey { pub fn treasury_tokens_pubkey() -> Pubkey { get_associated_token_address(&TREASURY_ADDRESS, &MINT_ADDRESS) } + +// #[derive(Debug, Deserialize)] +// pub struct Tip { +// 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, +// }