From 35fe41430cb181a62bb78c444844148ee11212fc Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:09:02 +0900 Subject: [PATCH 01/12] add jito instruction --- Cargo.lock | 3 + bin/gas-bench/Cargo.toml | 3 + bin/gas-bench/src/jito/api.rs | 3 + bin/gas-bench/src/jito/client.rs | 21 ++ bin/gas-bench/src/jito/mod.rs | 7 + bin/gas-bench/src/jito/model.rs | 58 ++++++ bin/gas-bench/src/main.rs | 192 ++++++++++++++++-- bin/gas-bench/src/solana_client.rs | 111 ++++++++++ crates/gem_solana/src/models/jito.rs | 147 ++++++++++++++ crates/gem_solana/src/models/mod.rs | 1 + crates/gem_solana/src/provider/preload.rs | 21 +- crates/primitives/src/lib.rs | 2 +- .../src/transaction_load_metadata.rs | 18 ++ gemstone/src/lib.rs | 1 + gemstone/src/models/transaction.rs | 20 ++ gemstone/src/solana/mod.rs | 6 + 16 files changed, 596 insertions(+), 18 deletions(-) create mode 100644 bin/gas-bench/src/jito/api.rs create mode 100644 bin/gas-bench/src/jito/client.rs create mode 100644 bin/gas-bench/src/jito/mod.rs create mode 100644 bin/gas-bench/src/jito/model.rs create mode 100644 bin/gas-bench/src/solana_client.rs create mode 100644 crates/gem_solana/src/models/jito.rs create mode 100644 gemstone/src/solana/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 946a3a17b..126639a39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3137,12 +3137,15 @@ version = "1.0.0" dependencies = [ "clap", "gem_evm", + "gem_jsonrpc", + "gem_solana", "gemstone", "num-bigint", "prettytable-rs", "primitives", "reqwest 0.13.1", "serde", + "serde_json", "serde_serializers", "tokio", ] diff --git a/bin/gas-bench/Cargo.toml b/bin/gas-bench/Cargo.toml index c2eb94502..7239e56ad 100644 --- a/bin/gas-bench/Cargo.toml +++ b/bin/gas-bench/Cargo.toml @@ -10,9 +10,12 @@ tokio = { workspace = true } num-bigint = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } prettytable-rs = "^0.10" primitives = { path = "../../crates/primitives" } gemstone = { path = "../../gemstone", features = ["reqwest_provider"] } gem_evm = { path = "../../crates/gem_evm" } +gem_solana = { path = "../../crates/gem_solana", features = ["reqwest"] } +gem_jsonrpc = { path = "../../crates/gem_jsonrpc", features = ["client"] } serde_serializers = { path = "../../crates/serde_serializers" } diff --git a/bin/gas-bench/src/jito/api.rs b/bin/gas-bench/src/jito/api.rs new file mode 100644 index 000000000..be36af9fd --- /dev/null +++ b/bin/gas-bench/src/jito/api.rs @@ -0,0 +1,3 @@ +pub const JITO_TIP_FLOOR_URL: &str = "https://bundles.jito.wtf/api/v1/bundles/tip_floor"; +pub const JUPITER_PROGRAM: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; +pub const ORCA_WHIRLPOOL: &str = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"; diff --git a/bin/gas-bench/src/jito/client.rs b/bin/gas-bench/src/jito/client.rs new file mode 100644 index 000000000..dcd652e2b --- /dev/null +++ b/bin/gas-bench/src/jito/client.rs @@ -0,0 +1,21 @@ +use std::error::Error; + +use super::{JitoTipFloor, JitoTipFloorEntry, JITO_TIP_FLOOR_URL}; + +#[derive(Default)] +pub struct JitoClient { + client: reqwest::Client, +} + +impl JitoClient { + pub fn new() -> Self { + Self::default() + } + + pub async fn fetch_tip_floor(&self) -> Result> { + let response = self.client.get(JITO_TIP_FLOOR_URL).send().await?; + let entries: Vec = response.json().await?; + let entry = entries.first().ok_or("No tip floor data returned from Jito API")?; + Ok(JitoTipFloor::from_entry(entry)) + } +} diff --git a/bin/gas-bench/src/jito/mod.rs b/bin/gas-bench/src/jito/mod.rs new file mode 100644 index 000000000..07eba59ec --- /dev/null +++ b/bin/gas-bench/src/jito/mod.rs @@ -0,0 +1,7 @@ +mod api; +mod client; +mod model; + +pub use api::*; +pub use client::*; +pub use model::*; diff --git a/bin/gas-bench/src/jito/model.rs b/bin/gas-bench/src/jito/model.rs new file mode 100644 index 000000000..5e51923a3 --- /dev/null +++ b/bin/gas-bench/src/jito/model.rs @@ -0,0 +1,58 @@ +use serde::Deserialize; + +const LAMPORTS_PER_SOL: f64 = 1_000_000_000.0; +const MICRO_LAMPORTS_PER_LAMPORT: f64 = 1_000_000.0; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JitoTipFloorEntry { + pub landed_tips_25th_percentile: f64, + pub landed_tips_50th_percentile: f64, + pub landed_tips_75th_percentile: f64, +} + +#[derive(Debug)] +pub struct JitoTipFloor { + pub p25_lamports: u64, + pub p50_lamports: u64, + pub p75_lamports: u64, +} + +impl JitoTipFloor { + pub fn from_entry(entry: &JitoTipFloorEntry) -> Self { + Self { + p25_lamports: sol_to_lamports(entry.landed_tips_25th_percentile), + p50_lamports: sol_to_lamports(entry.landed_tips_50th_percentile), + p75_lamports: sol_to_lamports(entry.landed_tips_75th_percentile), + } + } +} + +pub fn sol_to_lamports(sol: f64) -> u64 { + (sol * LAMPORTS_PER_SOL) as u64 +} + +pub fn lamports_to_sol(lamports: u64) -> String { + let sol = lamports as f64 / LAMPORTS_PER_SOL; + if sol < 0.000001 { + format!("{:.9}", sol) + } else if sol < 0.001 { + format!("{:.6}", sol) + } else { + format!("{:.4}", sol) + } +} + +pub fn priority_fee_to_lamports(micro_lamports_per_cu: u64, compute_units: u64) -> u64 { + (micro_lamports_per_cu as u128 * compute_units as u128 / MICRO_LAMPORTS_PER_LAMPORT as u128) as u64 +} + +pub fn format_micro_lamports(micro_lamports: u64) -> String { + if micro_lamports >= MICRO_LAMPORTS_PER_LAMPORT as u64 { + format!("{:.2}M", micro_lamports as f64 / MICRO_LAMPORTS_PER_LAMPORT) + } else if micro_lamports >= 1_000 { + format!("{:.1}K", micro_lamports as f64 / 1_000.0) + } else { + format!("{}", micro_lamports) + } +} diff --git a/bin/gas-bench/src/main.rs b/bin/gas-bench/src/main.rs index 9fe1a266a..1bd944aa6 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -1,8 +1,10 @@ mod client; mod etherscan; mod gasflow; +mod jito; +mod solana_client; -use clap::Parser; +use clap::{Parser, ValueEnum}; use prettytable::{Cell, Row, Table, format}; use std::error::Error; use std::{collections::HashMap, sync::Arc, time::Duration}; @@ -12,10 +14,20 @@ use crate::{ client::{GemstoneClient, GemstoneFeeData}, etherscan::EtherscanClient, gasflow::GasflowClient, + jito::{JitoClient, JitoTipFloor}, + solana_client::{SolanaFeeData, SolanaGasClient}, }; +use gem_evm::ether_conv::EtherConv; +use crate::jito::{format_micro_lamports, lamports_to_sol, priority_fee_to_lamports}; use gemstone::alien::reqwest_provider::NativeProvider; use primitives::fee::FeePriority; +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum ChainMode { + Ethereum, + Solana, +} + #[derive(Debug, Clone)] struct SourceFeeDetail { source_name: String, @@ -31,40 +43,57 @@ struct SourceFeeDetail { author, version, about, - long_about = "A CLI tool to benchmark Ethereum gas fees from multiple sources.\n\ -It periodically fetches gas fee data from Gemstone (local Ethereum node), Etherscan API, and Gasflow API.\n\ -The data is then grouped by block number and displayed in a comparative table, showing slow, normal, and fast priority fees along with suggested base fees from each source.\n\ -This allows for easy visual comparison and benchmarking of gas fee estimations." + long_about = "A CLI tool to benchmark gas/priority fees from multiple sources.\n\ +It periodically fetches fee data and displays comparative tables.\n\n\ +For Ethereum: fetches from Gemstone (local node), Etherscan API, and Gasflow API.\n\ +For Solana: fetches priority fees via RPC and compares with Jito tip floor API." )] struct Cli { + /// Chain to benchmark + #[clap(short, long, value_enum, default_value_t = ChainMode::Ethereum)] + chain: ChainMode, + /// Enable debug logging #[arg(long, short, action = clap::ArgAction::SetTrue)] debug: bool, - /// The number of blocks to fetch + /// The number of blocks to fetch (Ethereum only) #[clap(short, long, default_value_t = 4)] blocks: u64, - /// The reward percentiles to fetch + /// The reward percentiles to fetch (Ethereum only) #[clap(short, long, value_delimiter = ',', default_value = "20,40,60")] reward_percentiles: Vec, - /// The minimum priority fee in wei (default: 0.01 Gwei) + /// The minimum priority fee in wei (Ethereum only, default: 0.01 Gwei) #[clap(short, long, default_value_t = 10000000)] min_priority_fee: u64, - /// The Etherscan API key + /// The Etherscan API key (Ethereum only) #[clap(long, env = "ETHERSCAN_API_KEY")] - etherscan_api_key: String, + etherscan_api_key: Option, + + /// Compute units for priority fee calculation (Solana only) + #[clap(long, default_value_t = 200_000)] + compute_units: u64, + + /// Skip Jito API comparison (Solana only) + #[clap(long, action = clap::ArgAction::SetTrue)] + skip_jito: bool, } -async fn run(args: Cli) -> Result<(), Box> { +async fn run_ethereum(args: Cli) -> Result<(), Box> { + let etherscan_api_key = args.etherscan_api_key.ok_or("Etherscan API key is required for Ethereum mode")?; + let mut ticker = interval(Duration::from_secs(6)); let native_provider = Arc::new(NativeProvider::new().set_debug(args.debug)); let mut last_printed_block_opt: Option = None; let mut block_data: HashMap> = HashMap::new(); - println!("gas-bench: with history blocks: {}, reward percentiles: {:?}", args.blocks, args.reward_percentiles); + println!( + "gas-bench [Ethereum]: with history blocks: {}, reward percentiles: {:?}", + args.blocks, args.reward_percentiles + ); loop { ticker.tick().await; @@ -74,7 +103,7 @@ async fn run(args: Cli) -> Result<(), Box> { let gemstone_client_clone = GemstoneClient::new(native_provider.clone()); let reward_percentiles_clone = args.reward_percentiles.clone(); - let etherscan_api_key_clone = args.etherscan_api_key.clone(); + let etherscan_api_key_clone = etherscan_api_key.clone(); let fee_history_future = gemstone_client_clone.fetch_base_priority_fees(args.blocks, reward_percentiles_clone, args.min_priority_fee); @@ -99,7 +128,6 @@ async fn run(args: Cli) -> Result<(), Box> { let mut normal = "N/A".to_string(); let mut fast = "N/A".to_string(); for fee_record in &data.priority_fees { - use gem_evm::ether_conv::EtherConv; match fee_record.priority { FeePriority::Slow => slow = EtherConv::to_gwei(&fee_record.value), FeePriority::Normal => normal = EtherConv::to_gwei(&fee_record.value), @@ -215,7 +243,141 @@ async fn run(args: Cli) -> Result<(), Box> { } } } - // Ok(()) // Loop is infinite, this is not reached +} + +async fn run_solana(args: Cli) -> Result<(), Box> { + let mut ticker = interval(Duration::from_secs(6)); + let native_provider = Arc::new(NativeProvider::new().set_debug(args.debug)); + let solana_client = SolanaGasClient::new(native_provider); + let jito_client = if args.skip_jito { None } else { Some(JitoClient::new()) }; + + let mut last_printed_slot: Option = None; + + println!("gas-bench [Solana]: monitoring priority fees and Jito tips"); + println!(" Compute units for fee calculation: {}", args.compute_units); + println!(" Jito comparison: {}", if jito_client.is_some() { "enabled" } else { "disabled" }); + println!(); + + loop { + ticker.tick().await; + + if args.debug { + eprintln!("gas-bench: fetching Solana fee data..."); + } + + let solana_future = solana_client.fetch_fee_data(); + let jito_future = async { + match &jito_client { + Some(client) => Some(client.fetch_tip_floor().await), + None => None, + } + }; + + let (solana_res, jito_res) = tokio::join!(solana_future, jito_future); + + match solana_res { + Ok(fee_data) => { + if last_printed_slot.is_some_and(|s| s >= fee_data.slot) { + continue; + } + + print_solana_fee_data(&fee_data, &jito_res, args.compute_units); + last_printed_slot = Some(fee_data.slot); + } + Err(e) => { + if args.debug { + eprintln!("gas-bench: Error fetching Solana data: {e:?}"); + } + } + } + } +} + +fn print_solana_fee_data(fee_data: &SolanaFeeData, jito_res: &Option>>, compute_units: u64) { + println!("\n--- Slot: {} ---", fee_data.slot); + + let accounts = [ + ("Jupiter", &fee_data.account_fees.jupiter), + ("Orca", &fee_data.account_fees.orca), + ("USDC", &fee_data.account_fees.usdc), + ]; + let active_accounts: Vec<&str> = accounts + .iter() + .filter(|(_, data)| data.as_ref().is_some_and(|d| d.count > 0)) + .map(|(name, _)| *name) + .collect(); + + if !active_accounts.is_empty() { + println!("Sampling: {} (avg: {} µL/CU)", active_accounts.join(", "), fee_data.raw_fees.avg); + } + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + + let has_jito = jito_res.as_ref().is_some_and(|r| r.is_ok()); + if has_jito { + table.add_row(Row::new(vec![ + Cell::new("Level"), + Cell::new("Priority Fee"), + Cell::new("Est. Jito Tip"), + Cell::new("Jito Floor"), + Cell::new("Diff %"), + ])); + } else { + table.add_row(Row::new(vec![Cell::new("Level"), Cell::new("Priority Fee"), Cell::new("Est. Jito Tip")])); + } + + let levels = [ + ("Slow", fee_data.priority_fees.slow, fee_data.jito_tips.slow), + ("Normal", fee_data.priority_fees.normal, fee_data.jito_tips.normal), + ("Fast", fee_data.priority_fees.fast, fee_data.jito_tips.fast), + ]; + + let jito_floors = if let Some(Ok(jito_data)) = jito_res { + vec![jito_data.p25_lamports, jito_data.p50_lamports, jito_data.p75_lamports] + } else { + vec![0, 0, 0] + }; + + for (i, (level, priority_fee, jito_estimate)) in levels.iter().enumerate() { + let priority_lamports = priority_fee_to_lamports(*priority_fee, compute_units); + let priority_display = format!("{} ({})", format_micro_lamports(*priority_fee), lamports_to_sol(priority_lamports)); + let jito_estimate_display = format!("{} ({})", jito_estimate, lamports_to_sol(*jito_estimate)); + + if has_jito { + let jito_floor = jito_floors[i]; + let jito_floor_display = format!("{} ({})", jito_floor, lamports_to_sol(jito_floor)); + let diff_pct = if jito_floor > 0 { + let diff = (*jito_estimate as f64 - jito_floor as f64) / jito_floor as f64 * 100.0; + format!("{:+.1}%", diff) + } else { + "N/A".to_string() + }; + + table.add_row(Row::new(vec![ + Cell::new(level), + Cell::new(&priority_display), + Cell::new(&jito_estimate_display), + Cell::new(&jito_floor_display), + Cell::new(&diff_pct), + ])); + } else { + table.add_row(Row::new(vec![Cell::new(level), Cell::new(&priority_display), Cell::new(&jito_estimate_display)])); + } + } + + table.printstd(); + + if let Some(Err(e)) = jito_res { + println!(" (Jito API error: {})", e); + } +} + +async fn run(args: Cli) -> Result<(), Box> { + match args.chain { + ChainMode::Ethereum => run_ethereum(args).await, + ChainMode::Solana => run_solana(args).await, + } } #[tokio::main] diff --git a/bin/gas-bench/src/solana_client.rs b/bin/gas-bench/src/solana_client.rs new file mode 100644 index 000000000..38f1b85a1 --- /dev/null +++ b/bin/gas-bench/src/solana_client.rs @@ -0,0 +1,111 @@ +use std::error::Error; +use std::sync::Arc; + +use gem_jsonrpc::client::JsonRpcClient; +use gem_solana::models::jito::{FeeStats, JitoTipEstimates, calculate_fee_stats, estimate_jito_tips}; +use gem_solana::models::prioritization_fee::SolanaPrioritizationFee; +use gemstone::alien::{AlienProvider, new_alien_client, reqwest_provider::NativeProvider}; +use primitives::Chain; +use serde_json::json; + +use crate::jito::{JUPITER_PROGRAM, ORCA_WHIRLPOOL}; + +pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + +const MIN_SLOW_FEE: u64 = 1_000; +const MIN_NORMAL_FEE: u64 = 10_000; +const MIN_FAST_FEE: u64 = 100_000; + +#[derive(Debug)] +pub struct SolanaFeeData { + pub slot: u64, + pub priority_fees: PriorityFees, + pub jito_tips: JitoTipEstimates, + pub raw_fees: FeeStats, + pub account_fees: AccountFeeStats, +} + +#[derive(Debug)] +pub struct PriorityFees { + pub slow: u64, + pub normal: u64, + pub fast: u64, +} + +#[derive(Debug, Default)] +pub struct AccountFeeStats { + pub jupiter: Option, + pub orca: Option, + pub usdc: Option, +} + +pub struct SolanaGasClient { + native_provider: Arc, +} + +impl SolanaGasClient { + pub fn new(native_provider: Arc) -> Self { + Self { native_provider } + } + + pub async fn fetch_fee_data(&self) -> Result> { + let endpoint = self.native_provider.get_endpoint(Chain::Solana)?; + let alien_client = new_alien_client(endpoint, self.native_provider.clone()); + let client: JsonRpcClient<_> = JsonRpcClient::new(alien_client); + + let slot: u64 = client.call("getSlot", json!([])).await?; + + let global_fees: Vec = client.call("getRecentPrioritizationFees", json!([])).await.unwrap_or_default(); + + let mut account_fees = AccountFeeStats::default(); + + for (account, name) in [(JUPITER_PROGRAM, "jupiter"), (ORCA_WHIRLPOOL, "orca"), (USDC_MINT, "usdc")] { + let fees: Vec = client.call("getRecentPrioritizationFees", json!([[account]])).await.unwrap_or_default(); + + if !fees.is_empty() { + let values: Vec = fees.iter().map(|f| f.prioritization_fee).collect(); + let stats = calculate_fee_stats(&values); + match name { + "jupiter" => account_fees.jupiter = Some(stats), + "orca" => account_fees.orca = Some(stats), + "usdc" => account_fees.usdc = Some(stats), + _ => {} + } + } + } + + let global_values: Vec = global_fees.iter().map(|f| f.prioritization_fee).collect(); + let raw_fees = calculate_fee_stats(&global_values); + + let effective_stats = get_best_fee_stats(&raw_fees, &account_fees); + let priority_fees = calculate_priority_fees(&effective_stats); + let jito_tips = estimate_jito_tips(&effective_stats); + + Ok(SolanaFeeData { + slot, + priority_fees, + jito_tips, + raw_fees, + account_fees, + }) + } +} + +fn get_best_fee_stats(global: &FeeStats, accounts: &AccountFeeStats) -> FeeStats { + accounts + .jupiter + .as_ref() + .filter(|a| a.count > 0 && a.avg > 0) + .or_else(|| accounts.orca.as_ref().filter(|a| a.count > 0 && a.avg > 0)) + .or_else(|| accounts.usdc.as_ref().filter(|a| a.count > 0 && a.avg > 0)) + .cloned() + .unwrap_or_else(|| global.clone()) +} + +fn calculate_priority_fees(stats: &FeeStats) -> PriorityFees { + PriorityFees { + slow: (stats.median as u64).max(MIN_SLOW_FEE), + normal: (stats.p75 as u64).max(MIN_NORMAL_FEE), + fast: (stats.p90 as u64).max(MIN_FAST_FEE), + } +} diff --git a/crates/gem_solana/src/models/jito.rs b/crates/gem_solana/src/models/jito.rs new file mode 100644 index 000000000..7883fc7e7 --- /dev/null +++ b/crates/gem_solana/src/models/jito.rs @@ -0,0 +1,147 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use primitives::{SolanaAccountMeta, SolanaInstruction}; + +use crate::SYSTEM_PROGRAM_ID; + +pub const JITO_TIP_MIN_LAMPORTS: u64 = 1_000; + +pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", + "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", +]; + +#[derive(Debug, Clone)] +pub struct JitoTipEstimates { + pub slow: u64, + pub normal: u64, + pub fast: u64, +} + +#[derive(Debug, Clone, Default)] +pub struct FeeStats { + pub median: i64, + pub p75: i64, + pub p90: i64, + pub avg: i64, + pub count: usize, +} + +pub fn estimate_jito_tips(stats: &FeeStats) -> JitoTipEstimates { + const BASE_SLOW: u64 = 1_000; + const BASE_NORMAL: u64 = 10_000; + const BASE_FAST: u64 = 30_000; + const REFERENCE_FEE: f64 = 5_000.0; + + let congestion_multiplier = if stats.avg > 0 { + let raw_multiplier = stats.avg as f64 / REFERENCE_FEE; + (raw_multiplier.sqrt()).clamp(1.0, 5.0) + } else { + 1.0 + }; + + JitoTipEstimates { + slow: ((BASE_SLOW as f64 * congestion_multiplier) as u64).max(JITO_TIP_MIN_LAMPORTS), + normal: ((BASE_NORMAL as f64 * congestion_multiplier) as u64).max(JITO_TIP_MIN_LAMPORTS), + fast: ((BASE_FAST as f64 * congestion_multiplier) as u64).max(JITO_TIP_MIN_LAMPORTS), + } +} + +fn percentile(sorted_values: &[i64], p: usize) -> i64 { + if sorted_values.is_empty() { + return 0; + } + let idx = (p * sorted_values.len() / 100).min(sorted_values.len() - 1); + sorted_values[idx] +} + +pub fn calculate_fee_stats(fees: &[i64]) -> FeeStats { + if fees.is_empty() { + return FeeStats::default(); + } + + let mut values = fees.to_vec(); + values.sort(); + + let count = values.len(); + let sum: i64 = values.iter().sum(); + + FeeStats { + median: percentile(&values, 50), + p75: percentile(&values, 75), + p90: percentile(&values, 90), + avg: sum / count as i64, + count, + } +} + +fn get_random_tip_account() -> &'static str { + let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0); + let index = (nanos as usize) % JITO_TIP_ACCOUNTS.len(); + JITO_TIP_ACCOUNTS[index] +} + +pub fn create_jito_tip_instruction_json(from: &str, lamports: u64) -> String { + let tip_account = get_random_tip_account(); + + let mut data = Vec::with_capacity(12); + data.extend_from_slice(&2u32.to_le_bytes()); + data.extend_from_slice(&lamports.to_le_bytes()); + + let instruction = SolanaInstruction { + program_id: SYSTEM_PROGRAM_ID.to_string(), + accounts: vec![ + SolanaAccountMeta { + pubkey: from.to_string(), + is_signer: true, + is_writable: true, + }, + SolanaAccountMeta { + pubkey: tip_account.to_string(), + is_signer: false, + is_writable: true, + }, + ], + data: bs58::encode(&data).into_string(), + }; + + serde_json::to_string(&instruction).unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_jito_tip_instruction_json() { + let from = "7g2rVN8fAAQdPh1mkajpvELqYa3gWvFXJsBLnKfEQfqy"; + let lamports = 10_000u64; + + let json = create_jito_tip_instruction_json(from, lamports); + let instruction: SolanaInstruction = serde_json::from_str(&json).unwrap(); + + assert_eq!(instruction.program_id, SYSTEM_PROGRAM_ID); + assert_eq!(instruction.accounts.len(), 2); + assert_eq!(instruction.accounts[0].pubkey, from); + assert!(instruction.accounts[0].is_signer); + assert!(instruction.accounts[0].is_writable); + assert!(JITO_TIP_ACCOUNTS.contains(&instruction.accounts[1].pubkey.as_str())); + assert!(!instruction.accounts[1].is_signer); + assert!(instruction.accounts[1].is_writable); + + let data = bs58::decode(&instruction.data).into_vec().unwrap(); + assert_eq!(data.len(), 12); + + let instruction_index = u32::from_le_bytes(data[0..4].try_into().unwrap()); + assert_eq!(instruction_index, 2); + + let decoded_lamports = u64::from_le_bytes(data[4..12].try_into().unwrap()); + assert_eq!(decoded_lamports, lamports); + } +} diff --git a/crates/gem_solana/src/models/mod.rs b/crates/gem_solana/src/models/mod.rs index ee258b4fb..10cd64412 100644 --- a/crates/gem_solana/src/models/mod.rs +++ b/crates/gem_solana/src/models/mod.rs @@ -1,6 +1,7 @@ pub mod balances; pub mod block; pub mod blockhash; +pub mod jito; pub mod prioritization_fee; pub mod rpc; pub mod stake; diff --git a/crates/gem_solana/src/provider/preload.rs b/crates/gem_solana/src/provider/preload.rs index b95a9d7d5..c9dee62a2 100644 --- a/crates/gem_solana/src/provider/preload.rs +++ b/crates/gem_solana/src/provider/preload.rs @@ -2,10 +2,12 @@ use async_trait::async_trait; use chain_traits::ChainTransactionLoad; use std::error::Error; +use crate::models::jito::{calculate_fee_stats, estimate_jito_tips}; use crate::provider::preload_mapper::{calculate_fee_rates, calculate_transaction_fee}; use gem_client::Client; use primitives::{ - AssetType, Chain, FeeRate, SolanaTokenProgramId, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput, + AssetType, Chain, FeeRate, SolanaJitoTips, SolanaTokenProgramId, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, + TransactionPreloadInput, }; use crate::rpc::client::SolanaClient; @@ -48,7 +50,10 @@ impl ChainTransactionLoad for SolanaClient { Ok(None) }; - let (block_hash, sender_token_address, recipient_token_address) = futures::try_join!(self.get_latest_blockhash(), sender_token_future, recipient_token_future)?; + let prioritization_fees_future = self.get_recent_prioritization_fees(); + + let (block_hash, sender_token_address, recipient_token_address, prioritization_fees) = + futures::try_join!(self.get_latest_blockhash(), sender_token_future, recipient_token_future, prioritization_fees_future)?; let token_program = match source_asset.asset_type { AssetType::SPL => Some(SolanaTokenProgramId::Token), @@ -56,11 +61,23 @@ impl ChainTransactionLoad for SolanaClient { _ => None, }; + let jito_tips = { + let values: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); + let stats = calculate_fee_stats(&values); + let estimates = estimate_jito_tips(&stats); + Some(SolanaJitoTips { + slow: estimates.slow, + normal: estimates.normal, + fast: estimates.fast, + }) + }; + Ok(TransactionLoadMetadata::Solana { sender_token_address, recipient_token_address, token_program, block_hash: block_hash.value.blockhash, + jito_tips, }) } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 7f241a185..53a265a17 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -221,7 +221,7 @@ pub use self::transaction_fee::{FeeOption, TransactionFee}; pub mod stake_type; pub use self::stake_type::{RedelegateData, StakeData, StakeType}; pub mod transaction_load_metadata; -pub use self::transaction_load_metadata::{HyperliquidOrder, TransactionLoadMetadata}; +pub use self::transaction_load_metadata::{HyperliquidOrder, SolanaJitoTips, TransactionLoadMetadata}; pub mod transaction_input_type; pub use self::transaction_input_type::{TransactionInputType, TransactionLoadData, TransactionLoadInput}; pub mod transfer_data_extra; diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index eb3e22797..7241480d7 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use typeshare::typeshare; use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData}; @@ -14,6 +15,15 @@ pub struct HyperliquidOrder { pub agent_private_key: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +#[serde(rename_all = "camelCase")] +pub struct SolanaJitoTips { + pub slow: u64, + pub normal: u64, + pub fast: u64, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum TransactionLoadMetadata { None, @@ -22,6 +32,7 @@ pub enum TransactionLoadMetadata { recipient_token_address: Option, token_program: Option, block_hash: String, + jito_tips: Option, }, Ton { sender_token_address: Option, @@ -177,4 +188,11 @@ impl TransactionLoadMetadata { _ => Err("Message bytes not available for this metadata type".into()), } } + + pub fn get_jito_tips(&self) -> Result, Box> { + match self { + TransactionLoadMetadata::Solana { jito_tips, .. } => Ok(jito_tips.clone()), + _ => Err("Jito tips not available for this metadata type".into()), + } + } } diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index 3fef0361a..eeae74893 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -13,6 +13,7 @@ pub mod payment; pub mod perpetual; pub mod signer; pub mod siwe; +pub mod solana; pub mod wallet_connect; use alien::AlienError; diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index c4807f758..7a6139178 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -350,6 +350,13 @@ pub struct GemTransactionData { pub metadata: GemTransactionLoadMetadata, } +#[derive(Debug, Clone, uniffi::Record)] +pub struct GemSolanaJitoTips { + pub slow: u64, + pub normal: u64, + pub fast: u64, +} + #[derive(Debug, Clone, uniffi::Enum)] pub enum GemTransactionLoadMetadata { None, @@ -358,6 +365,7 @@ pub enum GemTransactionLoadMetadata { recipient_token_address: Option, token_program: Option, block_hash: String, + jito_tips: Option, }, Ton { sender_token_address: Option, @@ -440,11 +448,17 @@ impl From for GemTransactionLoadMetadata { recipient_token_address, token_program, block_hash, + jito_tips, } => GemTransactionLoadMetadata::Solana { sender_token_address, recipient_token_address, token_program, block_hash, + jito_tips: jito_tips.map(|t| GemSolanaJitoTips { + slow: t.slow, + normal: t.normal, + fast: t.fast, + }), }, TransactionLoadMetadata::Ton { sender_token_address, @@ -528,11 +542,17 @@ impl From for TransactionLoadMetadata { recipient_token_address, token_program, block_hash, + jito_tips, } => TransactionLoadMetadata::Solana { sender_token_address, recipient_token_address, token_program, block_hash, + jito_tips: jito_tips.map(|t| primitives::SolanaJitoTips { + slow: t.slow, + normal: t.normal, + fast: t.fast, + }), }, GemTransactionLoadMetadata::Ton { sender_token_address, diff --git a/gemstone/src/solana/mod.rs b/gemstone/src/solana/mod.rs new file mode 100644 index 000000000..7c54c8ae7 --- /dev/null +++ b/gemstone/src/solana/mod.rs @@ -0,0 +1,6 @@ +use gem_solana::models::jito; + +#[uniffi::export] +pub fn solana_create_jito_tip_instruction(from: String, lamports: u64) -> String { + jito::create_jito_tip_instruction_json(&from, lamports) +} From 93dbae2445b0b6dba3cca5ea932132f0e5e9856f Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:38:36 +0900 Subject: [PATCH 02/12] make jito_tips non optional --- crates/gem_solana/src/provider/preload.rs | 4 ++-- .../src/transaction_load_metadata.rs | 4 ++-- gemstone/src/models/transaction.rs | 22 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/gem_solana/src/provider/preload.rs b/crates/gem_solana/src/provider/preload.rs index c9dee62a2..1ec64443d 100644 --- a/crates/gem_solana/src/provider/preload.rs +++ b/crates/gem_solana/src/provider/preload.rs @@ -65,11 +65,11 @@ impl ChainTransactionLoad for SolanaClient { let values: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); let stats = calculate_fee_stats(&values); let estimates = estimate_jito_tips(&stats); - Some(SolanaJitoTips { + SolanaJitoTips { slow: estimates.slow, normal: estimates.normal, fast: estimates.fast, - }) + } }; Ok(TransactionLoadMetadata::Solana { diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index 7241480d7..aeb7eadf4 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -32,7 +32,7 @@ pub enum TransactionLoadMetadata { recipient_token_address: Option, token_program: Option, block_hash: String, - jito_tips: Option, + jito_tips: SolanaJitoTips, }, Ton { sender_token_address: Option, @@ -189,7 +189,7 @@ impl TransactionLoadMetadata { } } - pub fn get_jito_tips(&self) -> Result, Box> { + pub fn get_jito_tips(&self) -> Result> { match self { TransactionLoadMetadata::Solana { jito_tips, .. } => Ok(jito_tips.clone()), _ => Err("Jito tips not available for this metadata type".into()), diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 7a6139178..803e2818d 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -365,7 +365,7 @@ pub enum GemTransactionLoadMetadata { recipient_token_address: Option, token_program: Option, block_hash: String, - jito_tips: Option, + jito_tips: GemSolanaJitoTips, }, Ton { sender_token_address: Option, @@ -454,11 +454,11 @@ impl From for GemTransactionLoadMetadata { recipient_token_address, token_program, block_hash, - jito_tips: jito_tips.map(|t| GemSolanaJitoTips { - slow: t.slow, - normal: t.normal, - fast: t.fast, - }), + jito_tips: GemSolanaJitoTips { + slow: jito_tips.slow, + normal: jito_tips.normal, + fast: jito_tips.fast, + }, }, TransactionLoadMetadata::Ton { sender_token_address, @@ -548,11 +548,11 @@ impl From for TransactionLoadMetadata { recipient_token_address, token_program, block_hash, - jito_tips: jito_tips.map(|t| primitives::SolanaJitoTips { - slow: t.slow, - normal: t.normal, - fast: t.fast, - }), + jito_tips: primitives::SolanaJitoTips { + slow: jito_tips.slow, + normal: jito_tips.normal, + fast: jito_tips.fast, + }, }, GemTransactionLoadMetadata::Ton { sender_token_address, From cf7c358cc240d0d963d72d208e2eb5760e29ba93 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:16:23 +0900 Subject: [PATCH 03/12] Update model.rs --- bin/gas-bench/src/jito/model.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/gas-bench/src/jito/model.rs b/bin/gas-bench/src/jito/model.rs index 5e51923a3..412dd4bc2 100644 --- a/bin/gas-bench/src/jito/model.rs +++ b/bin/gas-bench/src/jito/model.rs @@ -4,7 +4,6 @@ const LAMPORTS_PER_SOL: f64 = 1_000_000_000.0; const MICRO_LAMPORTS_PER_LAMPORT: f64 = 1_000_000.0; #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct JitoTipFloorEntry { pub landed_tips_25th_percentile: f64, pub landed_tips_50th_percentile: f64, From 51b3746cb59c53ed7cb1865db72bb969d2157b96 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:23:29 +0900 Subject: [PATCH 04/12] adjust estimate_jito_tips algo --- crates/gem_solana/src/models/jito.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/gem_solana/src/models/jito.rs b/crates/gem_solana/src/models/jito.rs index 7883fc7e7..02a15e568 100644 --- a/crates/gem_solana/src/models/jito.rs +++ b/crates/gem_solana/src/models/jito.rs @@ -35,13 +35,13 @@ pub struct FeeStats { pub fn estimate_jito_tips(stats: &FeeStats) -> JitoTipEstimates { const BASE_SLOW: u64 = 1_000; - const BASE_NORMAL: u64 = 10_000; - const BASE_FAST: u64 = 30_000; - const REFERENCE_FEE: f64 = 5_000.0; + const BASE_NORMAL: u64 = 3_000; + const BASE_FAST: u64 = 10_000; + const REFERENCE_FEE: f64 = 10_000.0; let congestion_multiplier = if stats.avg > 0 { let raw_multiplier = stats.avg as f64 / REFERENCE_FEE; - (raw_multiplier.sqrt()).clamp(1.0, 5.0) + (1.0 + raw_multiplier.sqrt()).clamp(1.0, 10.0) } else { 1.0 }; From 799a8476dc36f512ffa8be7dddf95a86cbb49c4f Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:13:59 +0900 Subject: [PATCH 05/12] move jito tips to GasPriceType --- bin/gas-bench/src/jito/client.rs | 2 +- bin/gas-bench/src/main.rs | 2 +- crates/gem_solana/src/provider/preload.rs | 21 ++------------- .../gem_solana/src/provider/preload_mapper.rs | 12 +++++++++ crates/primitives/src/gas_price_type.rs | 27 ++++++++++++++++--- crates/primitives/src/lib.rs | 2 +- .../src/transaction_load_metadata.rs | 18 ------------- gemstone/src/models/gateway.rs | 18 ++++++++++--- gemstone/src/models/transaction.rs | 22 ++------------- 9 files changed, 57 insertions(+), 67 deletions(-) diff --git a/bin/gas-bench/src/jito/client.rs b/bin/gas-bench/src/jito/client.rs index dcd652e2b..76a255305 100644 --- a/bin/gas-bench/src/jito/client.rs +++ b/bin/gas-bench/src/jito/client.rs @@ -1,6 +1,6 @@ use std::error::Error; -use super::{JitoTipFloor, JitoTipFloorEntry, JITO_TIP_FLOOR_URL}; +use super::{JITO_TIP_FLOOR_URL, JitoTipFloor, JitoTipFloorEntry}; #[derive(Default)] pub struct JitoClient { diff --git a/bin/gas-bench/src/main.rs b/bin/gas-bench/src/main.rs index 1bd944aa6..49b523c3d 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -10,6 +10,7 @@ use std::error::Error; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::time::interval; +use crate::jito::{format_micro_lamports, lamports_to_sol, priority_fee_to_lamports}; use crate::{ client::{GemstoneClient, GemstoneFeeData}, etherscan::EtherscanClient, @@ -18,7 +19,6 @@ use crate::{ solana_client::{SolanaFeeData, SolanaGasClient}, }; use gem_evm::ether_conv::EtherConv; -use crate::jito::{format_micro_lamports, lamports_to_sol, priority_fee_to_lamports}; use gemstone::alien::reqwest_provider::NativeProvider; use primitives::fee::FeePriority; diff --git a/crates/gem_solana/src/provider/preload.rs b/crates/gem_solana/src/provider/preload.rs index 1ec64443d..b95a9d7d5 100644 --- a/crates/gem_solana/src/provider/preload.rs +++ b/crates/gem_solana/src/provider/preload.rs @@ -2,12 +2,10 @@ use async_trait::async_trait; use chain_traits::ChainTransactionLoad; use std::error::Error; -use crate::models::jito::{calculate_fee_stats, estimate_jito_tips}; use crate::provider::preload_mapper::{calculate_fee_rates, calculate_transaction_fee}; use gem_client::Client; use primitives::{ - AssetType, Chain, FeeRate, SolanaJitoTips, SolanaTokenProgramId, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, - TransactionPreloadInput, + AssetType, Chain, FeeRate, SolanaTokenProgramId, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput, }; use crate::rpc::client::SolanaClient; @@ -50,10 +48,7 @@ impl ChainTransactionLoad for SolanaClient { Ok(None) }; - let prioritization_fees_future = self.get_recent_prioritization_fees(); - - let (block_hash, sender_token_address, recipient_token_address, prioritization_fees) = - futures::try_join!(self.get_latest_blockhash(), sender_token_future, recipient_token_future, prioritization_fees_future)?; + let (block_hash, sender_token_address, recipient_token_address) = futures::try_join!(self.get_latest_blockhash(), sender_token_future, recipient_token_future)?; let token_program = match source_asset.asset_type { AssetType::SPL => Some(SolanaTokenProgramId::Token), @@ -61,23 +56,11 @@ impl ChainTransactionLoad for SolanaClient { _ => None, }; - let jito_tips = { - let values: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); - let stats = calculate_fee_stats(&values); - let estimates = estimate_jito_tips(&stats); - SolanaJitoTips { - slow: estimates.slow, - normal: estimates.normal, - fast: estimates.fast, - } - }; - Ok(TransactionLoadMetadata::Solana { sender_token_address, recipient_token_address, token_program, block_hash: block_hash.value.blockhash, - jito_tips, }) } diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 995704d71..3b5f2d56d 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -2,6 +2,7 @@ use num_bigint::BigInt; use primitives::{AssetSubtype, Chain, FeeOption, FeePriority, FeeRate, GasPriceType, TransactionFee, TransactionInputType}; use std::collections::HashMap; +use crate::models::jito::{calculate_fee_stats, estimate_jito_tips}; use crate::{constants::STATIC_BASE_FEE, models::prioritization_fee::SolanaPrioritizationFee}; pub fn calculate_transaction_fee(input_type: &TransactionInputType, gas_price_type: &GasPriceType, recipient_token_address: Option) -> TransactionFee { @@ -89,6 +90,10 @@ pub fn calculate_fee_rates(input_type: &TransactionInputType, prioritization_fee let gas_limit = get_gas_limit(input_type); + let all_fees: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); + let fee_stats = calculate_fee_stats(&all_fees); + let jito_tips = estimate_jito_tips(&fee_stats); + [FeePriority::Slow, FeePriority::Normal, FeePriority::Fast] .iter() .map(|priority| { @@ -98,12 +103,19 @@ pub fn calculate_fee_rates(input_type: &TransactionInputType, prioritization_fee FeePriority::Fast => &priority_fee_base * 3, }; + let jito_tip = match priority { + FeePriority::Slow => jito_tips.slow, + FeePriority::Normal => jito_tips.normal, + FeePriority::Fast => jito_tips.fast, + }; + FeeRate::new( *priority, GasPriceType::solana( static_base_fee.clone(), (priority_fee.clone() * gas_limit.clone()) / BigInt::from(1_000_000), priority_fee.clone(), + jito_tip, ), ) }) diff --git a/crates/primitives/src/gas_price_type.rs b/crates/primitives/src/gas_price_type.rs index fdf18c694..fd77645a8 100644 --- a/crates/primitives/src/gas_price_type.rs +++ b/crates/primitives/src/gas_price_type.rs @@ -3,9 +3,19 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum GasPriceType { - Regular { gas_price: BigInt }, - Eip1559 { gas_price: BigInt, priority_fee: BigInt }, - Solana { gas_price: BigInt, priority_fee: BigInt, unit_price: BigInt }, + Regular { + gas_price: BigInt, + }, + Eip1559 { + gas_price: BigInt, + priority_fee: BigInt, + }, + Solana { + gas_price: BigInt, + priority_fee: BigInt, + unit_price: BigInt, + jito_tip: u64, + }, } impl GasPriceType { @@ -20,11 +30,12 @@ impl GasPriceType { } } - pub fn solana, U: Into, V: Into>(gas_price: T, priority_fee: U, unit_price: V) -> Self { + pub fn solana, U: Into, V: Into>(gas_price: T, priority_fee: U, unit_price: V, jito_tip: u64) -> Self { Self::Solana { gas_price: gas_price.into(), priority_fee: priority_fee.into(), unit_price: unit_price.into(), + jito_tip, } } @@ -52,6 +63,14 @@ impl GasPriceType { } } + pub fn jito_tip(&self) -> u64 { + match self { + GasPriceType::Regular { .. } => 0, + GasPriceType::Eip1559 { .. } => 0, + GasPriceType::Solana { jito_tip, .. } => *jito_tip, + } + } + pub fn total_fee(&self) -> BigInt { self.gas_price() + self.priority_fee() } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 53a265a17..7f241a185 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -221,7 +221,7 @@ pub use self::transaction_fee::{FeeOption, TransactionFee}; pub mod stake_type; pub use self::stake_type::{RedelegateData, StakeData, StakeType}; pub mod transaction_load_metadata; -pub use self::transaction_load_metadata::{HyperliquidOrder, SolanaJitoTips, TransactionLoadMetadata}; +pub use self::transaction_load_metadata::{HyperliquidOrder, TransactionLoadMetadata}; pub mod transaction_input_type; pub use self::transaction_input_type::{TransactionInputType, TransactionLoadData, TransactionLoadInput}; pub mod transfer_data_extra; diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index aeb7eadf4..eb3e22797 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use typeshare::typeshare; use crate::{UTXO, solana_token_program::SolanaTokenProgramId, stake_type::StakeData}; @@ -15,15 +14,6 @@ pub struct HyperliquidOrder { pub agent_private_key: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[typeshare(swift = "Equatable, Hashable, Sendable")] -#[serde(rename_all = "camelCase")] -pub struct SolanaJitoTips { - pub slow: u64, - pub normal: u64, - pub fast: u64, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub enum TransactionLoadMetadata { None, @@ -32,7 +22,6 @@ pub enum TransactionLoadMetadata { recipient_token_address: Option, token_program: Option, block_hash: String, - jito_tips: SolanaJitoTips, }, Ton { sender_token_address: Option, @@ -188,11 +177,4 @@ impl TransactionLoadMetadata { _ => Err("Message bytes not available for this metadata type".into()), } } - - pub fn get_jito_tips(&self) -> Result> { - match self { - TransactionLoadMetadata::Solana { jito_tips, .. } => Ok(jito_tips.clone()), - _ => Err("Jito tips not available for this metadata type".into()), - } - } } diff --git a/gemstone/src/models/gateway.rs b/gemstone/src/models/gateway.rs index 069bdbfd7..e8f76fec4 100644 --- a/gemstone/src/models/gateway.rs +++ b/gemstone/src/models/gateway.rs @@ -20,9 +20,19 @@ pub struct BroadcastOptions { #[derive(Debug, Clone, uniffi::Enum)] pub enum GemGasPriceType { - Regular { gas_price: String }, - Eip1559 { gas_price: String, priority_fee: String }, - Solana { gas_price: String, priority_fee: String, unit_price: String }, + Regular { + gas_price: String, + }, + Eip1559 { + gas_price: String, + priority_fee: String, + }, + Solana { + gas_price: String, + priority_fee: String, + unit_price: String, + jito_tip: u64, + }, } #[derive(Debug, Clone, uniffi::Record)] @@ -50,10 +60,12 @@ impl From for GemGasPriceType { gas_price, priority_fee, unit_price, + jito_tip, } => GemGasPriceType::Solana { gas_price: gas_price.to_string(), priority_fee: priority_fee.to_string(), unit_price: unit_price.to_string(), + jito_tip, }, } } diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 803e2818d..1a6dd21a6 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -350,13 +350,6 @@ pub struct GemTransactionData { pub metadata: GemTransactionLoadMetadata, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct GemSolanaJitoTips { - pub slow: u64, - pub normal: u64, - pub fast: u64, -} - #[derive(Debug, Clone, uniffi::Enum)] pub enum GemTransactionLoadMetadata { None, @@ -365,7 +358,6 @@ pub enum GemTransactionLoadMetadata { recipient_token_address: Option, token_program: Option, block_hash: String, - jito_tips: GemSolanaJitoTips, }, Ton { sender_token_address: Option, @@ -448,17 +440,11 @@ impl From for GemTransactionLoadMetadata { recipient_token_address, token_program, block_hash, - jito_tips, } => GemTransactionLoadMetadata::Solana { sender_token_address, recipient_token_address, token_program, block_hash, - jito_tips: GemSolanaJitoTips { - slow: jito_tips.slow, - normal: jito_tips.normal, - fast: jito_tips.fast, - }, }, TransactionLoadMetadata::Ton { sender_token_address, @@ -542,17 +528,11 @@ impl From for TransactionLoadMetadata { recipient_token_address, token_program, block_hash, - jito_tips, } => TransactionLoadMetadata::Solana { sender_token_address, recipient_token_address, token_program, block_hash, - jito_tips: primitives::SolanaJitoTips { - slow: jito_tips.slow, - normal: jito_tips.normal, - fast: jito_tips.fast, - }, }, GemTransactionLoadMetadata::Ton { sender_token_address, @@ -751,10 +731,12 @@ impl From for GasPriceType { gas_price, priority_fee, unit_price, + jito_tip, } => GasPriceType::Solana { gas_price: gas_price.parse().unwrap_or_default(), priority_fee: priority_fee.parse().unwrap_or_default(), unit_price: unit_price.parse().unwrap_or_default(), + jito_tip, }, } } From 73face69f439a474668abd55ef616466e58cfded Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:27:35 +0900 Subject: [PATCH 06/12] Update gas_price_type.rs --- crates/primitives/src/gas_price_type.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/primitives/src/gas_price_type.rs b/crates/primitives/src/gas_price_type.rs index fd77645a8..5a405bf06 100644 --- a/crates/primitives/src/gas_price_type.rs +++ b/crates/primitives/src/gas_price_type.rs @@ -72,7 +72,7 @@ impl GasPriceType { } pub fn total_fee(&self) -> BigInt { - self.gas_price() + self.priority_fee() + self.gas_price() + self.priority_fee() + BigInt::from(self.jito_tip()) } } @@ -106,7 +106,7 @@ mod tests { let eip1559 = GasPriceType::eip1559(BigInt::from(2000u64), BigInt::from(500u64)); assert_eq!(eip1559.unit_price(), BigInt::from(0)); - let solana = GasPriceType::solana(BigInt::from(5000u64), BigInt::from(1000u64), BigInt::from(200u64)); + let solana = GasPriceType::solana(BigInt::from(5000u64), BigInt::from(1000u64), BigInt::from(200u64), 0); assert_eq!(solana.unit_price(), BigInt::from(200u64)); } @@ -118,7 +118,7 @@ mod tests { let eip1559 = GasPriceType::eip1559(BigInt::from(2000u64), BigInt::from(500u64)); assert_eq!(eip1559.total_fee(), BigInt::from(2500u64)); - let solana = GasPriceType::solana(BigInt::from(5000u64), BigInt::from(1000u64), BigInt::from(200u64)); - assert_eq!(solana.total_fee(), BigInt::from(6000u64)); + let solana = GasPriceType::solana(BigInt::from(5000u64), BigInt::from(1000u64), BigInt::from(200u64), 3000); + assert_eq!(solana.total_fee(), BigInt::from(9000u64)); // 5000 + 1000 + 3000 (jito_tip) } } From 6e8fc64300b7773c3c2d23c5b74e4b6a638f3304 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:41:56 +0900 Subject: [PATCH 07/12] use rand --- Cargo.lock | 1 + crates/gem_solana/Cargo.toml | 1 + crates/gem_solana/src/models/jito.rs | 6 ++---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 126639a39..eeae54879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3455,6 +3455,7 @@ dependencies = [ "gem_jsonrpc", "num-bigint", "primitives", + "rand 0.9.2", "serde", "serde_json", "serde_serializers", diff --git a/crates/gem_solana/Cargo.toml b/crates/gem_solana/Cargo.toml index 974bd549d..b7335b8a2 100644 --- a/crates/gem_solana/Cargo.toml +++ b/crates/gem_solana/Cargo.toml @@ -25,6 +25,7 @@ integration = [] curve25519-dalek = { workspace = true } sha2 = { workspace = true } bs58 = { workspace = true } +rand = { workspace = true } borsh = { workspace = true } base64 = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/gem_solana/src/models/jito.rs b/crates/gem_solana/src/models/jito.rs index 02a15e568..109a9c348 100644 --- a/crates/gem_solana/src/models/jito.rs +++ b/crates/gem_solana/src/models/jito.rs @@ -1,6 +1,5 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - use primitives::{SolanaAccountMeta, SolanaInstruction}; +use rand::Rng; use crate::SYSTEM_PROGRAM_ID; @@ -82,8 +81,7 @@ pub fn calculate_fee_stats(fees: &[i64]) -> FeeStats { } fn get_random_tip_account() -> &'static str { - let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0); - let index = (nanos as usize) % JITO_TIP_ACCOUNTS.len(); + let index = rand::rng().random_range(0..JITO_TIP_ACCOUNTS.len()); JITO_TIP_ACCOUNTS[index] } From f27ced461c1e7360823246d1613f5fb757b83c7e Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:50:03 +0900 Subject: [PATCH 08/12] fix review comments --- bin/gas-bench/src/jito/api.rs | 2 -- bin/gas-bench/src/jito/model.rs | 12 ++++++------ bin/gas-bench/src/main.rs | 21 +++++++++++---------- bin/gas-bench/src/solana_client.rs | 8 ++++---- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/bin/gas-bench/src/jito/api.rs b/bin/gas-bench/src/jito/api.rs index be36af9fd..e7b0efcef 100644 --- a/bin/gas-bench/src/jito/api.rs +++ b/bin/gas-bench/src/jito/api.rs @@ -1,3 +1 @@ pub const JITO_TIP_FLOOR_URL: &str = "https://bundles.jito.wtf/api/v1/bundles/tip_floor"; -pub const JUPITER_PROGRAM: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; -pub const ORCA_WHIRLPOOL: &str = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"; diff --git a/bin/gas-bench/src/jito/model.rs b/bin/gas-bench/src/jito/model.rs index 412dd4bc2..ffbf0f8ed 100644 --- a/bin/gas-bench/src/jito/model.rs +++ b/bin/gas-bench/src/jito/model.rs @@ -1,7 +1,7 @@ use serde::Deserialize; -const LAMPORTS_PER_SOL: f64 = 1_000_000_000.0; -const MICRO_LAMPORTS_PER_LAMPORT: f64 = 1_000_000.0; +const LAMPORTS_PER_SOL: u64 = 1_000_000_000; +const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_000; #[derive(Debug, Deserialize)] pub struct JitoTipFloorEntry { @@ -28,11 +28,11 @@ impl JitoTipFloor { } pub fn sol_to_lamports(sol: f64) -> u64 { - (sol * LAMPORTS_PER_SOL) as u64 + (sol * LAMPORTS_PER_SOL as f64) as u64 } pub fn lamports_to_sol(lamports: u64) -> String { - let sol = lamports as f64 / LAMPORTS_PER_SOL; + let sol = lamports as f64 / LAMPORTS_PER_SOL as f64; if sol < 0.000001 { format!("{:.9}", sol) } else if sol < 0.001 { @@ -47,8 +47,8 @@ pub fn priority_fee_to_lamports(micro_lamports_per_cu: u64, compute_units: u64) } pub fn format_micro_lamports(micro_lamports: u64) -> String { - if micro_lamports >= MICRO_LAMPORTS_PER_LAMPORT as u64 { - format!("{:.2}M", micro_lamports as f64 / MICRO_LAMPORTS_PER_LAMPORT) + if micro_lamports >= MICRO_LAMPORTS_PER_LAMPORT { + format!("{:.2}M", micro_lamports as f64 / MICRO_LAMPORTS_PER_LAMPORT as f64) } else if micro_lamports >= 1_000 { format!("{:.1}K", micro_lamports as f64 / 1_000.0) } else { diff --git a/bin/gas-bench/src/main.rs b/bin/gas-bench/src/main.rs index 49b523c3d..e31e28b06 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -314,8 +314,8 @@ fn print_solana_fee_data(fee_data: &SolanaFeeData, jito_res: &Option jito.p25_lamports, + "Normal" => jito.p50_lamports, + "Fast" => jito.p75_lamports, + _ => 0, + }; let jito_floor_display = format!("{} ({})", jito_floor, lamports_to_sol(jito_floor)); let diff_pct = if jito_floor > 0 { let diff = (*jito_estimate as f64 - jito_floor as f64) / jito_floor as f64 * 100.0; diff --git a/bin/gas-bench/src/solana_client.rs b/bin/gas-bench/src/solana_client.rs index 38f1b85a1..4ed2e9cac 100644 --- a/bin/gas-bench/src/solana_client.rs +++ b/bin/gas-bench/src/solana_client.rs @@ -8,9 +8,9 @@ use gemstone::alien::{AlienProvider, new_alien_client, reqwest_provider::NativeP use primitives::Chain; use serde_json::json; -use crate::jito::{JUPITER_PROGRAM, ORCA_WHIRLPOOL}; - pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +pub const JUPITER_PROGRAM: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; +pub const ORCA_WHIRLPOOL: &str = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"; const MIN_SLOW_FEE: u64 = 1_000; const MIN_NORMAL_FEE: u64 = 10_000; @@ -55,12 +55,12 @@ impl SolanaGasClient { let slot: u64 = client.call("getSlot", json!([])).await?; - let global_fees: Vec = client.call("getRecentPrioritizationFees", json!([])).await.unwrap_or_default(); + let global_fees: Vec = client.call("getRecentPrioritizationFees", json!([])).await?; let mut account_fees = AccountFeeStats::default(); for (account, name) in [(JUPITER_PROGRAM, "jupiter"), (ORCA_WHIRLPOOL, "orca"), (USDC_MINT, "usdc")] { - let fees: Vec = client.call("getRecentPrioritizationFees", json!([[account]])).await.unwrap_or_default(); + let fees: Vec = client.call("getRecentPrioritizationFees", json!([[account]])).await?; if !fees.is_empty() { let values: Vec = fees.iter().map(|f| f.prioritization_fee).collect(); From f7124c28b852e6eb31c292778879b217c2390c7e Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:32:53 +0900 Subject: [PATCH 09/12] remove api.rs and rename all_fees --- bin/gas-bench/src/jito/api.rs | 1 - bin/gas-bench/src/jito/client.rs | 3 ++- bin/gas-bench/src/jito/mod.rs | 2 -- crates/gem_solana/src/provider/preload_mapper.rs | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 bin/gas-bench/src/jito/api.rs diff --git a/bin/gas-bench/src/jito/api.rs b/bin/gas-bench/src/jito/api.rs deleted file mode 100644 index e7b0efcef..000000000 --- a/bin/gas-bench/src/jito/api.rs +++ /dev/null @@ -1 +0,0 @@ -pub const JITO_TIP_FLOOR_URL: &str = "https://bundles.jito.wtf/api/v1/bundles/tip_floor"; diff --git a/bin/gas-bench/src/jito/client.rs b/bin/gas-bench/src/jito/client.rs index 76a255305..5c3aa56ec 100644 --- a/bin/gas-bench/src/jito/client.rs +++ b/bin/gas-bench/src/jito/client.rs @@ -1,6 +1,7 @@ +use super::{JitoTipFloor, JitoTipFloorEntry}; use std::error::Error; -use super::{JITO_TIP_FLOOR_URL, JitoTipFloor, JitoTipFloorEntry}; +const JITO_TIP_FLOOR_URL: &str = "https://bundles.jito.wtf/api/v1/bundles/tip_floor"; #[derive(Default)] pub struct JitoClient { diff --git a/bin/gas-bench/src/jito/mod.rs b/bin/gas-bench/src/jito/mod.rs index 07eba59ec..6979124fc 100644 --- a/bin/gas-bench/src/jito/mod.rs +++ b/bin/gas-bench/src/jito/mod.rs @@ -1,7 +1,5 @@ -mod api; mod client; mod model; -pub use api::*; pub use client::*; pub use model::*; diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 3b5f2d56d..807d860d2 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -90,8 +90,8 @@ pub fn calculate_fee_rates(input_type: &TransactionInputType, prioritization_fee let gas_limit = get_gas_limit(input_type); - let all_fees: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); - let fee_stats = calculate_fee_stats(&all_fees); + let prioritization_fees: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); + let fee_stats = calculate_fee_stats(&prioritization_fees); let jito_tips = estimate_jito_tips(&fee_stats); [FeePriority::Slow, FeePriority::Normal, FeePriority::Fast] From c97e0601becb02c847fbc530e0791125fac1f330 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:41:26 +0900 Subject: [PATCH 10/12] use IndexedRandom --- crates/gem_solana/src/models/jito.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/gem_solana/src/models/jito.rs b/crates/gem_solana/src/models/jito.rs index 109a9c348..9eaac8705 100644 --- a/crates/gem_solana/src/models/jito.rs +++ b/crates/gem_solana/src/models/jito.rs @@ -1,5 +1,5 @@ use primitives::{SolanaAccountMeta, SolanaInstruction}; -use rand::Rng; +use rand::seq::IndexedRandom; use crate::SYSTEM_PROGRAM_ID; @@ -81,8 +81,7 @@ pub fn calculate_fee_stats(fees: &[i64]) -> FeeStats { } fn get_random_tip_account() -> &'static str { - let index = rand::rng().random_range(0..JITO_TIP_ACCOUNTS.len()); - JITO_TIP_ACCOUNTS[index] + JITO_TIP_ACCOUNTS.choose(&mut rand::rng()).unwrap() } pub fn create_jito_tip_instruction_json(from: &str, lamports: u64) -> String { From b1fccdf841056649f8264d5df79cdf23c094e9ca Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:03:38 +0900 Subject: [PATCH 11/12] implement 70/30 split and cleanup --- bin/gas-bench/src/helius/client.rs | 39 +++++ bin/gas-bench/src/helius/mod.rs | 5 + bin/gas-bench/src/helius/model.rs | 60 +++++++ bin/gas-bench/src/main.rs | 86 +++++++---- crates/gem_solana/src/models/jito.rs | 2 +- .../gem_solana/src/provider/preload_mapper.rs | 146 +++++------------- 6 files changed, 195 insertions(+), 143 deletions(-) create mode 100644 bin/gas-bench/src/helius/client.rs create mode 100644 bin/gas-bench/src/helius/mod.rs create mode 100644 bin/gas-bench/src/helius/model.rs diff --git a/bin/gas-bench/src/helius/client.rs b/bin/gas-bench/src/helius/client.rs new file mode 100644 index 000000000..21c831955 --- /dev/null +++ b/bin/gas-bench/src/helius/client.rs @@ -0,0 +1,39 @@ +use super::{HeliusPriorityFeeOptions, HeliusPriorityFeeParams, HeliusPriorityFeeRequest, HeliusPriorityFeeResponse, HeliusPriorityFees}; +use std::error::Error; + +pub struct HeliusClient { + client: reqwest::Client, + endpoint: String, +} + +impl HeliusClient { + pub fn new(api_key: &str) -> Self { + Self { + client: reqwest::Client::new(), + endpoint: format!("https://mainnet.helius-rpc.com/?api-key={}", api_key), + } + } + + pub async fn fetch_priority_fee_estimate(&self, account_keys: Option>) -> Result> { + let request = HeliusPriorityFeeRequest { + jsonrpc: "2.0", + id: "1", + method: "getPriorityFeeEstimate", + params: vec![HeliusPriorityFeeParams { + account_keys, + options: HeliusPriorityFeeOptions { + include_all_priority_fee_levels: true, + lookback_slots: 150, + }, + }], + }; + + let response = self.client.post(&self.endpoint).json(&request).send().await?; + + let result: HeliusPriorityFeeResponse = response.json().await?; + + let levels = result.result.priority_fee_levels.ok_or("No priority fee levels in response")?; + + Ok(HeliusPriorityFees::from_levels(&levels)) + } +} diff --git a/bin/gas-bench/src/helius/mod.rs b/bin/gas-bench/src/helius/mod.rs new file mode 100644 index 000000000..6979124fc --- /dev/null +++ b/bin/gas-bench/src/helius/mod.rs @@ -0,0 +1,5 @@ +mod client; +mod model; + +pub use client::*; +pub use model::*; diff --git a/bin/gas-bench/src/helius/model.rs b/bin/gas-bench/src/helius/model.rs new file mode 100644 index 000000000..63ae5c576 --- /dev/null +++ b/bin/gas-bench/src/helius/model.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +pub struct HeliusPriorityFeeRequest { + pub jsonrpc: &'static str, + pub id: &'static str, + pub method: &'static str, + pub params: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HeliusPriorityFeeParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub account_keys: Option>, + pub options: HeliusPriorityFeeOptions, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HeliusPriorityFeeOptions { + pub include_all_priority_fee_levels: bool, + pub lookback_slots: u32, +} + +#[derive(Debug, Deserialize)] +pub struct HeliusPriorityFeeResponse { + pub result: HeliusPriorityFeeResult, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HeliusPriorityFeeResult { + pub priority_fee_levels: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HeliusPriorityFeeLevels { + pub low: f64, + pub medium: f64, + pub high: f64, +} + +#[derive(Debug)] +pub struct HeliusPriorityFees { + pub low: u64, + pub medium: u64, + pub high: u64, +} + +impl HeliusPriorityFees { + pub fn from_levels(levels: &HeliusPriorityFeeLevels) -> Self { + Self { + low: levels.low as u64, + medium: levels.medium as u64, + high: levels.high as u64, + } + } +} diff --git a/bin/gas-bench/src/main.rs b/bin/gas-bench/src/main.rs index e31e28b06..0f38bd35c 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -1,6 +1,7 @@ mod client; mod etherscan; mod gasflow; +mod helius; mod jito; mod solana_client; @@ -15,8 +16,9 @@ use crate::{ client::{GemstoneClient, GemstoneFeeData}, etherscan::EtherscanClient, gasflow::GasflowClient, + helius::{HeliusClient, HeliusPriorityFees}, jito::{JitoClient, JitoTipFloor}, - solana_client::{SolanaFeeData, SolanaGasClient}, + solana_client::{JUPITER_PROGRAM, SolanaFeeData, SolanaGasClient}, }; use gem_evm::ether_conv::EtherConv; use gemstone::alien::reqwest_provider::NativeProvider; @@ -80,6 +82,10 @@ struct Cli { /// Skip Jito API comparison (Solana only) #[clap(long, action = clap::ArgAction::SetTrue)] skip_jito: bool, + + /// Helius API key for getPriorityFeeEstimate (Solana only) + #[clap(long, env = "HELIUS_API_KEY")] + helius_api_key: Option, } async fn run_ethereum(args: Cli) -> Result<(), Box> { @@ -250,12 +256,14 @@ async fn run_solana(args: Cli) -> Result<(), Box> { let native_provider = Arc::new(NativeProvider::new().set_debug(args.debug)); let solana_client = SolanaGasClient::new(native_provider); let jito_client = if args.skip_jito { None } else { Some(JitoClient::new()) }; + let helius_client = args.helius_api_key.as_ref().map(|key| HeliusClient::new(key)); let mut last_printed_slot: Option = None; println!("gas-bench [Solana]: monitoring priority fees and Jito tips"); println!(" Compute units for fee calculation: {}", args.compute_units); println!(" Jito comparison: {}", if jito_client.is_some() { "enabled" } else { "disabled" }); + println!(" Helius comparison: {}", if helius_client.is_some() { "enabled" } else { "disabled (set HELIUS_API_KEY)" }); println!(); loop { @@ -272,8 +280,14 @@ async fn run_solana(args: Cli) -> Result<(), Box> { None => None, } }; + let helius_future = async { + match &helius_client { + Some(client) => Some(client.fetch_priority_fee_estimate(Some(vec![JUPITER_PROGRAM.to_string()])).await), + None => None, + } + }; - let (solana_res, jito_res) = tokio::join!(solana_future, jito_future); + let (solana_res, jito_res, helius_res) = tokio::join!(solana_future, jito_future, helius_future); match solana_res { Ok(fee_data) => { @@ -281,7 +295,7 @@ async fn run_solana(args: Cli) -> Result<(), Box> { continue; } - print_solana_fee_data(&fee_data, &jito_res, args.compute_units); + print_solana_fee_data(&fee_data, &jito_res, &helius_res, args.compute_units); last_printed_slot = Some(fee_data.slot); } Err(e) => { @@ -293,7 +307,12 @@ async fn run_solana(args: Cli) -> Result<(), Box> { } } -fn print_solana_fee_data(fee_data: &SolanaFeeData, jito_res: &Option>>, compute_units: u64) { +fn print_solana_fee_data( + fee_data: &SolanaFeeData, + jito_res: &Option>>, + helius_res: &Option>>, + compute_units: u64, +) { println!("\n--- Slot: {} ---", fee_data.slot); let accounts = [ @@ -307,25 +326,30 @@ fn print_solana_fee_data(fee_data: &SolanaFeeData, jito_res: &Option jito.p75_lamports, _ => 0, }; - let jito_floor_display = format!("{} ({})", jito_floor, lamports_to_sol(jito_floor)); - let diff_pct = if jito_floor > 0 { - let diff = (*jito_estimate as f64 - jito_floor as f64) / jito_floor as f64 * 100.0; - format!("{:+.1}%", diff) - } else { - "N/A".to_string() - }; - - table.add_row(Row::new(vec![ - Cell::new(level), - Cell::new(&priority_display), - Cell::new(&jito_estimate_display), - Cell::new(&jito_floor_display), - Cell::new(&diff_pct), - ])); - } else { - table.add_row(Row::new(vec![Cell::new(level), Cell::new(&priority_display), Cell::new(&jito_estimate_display)])); + let jito_floor_display = lamports_to_sol(jito_floor); + row.push(Cell::new(&jito_floor_display)); } + + table.add_row(Row::new(row)); } table.printstd(); @@ -372,6 +387,9 @@ fn print_solana_fee_data(fee_data: &SolanaFeeData, jito_res: &Option Result<(), Box> { diff --git a/crates/gem_solana/src/models/jito.rs b/crates/gem_solana/src/models/jito.rs index 9eaac8705..2583d347e 100644 --- a/crates/gem_solana/src/models/jito.rs +++ b/crates/gem_solana/src/models/jito.rs @@ -3,7 +3,7 @@ use rand::seq::IndexedRandom; use crate::SYSTEM_PROGRAM_ID; -pub const JITO_TIP_MIN_LAMPORTS: u64 = 1_000; +pub const JITO_TIP_MIN_LAMPORTS: u64 = 10_000; // 0.00001 SOL pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 807d860d2..b37ab4951 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -2,7 +2,7 @@ use num_bigint::BigInt; use primitives::{AssetSubtype, Chain, FeeOption, FeePriority, FeeRate, GasPriceType, TransactionFee, TransactionInputType}; use std::collections::HashMap; -use crate::models::jito::{calculate_fee_stats, estimate_jito_tips}; +use crate::models::jito::JITO_TIP_MIN_LAMPORTS; use crate::{constants::STATIC_BASE_FEE, models::prioritization_fee::SolanaPrioritizationFee}; pub fn calculate_transaction_fee(input_type: &TransactionInputType, gas_price_type: &GasPriceType, recipient_token_address: Option) -> TransactionFee { @@ -80,7 +80,7 @@ pub fn calculate_fee_rates(input_type: &TransactionInputType, prioritization_fee let multiple_of = get_multiple_of(input_type); let static_base_fee = BigInt::from(STATIC_BASE_FEE); - let priority_fee_base = if fees.is_empty() { + let total_priority_base = if fees.is_empty() { BigInt::from(multiple_of) } else { let average = fees.iter().sum::() / fees.len() as i64; @@ -90,34 +90,23 @@ pub fn calculate_fee_rates(input_type: &TransactionInputType, prioritization_fee let gas_limit = get_gas_limit(input_type); - let prioritization_fees: Vec = prioritization_fees.iter().map(|f| f.prioritization_fee).collect(); - let fee_stats = calculate_fee_stats(&prioritization_fees); - let jito_tips = estimate_jito_tips(&fee_stats); - [FeePriority::Slow, FeePriority::Normal, FeePriority::Fast] .iter() .map(|priority| { - let priority_fee = match priority { - FeePriority::Slow => &priority_fee_base / 2, - FeePriority::Normal => priority_fee_base.clone(), - FeePriority::Fast => &priority_fee_base * 3, + let total_priority = match priority { + FeePriority::Slow => &total_priority_base / 2, + FeePriority::Normal => total_priority_base.clone(), + FeePriority::Fast => &total_priority_base * 3, }; - let jito_tip = match priority { - FeePriority::Slow => jito_tips.slow, - FeePriority::Normal => jito_tips.normal, - FeePriority::Fast => jito_tips.fast, - }; + // 70% priority fee, 30% jito tip https://docs.jito.wtf/lowlatencytxnsend/#id18 + let scaled_total = (total_priority.clone() * gas_limit.clone()) / BigInt::from(1_000_000); + let priority_fee = (&scaled_total * 70) / 100; + let jito_tip_calculated: BigInt = (&scaled_total * 30) / 100; + let unit_price = (&total_priority * 70) / 100; + let jito_tip = jito_tip_calculated.to_u64_digits().1.first().copied().unwrap_or(0).max(JITO_TIP_MIN_LAMPORTS); - FeeRate::new( - *priority, - GasPriceType::solana( - static_base_fee.clone(), - (priority_fee.clone() * gas_limit.clone()) / BigInt::from(1_000_000), - priority_fee.clone(), - jito_tip, - ), - ) + FeeRate::new(*priority, GasPriceType::solana(static_base_fee.clone(), priority_fee, unit_price, jito_tip)) }) .collect() } @@ -144,10 +133,6 @@ mod tests { let fee = calculate_transaction_fee(&input_type, &gas_price_type, None); - // Expected calculation: - // gas_price = 5000 - // priority_fee = 15000 - // fee = gas_price + priority_fee = 5000 + 15000 = 20000 assert_eq!(fee.fee, BigInt::from(20_000u64)); assert_eq!(fee.gas_price_type.gas_price(), BigInt::from(5000u64)); assert_eq!(fee.gas_price_type.priority_fee(), BigInt::from(15000u64)); @@ -183,10 +168,6 @@ mod tests { let fee = calculate_transaction_fee(&input_type, &gas_price_type, None); - // Expected calculation: - // gas_price = 5000 - // priority_fee = 30000 - // fee = gas_price + priority_fee = 5000 + 30000 = 35000 assert_eq!(fee.fee, BigInt::from(35_000u64)); assert_eq!(fee.gas_limit, BigInt::from(420_000u64)); } @@ -201,30 +182,6 @@ mod tests { assert!(!fee.options.contains_key(&FeeOption::TokenAccountCreation)); } - #[test] - fn test_calculate_transaction_fee_zero_priority_fee() { - // Test with zero priority fee - let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(0u64)); - let input_type = TransactionInputType::Transfer(Asset { - id: AssetId::from_chain(Chain::Solana), - chain: Chain::Solana, - token_id: None, - name: "SOL".to_string(), - symbol: "SOL".to_string(), - decimals: 9, - asset_type: AssetType::NATIVE, - }); - - let fee = calculate_transaction_fee(&input_type, &gas_price_type, None); - - // Expected calculation: - // gas_price = 5000 - // priority_fee = 0 - // fee = gas_price + priority_fee = 5000 + 0 = 5000 - assert_eq!(fee.fee, BigInt::from(5000u64)); - assert_eq!(fee.gas_price_type.priority_fee(), BigInt::from(0u64)); - } - #[test] fn test_calculate_priority_fee() { let fees = vec![SolanaPrioritizationFee { prioritization_fee: 150_000 }]; @@ -262,17 +219,21 @@ mod tests { for rate in &rates { assert_eq!(rate.gas_price_type.gas_price(), BigInt::from(5000u64)); } + assert_eq!(rates[0].priority, FeePriority::Slow); - assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(1_250)); - assert_eq!(rates[0].gas_price_type.unit_price(), BigInt::from(12_500)); + assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(875)); + assert_eq!(rates[0].gas_price_type.unit_price(), BigInt::from(8_750)); + assert_eq!(rates[0].gas_price_type.jito_tip(), 10_000); // min jito tip assert_eq!(rates[1].priority, FeePriority::Normal); - assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(2_500)); - assert_eq!(rates[1].gas_price_type.unit_price(), BigInt::from(25_000)); + assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(1_750)); + assert_eq!(rates[1].gas_price_type.unit_price(), BigInt::from(17_500)); + assert_eq!(rates[1].gas_price_type.jito_tip(), 10_000); // min jito tip assert_eq!(rates[2].priority, FeePriority::Fast); - assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(7_500)); - assert_eq!(rates[2].gas_price_type.unit_price(), BigInt::from(75_000)); + assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(5_250)); + assert_eq!(rates[2].gas_price_type.unit_price(), BigInt::from(52_500)); + assert_eq!(rates[2].gas_price_type.jito_tip(), 10_000); // min jito tip } #[test] @@ -290,16 +251,10 @@ mod tests { let rates = calculate_fee_rates(&input_type, &fees); - // Should return 3 fee rates even with empty fees assert_eq!(rates.len(), 3); - - // When fees are empty, priority_fee_base = multiple_of = 25_000 for native transfers - // Slow: 25_000 / 2 = 12_500, scaled: (12_500 * 100_000) / 1_000_000 = 1_250 - // Normal: 25_000, scaled: (25_000 * 100_000) / 1_000_000 = 2_500 - // Fast: 25_000 * 3 = 75_000, scaled: (75_000 * 100_000) / 1_000_000 = 7_500 - assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(1_250u64)); - assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(2_500u64)); - assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(7_500u64)); + assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(875u64)); + assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(1_750u64)); + assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(5_250u64)); } #[test] @@ -318,15 +273,9 @@ mod tests { let rates = calculate_fee_rates(&input_type, &fees); assert_eq!(rates.len(), 3); - // For SPL tokens, multiple_of = 50_000 - // 80_000 rounded up to nearest 50_000 = 100_000 - // priority_fee_base = max(100_000, 50_000) = 100_000 - // Slow: 100_000 / 2 = 50_000, scaled: (50_000 * 100_000) / 1_000_000 = 5_000 - // Normal: 100_000, scaled: (100_000 * 100_000) / 1_000_000 = 10_000 - // Fast: 100_000 * 3 = 300_000, scaled: (300_000 * 100_000) / 1_000_000 = 30_000 - assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(5_000u64)); - assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(10_000u64)); - assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(30_000u64)); + assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(3_500u64)); + assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(7_000u64)); + assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(21_000u64)); } #[test] @@ -357,20 +306,13 @@ mod tests { let rates = calculate_fee_rates(&input_type, &fees); assert_eq!(rates.len(), 3); - // For swaps, multiple_of = 100_000, gas_limit = 420_000 - // 150_000 rounded up to nearest 100_000 = 200_000 - // priority_fee_base = max(200_000, 100_000) = 200_000 - // Slow: 200_000 / 2 = 100_000, scaled: (100_000 * 420_000) / 1_000_000 = 42_000 - // Normal: 200_000, scaled: (200_000 * 420_000) / 1_000_000 = 84_000 - // Fast: 200_000 * 3 = 600_000, scaled: (600_000 * 420_000) / 1_000_000 = 252_000 - assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(42_000u64)); - assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(84_000u64)); - assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(252_000u64)); + assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(29_400u64)); + assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(58_800u64)); + assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(176_400u64)); } #[test] fn test_calculate_fee_rates_multiple_fees() { - // Test with multiple prioritization fees - should use top 5 and calculate average let fees = vec![ SolanaPrioritizationFee { prioritization_fee: 200_000 }, SolanaPrioritizationFee { prioritization_fee: 150_000 }, @@ -392,16 +334,9 @@ mod tests { let rates = calculate_fee_rates(&input_type, &fees); assert_eq!(rates.len(), 3); - // Top 5 fees (sorted desc): [225_000, 200_000, 175_000, 150_000, 125_000] - // Average = (225_000 + 200_000 + 175_000 + 150_000 + 125_000) / 5 = 175_000 - // Rounded up to nearest 25_000 = 175_000 - // priority_fee_base = max(175_000, 25_000) = 175_000 - // Slow: 175_000 / 2 = 87_500, scaled: (87_500 * 100_000) / 1_000_000 = 8_750 - // Normal: 175_000, scaled: (175_000 * 100_000) / 1_000_000 = 17_500 - // Fast: 175_000 * 3 = 525_000, scaled: (525_000 * 100_000) / 1_000_000 = 52_500 - assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(8_750u64)); - assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(17_500u64)); - assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(52_500u64)); + assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(6_125u64)); + assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(12_250u64)); + assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(36_750u64)); } #[test] @@ -419,14 +354,9 @@ mod tests { let rates = calculate_fee_rates(&input_type, &fees); - // priority_fee_base = 150_000 (already rounded to multiple of 25_000) - // Slow: 150_000 / 2 = 75_000, scaled: (75_000 * 100_000) / 1_000_000 = 7_500 - // Normal: 150_000, scaled: (150_000 * 100_000) / 1_000_000 = 15_000 - // Fast: 150_000 * 3 = 450_000, scaled: (450_000 * 100_000) / 1_000_000 = 45_000 - - assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(7_500)); - assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(15_000)); - assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(45_000)); + assert_eq!(rates[0].gas_price_type.priority_fee(), BigInt::from(5_250)); + assert_eq!(rates[1].gas_price_type.priority_fee(), BigInt::from(10_500)); + assert_eq!(rates[2].gas_price_type.priority_fee(), BigInt::from(31_500)); } #[test] From 7e9d6e8c9cc418d525ba19a8cc3871769fd07352 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:02:58 +0900 Subject: [PATCH 12/12] fix lint --- apps/api/src/devices/guard.rs | 2 +- crates/gem_hypercore/src/provider/websocket_mapper.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/src/devices/guard.rs b/apps/api/src/devices/guard.rs index f86b25dfd..a892461fe 100644 --- a/apps/api/src/devices/guard.rs +++ b/apps/api/src/devices/guard.rs @@ -132,7 +132,7 @@ impl<'r> FromRequest<'r> for AuthenticatedDeviceWallet { return auth_error_outcome(req, DeviceError::DeviceNotFound); }; - let Ok(wallet_row) = WalletsStore::get_wallet(&mut db_client, &wallet_id_str) else { + let Ok(wallet_row) = WalletsStore::get_wallet(&mut db_client, wallet_id_str) else { return auth_error_outcome(req, DeviceError::WalletNotFound); }; diff --git a/crates/gem_hypercore/src/provider/websocket_mapper.rs b/crates/gem_hypercore/src/provider/websocket_mapper.rs index d403966b2..e31b6c9d0 100644 --- a/crates/gem_hypercore/src/provider/websocket_mapper.rs +++ b/crates/gem_hypercore/src/provider/websocket_mapper.rs @@ -122,13 +122,13 @@ mod tests { assert_eq!(orders[0].oid, 8804521338); assert_eq!(orders[0].trigger_px, Some(110000.0)); assert_eq!(orders[0].limit_px, Some(110000.0)); - assert_eq!(orders[0].is_position_tpsl, true); + assert!(orders[0].is_position_tpsl); assert_eq!(orders[0].order_type, "Take Profit Market"); assert_eq!(orders[1].coin, "BTC"); assert_eq!(orders[1].oid, 8804521339); assert_eq!(orders[1].trigger_px, Some(95000.0)); assert_eq!(orders[1].limit_px, Some(95000.0)); - assert_eq!(orders[1].is_position_tpsl, true); + assert!(orders[1].is_position_tpsl); assert_eq!(orders[1].order_type, "Stop Market"); }