From 21bb64a276815a7e73ec5dbc43964edd84165075 Mon Sep 17 00:00:00 2001 From: sword_smith Date: Thu, 2 Oct 2025 16:41:01 +0200 Subject: [PATCH] feat: Add endpoint for total supply Total supply is defined as liquid supply plus timelocked supply. --- src/lib.rs | 1 + src/main.rs | 2 ++ src/rpc/circulating_supply.rs | 53 ++++--------------------------- src/rpc/mod.rs | 1 + src/rpc/total_supply.rs | 31 ++++++++++++++++++ src/shared.rs | 60 +++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 src/rpc/total_supply.rs create mode 100644 src/shared.rs diff --git a/src/lib.rs b/src/lib.rs index 7c39228..af560b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,3 +4,4 @@ pub mod http_util; pub mod model; pub mod neptune_rpc; pub mod rpc; +pub mod shared; diff --git a/src/main.rs b/src/main.rs index 5551139..18e3a11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use neptune_explorer::rpc::block_info::block_info; use neptune_explorer::rpc::circulating_supply::circulating_supply; use neptune_explorer::rpc::pow_puzzle::pow_puzzle; use neptune_explorer::rpc::provide_pow_solution::provide_pow_solution; +use neptune_explorer::rpc::total_supply::total_supply; use neptune_explorer::rpc::utxo_digest::utxo_digest; use tower_http::services::ServeDir; use tracing::info; @@ -60,6 +61,7 @@ pub fn setup_routes(app_state: AppState) -> Router { .route("/rpc/utxo_digest/:index", get(utxo_digest)) .route("/rpc/pow_puzzle/*address", get(pow_puzzle)) .route("/rpc/circulating_supply", get(circulating_supply)) + .route("/rpc/total_supply", get(total_supply)) .route("/rpc/provide_pow_solution", post(provide_pow_solution)) // -- Dynamic HTML pages -- .route("/", get(root)) diff --git a/src/rpc/circulating_supply.rs b/src/rpc/circulating_supply.rs index 8dad818..954f100 100644 --- a/src/rpc/circulating_supply.rs +++ b/src/rpc/circulating_supply.rs @@ -3,67 +3,28 @@ use std::sync::Arc; use axum::extract::State; use axum::response::Json; use axum::response::Response; -use neptune_cash::api::export::BlockHeight; -use neptune_cash::protocol::consensus::block::block_height::BLOCKS_PER_GENERATION; -use neptune_cash::protocol::consensus::block::block_height::NUM_BLOCKS_SKIPPED_BECAUSE_REBOOT; -use neptune_cash::protocol::consensus::block::Block; -use neptune_cash::protocol::consensus::block::PREMINE_MAX_SIZE; use tarpc::context; use crate::http_util::rpc_err; use crate::http_util::rpc_method_err; use crate::model::app_state::AppState; +use crate::shared::monetary_supplies; -/// Return the number of coins that are liquid, assuming all redemptions on the -/// old chain have successfully been made. +/// Return the monetary amount that is liquid, assuming all redemptions on the +/// old chain have successfully been made. Returned unit is nau, Neptune Atomic +/// Units. To convert to number of coins, divide by $4*10^{30}$. #[axum::debug_handler] pub async fn circulating_supply(State(state): State>) -> Result, Response> { let s = state.load(); - // TODO: Remove this local declaration once version of neptune-core with - // this value public is released. - let generation_0_subsidy = Block::block_subsidy(BlockHeight::genesis().next()); - let block_height: u64 = s + let block_height = s .rpc_client .block_height(context::current(), s.token()) .await .map_err(rpc_err)? - .map_err(rpc_method_err)? - .into(); - let effective_block_height = block_height + NUM_BLOCKS_SKIPPED_BECAUSE_REBOOT; - let (num_generations, num_blocks_in_generation): (u64, u32) = ( - effective_block_height / BLOCKS_PER_GENERATION, - (effective_block_height % BLOCKS_PER_GENERATION) - .try_into() - .expect("There are fewer than u32::MAX blocks per generation"), - ); + .map_err(rpc_method_err)?; - let mut liquid_supply = PREMINE_MAX_SIZE; - let mut liquid_subsidy = generation_0_subsidy.half(); - let blocks_per_generation: u32 = BLOCKS_PER_GENERATION - .try_into() - .expect("There are fewer than u32::MAX blocks per generation"); - for _ in 0..num_generations { - liquid_supply += liquid_subsidy.scalar_mul(blocks_per_generation); - liquid_subsidy = liquid_subsidy.half(); - } - - liquid_supply += liquid_subsidy.scalar_mul(num_blocks_in_generation); - - // How much of timelocked miner rewards have been unlocked? Assume that the - // timelock is exactly one generation long. In reality the timelock is - // is defined in relation to timestamp and not block heights, so this is - // only a (pretty good) approximation. - - let mut released_subsidy = generation_0_subsidy.half(); - for _ in 1..num_generations { - liquid_supply += released_subsidy.scalar_mul(blocks_per_generation); - released_subsidy = released_subsidy.half(); - } - - if num_generations > 0 { - liquid_supply += released_subsidy.scalar_mul(num_blocks_in_generation); - } + let (liquid_supply, _) = monetary_supplies(block_height); Ok(Json(liquid_supply.to_nau_f64())) } diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 4a64f05..c62cc52 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -3,4 +3,5 @@ pub mod block_info; pub mod circulating_supply; pub mod pow_puzzle; pub mod provide_pow_solution; +pub mod total_supply; pub mod utxo_digest; diff --git a/src/rpc/total_supply.rs b/src/rpc/total_supply.rs new file mode 100644 index 0000000..ba71d3f --- /dev/null +++ b/src/rpc/total_supply.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::response::Json; +use axum::response::Response; +use tarpc::context; + +use crate::http_util::rpc_err; +use crate::http_util::rpc_method_err; +use crate::model::app_state::AppState; +use crate::shared::monetary_supplies; + +/// Return the total monetary supply, the sum of the timeloced and liquid +/// supply. Assumes all redemptions on the old chain have successfully been +/// made. Returned unit is nau, Neptune Atomic Units. To convert to number of +/// coins, divide by $4*10^{30}$. +#[axum::debug_handler] +pub async fn total_supply(State(state): State>) -> Result, Response> { + let s = state.load(); + + let block_height = s + .rpc_client + .block_height(context::current(), s.token()) + .await + .map_err(rpc_err)? + .map_err(rpc_method_err)?; + + let (_, total_supply) = monetary_supplies(block_height); + + Ok(Json(total_supply.to_nau_f64())) +} diff --git a/src/shared.rs b/src/shared.rs new file mode 100644 index 0000000..f4064d5 --- /dev/null +++ b/src/shared.rs @@ -0,0 +1,60 @@ +use neptune_cash::api::export::BlockHeight; +use neptune_cash::api::export::NativeCurrencyAmount; +use neptune_cash::protocol::consensus::block::block_height::BLOCKS_PER_GENERATION; +use neptune_cash::protocol::consensus::block::block_height::NUM_BLOCKS_SKIPPED_BECAUSE_REBOOT; +use neptune_cash::protocol::consensus::block::Block; +use neptune_cash::protocol::consensus::block::PREMINE_MAX_SIZE; + +/// Return the pair (liquid supply, total supply) +/// +/// Assumes all redemption claims have been rewarded. +pub(crate) fn monetary_supplies( + block_height: BlockHeight, +) -> (NativeCurrencyAmount, NativeCurrencyAmount) { + let block_height: u64 = block_height.into(); + let generation_0_subsidy = Block::block_subsidy(BlockHeight::genesis().next()); + let effective_block_height = block_height + NUM_BLOCKS_SKIPPED_BECAUSE_REBOOT; + let (num_generations, num_blocks_in_curr_gen): (u64, u32) = ( + effective_block_height / BLOCKS_PER_GENERATION, + (effective_block_height % BLOCKS_PER_GENERATION) + .try_into() + .expect("There are fewer than u32::MAX blocks per generation"), + ); + + let mut liquid_supply = PREMINE_MAX_SIZE; + let mut liquid_subsidy = generation_0_subsidy.half(); + let mut total_supply = PREMINE_MAX_SIZE; + let blocks_per_generation: u32 = BLOCKS_PER_GENERATION + .try_into() + .expect("There are fewer than u32::MAX blocks per generation"); + for _ in 0..num_generations { + liquid_supply += liquid_subsidy.scalar_mul(blocks_per_generation); + total_supply += liquid_subsidy.scalar_mul(2); + liquid_subsidy = liquid_subsidy.half(); + } + + let liquid_supply_current_generation = liquid_subsidy.scalar_mul(num_blocks_in_curr_gen); + liquid_supply += liquid_supply_current_generation; + total_supply += liquid_supply_current_generation.scalar_mul(2); + + // How much of timelocked miner rewards have been unlocked? Assume that the + // timelock is exactly one generation long. In reality the timelock is + // is defined in relation to timestamp and not block heights, so this is + // only a (good) approximation. + + let mut released_subsidy = generation_0_subsidy.half(); + for _ in 1..num_generations { + liquid_supply += released_subsidy.scalar_mul(blocks_per_generation); + released_subsidy = released_subsidy.half(); + } + + if num_generations > 0 { + liquid_supply += released_subsidy.scalar_mul(num_blocks_in_curr_gen); + } + + // If you want correct results for anything but main net, and claims are + // only being refunded on main net, the size of the claims pool must be + // subtracted here, if that the network is not main net. + + (liquid_supply, total_supply) +}