// Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::nym_api::error::NymAPIError; use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; use crate::nym_nodes::SkimmedNodesWithMetadata; use crate::ValidatorClientError; use async_trait::async_trait; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse, EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse, VerifyEcashTicketBody, }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::network_monitor::{ KnownNetworkMonitorResponse, StressTestBatchSubmission, }; use nym_api_requests::models::node_families::NodeFamily; use nym_api_requests::models::{ AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse, StressTestBatchSubmissionResponse, }; use nym_api_requests::pagination::PaginatedResponse; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::{IdentityKeyRef, NodeId, NymNodeDetails}; use std::net::IpAddr; use time::format_description::BorrowedFormatItem; use time::Date; use tracing::instrument; pub use nym_api_requests::{ ecash::{ models::SpentCredentialsResponse, BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, VerifyEcashCredentialBody, }, models::{ GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, StakeSaturationResponse, UptimeResponse, }, nym_nodes::{ CachedNodesResponse, NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1, PaginatedCachedNodesResponseV2, SemiSkimmedNodeV1, SemiSkimmedNodeV3, SemiSkimmedNodesWithMetadata, SkimmedNodeV1, }, NymNetworkDetailsResponse, }; pub use nym_coconut_dkg_common::types::EpochId; pub mod error; pub mod routes; pub fn rfc_3339_date() -> Vec> { time::format_description::parse("[year]-[month]-[day]").unwrap() } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { /// Get the current API URL being used by the client fn api_url(&self) -> &url::Url; async fn health(&self) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::API_STATUS_ROUTES, routes::HEALTH, ], NO_PARAMS, ) .await } #[instrument(level = "debug", skip(self))] async fn build_information(&self) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::API_STATUS_ROUTES, routes::BUILD_INFORMATION, ], NO_PARAMS, ) .await } #[tracing::instrument(level = "debug", skip_all)] async fn get_node_performance_history( &self, node_id: NodeId, page: Option, per_page: Option, ) -> Result { let mut params = Vec::new(); if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } self.get_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_PERFORMANCE_HISTORY, &*node_id.to_string(), ], ¶ms, ) .await } #[tracing::instrument(level = "debug", skip_all)] #[deprecated(note = "use .get_nodes_described_v2 instead")] async fn get_nodes_described( &self, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } self.get_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_DESCRIBED, ], ¶ms, ) .await } #[tracing::instrument(level = "debug", skip_all)] async fn get_nodes_described_v2( &self, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } self.get_json( &[ routes::V2_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_DESCRIBED, ], ¶ms, ) .await } async fn get_current_rewarded_set(&self) -> Result { self.get_rewarded_set().await } async fn get_all_basic_nodes_with_metadata( &self, ) -> Result { // unroll first loop iteration in order to obtain the metadata let mut page = 0; let res = self .get_basic_nodes_v2(false, Some(page), None, true) .await?; let mut nodes = res.nodes.data; let metadata = res.metadata; if res.nodes.pagination.total == nodes.len() { return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); } page += 1; loop { let mut res = self .get_basic_nodes_v2(false, Some(page), None, true) .await?; if !metadata.consistency_check(&res.metadata) { // Create a custom error for inconsistent metadata return Err(NymAPIError::InternalResponseInconsistency { url: self.api_url().clone(), details: "Inconsistent paged metadata".to_string(), }); } nodes.append(&mut res.nodes.data); if nodes.len() >= res.nodes.pagination.total { break; } else { page += 1 } } Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } async fn get_all_basic_active_mixing_assigned_nodes_with_metadata( &self, ) -> Result { // Get all mixing nodes that are in the active/rewarded set let mut page = 0; let res = self .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false) .await?; let metadata = res.metadata; let mut nodes = res.nodes.data; if res.nodes.pagination.total == nodes.len() { return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); } page += 1; loop { let res = self .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false) .await?; if !metadata.consistency_check(&res.metadata) { return Err(NymAPIError::InternalResponseInconsistency { url: self.api_url().clone(), details: "Inconsistent paged metadata".to_string(), }); } nodes.append(&mut res.nodes.data.clone()); // Check if we've got all nodes if nodes.len() >= res.nodes.pagination.total { break; } else { page += 1; } } Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } async fn get_all_basic_entry_assigned_nodes_with_metadata( &self, ) -> Result { // Get all nodes that can act as entry gateways let mut page = 0; let res = self .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false) .await?; let metadata = res.metadata; let mut nodes = res.nodes.data; if res.nodes.pagination.total == nodes.len() { return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); } page += 1; loop { let res = self .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false) .await?; if !metadata.consistency_check(&res.metadata) { return Err(NymAPIError::InternalResponseInconsistency { url: self.api_url().clone(), details: "Inconsistent paged metadata".to_string(), }); } nodes.append(&mut res.nodes.data.clone()); // Check if we've got all nodes if nodes.len() >= res.nodes.pagination.total { break; } else { page += 1; } } Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } #[deprecated(note = "use .get_all_described_nodes_v2 instead")] #[allow(deprecated)] async fn get_all_described_nodes(&self) -> Result, NymAPIError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; let mut descriptions = Vec::new(); loop { let mut res = self.get_nodes_described(Some(page), None).await?; descriptions.append(&mut res.data); if descriptions.len() < res.pagination.total { page += 1 } else { break; } } Ok(descriptions) } async fn get_all_described_nodes_v2(&self) -> Result, NymAPIError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; let mut descriptions = Vec::new(); loop { let mut res = self.get_nodes_described_v2(Some(page), None).await?; descriptions.append(&mut res.data); if descriptions.len() < res.pagination.total { page += 1 } else { break; } } Ok(descriptions) } #[tracing::instrument(level = "debug", skip_all)] async fn get_nym_nodes( &self, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } self.get_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_BONDED, ], ¶ms, ) .await } async fn get_all_bonded_nym_nodes(&self) -> Result, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; let mut bonds = Vec::new(); loop { let mut res = self.get_nym_nodes(Some(page), None).await?; bonds.append(&mut res.data); if bonds.len() < res.pagination.total { page += 1 } else { break; } } Ok(bonds) } #[tracing::instrument(level = "debug", skip_all)] async fn get_node_families( &self, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } self.get_json( &[routes::V1_API_VERSION, routes::NODE_FAMILIES_ROUTES], ¶ms, ) .await } async fn get_all_node_families(&self) -> Result, NymAPIError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; let mut families = Vec::new(); loop { let mut res = self.get_node_families(Some(page), None).await?; families.append(&mut res.data); if families.len() < res.pagination.total { page += 1 } else { break; } } Ok(families) } #[deprecated] #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "mixnodes", "skimmed", ], NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_basic_gateways(&self) -> Result, NymAPIError> { self.get_json( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "gateways", "skimmed", ], NO_PARAMS, ) .await } #[instrument(level = "debug", skip(self))] async fn get_rewarded_set(&self) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_REWARDED_SET, ], NO_PARAMS, ) .await } /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes #[deprecated(note = "use get_basic_entry_assigned_nodes_v2")] #[instrument(level = "debug", skip(self))] async fn get_basic_entry_assigned_nodes( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", "entry-gateways", "all", ], ¶ms, ) .await } /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes #[instrument(level = "debug", skip(self))] async fn get_basic_entry_assigned_nodes_v2( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V2_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", "entry-gateways", ], ¶ms, ) .await } /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes #[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")] #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", "mixnodes", "active", ], ¶ms, ) .await } /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes_v2( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V2_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", "mixnodes", "active", ], ¶ms, ) .await } /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes #[deprecated(note = "use get_basic_mixing_capable_nodes_v2")] #[instrument(level = "debug", skip(self))] async fn get_basic_mixing_capable_nodes( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", "mixnodes", "all", ], ¶ms, ) .await } /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes #[instrument(level = "debug", skip(self))] async fn get_basic_mixing_capable_nodes_v2( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V2_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", "mixnodes", "all", ], ¶ms, ) .await } #[deprecated(note = "use get_basic_nodes_v2")] #[instrument(level = "debug", skip(self))] async fn get_basic_nodes( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn get_basic_nodes_v2( &self, no_legacy: bool, page: Option, per_page: Option, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response( &[ routes::V2_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn get_expanded_nodes( &self, no_legacy: bool, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { params.push(("no_legacy", "true".to_string())) } if let Some(page) = page { params.push(("page", page.to_string())) } if let Some(per_page) = per_page { params.push(("per_page", per_page.to_string())) } self.get_json( &[ routes::V2_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "semi-skimmed", ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn get_expanded_nodes_v3( &self, use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); if use_bincode { params.push(("output", "bincode".to_string())) } self.get_response("/v3/unstable/nym-nodes/semi-skimmed", ¶ms) .await } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnode_report( &self, mix_id: NodeId, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS, routes::MIXNODE, &mix_id.to_string(), routes::REPORT, ], NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS, routes::GATEWAY, identity, routes::REPORT, ], NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnode_history( &self, mix_id: NodeId, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS, routes::MIXNODE, &mix_id.to_string(), routes::HISTORY, ], NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS, routes::GATEWAY, identity, routes::HISTORY, ], NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, since: Option, ) -> Result { if let Some(since) = since { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::GATEWAY, identity, CORE_STATUS_COUNT, ], &[(SINCE_ARG, since.to_string())], ) .await } else { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::GATEWAY, identity, ], NO_PARAMS, ) .await } } #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnode_core_status_count( &self, mix_id: NodeId, since: Option, ) -> Result { if let Some(since) = since { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), CORE_STATUS_COUNT, ], &[(SINCE_ARG, since.to_string())], ) .await } else { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), CORE_STATUS_COUNT, ], NO_PARAMS, ) .await } } #[instrument(level = "debug", skip(self))] async fn get_current_node_performance( &self, node_id: NodeId, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_PERFORMANCE, &node_id.to_string(), ], NO_PARAMS, ) .await } async fn get_node_annotation( &self, node_id: NodeId, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_ANNOTATION, &node_id.to_string(), ], NO_PARAMS, ) .await } #[deprecated] async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), routes::AVG_UPTIME, ], NO_PARAMS, ) .await } #[instrument(level = "debug", skip(self, request_body))] async fn blind_sign( &self, request_body: &BlindSignRequestBody, ) -> Result { self.post_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_BLIND_SIGN, ], NO_PARAMS, request_body, ) .await } #[instrument(level = "debug", skip(self, request_body))] async fn verify_ecash_ticket( &self, request_body: &VerifyEcashTicketBody, ) -> Result { self.post_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::VERIFY_ECASH_TICKET, ], NO_PARAMS, request_body, ) .await } #[instrument(level = "debug", skip(self, request_body))] async fn batch_redeem_ecash_tickets( &self, request_body: &BatchRedeemTicketsBody, ) -> Result { self.post_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::BATCH_REDEEM_ECASH_TICKETS, ], NO_PARAMS, request_body, ) .await } #[instrument(level = "debug", skip(self))] async fn partial_expiration_date_signatures( &self, expiration_date: Option, epoch_id: Option, ) -> Result { let mut params = match expiration_date { None => Vec::new(), Some(exp) => vec![( ecash::EXPIRATION_DATE_PARAM, exp.format(&rfc_3339_date()).unwrap(), )], }; if let Some(epoch_id) = epoch_id { params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string())); } self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::PARTIAL_EXPIRATION_DATE_SIGNATURES, ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn partial_coin_indices_signatures( &self, epoch_id: Option, ) -> Result { let params = match epoch_id { None => Vec::new(), Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], }; self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::PARTIAL_COIN_INDICES_SIGNATURES, ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn global_expiration_date_signatures( &self, expiration_date: Option, epoch_id: Option, ) -> Result { let mut params = match expiration_date { None => Vec::new(), Some(exp) => vec![( ecash::EXPIRATION_DATE_PARAM, exp.format(&rfc_3339_date()).unwrap(), )], }; if let Some(epoch_id) = epoch_id { params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string())); } self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::GLOBAL_EXPIRATION_DATE_SIGNATURES, ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn global_coin_indices_signatures( &self, epoch_id: Option, ) -> Result { let params = match epoch_id { None => Vec::new(), Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], }; self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::GLOBAL_COIN_INDICES_SIGNATURES, ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn master_verification_key( &self, epoch_id: Option, ) -> Result { let params = match epoch_id { None => Vec::new(), Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], }; self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, ecash::MASTER_VERIFICATION_KEY, ], ¶ms, ) .await } #[instrument(level = "debug", skip(self))] async fn force_refresh_describe_cache( &self, request: &NodeRefreshBody, ) -> Result<(), NymAPIError> { self.post_json( &[ routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_REFRESH_DESCRIBED, ], NO_PARAMS, request, ) .await } #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_for( &self, expiration_date: Date, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_FOR, &expiration_date.to_string(), ], NO_PARAMS, ) .await } #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_for_count( &self, expiration_date: Date, ) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT, &expiration_date.to_string(), ], NO_PARAMS, ) .await } #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_challenge_commitment( &self, request: &IssuedTicketbooksChallengeCommitmentRequest, ) -> Result { self.post_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, ], NO_PARAMS, request, ) .await } #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_data( &self, request: &IssuedTicketbooksDataRequest, ) -> Result { self.post_json( &[ routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_DATA, ], NO_PARAMS, request, ) .await } async fn nodes_by_addresses( &self, addresses: Vec, ) -> Result { self.post_json( &[ routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, routes::nym_nodes::BY_ADDRESSES, ], NO_PARAMS, &NodesByAddressesRequestBody { addresses }, ) .await } #[instrument(level = "debug", skip(self))] async fn get_network_details(&self) -> Result { self.get_json( &[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS], NO_PARAMS, ) .await } #[instrument(level = "debug", skip(self))] async fn get_chain_status(&self) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::NETWORK, routes::CHAIN_STATUS, ], NO_PARAMS, ) .await } async fn get_chain_blocks_status(&self) -> Result { self.get_json("/v1/network/chain-blocks-status", NO_PARAMS) .await } #[instrument(level = "debug", skip(self))] async fn get_signer_status(&self) -> Result { self.get_json("/v1/ecash/signer-status", NO_PARAMS).await } #[instrument(level = "debug", skip(self))] async fn get_signer_information(&self) -> Result { self.get_json("/v1/api-status/signer-information", NO_PARAMS) .await } #[instrument(level = "debug", skip(self))] async fn get_key_rotation_info(&self) -> Result { self.get_json( &[ routes::V1_API_VERSION, routes::EPOCH, routes::KEY_ROTATION_INFO, ], NO_PARAMS, ) .await } /// Method to change the base API URLs being used by the client fn change_base_urls(&mut self, urls: Vec); /// Retrieve expanded information for all bonded nodes on the network async fn get_all_expanded_nodes(&self) -> Result { // Unroll the first iteration to get the metadata let mut page = 0; let res = self.get_expanded_nodes(false, Some(page), None).await?; let mut nodes = res.nodes.data; let metadata = res.metadata; if res.nodes.pagination.total == nodes.len() { return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)); } page += 1; loop { let mut res = self.get_expanded_nodes(false, Some(page), None).await?; nodes.append(&mut res.nodes.data); if nodes.len() < res.nodes.pagination.total { page += 1 } else { break; } } Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)) } /// Queries the nym-api for whether a particular ed25519 identity key is currently recognised /// as an authorised network monitor permitted to submit stress testing results. /// /// `identity_key` is expected to be the base58-encoded form of the ed25519 public key. #[instrument(level = "debug", skip(self))] async fn get_known_network_monitor( &self, identity_key: IdentityKeyRef<'_>, ) -> Result { self.get_json( &[ routes::V3_API_VERSION, routes::NYM_NODES_ROUTES, routes::STRESS_TESTING, routes::STRESS_TESTING_KNOWN_MONITORS, identity_key, ], NO_PARAMS, ) .await } /// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor /// orchestrator. /// /// The caller is expected to have produced `request` via /// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519 /// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose /// signature fails to verify. #[instrument(level = "debug", skip(self, request))] async fn submit_stress_testing_results( &self, request: &StressTestBatchSubmission, ) -> Result { self.post_json( &[ routes::V3_API_VERSION, routes::NYM_NODES_ROUTES, routes::STRESS_TESTING, routes::STRESS_TESTING_BATCH_SUBMIT, ], NO_PARAMS, request, ) .await } } // Client is already nym_http_api_client::Client (re-exported above), so just one impl needed #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl NymApiClientExt for nym_http_api_client::Client { fn api_url(&self) -> &url::Url { self.current_url().as_ref() } fn change_base_urls(&mut self, urls: Vec) { self.change_base_urls(urls.into_iter().map(|u| u.into()).collect()); } }