Skip to main content

nym_api_requests/models/described/
v1.rs

1// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::models::described::type_translation::{
5    AuthenticatorDetailsV1, DeclaredRolesV1, HostInformationV1, IpPacketRouterDetailsV1,
6    NetworkRequesterDetailsV1, NymNodeAuxiliaryDetailsV1, WebSocketsV1, WireguardDetailsV1,
7};
8use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
9use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNodeV1, SkimmedNodeV1};
10use nym_crypto::asymmetric::{ed25519, x25519};
11use nym_mixnet_contract_common::reward_params::Performance;
12use nym_mixnet_contract_common::NodeId;
13use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
14use serde::{Deserialize, Serialize};
15use tracing::warn;
16use utoipa::ToSchema;
17
18// to whoever is thinking of modifying this struct.
19// you MUST NOT change its structure in any way - adding, removing or changing fields
20// otherwise, it will break old clients as bincode serialisation is not backwards compatible
21// even if you put `#[serde(default)]` all over the place
22#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
23pub struct NymNodeDescriptionV1 {
24    #[schema(value_type = u32)]
25    pub node_id: NodeId,
26    pub contract_node_type: DescribedNodeTypeV1,
27    pub description: NymNodeDataV1,
28}
29
30impl NymNodeDescriptionV1 {
31    pub fn version(&self) -> &str {
32        &self.description.build_information.build_version
33    }
34
35    pub fn entry_information(&self) -> BasicEntryInformation {
36        BasicEntryInformation {
37            hostname: self.description.host_information.hostname.clone(),
38            ws_port: self.description.mixnet_websockets.ws_port,
39            wss_port: self.description.mixnet_websockets.wss_port,
40        }
41    }
42
43    pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
44        self.description.host_information.keys.ed25519
45    }
46
47    pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
48        let keys = &self.description.host_information.keys;
49
50        if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
51            // legacy case (i.e. node doesn't support rotation)
52            return keys.current_x25519_sphinx_key.public_key;
53        }
54
55        if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
56            // it's the 'current' key
57            return keys.current_x25519_sphinx_key.public_key;
58        }
59
60        if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
61            if pre_announced.rotation_id == current_rotation_id {
62                return pre_announced.public_key;
63            }
64        }
65
66        warn!(
67            "unexpected key rotation {current_rotation_id} for node {}",
68            self.node_id
69        );
70        // this should never be reached, but just in case, return the fallback option
71        keys.current_x25519_sphinx_key.public_key
72    }
73
74    pub fn to_skimmed_node(
75        &self,
76        current_rotation_id: u32,
77        role: NodeRole,
78        performance: Performance,
79    ) -> SkimmedNodeV1 {
80        let keys = &self.description.host_information.keys;
81        let entry = if self.description.declared_role.entry {
82            Some(self.entry_information())
83        } else {
84            None
85        };
86
87        SkimmedNodeV1 {
88            node_id: self.node_id,
89            ed25519_identity_pubkey: keys.ed25519,
90            ip_addresses: self.description.host_information.ip_address.clone(),
91            mix_port: self.description.mix_port(),
92            x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
93            // we can't use the declared roles, we have to take whatever was provided in the contract.
94            // why? say this node COULD operate as an exit, but it might be the case the contract decided
95            // to assign it an ENTRY role only. we have to use that one instead.
96            role,
97            supported_roles: self.description.declared_role,
98            entry,
99            performance,
100        }
101    }
102
103    pub fn to_semi_skimmed_node(
104        &self,
105        current_rotation_id: u32,
106        role: NodeRole,
107        performance: Performance,
108    ) -> SemiSkimmedNodeV1 {
109        let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
110
111        SemiSkimmedNodeV1 {
112            basic: skimmed_node,
113            x25519_noise_versioned_key: self
114                .description
115                .host_information
116                .keys
117                .x25519_versioned_noise,
118        }
119    }
120}
121
122// to whoever is thinking of modifying this struct.
123// you MUST NOT change its structure in any way - adding, removing or changing fields
124// otherwise, it will break old clients as bincode serialisation is not backwards compatible
125// even if you put `#[serde(default)]` all over the place
126#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
127#[serde(rename_all = "snake_case")]
128#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
129#[cfg_attr(
130    feature = "generate-ts",
131    ts(
132        export,
133        export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts"
134    )
135)]
136pub enum DescribedNodeTypeV1 {
137    LegacyMixnode,
138    LegacyGateway,
139    NymNode,
140}
141
142impl DescribedNodeTypeV1 {
143    pub fn is_nym_node(&self) -> bool {
144        matches!(self, DescribedNodeTypeV1::NymNode)
145    }
146}
147
148// to whoever is thinking of modifying this struct.
149// you MUST NOT change its structure in any way - adding, removing or changing fields
150// otherwise, it will break old clients as bincode serialisation is not backwards compatible
151// even if you put `#[serde(default)]` all over the place
152#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
153pub struct NymNodeDataV1 {
154    #[serde(default)]
155    pub last_polled: OffsetDateTimeJsonSchemaWrapper,
156
157    pub host_information: HostInformationV1,
158
159    #[serde(default)]
160    pub declared_role: DeclaredRolesV1,
161
162    #[serde(default)]
163    pub auxiliary_details: NymNodeAuxiliaryDetailsV1,
164
165    // TODO: do we really care about ALL build info or just the version?
166    pub build_information: BinaryBuildInformationOwned,
167
168    #[serde(default)]
169    pub network_requester: Option<NetworkRequesterDetailsV1>,
170
171    #[serde(default)]
172    pub ip_packet_router: Option<IpPacketRouterDetailsV1>,
173
174    #[serde(default)]
175    pub authenticator: Option<AuthenticatorDetailsV1>,
176
177    #[serde(default)]
178    pub wireguard: Option<WireguardDetailsV1>,
179
180    // for now we only care about their ws/wss situation, nothing more
181    pub mixnet_websockets: WebSocketsV1,
182}
183
184impl NymNodeDataV1 {
185    pub fn mix_port(&self) -> u16 {
186        self.auxiliary_details
187            .announce_ports
188            .mix_port
189            .unwrap_or(DEFAULT_MIX_LISTENING_PORT)
190    }
191
192    pub fn verloc_port(&self) -> u16 {
193        self.auxiliary_details
194            .announce_ports
195            .verloc_port
196            .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
197    }
198}