188 lines
8.3 KiB
Rust
188 lines
8.3 KiB
Rust
// file: crates/ksp-app-wallet-desk/src/wallet_balance.rs
|
|
// version: 4
|
|
|
|
//! Safe Wallet balance projection produced by the typed HTTP Transport path.
|
|
|
|
use ts_rs::TS; // rust-rules: trait-import
|
|
|
|
/// Safe balance snapshot for the currently authorized Wallet.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts")]
|
|
pub(crate) struct WalletBalanceDto {
|
|
/// Optional Solana RPC API version reported by the selected node.
|
|
pub(crate) api_version: std::option::Option<String>,
|
|
/// Exact decimal account balance in lamports, encoded as text to avoid JavaScript integer precision loss.
|
|
pub(crate) lamports: String,
|
|
/// RPC context slot associated with this balance.
|
|
pub(crate) slot: u64,
|
|
/// Exact decimal SOL presentation derived from lamports without floating point.
|
|
pub(crate) sol: String,
|
|
/// Arithmetic mean of successful SOL/USD observations produced by this refresh, or unavailable when no provider returned one.
|
|
pub(crate) sol_usd_average: std::option::Option<String>,
|
|
/// Exact USD equivalent of the current SOL balance using `sol_usd_average`, or unavailable when the auxiliary price is unavailable.
|
|
pub(crate) sol_usd_equivalent: std::option::Option<String>,
|
|
/// Composite-selected Transport profile used for the RPC.
|
|
pub(crate) transport_profile: String,
|
|
/// Logical Transport role used for the RPC.
|
|
pub(crate) transport_role: String,
|
|
/// Root-scoped Wallet identifier whose authorized Pubkey was queried.
|
|
pub(crate) wallet_id: String,
|
|
}
|
|
|
|
/// Formats exact lamports as a decimal SOL string with nine fractional digits.
|
|
#[must_use]
|
|
pub(crate) fn format_lamports_as_sol(lamports: u64) -> String {
|
|
let whole = lamports / 1_000_000_000;
|
|
let fractional = lamports % 1_000_000_000;
|
|
return format!("{whole}.{fractional:09}");
|
|
}
|
|
|
|
/// Computes a deterministic SOL/USD arithmetic mean without IEEE-754 conversion.
|
|
///
|
|
/// The output is rounded half-up to at most eight fractional digits and trailing zeroes are removed. Arithmetic overflow degrades to `None`, which Wallet Desk
|
|
/// renders as `N.A.` without affecting the authoritative SOL balance.
|
|
#[must_use]
|
|
pub(crate) fn average_sol_usd_prices(prices: &[ksp_offchain_transport_lib::MarketPriceDecimal]) -> std::option::Option<String> {
|
|
const OUTPUT_SCALE: u8 = 8;
|
|
if prices.is_empty() {
|
|
return std::option::Option::None;
|
|
}
|
|
let max_scale = prices.iter().map(ksp_offchain_transport_lib::MarketPriceDecimal::scale).max();
|
|
let max_scale = match max_scale {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let mut sum = 0_u128;
|
|
for price in prices {
|
|
let exponent = u32::from(max_scale.saturating_sub(price.scale()));
|
|
let factor = 10_u128.checked_pow(exponent);
|
|
let factor = match factor {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let coefficient = price.coefficient().checked_mul(factor);
|
|
let coefficient = match coefficient {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
sum = match sum.checked_add(coefficient) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
}
|
|
let count = match u128::try_from(prices.len()) {
|
|
std::result::Result::Ok(value) if value > 0 => value,
|
|
_ => return std::option::Option::None,
|
|
};
|
|
let target_scale = OUTPUT_SCALE;
|
|
let (numerator, denominator) = if max_scale <= target_scale {
|
|
let factor = 10_u128.checked_pow(u32::from(target_scale - max_scale));
|
|
let factor = match factor {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let numerator = match sum.checked_mul(factor) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
(numerator, count)
|
|
} else {
|
|
let factor = 10_u128.checked_pow(u32::from(max_scale - target_scale));
|
|
let factor = match factor {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let denominator = match count.checked_mul(factor) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
(sum, denominator)
|
|
};
|
|
let quotient = numerator / denominator;
|
|
let remainder = numerator % denominator;
|
|
let round_up = remainder >= denominator.div_ceil(2);
|
|
let coefficient = if round_up {
|
|
match quotient.checked_add(1) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
}
|
|
} else {
|
|
quotient
|
|
};
|
|
return std::option::Option::Some(format_scaled_decimal(coefficient, target_scale));
|
|
}
|
|
|
|
/// Computes the exact USD equivalent of a lamport balance from the current SOL/USD average.
|
|
///
|
|
/// The output is rounded half-up to at most eight fractional digits. Missing or invalid auxiliary price data and arithmetic overflow degrade to `None`; the
|
|
/// authoritative SOL balance remains unaffected.
|
|
#[must_use]
|
|
pub(crate) fn sol_usd_equivalent(lamports: u64, sol_usd_average: std::option::Option<&str>) -> std::option::Option<String> {
|
|
const LAMPORT_SCALE: u8 = 9;
|
|
const OUTPUT_SCALE: u8 = 8;
|
|
let source = match sol_usd_average {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let average = match ksp_offchain_transport_lib::MarketPriceDecimal::parse(source) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let coefficient = match u128::from(lamports).checked_mul(average.coefficient()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let source_scale = u16::from(LAMPORT_SCALE) + u16::from(average.scale());
|
|
let target_scale = u16::from(OUTPUT_SCALE);
|
|
let rounded_coefficient = if source_scale <= target_scale {
|
|
let exponent = u32::from(target_scale - source_scale);
|
|
let factor = match 10_u128.checked_pow(exponent) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
match coefficient.checked_mul(factor) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
}
|
|
} else {
|
|
let exponent = u32::from(source_scale - target_scale);
|
|
let divisor = match 10_u128.checked_pow(exponent) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let quotient = coefficient / divisor;
|
|
let remainder = coefficient % divisor;
|
|
if remainder >= divisor.div_ceil(2) {
|
|
match quotient.checked_add(1) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
}
|
|
} else {
|
|
quotient
|
|
}
|
|
};
|
|
return std::option::Option::Some(format_scaled_decimal(rounded_coefficient, OUTPUT_SCALE));
|
|
}
|
|
|
|
fn format_scaled_decimal(mut coefficient: u128, mut scale: u8) -> String {
|
|
while scale > 0 && coefficient.is_multiple_of(10) {
|
|
coefficient /= 10;
|
|
scale -= 1;
|
|
}
|
|
let digits = coefficient.to_string();
|
|
if scale == 0 {
|
|
return digits;
|
|
}
|
|
let scale = usize::from(scale);
|
|
if digits.len() > scale {
|
|
let split = digits.len() - scale;
|
|
return format!("{}.{}", &digits[..split], &digits[split..]);
|
|
}
|
|
return format!("0.{}{}", "0".repeat(scale - digits.len()), digits);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/wallet_balance.rs"]
|
|
mod tests;
|