Files
nym/explorer-api/src/unstable/http.rs
T
Mark Sinclair 15c3012199 explorer-api: add nym node endpoints + UI to show nym-nodes and account balances (#5183)
* explorer-api: add nym node endpoints + UI to show nym-nodes and account balances

* explorer-api: add endpoints to get operator rewards
explorer-ui: show delegations on nym-nodes, show operator rewards, bug fixes

* explorer-ui: change summary screen to only show nym-node stats

* explorer-api: add unstable routes to get legacy mixnodes and gateways from the contract instead of the Nym API
explorer-ui: adapt front-end to show less information in legacy nodes with plain bond types

* explorer-ui: fix up source of legacy mixnode data

* explorer-ui: add more account page null and undefined checks

* explorer-ui: filter out null gateway versions

* explorer-ui: sanitise gateway versions

* explorer-ui: add more guards on the balance parts to check that greater than 0

* explorer-api: make /tmp/unstable/gateways endpoint compatible with the current Harbour Master API

* explorer-ui: fix typo

* cargo fmt

* Add node-id, total stake and links to nodes list

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2024-12-05 08:17:30 +00:00

259 lines
8.8 KiB
Rust

// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::{
NymNodeWithDescriptionAndLocation, NymNodeWithDescriptionAndLocationAndDelegations,
NymVestingAccount, PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond,
};
use nym_mixnet_contract_common::{Addr, Coin, NodeId};
use nym_validator_client::nyxd::AccountId;
use okapi::openapi3::OpenApi;
use rocket::response::status::NotFound;
use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize};
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
use std::collections::HashMap;
use std::str::FromStr;
pub fn unstable_temp_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways, all_gateway_bonds, all_mixnode_bonds, all_nym_nodes, get_nym_node_by_id, get_account_by_addr]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_legacy_detailed_gateways().await;
let mut nym_node_gateways: Vec<PrettyDetailedGatewayBond> = state
.inner
.nymnodes
.pretty_gateways()
.await
.clone()
.into_iter()
.filter(|g| {
!gateways
.iter()
.any(|g2| g.gateway.identity_key == g2.gateway.identity_key)
})
.collect();
gateways.append(&mut nym_node_gateways);
Json(gateways)
}
#[openapi(tag = "UNSTABLE")]
#[get("/legacy-gateway-bonds")]
pub(crate) async fn all_gateway_bonds(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
Json(state.inner.gateways.get_legacy_detailed_gateways().await)
}
#[openapi(tag = "UNSTABLE")]
#[get("/legacy-mixnode-bonds")]
pub(crate) async fn all_mixnode_bonds(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedMixNodeBond>> {
Json(state.inner.mixnodes.get_legacy_detailed_mixnodes().await)
}
#[openapi(tag = "UNSTABLE")]
#[get("/nym-nodes")]
pub(crate) async fn all_nym_nodes(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<NymNodeWithDescriptionAndLocation>> {
let nodes = state
.inner
.nymnodes
.get_bonded_nymnodes_with_description_and_location()
.await;
Json(nodes.values().cloned().collect())
}
#[openapi(tag = "UNSTABLE")]
#[get("/nym-nodes/<node_id>")]
pub(crate) async fn get_nym_node_by_id(
node_id: NodeId,
state: &State<ExplorerApiStateContext>,
) -> Json<Option<NymNodeWithDescriptionAndLocationAndDelegations>> {
let nodes = state
.inner
.nymnodes
.get_bonded_nymnodes_with_description_and_location()
.await;
Json(match nodes.get(&node_id).cloned() {
None => None,
Some(node) => {
let delegations = state.inner.get_delegations_by_node(node_id).await.ok();
Some(NymNodeWithDescriptionAndLocationAndDelegations {
node_id: node.node_id,
contract_node_type: node.contract_node_type,
description: node.description,
bond_information: node.bond_information,
rewarding_details: node.rewarding_details,
location: node.location,
delegations,
})
}
})
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NyxAccountDelegationDetails {
pub node_id: NodeId,
pub delegated: Coin,
pub height: u64,
pub proxy: Option<Addr>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NyxAccountDelegationRewardDetails {
pub node_id: NodeId,
pub rewards: Coin,
pub amount_staked: Coin,
pub node_still_fully_bonded: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NyxAccountDetails {
pub address: String,
pub balances: Vec<Coin>,
pub total_value: Coin,
pub delegations: Vec<NyxAccountDelegationDetails>,
pub accumulated_rewards: Vec<NyxAccountDelegationRewardDetails>,
pub total_delegations: Coin,
pub claimable_rewards: Coin,
pub vesting_account: Option<NymVestingAccount>,
pub operator_rewards: Option<Coin>,
}
#[openapi(tag = "UNSTABLE")]
#[get("/account/<addr>")]
pub(crate) async fn get_account_by_addr(
addr: String,
state: &State<ExplorerApiStateContext>,
) -> Result<Json<NyxAccountDetails>, NotFound<String>> {
match AccountId::from_str(&addr) {
Ok(address) => {
let mut total_value = 0u128;
// 1. get balances of chain tokens
let balances: Vec<Coin> = state
.inner
.get_balance(&address)
.await?
.into_iter()
.map(|c| {
if c.denom == "unym" {
total_value += c.amount;
}
c.into()
})
.collect();
// 2. get list of delegations (history)
let delegations: Vec<NyxAccountDelegationDetails> = state
.inner
.get_delegations(&address)
.await?
.into_iter()
.map(|d| NyxAccountDelegationDetails {
delegated: d.amount,
height: d.height,
node_id: d.node_id,
proxy: d.proxy,
})
.collect();
// 3. get the current reward for each active delegation
let mut rewards_map: HashMap<&NodeId, NyxAccountDelegationRewardDetails> =
HashMap::new();
for d in &delegations {
if rewards_map.contains_key(&d.node_id) {
continue;
}
if let Ok(r) = state
.inner
.get_delegation_rewards(&address, &d.node_id, &d.proxy)
.await
{
if let Some(rewards) = r.amount_earned {
rewards_map.insert(
&d.node_id,
NyxAccountDelegationRewardDetails {
node_id: d.node_id,
rewards,
amount_staked: r.amount_staked.unwrap_or_default(),
node_still_fully_bonded: r.node_still_fully_bonded,
},
);
}
}
}
// 4. make the map of rewards into a vec and sum the rewards and delegations
let accumulated_rewards: Vec<NyxAccountDelegationRewardDetails> =
rewards_map.values().cloned().collect();
let mut claimable_rewards = 0u128;
let mut total_delegations = 0u128;
for r in &accumulated_rewards {
claimable_rewards += r.rewards.amount.u128();
total_delegations += r.amount_staked.amount.u128();
total_value += r.rewards.amount.u128();
total_value += r.amount_staked.amount.u128();
}
// 5. get vesting account details (if present)
let vesting_account = state
.inner
.get_vesting_balance(&address)
.await
.unwrap_or_default();
if let Some(vesting_account) = vesting_account.clone() {
total_value += vesting_account.locked.amount.u128();
total_value += vesting_account.spendable.amount.u128();
}
// 6. get operator rewards
let operator_rewards: Option<Coin> = if let Ok(operator_rewards_res) =
state.inner.get_operator_rewards(&address).await
{
if let Some(operator_reward_amount) = &operator_rewards_res.amount_earned {
total_value += operator_reward_amount.amount.u128();
}
operator_rewards_res.amount_earned
} else {
None
};
// 7. convert totals
let claimable_rewards = Coin::new(claimable_rewards, "unym");
let total_delegations = Coin::new(total_delegations, "unym");
let total_value = Coin::new(total_value, "unym");
Ok(Json(NyxAccountDetails {
address: address.to_string(),
balances,
delegations,
accumulated_rewards,
claimable_rewards,
total_delegations,
total_value,
vesting_account,
operator_rewards,
}))
}
Err(_e) => Err(NotFound("Account not found".to_string())),
}
}