Skip to main content

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