use itertools::Itertools; use jsonrpsee::core::RpcResult; use prometheus::{opts, register_int_counter, IntCounter}; use solana_account_decoder::UiAccount; use solana_lite_rpc_accounts::account_service::AccountService; use solana_lite_rpc_prioritization_fees::account_prio_service::AccountPrioService; use solana_lite_rpc_prioritization_fees::prioritization_fee_calculation_method::PrioritizationFeeCalculationMethod; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_rpc_client_api::config::RpcAccountInfoConfig; use solana_rpc_client_api::response::{OptionalContext, RpcKeyedAccount}; use solana_rpc_client_api::{ config::{ RpcBlocksConfigWrapper, RpcContextConfig, RpcGetVoteAccountsConfig, RpcLeaderScheduleConfig, RpcProgramAccountsConfig, RpcRequestAirdropConfig, RpcSignatureStatusConfig, RpcSignaturesForAddressConfig, }, response::{ Response as RpcResponse, RpcBlockhash, RpcConfirmedTransactionStatusWithSignature, RpcContactInfo, RpcPerfSample, RpcPrioritizationFee, RpcResponseContext, RpcVersionInfo, RpcVoteAccountStatus, }, }; use solana_sdk::epoch_info::EpochInfo; use solana_sdk::signature::Signature; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, slot_history::Slot}; use solana_transaction_status::{TransactionStatus, UiConfirmedBlock}; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use solana_lite_rpc_blockstore::history::History; use solana_lite_rpc_core::solana_utils::hash_from_str; use solana_lite_rpc_core::{ encoding, stores::{block_information_store::BlockInformation, data_cache::DataCache}, }; use solana_lite_rpc_services::{ transaction_service::TransactionService, tx_sender::TXS_IN_CHANNEL, }; use crate::rpc_errors::RpcErrors; use crate::{ configs::{IsBlockHashValidConfig, SendTransactionConfig}, rpc::LiteRpcServer, }; use solana_lite_rpc_prioritization_fees::rpc_data::{AccountPrioFeesStats, PrioFeesStats}; use solana_lite_rpc_prioritization_fees::PrioFeesService; lazy_static::lazy_static! { static ref RPC_SEND_TX: IntCounter = register_int_counter!(opts!("literpc_rpc_send_tx", "RPC call send transaction")).unwrap(); static ref RPC_GET_LATEST_BLOCKHASH: IntCounter = register_int_counter!(opts!("literpc_rpc_get_latest_blockhash", "RPC call to get latest block hash")).unwrap(); static ref RPC_IS_BLOCKHASH_VALID: IntCounter = register_int_counter!(opts!("literpc_rpc_is_blockhash_valid", "RPC call to check if blockhash is vali calld")).unwrap(); static ref RPC_GET_SIGNATURE_STATUSES: IntCounter = register_int_counter!(opts!("literpc_rpc_get_signature_statuses", "RPC call to get signature statuses")).unwrap(); static ref RPC_GET_VERSION: IntCounter = register_int_counter!(opts!("literpc_rpc_get_version", "RPC call to version")).unwrap(); static ref RPC_REQUEST_AIRDROP: IntCounter = register_int_counter!(opts!("literpc_rpc_airdrop", "RPC call to request airdrop")).unwrap(); } /// A bridge between clients and tpu #[allow(dead_code)] pub struct LiteBridge { rpc_client: Arc, data_cache: DataCache, transaction_service: TransactionService, history: History, prio_fees_service: PrioFeesService, account_priofees_service: AccountPrioService, accounts_service: Option, } impl LiteBridge { pub fn new( rpc_client: Arc, data_cache: DataCache, transaction_service: TransactionService, history: History, prio_fees_service: PrioFeesService, account_priofees_service: AccountPrioService, accounts_service: Option, ) -> Self { Self { rpc_client, data_cache, transaction_service, history, prio_fees_service, account_priofees_service, accounts_service, } } } #[jsonrpsee::core::async_trait] impl LiteRpcServer for LiteBridge { async fn get_block(&self, _slot: u64) -> RpcResult> { // let block = self.blockstore.block_storage.query_block(slot).await; // if block.is_ok() { // // TO DO Convert to UIConfirmed Block // Err(jsonrpsee::core::Error::HttpNotImplemented) // } else { // Ok(None) // } // TODO get_block might deserve different implementation based on whether we serve from "blockstore module" vs. from "send tx module" // under progress Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } async fn get_blocks( &self, _start_slot: Slot, _config: Option, _commitment: Option, ) -> RpcResult> { // under progress Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } async fn get_signatures_for_address( &self, _address: String, _config: Option, ) -> RpcResult> { // under progress Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } async fn get_cluster_nodes(&self) -> RpcResult> { Ok(self .data_cache .cluster_info .cluster_nodes .iter() .map(|v| v.value().as_ref().clone()) .collect_vec()) } async fn get_slot(&self, config: Option) -> RpcResult { let commitment_config = config .map(|config| config.commitment.unwrap_or_default()) .unwrap_or_default(); let BlockInformation { slot, .. } = self .data_cache .block_information_store .get_latest_block_information(commitment_config) .await; Ok(slot) } async fn get_block_height(&self, config: Option) -> RpcResult { let commitment_config = config.map_or(CommitmentConfig::finalized(), |x| { x.commitment.unwrap_or_default() }); let block_info = self .data_cache .block_information_store .get_latest_block_information(commitment_config) .await; Ok(block_info.block_height) } async fn get_block_time(&self, slot: u64) -> RpcResult { let block_info = self .data_cache .block_information_store .get_block_info_by_slot(slot); match block_info { Some(info) => Ok(info.block_time), None => Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()), } } async fn get_first_available_block(&self) -> RpcResult { // under progress Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } async fn get_latest_blockhash( &self, config: Option, ) -> RpcResult> { RPC_GET_LATEST_BLOCKHASH.inc(); let commitment_config = config .map(|config| config.commitment.unwrap_or(CommitmentConfig::confirmed())) .unwrap_or_default(); let BlockInformation { slot, block_height, blockhash, .. } = self .data_cache .block_information_store .get_latest_block_information(commitment_config) .await; log::trace!("glb {blockhash} {slot} {block_height}"); Ok(RpcResponse { context: RpcResponseContext { slot, api_version: None, }, value: RpcBlockhash { blockhash: blockhash.to_string(), last_valid_block_height: block_height + 150, }, }) } async fn is_blockhash_valid( &self, blockhash: String, config: Option, ) -> RpcResult> { RPC_IS_BLOCKHASH_VALID.inc(); let commitment = config.unwrap_or_default().commitment.unwrap_or_default(); let commitment = CommitmentConfig { commitment }; let (is_valid, slot) = self .data_cache .block_information_store .is_blockhash_valid( &hash_from_str(&blockhash).expect("valid blockhash"), commitment, ) .await; Ok(RpcResponse { context: RpcResponseContext { slot, api_version: None, }, value: is_valid, }) } async fn get_epoch_info(&self, config: Option) -> RpcResult { let commitment_config = config .map(|config| config.commitment.unwrap_or_default()) .unwrap_or_default(); let block_info = self .data_cache .block_information_store .get_latest_block_information(commitment_config) .await; //TODO manage transaction_count of epoch info. Currently None. let epoch_info = self .data_cache .get_current_epoch(commitment_config) .await .as_epoch_info(block_info.block_height, None); Ok(epoch_info) } async fn get_recent_performance_samples( &self, limit: Option, ) -> RpcResult> { // TODO: implement our own perofmrance samples from blockstream and slot stream // For now just use normal rpc to get the data self.rpc_client .get_recent_performance_samples(limit) .await .map_err(|_| jsonrpsee::types::error::ErrorCode::InternalError.into()) } async fn get_signature_statuses( &self, sigs: Vec, _config: Option, ) -> RpcResult>>> { RPC_GET_SIGNATURE_STATUSES.inc(); let sig_statuses = sigs .iter() .map(|sig| Signature::from_str(sig).expect("signature must be valid")) .map(|sig| self.data_cache.txs.get(&sig).and_then(|v| v.status)) .collect(); Ok(RpcResponse { context: RpcResponseContext { slot: self .data_cache .block_information_store .get_latest_block_information(CommitmentConfig::finalized()) .await .slot, api_version: None, }, value: sig_statuses, }) } async fn get_recent_prioritization_fees( &self, pubkey_strs: Vec, ) -> RpcResult> { // This method will get the latest global and account prioritization fee stats and then send the maximum p75 const PERCENTILE: f32 = 0.75; let accounts = pubkey_strs .iter() .filter_map(|pubkey| Pubkey::from_str(pubkey).ok()) .collect_vec(); if accounts.len() != pubkey_strs.len() { // if lengths do not match it means some of the accounts are invalid return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()); } let global_prio_fees = self.prio_fees_service.get_latest_priofees().await; let max_p75 = global_prio_fees .map(|(_, fees)| { let fees = fees.get_percentile(PERCENTILE).unwrap_or_default(); std::cmp::max(fees.0, fees.1) }) .unwrap_or_default(); let ret: Vec = accounts .iter() .map(|account| { let (slot, stats) = self.account_priofees_service.get_latest_stats(account); let stat = stats .all_stats .get_percentile(PERCENTILE) .unwrap_or_default(); RpcPrioritizationFee { slot, prioritization_fee: std::cmp::max(max_p75, std::cmp::max(stat.0, stat.1)), } }) .collect_vec(); Ok(ret) } async fn send_transaction( &self, tx: String, send_transaction_config: Option, ) -> RpcResult { RPC_SEND_TX.inc(); // Copied these constants from solana labs code const MAX_BASE58_SIZE: usize = 1683; const MAX_BASE64_SIZE: usize = 1644; let SendTransactionConfig { encoding, max_retries, } = send_transaction_config.unwrap_or_default(); let expected_size = match encoding { encoding::BinaryEncoding::Base58 => MAX_BASE58_SIZE, encoding::BinaryEncoding::Base64 => MAX_BASE64_SIZE, }; if tx.len() > expected_size { return Err(jsonrpsee::types::error::ErrorCode::OversizedRequest.into()); } let raw_tx = match encoding.decode(tx) { Ok(raw_tx) => raw_tx, Err(_) => { return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()); } }; match self .transaction_service .send_wire_transaction(raw_tx, max_retries) .await { Ok(sig) => { TXS_IN_CHANNEL.inc(); Ok(sig) } Err(_) => Err(jsonrpsee::types::error::ErrorCode::InternalError.into()), } } fn get_version(&self) -> RpcResult { RPC_GET_VERSION.inc(); let version = solana_version::Version::default(); Ok(RpcVersionInfo { solana_core: version.to_string(), feature_set: Some(version.feature_set), }) } async fn request_airdrop( &self, _pubkey_str: String, _lamports: u64, _config: Option, ) -> RpcResult { RPC_REQUEST_AIRDROP.inc(); Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } async fn get_leader_schedule( &self, slot: Option, config: Option, ) -> RpcResult>>> { //TODO verify leader identity. let schedule = self .data_cache .leader_schedule .read() .await .get_leader_schedule_for_slot(slot, config.and_then(|c| c.commitment), &self.data_cache) .await; Ok(schedule) } async fn get_slot_leaders(&self, start_slot: u64, limit: u64) -> RpcResult> { let epock_schedule = self.data_cache.epoch_data.get_epoch_schedule(); self.data_cache .leader_schedule .read() .await .get_slot_leaders(start_slot, limit, epock_schedule) .await .map_err(|err| { log::error!("Error processing get leader schedule : {err:?}"); jsonrpsee::types::error::ErrorCode::InternalError.into() }) } async fn get_vote_accounts( &self, _config: Option, ) -> RpcResult { // under progress Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } async fn get_latest_block_priofees( &self, method: Option, ) -> RpcResult> { let method = method.unwrap_or_default(); let res = match method { PrioritizationFeeCalculationMethod::Latest => { self.prio_fees_service.get_latest_priofees().await } PrioritizationFeeCalculationMethod::LastNBlocks(nb) => { self.prio_fees_service .get_last_n_priofees_aggregate(nb) .await } _ => { // method is invalid return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()); } }; match res { Some((confirmation_slot, priofees)) => Ok(RpcResponse { context: RpcResponseContext { slot: confirmation_slot, api_version: None, }, value: priofees, }), None => Err(jsonrpsee::types::error::ErrorCode::InternalError.into()), } } async fn get_latest_account_priofees( &self, account: String, method: Option, ) -> RpcResult> { if let Ok(account) = Pubkey::from_str(&account) { let method = method.unwrap_or_default(); let (slot, value) = match method { PrioritizationFeeCalculationMethod::Latest => { self.account_priofees_service.get_latest_stats(&account) } PrioritizationFeeCalculationMethod::LastNBlocks(nb) => { self.account_priofees_service.get_n_last_stats(&account, nb) } _ => return Err(jsonrpsee::types::error::ErrorCode::InternalError.into()), }; Ok(RpcResponse { context: RpcResponseContext { slot, api_version: None, }, value, }) } else { // Account key is invalid Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()) } } async fn get_account_info( &self, pubkey_str: String, config: Option, ) -> RpcResult>> { let Ok(pubkey) = Pubkey::from_str(&pubkey_str) else { // pubkey is invalid return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()); }; if let Some(account_service) = &self.accounts_service { let commitment = config .as_ref() .and_then(|x| x.commitment) .unwrap_or_default(); let current_block_info = self .data_cache .block_information_store .get_latest_block_information(commitment) .await; match account_service.get_account(pubkey, config).await { Ok((_, ui_account)) => Ok(RpcResponse { context: RpcResponseContext { slot: current_block_info.slot, api_version: None, }, value: ui_account, }), Err(_) => { // account not found Err(jsonrpsee::types::error::ErrorCode::ServerError( RpcErrors::AccountNotFound as i32, ) .into()) } } } else { // accounts are disabled Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } } async fn get_multiple_accounts( &self, pubkey_strs: Vec, config: Option, ) -> RpcResult>>> { let pubkeys = pubkey_strs .iter() .map(|key| Pubkey::from_str(key)) .collect_vec(); if pubkeys.iter().any(|res| res.is_err()) { return Err(jsonrpsee::types::error::ErrorCode::InternalError.into()); }; if let Some(account_service) = &self.accounts_service { let mut ui_accounts = vec![]; for pubkey in pubkeys { match account_service .get_account(pubkey.unwrap(), config.clone()) .await { Ok((_, ui_account)) => { ui_accounts.push(ui_account); } Err(_) => { ui_accounts.push(None); } } } let commitment = config .as_ref() .and_then(|x| x.commitment) .unwrap_or_default(); let current_block_info = self .data_cache .block_information_store .get_latest_block_information(commitment) .await; assert_eq!(ui_accounts.len(), pubkey_strs.len()); Ok(RpcResponse { context: RpcResponseContext { slot: current_block_info.slot, api_version: None, }, value: ui_accounts, }) } else { // accounts are disabled Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } } async fn get_program_accounts( &self, program_id_str: String, config: Option, ) -> RpcResult>> { let Ok(program_id) = Pubkey::from_str(&program_id_str) else { return Err(jsonrpsee::types::error::ErrorCode::InternalError.into()); }; let with_context = config .as_ref() .map(|value| value.with_context.unwrap_or_default()) .unwrap_or_default(); let commitment: CommitmentConfig = config .as_ref() .and_then(|x| x.account_config.commitment) .unwrap_or_default(); let current_block_info = self .data_cache .block_information_store .get_latest_block_information(commitment) .await; if let Some(account_service) = &self.accounts_service { match account_service .get_program_accounts(program_id, config) .await { Ok((_, ui_account)) => { if with_context { Ok(OptionalContext::Context(RpcResponse { context: RpcResponseContext { slot: current_block_info.slot, api_version: None, }, value: ui_account, })) } else { Ok(OptionalContext::NoContext(ui_account)) } } Err(_) => { return Err(jsonrpsee::types::error::ErrorCode::ServerError( RpcErrors::AccountNotFound as i32, ) .into()); } } } else { // accounts are disabled Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } } async fn get_balance( &self, pubkey_str: String, config: Option, ) -> RpcResult> { let Ok(pubkey) = Pubkey::from_str(&pubkey_str) else { // pubkey is invalid return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()); }; let config = config.map(|x| RpcAccountInfoConfig { encoding: None, data_slice: None, commitment: x.commitment, min_context_slot: x.min_context_slot, }); let commitment = config .as_ref() .and_then(|x| x.commitment) .unwrap_or_default(); let current_block_info = self .data_cache .block_information_store .get_latest_block_information(commitment) .await; if let Some(account_service) = &self.accounts_service { match account_service.get_account(pubkey, config).await { Ok((_, ui_account)) => Ok(RpcResponse { context: RpcResponseContext { slot: current_block_info.slot, api_version: None, }, value: ui_account.map(|x| x.lamports).unwrap_or_default(), }), Err(_) => { // account not found Err(jsonrpsee::types::error::ErrorCode::ServerError( RpcErrors::AccountNotFound as i32, ) .into()) } } } else { // accounts are disabled Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into()) } } }