diff --git a/src/main.rs b/src/main.rs index 7feaa66..5551139 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use neptune_explorer::model::app_state::AppState; use neptune_explorer::neptune_rpc; use neptune_explorer::rpc::block_digest::block_digest; 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::utxo_digest::utxo_digest; @@ -58,6 +59,7 @@ pub fn setup_routes(app_state: AppState) -> Router { .route("/rpc/block_digest/*selector", get(block_digest)) .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/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 new file mode 100644 index 0000000..8dad818 --- /dev/null +++ b/src/rpc/circulating_supply.rs @@ -0,0 +1,69 @@ +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; + +/// Return the number of coins that are liquid, assuming all redemptions on the +/// old chain have successfully been made. +#[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 + .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"), + ); + + 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); + } + + Ok(Json(liquid_supply.to_nau_f64())) +} diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 0d9f7ec..4a64f05 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -1,5 +1,6 @@ pub mod block_digest; pub mod block_info; +pub mod circulating_supply; pub mod pow_puzzle; pub mod provide_pow_solution; pub mod utxo_digest;