1use nym_api_requests::models::DeclaredRoles;
5use nym_api_requests::nym_nodes::SkimmedNode;
6use nym_crypto::asymmetric::{ed25519, x25519};
7use nym_mixnet_contract_common::NodeId;
8use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
9use nym_sphinx_types::Node as SphinxNode;
10use serde::{Deserialize, Serialize};
11use std::net::{IpAddr, SocketAddr};
12use thiserror::Error;
13
14pub use nym_mixnet_contract_common::LegacyMixLayer;
15
16#[derive(Error, Debug)]
17pub enum RoutingNodeError {
18 #[error("node {node_id} ('{identity}') has not provided any valid ip addresses")]
19 NoIpAddressesProvided { node_id: NodeId, identity: String },
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct EntryDetails {
24 pub ip_addresses: Vec<IpAddr>,
26 pub clients_ws_port: u16,
27 pub hostname: Option<String>,
28 pub clients_wss_port: Option<u16>,
29}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
32pub struct SupportedRoles {
33 pub mixnode: bool,
34 pub mixnet_entry: bool,
35 pub mixnet_exit: bool,
36}
37
38impl From<DeclaredRoles> for SupportedRoles {
39 fn from(value: DeclaredRoles) -> Self {
40 SupportedRoles {
41 mixnode: value.mixnode,
42 mixnet_entry: value.entry,
43 mixnet_exit: value.exit_nr && value.exit_ipr,
44 }
45 }
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct RoutingNode {
50 pub node_id: NodeId,
51
52 pub mix_host: SocketAddr,
53
54 pub entry: Option<EntryDetails>,
55 pub identity_key: ed25519::PublicKey,
56 pub sphinx_key: x25519::PublicKey,
57
58 pub supported_roles: SupportedRoles,
59}
60
61impl RoutingNode {
62 pub fn ws_entry_address_tls(&self) -> Option<String> {
63 let entry = self.entry.as_ref()?;
64 let hostname = entry.hostname.as_ref()?;
65 let wss_port = entry.clients_wss_port?;
66
67 Some(format!("wss://{hostname}:{wss_port}"))
68 }
69
70 pub fn ws_entry_address_no_tls(&self, prefer_ipv6: bool) -> Option<String> {
71 let entry = self.entry.as_ref()?;
72
73 if let Some(hostname) = entry.hostname.as_ref() {
74 return Some(format!("ws://{hostname}:{}", entry.clients_ws_port));
75 }
76
77 if prefer_ipv6 && let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) {
78 return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port));
79 }
80
81 let any_ip = entry.ip_addresses.first()?;
82 Some(format!("ws://{any_ip}:{}", entry.clients_ws_port))
83 }
84
85 pub fn ws_entry_address(&self, prefer_ipv6: bool) -> Option<String> {
86 if let Some(tls) = self.ws_entry_address_tls() {
87 return Some(tls);
88 }
89 self.ws_entry_address_no_tls(prefer_ipv6)
90 }
91
92 pub fn ws_entry_address_with_fallback(
93 &self,
94 prefer_ipv6: bool,
95 no_hostname: bool,
96 ) -> (Option<String>, Option<String>) {
97 let Some(entry) = &self.entry else {
98 return (None, None);
99 };
100
101 let maybe_hostname = if !no_hostname {
103 entry.hostname.clone()
104 } else {
105 None
106 };
107
108 let ips: Vec<&IpAddr> = if prefer_ipv6 {
110 entry
111 .ip_addresses
112 .iter()
113 .filter(|ip| ip.is_ipv6())
114 .chain(entry.ip_addresses.iter().filter(|ip| ip.is_ipv4()))
115 .collect()
116 } else {
117 entry.ip_addresses.iter().collect()
118 };
119
120 let ws_addresses: Vec<_> = maybe_hostname
122 .into_iter()
123 .chain(ips.into_iter().map(|ip| ip.to_string()))
124 .take(2)
125 .map(|host| format!("ws://{host}:{}", entry.clients_ws_port))
126 .collect();
127
128 (ws_addresses.first().cloned(), ws_addresses.get(1).cloned())
129 }
130
131 pub fn identity(&self) -> ed25519::PublicKey {
132 self.identity_key
133 }
134}
135
136impl<'a> From<&'a RoutingNode> for SphinxNode {
137 fn from(node: &'a RoutingNode) -> Self {
138 #[allow(clippy::unwrap_used)]
141 let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host)
142 .try_into()
143 .unwrap();
144
145 SphinxNode::new(node_address_bytes, node.sphinx_key.into())
146 }
147}
148
149impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode {
150 type Error = RoutingNodeError;
151
152 fn try_from(value: &'a SkimmedNode) -> Result<Self, Self::Error> {
153 let Some(first_ip) = value.ip_addresses.first() else {
158 return Err(RoutingNodeError::NoIpAddressesProvided {
159 node_id: value.node_id,
160 identity: value.ed25519_identity_pubkey.to_string(),
161 });
162 };
163
164 let entry = value.entry.as_ref().map(|entry| EntryDetails {
165 ip_addresses: value.ip_addresses.clone(),
166 clients_ws_port: entry.ws_port,
167 hostname: entry.hostname.clone(),
168 clients_wss_port: entry.wss_port,
169 });
170
171 Ok(RoutingNode {
172 node_id: value.node_id,
173 mix_host: SocketAddr::new(*first_ip, value.mix_port),
174 entry,
175 identity_key: value.ed25519_identity_pubkey,
176 sphinx_key: value.x25519_sphinx_pubkey,
177 supported_roles: value.supported_roles.into(),
178 })
179 }
180}