diff --git a/Cargo.lock b/Cargo.lock index e45cc5e1f7..2aaf965a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 14a91a4459..458ba45c27 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -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, diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4db77f3ccd..f95baabd00 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -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 } diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 3d66ad2bb9..9ca0fe6ea4 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -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 } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index ba5d6a94b2..2558613c8c 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1215,6 +1215,7 @@ impl From 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(), } } diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index c31ecc2ede..e1f0c38083 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -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, diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 708a1cf86f..a308a9fa82 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -1,9 +1,11 @@ // Copyright 2023 - Nym Technologies SA // 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 { 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) -> AxumResult> { + 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)] diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 1819ce024d..066b473921 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // 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 { pub(crate) address: Option, pub(crate) details: Option, } + +#[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 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 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. + /// + /// + #[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 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. + /// + /// + /// + /// Default implementation is an empty Id as defined by the Go implementation in + /// . + /// + /// 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: + #[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. + /// + /// + /// + /// 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 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 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. + /// + /// + #[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, + + /// Commit from validators from the last block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_commit_hash: Option, + + /// Merkle root of transaction hashes + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub data_hash: Option, + + /// 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")] + #[schema(value_type = Option)] + pub last_results_hash: Option, + + /// Hash of evidence included in the block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub evidence_hash: Option, + + /// Original proposer of the block + #[serde(with = "nym_serde_helpers::hex")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub proposer_address: Vec, + } + + impl From 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(), + } + } + } +} diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 53f5d3b7d5..b2af9a6c52 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -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 for AxumErrorResponse { } } +impl From 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}")] diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index 8b177cadd6..1a280eb5df 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -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 { Router::new() @@ -29,9 +33,34 @@ pub(crate) fn api_status_routes() -> Router { (status = 200, body = ApiHealthResponse) ) )] -async fn health(State(state): State) -> Json { - let uptime = state.startup_time.elapsed(); - let health = ApiHealthResponse::new_healthy(uptime); +async fn health(State(state): State) -> Json { + 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) } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 836f048930..9ab4828e65 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -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>; #[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>>, +} + +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 { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option { + 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 { + // 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 diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 01a32decec..889ffc0acd 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -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 { + Ok(nyxd_query!(self, abci_info().await?)) + } + + pub(crate) async fn block_info(&self, height: u32) -> Result { + Ok(nyxd_query!(self, block(height).await?)) + } + pub(crate) async fn client_address(&self) -> Option { let guard = self.inner.read().await; match &*guard { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 070d93ead9..7fb3e6a067 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -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",