Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/src/rpc_economics.rs
2026-08-20 09:24:10 +02:00

592 lines
22 KiB
Rust

// 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<Self> {
let decoded = crate::decode_wire_json::<WireInflationGovernor>(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<Self> {
let decoded = crate::decode_wire_json::<WireInflationRate>(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<u64>,
commitment: std::option::Option<crate::SolanaCommitment>,
min_context_slot: std::option::Option<u64>,
}
impl SolanaInflationRewardConfig {
/// Creates an inflation-reward configuration.
#[must_use]
pub const fn new(
epoch: std::option::Option<u64>,
commitment: std::option::Option<crate::SolanaCommitment>,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { epoch, commitment, min_context_slot };
}
/// Returns the optional target epoch.
#[must_use]
pub const fn epoch(&self) -> std::option::Option<u64> {
return self.epoch;
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional minimum context slot.
#[must_use]
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
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<u8>,
commission_bps: crate::SolanaWireField<u16>,
}
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<u8> {
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<u16> {
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<Self> {
let decoded = crate::decode_wire_json::<WireInflationReward>(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<crate::SolanaCommitment>,
exclude_non_circulating_accounts_list: std::option::Option<bool>,
}
impl SolanaSupplyConfig {
/// Creates a supply configuration.
#[must_use]
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>, exclude_non_circulating_accounts_list: std::option::Option<bool>) -> Self {
return Self { commitment, exclude_non_circulating_accounts_list };
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
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<bool> {
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<ksp_core_lib::Pubkey>,
}
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<Self> {
let decoded = crate::decode_wire_json::<WireSupply>(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<crate::SolanaInflationGovernor> {
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<crate::SolanaInflationRate> {
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<std::vec::Vec<std::option::Option<crate::SolanaInflationReward>>> {
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::<std::vec::Vec<_>>();
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<crate::SolanaRpcResponse<u64>> {
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<crate::SolanaRpcResponse<crate::SolanaSupply>> {
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<serde_json::Value>,
) -> ksp_core_lib::Result<serde_json::Value> {
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<T> {
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<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<std::string::String>,
}
fn push_economics_commitment_config(params: &mut std::vec::Vec<serde_json::Value>, 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<serde_json::Value>, 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<std::vec::Vec<std::option::Option<crate::SolanaInflationReward>>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::option::Option<serde_json::Value>>>(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<crate::SolanaRpcResponse<u64>> {
let decoded = crate::decode_wire_json::<WireEconomicsRpcResponse<u64>>(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<crate::SolanaRpcResponse<crate::SolanaSupply>> {
let decoded = crate::decode_wire_json::<WireEconomicsRpcResponse<serde_json::Value>>(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;