feat: Add endpoint for total supply

Total supply is defined as liquid supply plus timelocked supply.
This commit is contained in:
sword_smith 2025-10-02 16:41:01 +02:00
parent d22fe50de6
commit 21bb64a276
No known key found for this signature in database
GPG Key ID: 02593B11D184AEC7
6 changed files with 102 additions and 46 deletions

View File

@ -4,3 +4,4 @@ pub mod http_util;
pub mod model; pub mod model;
pub mod neptune_rpc; pub mod neptune_rpc;
pub mod rpc; pub mod rpc;
pub mod shared;

View File

@ -16,6 +16,7 @@ use neptune_explorer::rpc::block_info::block_info;
use neptune_explorer::rpc::circulating_supply::circulating_supply; use neptune_explorer::rpc::circulating_supply::circulating_supply;
use neptune_explorer::rpc::pow_puzzle::pow_puzzle; use neptune_explorer::rpc::pow_puzzle::pow_puzzle;
use neptune_explorer::rpc::provide_pow_solution::provide_pow_solution; 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 neptune_explorer::rpc::utxo_digest::utxo_digest;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::info; 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/utxo_digest/:index", get(utxo_digest))
.route("/rpc/pow_puzzle/*address", get(pow_puzzle)) .route("/rpc/pow_puzzle/*address", get(pow_puzzle))
.route("/rpc/circulating_supply", get(circulating_supply)) .route("/rpc/circulating_supply", get(circulating_supply))
.route("/rpc/total_supply", get(total_supply))
.route("/rpc/provide_pow_solution", post(provide_pow_solution)) .route("/rpc/provide_pow_solution", post(provide_pow_solution))
// -- Dynamic HTML pages -- // -- Dynamic HTML pages --
.route("/", get(root)) .route("/", get(root))

View File

@ -3,67 +3,28 @@ use std::sync::Arc;
use axum::extract::State; use axum::extract::State;
use axum::response::Json; use axum::response::Json;
use axum::response::Response; 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 tarpc::context;
use crate::http_util::rpc_err; use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err; use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState; use crate::model::app_state::AppState;
use crate::shared::monetary_supplies;
/// Return the number of coins that are liquid, assuming all redemptions on the /// Return the monetary amount that is liquid, assuming all redemptions on the
/// old chain have successfully been made. /// 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] #[axum::debug_handler]
pub async fn circulating_supply(State(state): State<Arc<AppState>>) -> Result<Json<f64>, Response> { pub async fn circulating_supply(State(state): State<Arc<AppState>>) -> Result<Json<f64>, Response> {
let s = state.load(); let s = state.load();
// TODO: Remove this local declaration once version of neptune-core with let block_height = s
// this value public is released.
let generation_0_subsidy = Block::block_subsidy(BlockHeight::genesis().next());
let block_height: u64 = s
.rpc_client .rpc_client
.block_height(context::current(), s.token()) .block_height(context::current(), s.token())
.await .await
.map_err(rpc_err)? .map_err(rpc_err)?
.map_err(rpc_method_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"),
);
let mut liquid_supply = PREMINE_MAX_SIZE; let (liquid_supply, _) = monetary_supplies(block_height);
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);
}
Ok(Json(liquid_supply.to_nau_f64())) Ok(Json(liquid_supply.to_nau_f64()))
} }

View File

@ -3,4 +3,5 @@ pub mod block_info;
pub mod circulating_supply; pub mod circulating_supply;
pub mod pow_puzzle; pub mod pow_puzzle;
pub mod provide_pow_solution; pub mod provide_pow_solution;
pub mod total_supply;
pub mod utxo_digest; pub mod utxo_digest;

31
src/rpc/total_supply.rs Normal file
View File

@ -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<Arc<AppState>>) -> Result<Json<f64>, 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()))
}

60
src/shared.rs Normal file
View File

@ -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)
}