Feature/chain status api (#5539)
* nym-api endpoint to return latest block information * attached chain health to health query * fixed serde casing * one of the most nastiest work arounds in test code
This commit is contained in:
committed by
GitHub
parent
128f69a5d6
commit
4f7124e661
Generated
+2
@@ -4814,6 +4814,7 @@ dependencies = [
|
||||
"sha2 0.9.9",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"tendermint 0.40.1",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tokio",
|
||||
@@ -4838,6 +4839,7 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"ecdsa",
|
||||
"getset",
|
||||
"humantime-serde",
|
||||
"nym-compact-ecash",
|
||||
"nym-config",
|
||||
"nym-contracts-common",
|
||||
|
||||
@@ -28,7 +28,6 @@ use nym_network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::time::SystemTime;
|
||||
use tendermint_rpc::endpoint::block::Response as BlockResponse;
|
||||
use tendermint_rpc::endpoint::*;
|
||||
use tendermint_rpc::{Error as TendermintRpcError, Order};
|
||||
use url::Url;
|
||||
@@ -63,6 +62,7 @@ pub use cw3;
|
||||
pub use cw4;
|
||||
pub use cw_controllers;
|
||||
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||
pub use tendermint_rpc::endpoint::block::Response as BlockResponse;
|
||||
pub use tendermint_rpc::{
|
||||
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
|
||||
query::Query,
|
||||
|
||||
@@ -46,6 +46,7 @@ tokio-stream = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
tendermint = { workspace = true }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -13,6 +13,7 @@ cosmwasm-std = { workspace = true }
|
||||
getset = { workspace = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
humantime-serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2.workspace = true
|
||||
tendermint = { workspace = true }
|
||||
|
||||
@@ -1215,6 +1215,7 @@ impl From<Wireguard> for WireguardDetails {
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct ApiHealthResponse {
|
||||
pub status: ApiStatus,
|
||||
pub chain_status: ChainStatus,
|
||||
pub uptime: u64,
|
||||
}
|
||||
|
||||
@@ -1224,10 +1225,25 @@ pub enum ApiStatus {
|
||||
Up,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChainStatus {
|
||||
Synced,
|
||||
Unknown,
|
||||
Stalled {
|
||||
#[serde(
|
||||
serialize_with = "humantime_serde::serialize",
|
||||
deserialize_with = "humantime_serde::deserialize"
|
||||
)]
|
||||
approximate_amount: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
impl ApiHealthResponse {
|
||||
pub fn new_healthy(uptime: Duration) -> Self {
|
||||
ApiHealthResponse {
|
||||
status: ApiStatus::Up,
|
||||
chain_status: ChainStatus::Synced,
|
||||
uptime: uptime.as_secs(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::status::ApiStatusState;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::config;
|
||||
use crate::support::http::state::{AppState, ForcedRefresh};
|
||||
use crate::support::http::state::{AppState, ChainStatusCache, ForcedRefresh};
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
@@ -46,7 +47,7 @@ use nym_coconut_dkg_common::types::{
|
||||
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
||||
use nym_compact_ecash::BlindedSignature;
|
||||
use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth};
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
use nym_config::defaults::{NymNetworkDetails, ValidatorDetails};
|
||||
use nym_contracts_common::IdentityKey;
|
||||
use nym_credentials::IssuanceTicketBook;
|
||||
use nym_credentials_interface::TicketType;
|
||||
@@ -70,6 +71,7 @@ use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use time::Date;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -1264,8 +1266,14 @@ struct TestFixture {
|
||||
}
|
||||
|
||||
impl TestFixture {
|
||||
fn build_app_state(storage: NymApiStorage, ecash_state: EcashState) -> AppState {
|
||||
fn build_app_state(
|
||||
storage: NymApiStorage,
|
||||
ecash_state: EcashState,
|
||||
nyxd_client: Client,
|
||||
) -> AppState {
|
||||
AppState {
|
||||
nyxd_client,
|
||||
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
|
||||
forced_refresh: ForcedRefresh::new(true),
|
||||
nym_contract_cache: NymContractCache::new(),
|
||||
node_status_cache: NodeStatusCache::new(),
|
||||
@@ -1337,12 +1345,22 @@ impl TestFixture {
|
||||
TaskClient::dummy(),
|
||||
);
|
||||
|
||||
// ideally this would have been generic, but that's way too much work
|
||||
// since then `AppState` would have had to be made generic
|
||||
// also, this is such a disgusting workaround to make it 'work'. yuck
|
||||
let mut dummy = NymNetworkDetails::new_empty();
|
||||
dummy.endpoints = vec![ValidatorDetails::new(
|
||||
"http://127.0.0.1:26657",
|
||||
Some("http://why-do-we-even-need-api-url-set-here.wtf"),
|
||||
None,
|
||||
)];
|
||||
dummy.export_to_env();
|
||||
let another_fake_nyxd_client = Client::new(&config).unwrap();
|
||||
|
||||
TestFixture {
|
||||
axum: TestServer::new(
|
||||
Router::new()
|
||||
.nest("/v1/ecash", ecash_routes())
|
||||
.with_state(Self::build_app_state(storage.clone(), ecash_state)),
|
||||
)
|
||||
axum: TestServer::new(Router::new().nest("/v1/ecash", ecash_routes()).with_state(
|
||||
Self::build_app_state(storage.clone(), ecash_state, another_fake_nyxd_client),
|
||||
))
|
||||
.unwrap(),
|
||||
storage,
|
||||
chain_state,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::network::models::{ContractInformation, NetworkDetails};
|
||||
use crate::network::models::{ChainStatusResponse, ContractInformation, NetworkDetails};
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::{extract, Router};
|
||||
use axum::extract::State;
|
||||
use axum::{extract, Json, Router};
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use std::collections::HashMap;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
@@ -12,6 +14,7 @@ use utoipa::ToSchema;
|
||||
pub(crate) fn nym_network_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/details", axum::routing::get(network_details))
|
||||
.route("/chain-status", axum::routing::get(chain_status))
|
||||
.route("/nym-contracts", axum::routing::get(nym_contracts))
|
||||
.route(
|
||||
"/nym-contracts-detailed",
|
||||
@@ -34,6 +37,28 @@ async fn network_details(
|
||||
state.network_details().to_owned().into()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "network",
|
||||
get,
|
||||
path = "/v1/network/chain-status",
|
||||
responses(
|
||||
(status = 200, body = ChainStatusResponse)
|
||||
)
|
||||
)]
|
||||
async fn chain_status(State(state): State<AppState>) -> AxumResult<Json<ChainStatusResponse>> {
|
||||
let chain_status = state
|
||||
.chain_status_cache
|
||||
.get_or_refresh(&state.nyxd_client)
|
||||
.await?;
|
||||
|
||||
let connected_nyxd = state.network_details.connected_nyxd;
|
||||
|
||||
Ok(Json(ChainStatusResponse {
|
||||
connected_nyxd,
|
||||
status: chain_status,
|
||||
}))
|
||||
}
|
||||
|
||||
// it's used for schema generation so dead_code is fine
|
||||
#[allow(dead_code)]
|
||||
#[derive(ToSchema)]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::network::models::tendermint_types::{AbciInfo, BlockHeader, BlockId};
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
use nym_validator_client::nyxd::BlockResponse;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -27,3 +29,292 @@ pub struct ContractInformation<T> {
|
||||
pub(crate) address: Option<String>,
|
||||
pub(crate) details: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct ChainStatusResponse {
|
||||
pub connected_nyxd: String,
|
||||
pub status: ChainStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct ChainStatus {
|
||||
pub abci: AbciInfo,
|
||||
pub latest_block: BlockInfo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct BlockInfo {
|
||||
pub block_id: BlockId,
|
||||
pub block: FullBlockInfo,
|
||||
// if necessary we might put block data here later too
|
||||
}
|
||||
|
||||
impl From<BlockResponse> for BlockInfo {
|
||||
fn from(value: BlockResponse) -> Self {
|
||||
BlockInfo {
|
||||
block_id: value.block_id.into(),
|
||||
block: FullBlockInfo {
|
||||
header: value.block.header.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct FullBlockInfo {
|
||||
pub header: BlockHeader,
|
||||
}
|
||||
|
||||
// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields
|
||||
pub mod tendermint_types {
|
||||
use nym_validator_client::nyxd::Hash;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tendermint::abci::response::Info;
|
||||
use tendermint::block;
|
||||
use tendermint::block::header::Version;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct AbciInfo {
|
||||
/// Some arbitrary information.
|
||||
pub data: String,
|
||||
|
||||
/// The application software semantic version.
|
||||
pub version: String,
|
||||
|
||||
/// The application protocol version.
|
||||
pub app_version: u64,
|
||||
|
||||
/// The latest block for which the app has called [`Commit`].
|
||||
pub last_block_height: u64,
|
||||
|
||||
/// The latest result of [`Commit`].
|
||||
pub last_block_app_hash: String,
|
||||
}
|
||||
|
||||
impl From<Info> for AbciInfo {
|
||||
fn from(value: Info) -> Self {
|
||||
AbciInfo {
|
||||
data: value.data,
|
||||
version: value.version,
|
||||
app_version: value.app_version,
|
||||
last_block_height: value.last_block_height.value(),
|
||||
last_block_app_hash: value.last_block_app_hash.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Version` contains the protocol version for the blockchain and the
|
||||
/// application.
|
||||
///
|
||||
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version>
|
||||
#[derive(
|
||||
Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema,
|
||||
)]
|
||||
pub struct HeaderVersion {
|
||||
/// Block version
|
||||
pub block: u64,
|
||||
|
||||
/// App version
|
||||
pub app: u64,
|
||||
}
|
||||
|
||||
impl From<tendermint::block::header::Version> for HeaderVersion {
|
||||
fn from(value: Version) -> Self {
|
||||
HeaderVersion {
|
||||
block: value.block,
|
||||
app: value.app,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block identifiers which contain two distinct Merkle roots of the block,
|
||||
/// as well as the number of parts in the block.
|
||||
///
|
||||
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid>
|
||||
///
|
||||
/// Default implementation is an empty Id as defined by the Go implementation in
|
||||
/// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>.
|
||||
///
|
||||
/// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None).
|
||||
/// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId.
|
||||
/// See: <https://github.com/informalsystems/tendermint-rs/issues/663>
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
JsonSchema,
|
||||
ToSchema,
|
||||
)]
|
||||
pub struct BlockId {
|
||||
/// The block's main hash is the Merkle root of all the fields in the
|
||||
/// block header.
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub hash: Hash,
|
||||
|
||||
/// Parts header (if available) is used for secure gossipping of the block
|
||||
/// during consensus. It is the Merkle root of the complete serialized block
|
||||
/// cut into parts.
|
||||
///
|
||||
/// PartSet is used to split a byteslice of data into parts (pieces) for
|
||||
/// transmission. By splitting data into smaller parts and computing a
|
||||
/// Merkle root hash on the list, you can verify that a part is
|
||||
/// legitimately part of the complete data, and the part can be forwarded
|
||||
/// to other peers before all the parts are known. In short, it's a fast
|
||||
/// way to propagate a large file over a gossip network.
|
||||
///
|
||||
/// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset>
|
||||
///
|
||||
/// PartSetHeader in protobuf is defined as never nil using the gogoproto
|
||||
/// annotations. This does not translate to Rust, but we can indicate this
|
||||
/// in the domain type.
|
||||
pub part_set_header: PartSetHeader,
|
||||
}
|
||||
|
||||
impl From<block::Id> for BlockId {
|
||||
fn from(value: block::Id) -> Self {
|
||||
BlockId {
|
||||
hash: value.hash,
|
||||
part_set_header: value.part_set_header.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block parts header
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Deserialize,
|
||||
Serialize,
|
||||
JsonSchema,
|
||||
ToSchema,
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub struct PartSetHeader {
|
||||
/// Number of parts in this block
|
||||
pub total: u32,
|
||||
|
||||
/// Hash of the parts set header,
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub hash: Hash,
|
||||
}
|
||||
|
||||
impl From<tendermint::block::parts::Header> for PartSetHeader {
|
||||
fn from(value: block::parts::Header) -> Self {
|
||||
PartSetHeader {
|
||||
total: value.total,
|
||||
hash: value.hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block `Header` values contain metadata about the block and about the
|
||||
/// consensus, as well as commitments to the data in the current block, the
|
||||
/// previous block, and the results returned by the application.
|
||||
///
|
||||
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header>
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct BlockHeader {
|
||||
/// Header version
|
||||
pub version: HeaderVersion,
|
||||
|
||||
/// Chain ID
|
||||
pub chain_id: String,
|
||||
|
||||
/// Current block height
|
||||
pub height: u64,
|
||||
|
||||
/// Current timestamp
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub time: tendermint::Time,
|
||||
|
||||
/// Previous block info
|
||||
pub last_block_id: Option<BlockId>,
|
||||
|
||||
/// Commit from validators from the last block
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub last_commit_hash: Option<Hash>,
|
||||
|
||||
/// Merkle root of transaction hashes
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub data_hash: Option<Hash>,
|
||||
|
||||
/// Validators for the current block
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub validators_hash: Hash,
|
||||
|
||||
/// Validators for the next block
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub next_validators_hash: Hash,
|
||||
|
||||
/// Consensus params for the current block
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub consensus_hash: Hash,
|
||||
|
||||
/// State after txs from the previous block
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub app_hash: Hash,
|
||||
|
||||
/// Root hash of all results from the txs from the previous block
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub last_results_hash: Option<Hash>,
|
||||
|
||||
/// Hash of evidence included in the block
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub evidence_hash: Option<Hash>,
|
||||
|
||||
/// Original proposer of the block
|
||||
#[serde(with = "nym_serde_helpers::hex")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub proposer_address: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<block::Header> for BlockHeader {
|
||||
fn from(value: block::Header) -> Self {
|
||||
BlockHeader {
|
||||
version: value.version.into(),
|
||||
chain_id: value.chain_id.to_string(),
|
||||
height: value.height.value(),
|
||||
time: value.time,
|
||||
last_block_id: value.last_block_id.map(Into::into),
|
||||
last_commit_hash: value.last_commit_hash,
|
||||
data_hash: value.data_hash,
|
||||
validators_hash: value.validators_hash,
|
||||
next_validators_hash: value.next_validators_hash,
|
||||
consensus_hash: value.consensus_hash,
|
||||
app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(),
|
||||
last_results_hash: value.last_results_hash,
|
||||
evidence_hash: value.evidence_hash,
|
||||
proposer_address: value.proposer_address.as_bytes().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use nym_contracts_common::NaiveFloat;
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use nym_mixnet_contract_common::{IdentityKey, NodeId};
|
||||
use nym_serde_helpers::date::DATE_FORMAT;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use reqwest::StatusCode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -452,6 +453,15 @@ impl From<RedemptionError> for AxumErrorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NyxdError> for AxumErrorResponse {
|
||||
fn from(value: NyxdError) -> Self {
|
||||
Self {
|
||||
message: RequestError::new(value.to_string()),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NymApiStorageError {
|
||||
#[error("could not find status report associated with mixnode {mix_id}")]
|
||||
|
||||
@@ -7,9 +7,13 @@ use crate::support::http::state::AppState;
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use axum::Router;
|
||||
use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse};
|
||||
use nym_api_requests::models::{
|
||||
ApiHealthResponse, ApiStatus, ChainStatus, SignerInformationResponse,
|
||||
};
|
||||
use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
use nym_compact_ecash::Base58;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) fn api_status_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
@@ -29,9 +33,34 @@ pub(crate) fn api_status_routes() -> Router<AppState> {
|
||||
(status = 200, body = ApiHealthResponse)
|
||||
)
|
||||
)]
|
||||
async fn health(State(state): State<ApiStatusState>) -> Json<ApiHealthResponse> {
|
||||
let uptime = state.startup_time.elapsed();
|
||||
let health = ApiHealthResponse::new_healthy(uptime);
|
||||
async fn health(State(state): State<AppState>) -> Json<ApiHealthResponse> {
|
||||
const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
let uptime = state.api_status.startup_time.elapsed();
|
||||
let chain_status = match state
|
||||
.chain_status_cache
|
||||
.get_or_refresh(&state.nyxd_client)
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let block_time: OffsetDateTime = res.latest_block.block.header.time.into();
|
||||
let diff = now - block_time;
|
||||
if diff > CHAIN_STALL_THRESHOLD {
|
||||
ChainStatus::Stalled {
|
||||
approximate_amount: diff.unsigned_abs(),
|
||||
}
|
||||
} else {
|
||||
ChainStatus::Synced
|
||||
}
|
||||
}
|
||||
Err(_) => ChainStatus::Unknown,
|
||||
};
|
||||
let health = ApiHealthResponse {
|
||||
status: ApiStatus::Up,
|
||||
chain_status,
|
||||
uptime: uptime.as_secs(),
|
||||
};
|
||||
Json(health)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::status::{ApiStatusState, SignerState};
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::config::helpers::try_load_current_config;
|
||||
use crate::support::config::Config;
|
||||
use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL};
|
||||
use crate::support::http::state::{
|
||||
AppState, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S,
|
||||
AppState, ChainStatusCache, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S,
|
||||
};
|
||||
use crate::support::http::RouterBuilder;
|
||||
use crate::support::nyxd;
|
||||
@@ -191,6 +191,8 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
|
||||
|
||||
ecash_state.spawn_background_cleaner();
|
||||
let router = router.with_state(AppState {
|
||||
nyxd_client: nyxd_client.clone(),
|
||||
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
|
||||
forced_refresh: ForcedRefresh::new(
|
||||
config.topology_cacher.debug.node_describe_allow_illegal_ips,
|
||||
),
|
||||
|
||||
@@ -56,6 +56,9 @@ const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(
|
||||
pub(crate) const DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL: Duration = Duration::from_secs(4500);
|
||||
pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50;
|
||||
|
||||
// TODO: make it configurable
|
||||
pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
|
||||
const DEFAULT_MONITOR_THRESHOLD: u8 = 60;
|
||||
const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50;
|
||||
const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::network::models::{ChainStatus, NetworkDetails};
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_status_api::handlers::unstable;
|
||||
use crate::node_status_api::models::AxumErrorResponse;
|
||||
@@ -12,6 +12,7 @@ use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::status::ApiStatusState;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::Cache;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage;
|
||||
use axum::extract::FromRef;
|
||||
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation};
|
||||
@@ -20,6 +21,7 @@ use nym_task::TaskManager;
|
||||
use nym_topology::CachedEpochRewardedSet;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -76,6 +78,11 @@ type AxumJoinHandle = JoinHandle<std::io::Result<()>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppState {
|
||||
// ideally this would have been made generic to make tests easier,
|
||||
// however, it'd be a way bigger change (I tried)
|
||||
pub(crate) nyxd_client: Client,
|
||||
pub(crate) chain_status_cache: ChainStatusCache,
|
||||
|
||||
pub(crate) forced_refresh: ForcedRefresh,
|
||||
pub(crate) nym_contract_cache: NymContractCache,
|
||||
pub(crate) node_status_cache: NodeStatusCache,
|
||||
@@ -127,6 +134,87 @@ impl ForcedRefresh {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ChainStatusCache {
|
||||
cache_ttl: Duration,
|
||||
inner: Arc<RwLock<Option<ChainStatusCacheInner>>>,
|
||||
}
|
||||
|
||||
impl ChainStatusCache {
|
||||
pub(crate) fn new(cache_ttl: Duration) -> Self {
|
||||
ChainStatusCache {
|
||||
cache_ttl,
|
||||
inner: Arc::new(Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChainStatusCacheInner {
|
||||
last_refreshed_at: OffsetDateTime,
|
||||
cache_value: ChainStatus,
|
||||
}
|
||||
|
||||
impl ChainStatusCacheInner {
|
||||
fn is_valid(&self, ttl: Duration) -> bool {
|
||||
if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainStatusCache {
|
||||
pub(crate) async fn get_or_refresh(
|
||||
&self,
|
||||
client: &Client,
|
||||
) -> Result<ChainStatus, AxumErrorResponse> {
|
||||
if let Some(cached) = self.check_cache().await {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
self.refresh(client).await
|
||||
}
|
||||
|
||||
async fn check_cache(&self) -> Option<ChainStatus> {
|
||||
let guard = self.inner.read().await;
|
||||
let inner = guard.as_ref()?;
|
||||
if inner.is_valid(self.cache_ttl) {
|
||||
return Some(inner.cache_value.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn refresh(&self, client: &Client) -> Result<ChainStatus, AxumErrorResponse> {
|
||||
// 1. attempt to get write lock permit
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
// 2. check if another task hasn't already updated the cache whilst we were waiting for the permit
|
||||
if let Some(cached) = guard.as_ref() {
|
||||
if cached.is_valid(self.cache_ttl) {
|
||||
return Ok(cached.cache_value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. attempt to query the chain for the chain data
|
||||
let abci = client.abci_info().await?;
|
||||
let block = client
|
||||
.block_info(abci.last_block_height.value() as u32)
|
||||
.await?;
|
||||
|
||||
let status = ChainStatus {
|
||||
abci: abci.into(),
|
||||
latest_block: block.into(),
|
||||
};
|
||||
|
||||
*guard = Some(ChainStatusCacheInner {
|
||||
last_refreshed_at: OffsetDateTime::now_utc(),
|
||||
cache_value: status.clone(),
|
||||
});
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub(crate) fn nym_contract_cache(&self) -> &NymContractCache {
|
||||
&self.nym_contract_cache
|
||||
|
||||
@@ -44,7 +44,7 @@ use nym_validator_client::nyxd::{
|
||||
PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient,
|
||||
},
|
||||
cosmwasm_client::types::ExecuteResult,
|
||||
CosmWasmClient, Fee,
|
||||
BlockResponse, CosmWasmClient, Fee, TendermintRpcClient,
|
||||
};
|
||||
use nym_validator_client::nyxd::{
|
||||
hash::{Hash, SHA256_HASH_SIZE},
|
||||
@@ -56,6 +56,7 @@ use nym_validator_client::{
|
||||
use nym_vesting_contract_common::AccountVestingCoins;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tendermint::abci::response::Info;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
#[macro_export]
|
||||
@@ -132,6 +133,14 @@ impl Client {
|
||||
self.inner.read().await
|
||||
}
|
||||
|
||||
pub(crate) async fn abci_info(&self) -> Result<Info, NyxdError> {
|
||||
Ok(nyxd_query!(self, abci_info().await?))
|
||||
}
|
||||
|
||||
pub(crate) async fn block_info(&self, height: u32) -> Result<BlockResponse, NyxdError> {
|
||||
Ok(nyxd_query!(self, block(height).await?))
|
||||
}
|
||||
|
||||
pub(crate) async fn client_address(&self) -> Option<AccountId> {
|
||||
let guard = self.inner.read().await;
|
||||
match &*guard {
|
||||
|
||||
Generated
+3
-2
@@ -2250,9 +2250,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.3"
|
||||
version = "0.24.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf287bde7b776e85d7188e6e5db7cf410a2f9531fe82817eb87feed034c8d14"
|
||||
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -3267,6 +3267,7 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"ecdsa",
|
||||
"getset",
|
||||
"humantime-serde",
|
||||
"nym-compact-ecash",
|
||||
"nym-config",
|
||||
"nym-contracts-common",
|
||||
|
||||
Reference in New Issue
Block a user