// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs // version: 6 /// Inflation-governor values returned by `getInflationGovernor`. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SolanaInflationGovernor { initial: f64, terminal: f64, taper: f64, foundation: f64, foundation_term: f64, } impl SolanaInflationGovernor { /// Returns the initial inflation rate. #[must_use] pub const fn initial(&self) -> f64 { return self.initial; } /// Returns the terminal inflation rate. #[must_use] pub const fn terminal(&self) -> f64 { return self.terminal; } /// Returns the taper/disinflation parameter reported by the runtime. #[must_use] pub const fn taper(&self) -> f64 { return self.taper; } /// Returns the foundation allocation rate. #[must_use] pub const fn foundation(&self) -> f64 { return self.foundation; } /// Returns the foundation allocation term. #[must_use] pub const fn foundation_term(&self) -> f64 { return self.foundation_term; } /// Decodes an inflation-governor result from its Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { std::result::Result::Ok(wire) => std::result::Result::Ok(Self { initial: wire.initial, terminal: wire.terminal, taper: wire.taper, foundation: wire.foundation, foundation_term: wire.foundation_term, }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Current inflation-rate values returned by `getInflationRate`. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SolanaInflationRate { total: f64, validator: f64, foundation: f64, epoch: u64, } impl SolanaInflationRate { /// Returns the total inflation rate. #[must_use] pub const fn total(&self) -> f64 { return self.total; } /// Returns the validator inflation rate. #[must_use] pub const fn validator(&self) -> f64 { return self.validator; } /// Returns the foundation inflation rate. #[must_use] pub const fn foundation(&self) -> f64 { return self.foundation; } /// Returns the epoch associated with the rate. #[must_use] pub const fn epoch(&self) -> u64 { return self.epoch; } /// Decodes an inflation-rate result from its Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { std::result::Result::Ok(wire) => { std::result::Result::Ok(Self { total: wire.total, validator: wire.validator, foundation: wire.foundation, epoch: wire.epoch }) }, std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Optional epoch/context configuration accepted by `getInflationReward`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaInflationRewardConfig { epoch: std::option::Option, commitment: std::option::Option, min_context_slot: std::option::Option, } impl SolanaInflationRewardConfig { /// Creates an inflation-reward configuration. #[must_use] pub const fn new( epoch: std::option::Option, commitment: std::option::Option, min_context_slot: std::option::Option, ) -> Self { return Self { epoch, commitment, min_context_slot }; } /// Returns the optional target epoch. #[must_use] pub const fn epoch(&self) -> std::option::Option { return self.epoch; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional minimum context slot. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { return self.min_context_slot; } /// Returns whether this config would serialize to an empty object. pub(crate) const fn is_empty(&self) -> bool { return self.epoch.is_none() && self.commitment.is_none() && self.min_context_slot.is_none(); } /// Serializes this config to the exact Solana JSON-RPC object. #[must_use] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(epoch) = self.epoch { object.insert("epoch".to_owned(), serde_json::Value::Number(epoch.into())); } if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); } if let std::option::Option::Some(min_context_slot) = self.min_context_slot { object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into())); } return serde_json::Value::Object(object); } } /// One non-null positional reward returned by `getInflationReward`. #[derive(Clone, Debug, PartialEq)] pub struct SolanaInflationReward { epoch: u64, effective_slot: u64, amount: u64, post_balance: u64, commission: std::option::Option, commission_bps: crate::SolanaWireField, } impl SolanaInflationReward { /// Returns the rewarded epoch. #[must_use] pub const fn epoch(&self) -> u64 { return self.epoch; } /// Returns the slot at which the reward became effective. #[must_use] pub const fn effective_slot(&self) -> u64 { return self.effective_slot; } /// Returns the reward amount in lamports. #[must_use] pub const fn amount(&self) -> u64 { return self.amount; } /// Returns the post-reward account balance in lamports. #[must_use] pub const fn post_balance(&self) -> u64 { return self.post_balance; } /// Returns the nullable legacy/effective commission percentage. #[must_use] pub const fn commission(&self) -> std::option::Option { return self.commission; } /// Returns the basis-point commission while preserving omitted/null/present wire states. #[must_use] pub const fn commission_bps(&self) -> &crate::SolanaWireField { return &self.commission_bps; } /// Decodes one non-null inflation reward from its Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { std::result::Result::Ok(wire) => std::result::Result::Ok(Self { epoch: wire.epoch, effective_slot: wire.effective_slot, amount: wire.amount, post_balance: wire.post_balance, commission: wire.commission, commission_bps: wire.commission_bps, }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Optional configuration accepted by `getSupply`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaSupplyConfig { commitment: std::option::Option, exclude_non_circulating_accounts_list: std::option::Option, } impl SolanaSupplyConfig { /// Creates a supply configuration. #[must_use] pub const fn new(commitment: std::option::Option, exclude_non_circulating_accounts_list: std::option::Option) -> Self { return Self { commitment, exclude_non_circulating_accounts_list }; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns whether the request explicitly controls omission of the non-circulating account list. #[must_use] pub const fn exclude_non_circulating_accounts_list(&self) -> std::option::Option { return self.exclude_non_circulating_accounts_list; } /// Returns whether this config would serialize to an empty object. pub(crate) const fn is_empty(&self) -> bool { return self.commitment.is_none() && self.exclude_non_circulating_accounts_list.is_none(); } /// Serializes this config to the exact Solana JSON-RPC object. #[must_use] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); } if let std::option::Option::Some(exclude) = self.exclude_non_circulating_accounts_list { object.insert("excludeNonCirculatingAccountsList".to_owned(), serde_json::Value::Bool(exclude)); } return serde_json::Value::Object(object); } } /// Supply totals returned inside the contextual `getSupply` response. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SolanaSupply { total: u64, circulating: u64, non_circulating: u64, non_circulating_accounts: std::vec::Vec, } impl SolanaSupply { /// Returns the total supply in lamports. #[must_use] pub const fn total(&self) -> u64 { return self.total; } /// Returns the circulating supply in lamports. #[must_use] pub const fn circulating(&self) -> u64 { return self.circulating; } /// Returns the non-circulating supply in lamports. #[must_use] pub const fn non_circulating(&self) -> u64 { return self.non_circulating; } /// Returns the ordered non-circulating account list supplied by the runtime. #[must_use] pub fn non_circulating_accounts(&self) -> &[ksp_core_lib::Pubkey] { return self.non_circulating_accounts.as_slice(); } /// Decodes a supply result from its Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut accounts = std::vec::Vec::with_capacity(wire.non_circulating_accounts.len()); for account in wire.non_circulating_accounts { let parsed = crate::parse_wire_pubkey(method, "nonCirculatingAccounts", account.as_str()); match parsed { std::result::Result::Ok(pubkey) => accounts.push(pubkey), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(Self { total: wire.total, circulating: wire.circulating, non_circulating: wire.non_circulating, non_circulating_accounts: accounts, }); } } impl crate::HttpTransportPool { /// Executes typed `getInflationGovernor` and returns the runtime-provided schedule values unchanged. pub async fn get_inflation_governor( &self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaCommitmentConfig>, ) -> ksp_core_lib::Result { let mut params = std::vec::Vec::new(); push_economics_commitment_config(&mut params, config); let value = self.execute_economics_rpc("getInflationGovernor", role, params).await; return match value { std::result::Result::Ok(value) => crate::SolanaInflationGovernor::decode_wire("getInflationGovernor", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getInflationRate` without locally recalculating the inflation schedule. pub async fn get_inflation_rate(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { let value = self.execute_economics_rpc("getInflationRate", role, std::vec::Vec::new()).await; return match value { std::result::Result::Ok(value) => crate::SolanaInflationRate::decode_wire("getInflationRate", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getInflationReward`, preserving input order, positional nulls and runtime commission extensions. pub async fn get_inflation_reward( &self, role: &crate::HttpRoleName, addresses: &[ksp_core_lib::Pubkey], config: std::option::Option<&crate::SolanaInflationRewardConfig>, ) -> ksp_core_lib::Result>> { if let std::option::Option::Some(config) = config && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getInflationReward commitment must be confirmed or finalized when explicitly provided", ) .with_context("rpc_method", "getInflationReward") .with_context("commitment", "processed"), ); } let address_values = addresses.iter().map(|address| return serde_json::Value::String(address.to_string())).collect::>(); let mut params = std::vec![serde_json::Value::Array(address_values)]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push((*config).to_json_value()); } let value = self.execute_economics_rpc("getInflationReward", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_inflation_rewards("getInflationReward", value, addresses.len()); } /// Executes typed `getStakeMinimumDelegation` and preserves the contextual runtime value in lamports. pub async fn get_stake_minimum_delegation( &self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result> { let mut params = std::vec::Vec::new(); push_economics_context_config(&mut params, config); let value = self.execute_economics_rpc("getStakeMinimumDelegation", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_economics_u64_response("getStakeMinimumDelegation", value); } /// Executes typed `getSupply`, preserving the explicit account-list exclusion flag and contextual totals. pub async fn get_supply( &self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaSupplyConfig>, ) -> ksp_core_lib::Result> { let mut params = std::vec::Vec::new(); if let std::option::Option::Some(config) = config && !config.is_empty() { params.push((*config).to_json_value()); } let value = self.execute_economics_rpc("getSupply", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_supply_response("getSupply", value); } async fn execute_economics_rpc( &self, method_name: &'static str, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result { let descriptor = economics_descriptor(method_name); let descriptor = match descriptor { std::result::Result::Ok(descriptor) => descriptor, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.execute_standard_rpc(role, descriptor, params).await; } } #[derive(serde::Deserialize)] struct WireEconomicsRpcResponse { context: serde_json::Value, value: T, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireInflationGovernor { initial: f64, terminal: f64, taper: f64, foundation: f64, foundation_term: f64, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireInflationRate { total: f64, validator: f64, foundation: f64, epoch: u64, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireInflationReward { epoch: u64, effective_slot: u64, amount: u64, post_balance: u64, #[serde(default)] commission: std::option::Option, #[serde(default)] commission_bps: crate::SolanaWireField, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireSupply { total: u64, circulating: u64, non_circulating: u64, non_circulating_accounts: std::vec::Vec, } fn push_economics_commitment_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaCommitmentConfig>) { if let std::option::Option::Some(config) = config && config.commitment().is_some() { params.push((*config).to_json_value()); } return; } fn push_economics_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { if let std::option::Option::Some(config) = config && (config.commitment().is_some() || config.min_context_slot().is_some()) { params.push((*config).to_json_value()); } return; } fn decode_inflation_rewards( method: &str, value: serde_json::Value, expected_count: usize, ) -> ksp_core_lib::Result>> { let decoded = crate::decode_wire_json::>>(method, value); let values = match decoded { std::result::Result::Ok(values) => values, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if values.len() != expected_count { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "getInflationReward result count does not match the requested address count") .with_context("rpc_method", method) .with_context("expected_count", expected_count.to_string()) .with_context("actual_count", values.len().to_string()), ); } let mut rewards = std::vec::Vec::with_capacity(values.len()); for value in values { match value { std::option::Option::Some(value) => { let reward = crate::SolanaInflationReward::decode_wire(method, value); match reward { std::result::Result::Ok(reward) => rewards.push(std::option::Option::Some(reward)), std::result::Result::Err(error) => return std::result::Result::Err(error), } }, std::option::Option::None => rewards.push(std::option::Option::None), } } return std::result::Result::Ok(rewards); } fn decode_economics_u64_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let decoded = crate::decode_wire_json::>(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let context = crate::SolanaRpcContext::decode_wire(method, wire.context); return match context { std::result::Result::Ok(context) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, wire.value)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn decode_supply_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let decoded = crate::decode_wire_json::>(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let context = crate::SolanaRpcContext::decode_wire(method, wire.context); let context = match context { std::result::Result::Ok(context) => context, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let supply = crate::SolanaSupply::decode_wire(method, wire.value); return match supply { std::result::Result::Ok(supply) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, supply)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn economics_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { let descriptor = crate::find_http_rpc_method(method); return match descriptor { std::option::Option::Some(descriptor) if descriptor.category() == crate::HttpRpcCategory::Economics && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_4 => { std::result::Result::Ok(descriptor) }, _ => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Economics descriptor is missing from the audited 0.2.4 registry") .with_context("rpc_method", method), ), }; } #[cfg(test)] #[path = "../unit_tests/rpc_economics.rs"] mod tests;