use {
super::{Error, Result, TransactionBuilder},
crate::TransactionRpcProvider,
solana_compute_budget_interface::ComputeBudgetInstruction,
solana_pubkey::Pubkey,
solana_rpc_client_api::{
config::RpcSimulateTransactionConfig,
response::{RpcPrioritizationFee, RpcSimulateTransactionResult},
},
};
const SOLANA_MAX_COMPUTE_UNITS: u32 = 1_400_000;
const MAX_ACCEPTABLE_PRIORITY_FEE_MICROLAMPORTS: u64 = 90_000 * 1_000_000;
#[derive(Debug, Clone)]
pub struct CalcFeeResult {
pub priority_fee: u64,
pub units: u32,
pub prioritization_fees: Vec<RpcPrioritizationFee>,
}
impl TransactionBuilder {
pub fn prepend_compute_budget_instructions(
mut self,
units: u32,
priority_fees: u64,
) -> Result<Self> {
if self
.instructions
.iter()
.any(|ix| ix.program_id == solana_compute_budget_interface::ID)
{
return Err(crate::Error::ComputeBudgetAlreadyPresent);
}
self.instructions.splice(0..0, vec![
ComputeBudgetInstruction::set_compute_unit_limit(units),
ComputeBudgetInstruction::set_compute_unit_price(priority_fees),
]);
Ok(self)
}
fn calc_fee_internal(
&self,
prioritization_fees: Vec<RpcPrioritizationFee>,
sim_result: RpcSimulateTransactionResult,
max_prioritization_fee: u64,
percentile: Option<u8>,
) -> Result<CalcFeeResult> {
let percentile = percentile.unwrap_or(75).min(100);
let mut sorted_fees: Vec<u64> = prioritization_fees
.iter()
.map(|f| f.prioritization_fee)
.collect();
sorted_fees.sort();
let index = (sorted_fees.len() * percentile as usize).saturating_sub(1) / 100;
let priority_fee = sorted_fees[index].min(max_prioritization_fee);
if priority_fee > MAX_ACCEPTABLE_PRIORITY_FEE_MICROLAMPORTS {
return Err(crate::Error::PriorityFeeTooHigh(
priority_fee,
MAX_ACCEPTABLE_PRIORITY_FEE_MICROLAMPORTS,
));
}
let compute_unit_limit: u32 = sim_result
.units_consumed
.ok_or(crate::Error::InvalidComputeUnits(
0,
"RPC returned no units".to_string(),
))?
.try_into()?;
let buffered_limit = compute_unit_limit
.saturating_add(compute_unit_limit / 10)
.min(SOLANA_MAX_COMPUTE_UNITS);
Ok(CalcFeeResult {
priority_fee,
units: buffered_limit,
prioritization_fees,
})
}
}
impl TransactionBuilder {
pub async fn get_recent_prioritization_fees<T: TransactionRpcProvider>(
rpc: &T,
accounts: &[Pubkey],
) -> Result<Vec<RpcPrioritizationFee>> {
rpc.get_recent_prioritization_fees(accounts)
.await
.map_err(|e| {
Error::SolanaRpcError(format!("failed to get_recent_prioritization_fees: {e}"))
})
}
pub async fn calc_fee<T: TransactionRpcProvider>(
&self,
payer: &Pubkey,
rpc: &T,
accounts: &[Pubkey],
max_prioritization_fee: u64,
percentile: Option<u8>,
) -> Result<CalcFeeResult> {
if self.instructions.is_empty() {
return Err(crate::Error::NoInstructions);
}
let prioritization_fees =
TransactionBuilder::get_recent_prioritization_fees(rpc, accounts).await?;
if prioritization_fees.is_empty() {
return Err(crate::Error::SolanaRpcError(
"No prioritization fees available".to_string(),
));
}
let tx = self.unsigned_tx(payer, rpc).await?;
let sim_result = self
.simulate_internal(rpc, &tx, RpcSimulateTransactionConfig {
sig_verify: false,
..Default::default()
})
.await?;
self.calc_fee_internal(
prioritization_fees,
sim_result,
max_prioritization_fee,
percentile,
)
}
#[tracing::instrument(skip(rpc, payer, accounts), level = tracing::Level::DEBUG)]
pub async fn with_priority_fees<T: TransactionRpcProvider>(
self,
payer: &Pubkey,
rpc: &T,
accounts: &[Pubkey],
max_prioritization_fee: u64,
percentile: Option<u8>,
) -> Result<Self> {
if self
.instructions
.iter()
.any(|ix| ix.program_id == solana_compute_budget_interface::ID)
{
tracing::warn!("ComputeBudgetProgram already exists");
return Ok(self);
}
let result = self
.calc_fee(payer, rpc, accounts, max_prioritization_fee, percentile)
.await?;
self.prepend_compute_budget_instructions(result.units, result.priority_fee)
}
}