nym_validator_client/nym_api/
mod.rs

1// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::nym_api::error::NymAPIError;
5use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
6use crate::nym_nodes::SkimmedNodesWithMetadata;
7use async_trait::async_trait;
8use nym_api_requests::ecash::models::{
9    AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
10    BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse,
11    EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest,
12    IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest,
13    IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
14    VerifyEcashTicketBody,
15};
16use nym_api_requests::ecash::VerificationKeyResponse;
17use nym_api_requests::models::{
18    AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
19    ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
20    NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
21};
22use nym_api_requests::nym_nodes::{
23    NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
24    PaginatedCachedNodesResponseV2,
25};
26use nym_api_requests::pagination::PaginatedResponse;
27pub use nym_api_requests::{
28    ecash::{
29        models::SpentCredentialsResponse, BlindSignRequestBody, BlindedSignatureResponse,
30        PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
31        VerifyEcashCredentialBody,
32    },
33    models::{
34        GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
35        MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
36        MixnodeUptimeHistoryResponse, StakeSaturationResponse, UptimeResponse,
37    },
38    nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, SkimmedNode},
39    NymNetworkDetailsResponse,
40};
41use nym_http_api_client::{ApiClient, NO_PARAMS};
42use nym_mixnet_contract_common::{IdentityKeyRef, NodeId, NymNodeDetails};
43use std::net::IpAddr;
44use time::format_description::BorrowedFormatItem;
45use time::Date;
46use tracing::instrument;
47
48use crate::ValidatorClientError;
49pub use nym_coconut_dkg_common::types::EpochId;
50
51pub mod error;
52pub mod routes;
53
54pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
55    time::format_description::parse("[year]-[month]-[day]").unwrap()
56}
57
58#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
59#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
60pub trait NymApiClientExt: ApiClient {
61    /// Get the current API URL being used by the client
62    fn api_url(&self) -> &url::Url;
63
64    async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
65        self.get_json(
66            &[
67                routes::V1_API_VERSION,
68                routes::API_STATUS_ROUTES,
69                routes::HEALTH,
70            ],
71            NO_PARAMS,
72        )
73        .await
74    }
75
76    #[instrument(level = "debug", skip(self))]
77    async fn build_information(&self) -> Result<BinaryBuildInformationOwned, NymAPIError> {
78        self.get_json(
79            &[
80                routes::V1_API_VERSION,
81                routes::API_STATUS_ROUTES,
82                routes::BUILD_INFORMATION,
83            ],
84            NO_PARAMS,
85        )
86        .await
87    }
88
89    #[tracing::instrument(level = "debug", skip_all)]
90    async fn get_node_performance_history(
91        &self,
92        node_id: NodeId,
93        page: Option<u32>,
94        per_page: Option<u32>,
95    ) -> Result<PerformanceHistoryResponse, NymAPIError> {
96        let mut params = Vec::new();
97
98        if let Some(page) = page {
99            params.push(("page", page.to_string()))
100        }
101
102        if let Some(per_page) = per_page {
103            params.push(("per_page", per_page.to_string()))
104        }
105
106        self.get_json(
107            &[
108                routes::V1_API_VERSION,
109                routes::NYM_NODES_ROUTES,
110                routes::NYM_NODES_PERFORMANCE_HISTORY,
111                &*node_id.to_string(),
112            ],
113            &params,
114        )
115        .await
116    }
117
118    #[tracing::instrument(level = "debug", skip_all)]
119    async fn get_nodes_described(
120        &self,
121        page: Option<u32>,
122        per_page: Option<u32>,
123    ) -> Result<PaginatedResponse<NymNodeDescription>, NymAPIError> {
124        let mut params = Vec::new();
125
126        if let Some(page) = page {
127            params.push(("page", page.to_string()))
128        }
129
130        if let Some(per_page) = per_page {
131            params.push(("per_page", per_page.to_string()))
132        }
133
134        self.get_json(
135            &[
136                routes::V1_API_VERSION,
137                routes::NYM_NODES_ROUTES,
138                routes::NYM_NODES_DESCRIBED,
139            ],
140            &params,
141        )
142        .await
143    }
144
145    async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
146        self.get_rewarded_set().await
147    }
148
149    async fn get_all_basic_nodes_with_metadata(
150        &self,
151    ) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
152        // unroll first loop iteration in order to obtain the metadata
153        let mut page = 0;
154        let res = self
155            .get_basic_nodes_v2(false, Some(page), None, true)
156            .await?;
157        let mut nodes = res.nodes.data;
158        let metadata = res.metadata;
159
160        if res.nodes.pagination.total == nodes.len() {
161            return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
162        }
163
164        page += 1;
165
166        loop {
167            let mut res = self
168                .get_basic_nodes_v2(false, Some(page), None, true)
169                .await?;
170
171            if !metadata.consistency_check(&res.metadata) {
172                // Create a custom error for inconsistent metadata
173                return Err(NymAPIError::InternalResponseInconsistency {
174                    url: self.api_url().clone(),
175                    details: "Inconsistent paged metadata".to_string(),
176                });
177            }
178
179            nodes.append(&mut res.nodes.data);
180            if nodes.len() >= res.nodes.pagination.total {
181                break;
182            } else {
183                page += 1
184            }
185        }
186
187        Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
188    }
189
190    async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
191        &self,
192    ) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
193        // Get all mixing nodes that are in the active/rewarded set
194        let mut page = 0;
195        let res = self
196            .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false)
197            .await?;
198
199        let metadata = res.metadata;
200        let mut nodes = res.nodes.data;
201
202        if res.nodes.pagination.total == nodes.len() {
203            return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
204        }
205
206        page += 1;
207
208        loop {
209            let res = self
210                .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false)
211                .await?;
212
213            if !metadata.consistency_check(&res.metadata) {
214                return Err(NymAPIError::InternalResponseInconsistency {
215                    url: self.api_url().clone(),
216                    details: "Inconsistent paged metadata".to_string(),
217                });
218            }
219
220            nodes.append(&mut res.nodes.data.clone());
221
222            // Check if we've got all nodes
223            if nodes.len() >= res.nodes.pagination.total {
224                break;
225            } else {
226                page += 1;
227            }
228        }
229
230        Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
231    }
232
233    async fn get_all_basic_entry_assigned_nodes_with_metadata(
234        &self,
235    ) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
236        // Get all nodes that can act as entry gateways
237        let mut page = 0;
238        let res = self
239            .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false)
240            .await?;
241
242        let metadata = res.metadata;
243        let mut nodes = res.nodes.data;
244
245        if res.nodes.pagination.total == nodes.len() {
246            return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
247        }
248
249        page += 1;
250
251        loop {
252            let res = self
253                .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false)
254                .await?;
255
256            if !metadata.consistency_check(&res.metadata) {
257                return Err(NymAPIError::InternalResponseInconsistency {
258                    url: self.api_url().clone(),
259                    details: "Inconsistent paged metadata".to_string(),
260                });
261            }
262
263            nodes.append(&mut res.nodes.data.clone());
264
265            // Check if we've got all nodes
266            if nodes.len() >= res.nodes.pagination.total {
267                break;
268            } else {
269                page += 1;
270            }
271        }
272
273        Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
274    }
275
276    async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescription>, NymAPIError> {
277        // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
278        let mut page = 0;
279        let mut descriptions = Vec::new();
280
281        loop {
282            let mut res = self.get_nodes_described(Some(page), None).await?;
283
284            descriptions.append(&mut res.data);
285            if descriptions.len() < res.pagination.total {
286                page += 1
287            } else {
288                break;
289            }
290        }
291
292        Ok(descriptions)
293    }
294
295    #[tracing::instrument(level = "debug", skip_all)]
296    async fn get_nym_nodes(
297        &self,
298        page: Option<u32>,
299        per_page: Option<u32>,
300    ) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
301        let mut params = Vec::new();
302
303        if let Some(page) = page {
304            params.push(("page", page.to_string()))
305        }
306
307        if let Some(per_page) = per_page {
308            params.push(("per_page", per_page.to_string()))
309        }
310
311        self.get_json(
312            &[
313                routes::V1_API_VERSION,
314                routes::NYM_NODES_ROUTES,
315                routes::NYM_NODES_BONDED,
316            ],
317            &params,
318        )
319        .await
320    }
321
322    async fn get_all_bonded_nym_nodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
323        // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
324        let mut page = 0;
325        let mut bonds = Vec::new();
326
327        loop {
328            let mut res = self.get_nym_nodes(Some(page), None).await?;
329
330            bonds.append(&mut res.data);
331            if bonds.len() < res.pagination.total {
332                page += 1
333            } else {
334                break;
335            }
336        }
337
338        Ok(bonds)
339    }
340
341    #[deprecated]
342    #[tracing::instrument(level = "debug", skip_all)]
343    async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
344        self.get_json(
345            &[
346                routes::V1_API_VERSION,
347                "unstable",
348                routes::NYM_NODES_ROUTES,
349                "mixnodes",
350                "skimmed",
351            ],
352            NO_PARAMS,
353        )
354        .await
355    }
356
357    #[deprecated]
358    #[instrument(level = "debug", skip(self))]
359    async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
360        self.get_json(
361            &[
362                routes::V1_API_VERSION,
363                "unstable",
364                routes::NYM_NODES_ROUTES,
365                "gateways",
366                "skimmed",
367            ],
368            NO_PARAMS,
369        )
370        .await
371    }
372
373    #[instrument(level = "debug", skip(self))]
374    async fn get_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
375        self.get_json(
376            &[
377                routes::V1_API_VERSION,
378                routes::NYM_NODES_ROUTES,
379                routes::NYM_NODES_REWARDED_SET,
380            ],
381            NO_PARAMS,
382        )
383        .await
384    }
385
386    /// retrieve basic information for nodes are capable of operating as an entry gateway
387    /// this includes legacy gateways and nym-nodes
388    #[deprecated(note = "use get_basic_entry_assigned_nodes_v2")]
389    #[instrument(level = "debug", skip(self))]
390    async fn get_basic_entry_assigned_nodes(
391        &self,
392        no_legacy: bool,
393        page: Option<u32>,
394        per_page: Option<u32>,
395        use_bincode: bool,
396    ) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
397        let mut params = Vec::new();
398
399        if no_legacy {
400            params.push(("no_legacy", "true".to_string()))
401        }
402
403        if let Some(page) = page {
404            params.push(("page", page.to_string()))
405        }
406
407        if let Some(per_page) = per_page {
408            params.push(("per_page", per_page.to_string()))
409        }
410
411        if use_bincode {
412            params.push(("output", "bincode".to_string()))
413        }
414
415        self.get_response(
416            &[
417                routes::V1_API_VERSION,
418                "unstable",
419                routes::NYM_NODES_ROUTES,
420                "skimmed",
421                "entry-gateways",
422                "all",
423            ],
424            &params,
425        )
426        .await
427    }
428
429    /// retrieve basic information for nodes are capable of operating as an entry gateway
430    /// this includes legacy gateways and nym-nodes
431    #[instrument(level = "debug", skip(self))]
432    async fn get_basic_entry_assigned_nodes_v2(
433        &self,
434        no_legacy: bool,
435        page: Option<u32>,
436        per_page: Option<u32>,
437        use_bincode: bool,
438    ) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
439        let mut params = Vec::new();
440
441        if no_legacy {
442            params.push(("no_legacy", "true".to_string()))
443        }
444
445        if let Some(page) = page {
446            params.push(("page", page.to_string()))
447        }
448
449        if let Some(per_page) = per_page {
450            params.push(("per_page", per_page.to_string()))
451        }
452
453        if use_bincode {
454            params.push(("output", "bincode".to_string()))
455        }
456
457        self.get_response(
458            &[
459                routes::V2_API_VERSION,
460                "unstable",
461                routes::NYM_NODES_ROUTES,
462                "skimmed",
463                "entry-gateways",
464            ],
465            &params,
466        )
467        .await
468    }
469
470    /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
471    /// this includes legacy mixnodes and nym-nodes
472    #[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")]
473    #[instrument(level = "debug", skip(self))]
474    async fn get_basic_active_mixing_assigned_nodes(
475        &self,
476        no_legacy: bool,
477        page: Option<u32>,
478        per_page: Option<u32>,
479        use_bincode: bool,
480    ) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
481        let mut params = Vec::new();
482
483        if no_legacy {
484            params.push(("no_legacy", "true".to_string()))
485        }
486
487        if let Some(page) = page {
488            params.push(("page", page.to_string()))
489        }
490
491        if let Some(per_page) = per_page {
492            params.push(("per_page", per_page.to_string()))
493        }
494
495        if use_bincode {
496            params.push(("output", "bincode".to_string()))
497        }
498
499        self.get_response(
500            &[
501                routes::V1_API_VERSION,
502                "unstable",
503                routes::NYM_NODES_ROUTES,
504                "skimmed",
505                "mixnodes",
506                "active",
507            ],
508            &params,
509        )
510        .await
511    }
512
513    /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
514    /// this includes legacy mixnodes and nym-nodes
515    #[instrument(level = "debug", skip(self))]
516    async fn get_basic_active_mixing_assigned_nodes_v2(
517        &self,
518        no_legacy: bool,
519        page: Option<u32>,
520        per_page: Option<u32>,
521        use_bincode: bool,
522    ) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
523        let mut params = Vec::new();
524
525        if no_legacy {
526            params.push(("no_legacy", "true".to_string()))
527        }
528
529        if let Some(page) = page {
530            params.push(("page", page.to_string()))
531        }
532
533        if let Some(per_page) = per_page {
534            params.push(("per_page", per_page.to_string()))
535        }
536
537        if use_bincode {
538            params.push(("output", "bincode".to_string()))
539        }
540
541        self.get_response(
542            &[
543                routes::V2_API_VERSION,
544                "unstable",
545                routes::NYM_NODES_ROUTES,
546                "skimmed",
547                "mixnodes",
548                "active",
549            ],
550            &params,
551        )
552        .await
553    }
554
555    /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
556    /// this includes legacy mixnodes and nym-nodes
557    #[deprecated(note = "use get_basic_mixing_capable_nodes_v2")]
558    #[instrument(level = "debug", skip(self))]
559    async fn get_basic_mixing_capable_nodes(
560        &self,
561        no_legacy: bool,
562        page: Option<u32>,
563        per_page: Option<u32>,
564        use_bincode: bool,
565    ) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
566        let mut params = Vec::new();
567
568        if no_legacy {
569            params.push(("no_legacy", "true".to_string()))
570        }
571
572        if let Some(page) = page {
573            params.push(("page", page.to_string()))
574        }
575
576        if let Some(per_page) = per_page {
577            params.push(("per_page", per_page.to_string()))
578        }
579
580        if use_bincode {
581            params.push(("output", "bincode".to_string()))
582        }
583
584        self.get_response(
585            &[
586                routes::V1_API_VERSION,
587                "unstable",
588                routes::NYM_NODES_ROUTES,
589                "skimmed",
590                "mixnodes",
591                "all",
592            ],
593            &params,
594        )
595        .await
596    }
597
598    /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
599    /// this includes legacy mixnodes and nym-nodes
600    #[instrument(level = "debug", skip(self))]
601    async fn get_basic_mixing_capable_nodes_v2(
602        &self,
603        no_legacy: bool,
604        page: Option<u32>,
605        per_page: Option<u32>,
606        use_bincode: bool,
607    ) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
608        let mut params = Vec::new();
609
610        if no_legacy {
611            params.push(("no_legacy", "true".to_string()))
612        }
613
614        if let Some(page) = page {
615            params.push(("page", page.to_string()))
616        }
617
618        if let Some(per_page) = per_page {
619            params.push(("per_page", per_page.to_string()))
620        }
621
622        if use_bincode {
623            params.push(("output", "bincode".to_string()))
624        }
625
626        self.get_response(
627            &[
628                routes::V2_API_VERSION,
629                "unstable",
630                routes::NYM_NODES_ROUTES,
631                "skimmed",
632                "mixnodes",
633                "all",
634            ],
635            &params,
636        )
637        .await
638    }
639
640    #[deprecated(note = "use get_basic_nodes_v2")]
641    #[instrument(level = "debug", skip(self))]
642    async fn get_basic_nodes(
643        &self,
644        no_legacy: bool,
645        page: Option<u32>,
646        per_page: Option<u32>,
647        use_bincode: bool,
648    ) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
649        let mut params = Vec::new();
650
651        if no_legacy {
652            params.push(("no_legacy", "true".to_string()))
653        }
654
655        if let Some(page) = page {
656            params.push(("page", page.to_string()))
657        }
658
659        if let Some(per_page) = per_page {
660            params.push(("per_page", per_page.to_string()))
661        }
662
663        if use_bincode {
664            params.push(("output", "bincode".to_string()))
665        }
666
667        self.get_response(
668            &[
669                routes::V1_API_VERSION,
670                "unstable",
671                routes::NYM_NODES_ROUTES,
672                "skimmed",
673            ],
674            &params,
675        )
676        .await
677    }
678
679    #[instrument(level = "debug", skip(self))]
680    async fn get_basic_nodes_v2(
681        &self,
682        no_legacy: bool,
683        page: Option<u32>,
684        per_page: Option<u32>,
685        use_bincode: bool,
686    ) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
687        let mut params = Vec::new();
688
689        if no_legacy {
690            params.push(("no_legacy", "true".to_string()))
691        }
692
693        if let Some(page) = page {
694            params.push(("page", page.to_string()))
695        }
696
697        if let Some(per_page) = per_page {
698            params.push(("per_page", per_page.to_string()))
699        }
700
701        if use_bincode {
702            params.push(("output", "bincode".to_string()))
703        }
704
705        self.get_response(
706            &[
707                routes::V2_API_VERSION,
708                "unstable",
709                routes::NYM_NODES_ROUTES,
710                "skimmed",
711            ],
712            &params,
713        )
714        .await
715    }
716
717    #[instrument(level = "debug", skip(self))]
718    async fn get_expanded_nodes(
719        &self,
720        no_legacy: bool,
721        page: Option<u32>,
722        per_page: Option<u32>,
723    ) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNode>, NymAPIError> {
724        let mut params = Vec::new();
725
726        if no_legacy {
727            params.push(("no_legacy", "true".to_string()))
728        }
729
730        if let Some(page) = page {
731            params.push(("page", page.to_string()))
732        }
733
734        if let Some(per_page) = per_page {
735            params.push(("per_page", per_page.to_string()))
736        }
737
738        self.get_json(
739            &[
740                routes::V2_API_VERSION,
741                "unstable",
742                routes::NYM_NODES_ROUTES,
743                "semi-skimmed",
744            ],
745            &params,
746        )
747        .await
748    }
749
750    #[deprecated]
751    #[instrument(level = "debug", skip(self))]
752    async fn get_mixnode_report(
753        &self,
754        mix_id: NodeId,
755    ) -> Result<MixnodeStatusReportResponse, NymAPIError> {
756        self.get_json(
757            &[
758                routes::V1_API_VERSION,
759                routes::STATUS,
760                routes::MIXNODE,
761                &mix_id.to_string(),
762                routes::REPORT,
763            ],
764            NO_PARAMS,
765        )
766        .await
767    }
768
769    #[deprecated]
770    #[instrument(level = "debug", skip(self))]
771    async fn get_gateway_report(
772        &self,
773        identity: IdentityKeyRef<'_>,
774    ) -> Result<GatewayStatusReportResponse, NymAPIError> {
775        self.get_json(
776            &[
777                routes::V1_API_VERSION,
778                routes::STATUS,
779                routes::GATEWAY,
780                identity,
781                routes::REPORT,
782            ],
783            NO_PARAMS,
784        )
785        .await
786    }
787
788    #[deprecated]
789    #[instrument(level = "debug", skip(self))]
790    async fn get_mixnode_history(
791        &self,
792        mix_id: NodeId,
793    ) -> Result<MixnodeUptimeHistoryResponse, NymAPIError> {
794        self.get_json(
795            &[
796                routes::V1_API_VERSION,
797                routes::STATUS,
798                routes::MIXNODE,
799                &mix_id.to_string(),
800                routes::HISTORY,
801            ],
802            NO_PARAMS,
803        )
804        .await
805    }
806
807    #[deprecated]
808    #[instrument(level = "debug", skip(self))]
809    async fn get_gateway_history(
810        &self,
811        identity: IdentityKeyRef<'_>,
812    ) -> Result<GatewayUptimeHistoryResponse, NymAPIError> {
813        self.get_json(
814            &[
815                routes::V1_API_VERSION,
816                routes::STATUS,
817                routes::GATEWAY,
818                identity,
819                routes::HISTORY,
820            ],
821            NO_PARAMS,
822        )
823        .await
824    }
825
826    #[deprecated]
827    #[instrument(level = "debug", skip(self))]
828    async fn get_gateway_core_status_count(
829        &self,
830        identity: IdentityKeyRef<'_>,
831        since: Option<i64>,
832    ) -> Result<GatewayCoreStatusResponse, NymAPIError> {
833        if let Some(since) = since {
834            self.get_json(
835                &[
836                    routes::V1_API_VERSION,
837                    routes::STATUS_ROUTES,
838                    routes::GATEWAY,
839                    identity,
840                    CORE_STATUS_COUNT,
841                ],
842                &[(SINCE_ARG, since.to_string())],
843            )
844            .await
845        } else {
846            self.get_json(
847                &[
848                    routes::V1_API_VERSION,
849                    routes::STATUS_ROUTES,
850                    routes::GATEWAY,
851                    identity,
852                ],
853                NO_PARAMS,
854            )
855            .await
856        }
857    }
858
859    #[deprecated]
860    #[instrument(level = "debug", skip(self))]
861    async fn get_mixnode_core_status_count(
862        &self,
863        mix_id: NodeId,
864        since: Option<i64>,
865    ) -> Result<MixnodeCoreStatusResponse, NymAPIError> {
866        if let Some(since) = since {
867            self.get_json(
868                &[
869                    routes::V1_API_VERSION,
870                    routes::STATUS_ROUTES,
871                    routes::MIXNODE,
872                    &mix_id.to_string(),
873                    CORE_STATUS_COUNT,
874                ],
875                &[(SINCE_ARG, since.to_string())],
876            )
877            .await
878        } else {
879            self.get_json(
880                &[
881                    routes::V1_API_VERSION,
882                    routes::STATUS_ROUTES,
883                    routes::MIXNODE,
884                    &mix_id.to_string(),
885                    CORE_STATUS_COUNT,
886                ],
887                NO_PARAMS,
888            )
889            .await
890        }
891    }
892
893    #[instrument(level = "debug", skip(self))]
894    async fn get_current_node_performance(
895        &self,
896        node_id: NodeId,
897    ) -> Result<NodePerformanceResponse, NymAPIError> {
898        self.get_json(
899            &[
900                routes::V1_API_VERSION,
901                routes::NYM_NODES_ROUTES,
902                routes::NYM_NODES_PERFORMANCE,
903                &node_id.to_string(),
904            ],
905            NO_PARAMS,
906        )
907        .await
908    }
909
910    async fn get_node_annotation(
911        &self,
912        node_id: NodeId,
913    ) -> Result<AnnotationResponse, NymAPIError> {
914        self.get_json(
915            &[
916                routes::V1_API_VERSION,
917                routes::NYM_NODES_ROUTES,
918                routes::NYM_NODES_ANNOTATION,
919                &node_id.to_string(),
920            ],
921            NO_PARAMS,
922        )
923        .await
924    }
925
926    #[deprecated]
927    async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
928        self.get_json(
929            &[
930                routes::V1_API_VERSION,
931                routes::STATUS_ROUTES,
932                routes::MIXNODE,
933                &mix_id.to_string(),
934                routes::AVG_UPTIME,
935            ],
936            NO_PARAMS,
937        )
938        .await
939    }
940
941    #[instrument(level = "debug", skip(self, request_body))]
942    async fn blind_sign(
943        &self,
944        request_body: &BlindSignRequestBody,
945    ) -> Result<BlindedSignatureResponse, NymAPIError> {
946        self.post_json(
947            &[
948                routes::V1_API_VERSION,
949                routes::ECASH_ROUTES,
950                routes::ECASH_BLIND_SIGN,
951            ],
952            NO_PARAMS,
953            request_body,
954        )
955        .await
956    }
957
958    #[instrument(level = "debug", skip(self, request_body))]
959    async fn verify_ecash_ticket(
960        &self,
961        request_body: &VerifyEcashTicketBody,
962    ) -> Result<EcashTicketVerificationResponse, NymAPIError> {
963        self.post_json(
964            &[
965                routes::V1_API_VERSION,
966                routes::ECASH_ROUTES,
967                routes::VERIFY_ECASH_TICKET,
968            ],
969            NO_PARAMS,
970            request_body,
971        )
972        .await
973    }
974
975    #[instrument(level = "debug", skip(self, request_body))]
976    async fn batch_redeem_ecash_tickets(
977        &self,
978        request_body: &BatchRedeemTicketsBody,
979    ) -> Result<EcashBatchTicketRedemptionResponse, NymAPIError> {
980        self.post_json(
981            &[
982                routes::V1_API_VERSION,
983                routes::ECASH_ROUTES,
984                routes::BATCH_REDEEM_ECASH_TICKETS,
985            ],
986            NO_PARAMS,
987            request_body,
988        )
989        .await
990    }
991
992    #[instrument(level = "debug", skip(self))]
993    async fn partial_expiration_date_signatures(
994        &self,
995        expiration_date: Option<Date>,
996        epoch_id: Option<EpochId>,
997    ) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
998        let mut params = match expiration_date {
999            None => Vec::new(),
1000            Some(exp) => vec![(
1001                ecash::EXPIRATION_DATE_PARAM,
1002                exp.format(&rfc_3339_date()).unwrap(),
1003            )],
1004        };
1005
1006        if let Some(epoch_id) = epoch_id {
1007            params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
1008        }
1009
1010        self.get_json(
1011            &[
1012                routes::V1_API_VERSION,
1013                routes::ECASH_ROUTES,
1014                routes::PARTIAL_EXPIRATION_DATE_SIGNATURES,
1015            ],
1016            &params,
1017        )
1018        .await
1019    }
1020
1021    #[instrument(level = "debug", skip(self))]
1022    async fn partial_coin_indices_signatures(
1023        &self,
1024        epoch_id: Option<EpochId>,
1025    ) -> Result<PartialCoinIndicesSignatureResponse, NymAPIError> {
1026        let params = match epoch_id {
1027            None => Vec::new(),
1028            Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())],
1029        };
1030
1031        self.get_json(
1032            &[
1033                routes::V1_API_VERSION,
1034                routes::ECASH_ROUTES,
1035                routes::PARTIAL_COIN_INDICES_SIGNATURES,
1036            ],
1037            &params,
1038        )
1039        .await
1040    }
1041
1042    #[instrument(level = "debug", skip(self))]
1043    async fn global_expiration_date_signatures(
1044        &self,
1045        expiration_date: Option<Date>,
1046        epoch_id: Option<EpochId>,
1047    ) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
1048        let mut params = match expiration_date {
1049            None => Vec::new(),
1050            Some(exp) => vec![(
1051                ecash::EXPIRATION_DATE_PARAM,
1052                exp.format(&rfc_3339_date()).unwrap(),
1053            )],
1054        };
1055
1056        if let Some(epoch_id) = epoch_id {
1057            params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
1058        }
1059
1060        self.get_json(
1061            &[
1062                routes::V1_API_VERSION,
1063                routes::ECASH_ROUTES,
1064                routes::GLOBAL_EXPIRATION_DATE_SIGNATURES,
1065            ],
1066            &params,
1067        )
1068        .await
1069    }
1070
1071    #[instrument(level = "debug", skip(self))]
1072    async fn global_coin_indices_signatures(
1073        &self,
1074        epoch_id: Option<EpochId>,
1075    ) -> Result<AggregatedCoinIndicesSignatureResponse, NymAPIError> {
1076        let params = match epoch_id {
1077            None => Vec::new(),
1078            Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())],
1079        };
1080
1081        self.get_json(
1082            &[
1083                routes::V1_API_VERSION,
1084                routes::ECASH_ROUTES,
1085                routes::GLOBAL_COIN_INDICES_SIGNATURES,
1086            ],
1087            &params,
1088        )
1089        .await
1090    }
1091
1092    #[instrument(level = "debug", skip(self))]
1093    async fn master_verification_key(
1094        &self,
1095        epoch_id: Option<EpochId>,
1096    ) -> Result<VerificationKeyResponse, NymAPIError> {
1097        let params = match epoch_id {
1098            None => Vec::new(),
1099            Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())],
1100        };
1101        self.get_json(
1102            &[
1103                routes::V1_API_VERSION,
1104                routes::ECASH_ROUTES,
1105                ecash::MASTER_VERIFICATION_KEY,
1106            ],
1107            &params,
1108        )
1109        .await
1110    }
1111
1112    #[instrument(level = "debug", skip(self))]
1113    async fn force_refresh_describe_cache(
1114        &self,
1115        request: &NodeRefreshBody,
1116    ) -> Result<(), NymAPIError> {
1117        self.post_json(
1118            &[
1119                routes::V1_API_VERSION,
1120                routes::NYM_NODES_ROUTES,
1121                routes::NYM_NODES_REFRESH_DESCRIBED,
1122            ],
1123            NO_PARAMS,
1124            request,
1125        )
1126        .await
1127    }
1128
1129    #[instrument(level = "debug", skip(self))]
1130    async fn issued_ticketbooks_for(
1131        &self,
1132        expiration_date: Date,
1133    ) -> Result<IssuedTicketbooksForResponse, NymAPIError> {
1134        self.get_json(
1135            &[
1136                routes::V1_API_VERSION,
1137                routes::ECASH_ROUTES,
1138                routes::ECASH_ISSUED_TICKETBOOKS_FOR,
1139                &expiration_date.to_string(),
1140            ],
1141            NO_PARAMS,
1142        )
1143        .await
1144    }
1145
1146    #[instrument(level = "debug", skip(self))]
1147    async fn issued_ticketbooks_for_count(
1148        &self,
1149        expiration_date: Date,
1150    ) -> Result<IssuedTicketbooksForCountResponse, NymAPIError> {
1151        self.get_json(
1152            &[
1153                routes::V1_API_VERSION,
1154                routes::ECASH_ROUTES,
1155                routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT,
1156                &expiration_date.to_string(),
1157            ],
1158            NO_PARAMS,
1159        )
1160        .await
1161    }
1162
1163    #[instrument(level = "debug", skip(self))]
1164    async fn issued_ticketbooks_challenge_commitment(
1165        &self,
1166        request: &IssuedTicketbooksChallengeCommitmentRequest,
1167    ) -> Result<IssuedTicketbooksChallengeCommitmentResponse, NymAPIError> {
1168        self.post_json(
1169            &[
1170                routes::V1_API_VERSION,
1171                routes::ECASH_ROUTES,
1172                routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT,
1173            ],
1174            NO_PARAMS,
1175            request,
1176        )
1177        .await
1178    }
1179
1180    #[instrument(level = "debug", skip(self))]
1181    async fn issued_ticketbooks_data(
1182        &self,
1183        request: &IssuedTicketbooksDataRequest,
1184    ) -> Result<IssuedTicketbooksDataResponse, NymAPIError> {
1185        self.post_json(
1186            &[
1187                routes::V1_API_VERSION,
1188                routes::ECASH_ROUTES,
1189                routes::ECASH_ISSUED_TICKETBOOKS_DATA,
1190            ],
1191            NO_PARAMS,
1192            request,
1193        )
1194        .await
1195    }
1196
1197    async fn nodes_by_addresses(
1198        &self,
1199        addresses: Vec<IpAddr>,
1200    ) -> Result<NodesByAddressesResponse, NymAPIError> {
1201        self.post_json(
1202            &[
1203                routes::V1_API_VERSION,
1204                "unstable",
1205                routes::NYM_NODES_ROUTES,
1206                routes::nym_nodes::BY_ADDRESSES,
1207            ],
1208            NO_PARAMS,
1209            &NodesByAddressesRequestBody { addresses },
1210        )
1211        .await
1212    }
1213
1214    #[instrument(level = "debug", skip(self))]
1215    async fn get_network_details(&self) -> Result<NymNetworkDetailsResponse, NymAPIError> {
1216        self.get_json(
1217            &[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS],
1218            NO_PARAMS,
1219        )
1220        .await
1221    }
1222
1223    #[instrument(level = "debug", skip(self))]
1224    async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
1225        self.get_json(
1226            &[
1227                routes::V1_API_VERSION,
1228                routes::NETWORK,
1229                routes::CHAIN_STATUS,
1230            ],
1231            NO_PARAMS,
1232        )
1233        .await
1234    }
1235
1236    async fn get_chain_blocks_status(&self) -> Result<ChainBlocksStatusResponse, NymAPIError> {
1237        self.get_json("/v1/network/chain-blocks-status", NO_PARAMS)
1238            .await
1239    }
1240
1241    #[instrument(level = "debug", skip(self))]
1242    async fn get_signer_status(&self) -> Result<EcashSignerStatusResponse, NymAPIError> {
1243        self.get_json("/v1/ecash/signer-status", NO_PARAMS).await
1244    }
1245
1246    #[instrument(level = "debug", skip(self))]
1247    async fn get_signer_information(&self) -> Result<SignerInformationResponse, NymAPIError> {
1248        self.get_json("/v1/api-status/signer-information", NO_PARAMS)
1249            .await
1250    }
1251
1252    #[instrument(level = "debug", skip(self))]
1253    async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
1254        self.get_json(
1255            &[
1256                routes::V1_API_VERSION,
1257                routes::EPOCH,
1258                routes::KEY_ROTATION_INFO,
1259            ],
1260            NO_PARAMS,
1261        )
1262        .await
1263    }
1264
1265    /// Method to change the base API URLs being used by the client
1266    fn change_base_urls(&mut self, urls: Vec<url::Url>);
1267
1268    /// Retrieve expanded information for all bonded nodes on the network
1269    async fn get_all_expanded_nodes(&self) -> Result<SemiSkimmedNodesWithMetadata, NymAPIError> {
1270        // Unroll the first iteration to get the metadata
1271        let mut page = 0;
1272
1273        let res = self.get_expanded_nodes(false, Some(page), None).await?;
1274        let mut nodes = res.nodes.data;
1275        let metadata = res.metadata;
1276
1277        if res.nodes.pagination.total == nodes.len() {
1278            return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata));
1279        }
1280
1281        page += 1;
1282
1283        loop {
1284            let mut res = self.get_expanded_nodes(false, Some(page), None).await?;
1285
1286            nodes.append(&mut res.nodes.data);
1287            if nodes.len() < res.nodes.pagination.total {
1288                page += 1
1289            } else {
1290                break;
1291            }
1292        }
1293
1294        Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
1295    }
1296}
1297
1298// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
1299#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1300#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1301impl NymApiClientExt for nym_http_api_client::Client {
1302    fn api_url(&self) -> &url::Url {
1303        self.current_url().as_ref()
1304    }
1305
1306    fn change_base_urls(&mut self, urls: Vec<url::Url>) {
1307        self.change_base_urls(urls.into_iter().map(|u| u.into()).collect());
1308    }
1309}