1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4pub struct RelayerNode {
5 pub address: String,
6 pub sphinx_key: String,
7 pub url: String,
9 pub stake: String,
11 pub last_seen: u64,
12 pub is_privileged: bool,
13 pub layer: u8,
15 #[serde(default = "default_role")]
17 pub role: u8,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub ingress_url: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub metadata_url: Option<String>,
24}
25
26fn default_role() -> u8 {
27 3
28}
29
30#[must_use]
33pub fn layers_for_role(role: u8) -> &'static [u8] {
34 match role {
35 1 => &[0, 1],
36 _ => &[0, 1, 2],
37 }
38}
39
40impl RelayerNode {
41 #[must_use]
42 pub fn new(address: String, sphinx_key: String, url: String, stake: String, role: u8) -> Self {
43 let is_privileged = Self::parse_stake(&stake).is_some_and(|s| s == 0);
44 Self {
45 address,
46 sphinx_key,
47 url,
48 stake,
49 last_seen: 0,
50 is_privileged,
51 layer: 0,
52 role,
53 ingress_url: None,
54 metadata_url: None,
55 }
56 }
57
58 #[must_use]
59 pub fn with_ingress_url(mut self, ingress_url: String) -> Self {
60 self.ingress_url = Some(ingress_url);
61 self
62 }
63
64 #[must_use]
66 pub fn parse_stake(stake_str: &str) -> Option<u128> {
67 stake_str.parse::<u128>().ok()
68 }
69
70 #[allow(clippy::must_use_candidate)]
71 pub fn stake_value(&self) -> u128 {
72 Self::parse_stake(&self.stake).unwrap_or(0)
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
77pub struct TopologySnapshot {
78 pub nodes: Vec<RelayerNode>,
79 pub fingerprint: String,
81 pub timestamp: u64,
82 pub block_number: u64,
83 #[serde(default)]
85 pub pow_difficulty: u32,
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 fn make_node(addr: &str, stake: &str, role: u8) -> RelayerNode {
93 RelayerNode::new(
94 addr.to_string(),
95 "0xdead".to_string(),
96 "/ip4/127.0.0.1/tcp/9000".to_string(),
97 stake.to_string(),
98 role,
99 )
100 }
101
102 #[test]
103 fn test_new_sets_defaults() {
104 let node = make_node("0xabc", "1000", 3);
105 assert_eq!(node.address, "0xabc");
106 assert_eq!(node.sphinx_key, "0xdead");
107 assert_eq!(node.stake, "1000");
108 assert_eq!(node.role, 3);
109 assert_eq!(node.layer, 0);
110 assert_eq!(node.last_seen, 0);
111 assert!(!node.is_privileged);
112 assert!(node.ingress_url.is_none());
113 }
114
115 #[test]
116 fn test_new_privileged_when_stake_zero() {
117 let node = make_node("0xabc", "0", 1);
118 assert!(
119 node.is_privileged,
120 "Zero-stake nodes are privileged (admin)"
121 );
122 }
123
124 #[test]
125 fn test_new_not_privileged_when_stake_nonzero() {
126 let node = make_node("0xabc", "100", 2);
127 assert!(!node.is_privileged);
128 }
129
130 #[test]
131 fn test_new_not_privileged_when_stake_unparseable() {
132 let node = make_node("0xabc", "not_a_number", 1);
133 assert!(!node.is_privileged, "Unparseable stake is not privileged");
134 }
135
136 #[test]
137 fn test_with_ingress_url() {
138 let node =
139 make_node("0xabc", "1000", 3).with_ingress_url("http://1.2.3.4:8080".to_string());
140 assert_eq!(node.ingress_url.as_deref(), Some("http://1.2.3.4:8080"));
141 }
142
143 #[test]
144 fn test_parse_stake_valid() {
145 assert_eq!(RelayerNode::parse_stake("42"), Some(42));
146 assert_eq!(RelayerNode::parse_stake("0"), Some(0));
147 assert_eq!(
148 RelayerNode::parse_stake("340282366920938463463374607431768211455"),
149 Some(u128::MAX),
150 );
151 }
152
153 #[test]
154 fn test_parse_stake_invalid() {
155 assert_eq!(RelayerNode::parse_stake(""), None);
156 assert_eq!(RelayerNode::parse_stake("abc"), None);
157 assert_eq!(RelayerNode::parse_stake("-1"), None);
158 }
159
160 #[test]
161 fn test_stake_value_returns_parsed() {
162 let node = make_node("0xabc", "9999", 1);
163 assert_eq!(node.stake_value(), 9999);
164 }
165
166 #[test]
167 fn test_stake_value_returns_zero_on_failure() {
168 let node = make_node("0xabc", "garbage", 1);
169 assert_eq!(node.stake_value(), 0);
170 }
171
172 #[test]
173 fn test_relayer_node_json_roundtrip() {
174 let node =
175 make_node("0xabc", "500", 2).with_ingress_url("http://localhost:8080".to_string());
176 let json = serde_json::to_string(&node).expect("serialize");
177 let back: RelayerNode = serde_json::from_str(&json).expect("deserialize");
178 assert_eq!(node, back);
179 }
180
181 #[test]
182 fn test_relayer_node_deserialize_missing_role_defaults_to_full() {
183 let json = r#"{
185 "address": "0xabc",
186 "sphinx_key": "0xdead",
187 "url": "/ip4/127.0.0.1/tcp/9000",
188 "stake": "1000",
189 "last_seen": 0,
190 "is_privileged": false,
191 "layer": 0
192 }"#;
193 let node: RelayerNode = serde_json::from_str(json).expect("deserialize");
194 assert_eq!(node.role, 3, "Missing role should default to 3 (Full)");
195 }
196
197 #[test]
198 fn test_relayer_node_deserialize_missing_ingress_url_defaults_to_none() {
199 let json = r#"{
200 "address": "0xabc",
201 "sphinx_key": "0xdead",
202 "url": "/ip4/127.0.0.1/tcp/9000",
203 "stake": "1000",
204 "last_seen": 0,
205 "is_privileged": false,
206 "layer": 0,
207 "role": 2
208 }"#;
209 let node: RelayerNode = serde_json::from_str(json).expect("deserialize");
210 assert!(node.ingress_url.is_none());
211 }
212
213 #[test]
214 fn test_topology_snapshot_json_roundtrip() {
215 let snapshot = TopologySnapshot {
216 nodes: vec![make_node("0x1", "100", 1), make_node("0x2", "200", 2)],
217 fingerprint: "abcdef1234567890".to_string(),
218 timestamp: 1700000000,
219 block_number: 42,
220 pow_difficulty: 0,
221 };
222 let json = serde_json::to_string(&snapshot).expect("serialize");
223 let back: TopologySnapshot = serde_json::from_str(&json).expect("deserialize");
224 assert_eq!(snapshot, back);
225 }
226
227 #[test]
228 fn test_layers_for_role_relay() {
229 assert_eq!(layers_for_role(1), &[0, 1]);
230 }
231
232 #[test]
233 fn test_layers_for_role_exit() {
234 assert_eq!(layers_for_role(2), &[0, 1, 2]);
235 }
236
237 #[test]
238 fn test_layers_for_role_full() {
239 assert_eq!(layers_for_role(3), &[0, 1, 2]);
240 }
241
242 #[test]
243 fn test_layers_for_role_unknown_defaults_to_all() {
244 assert_eq!(layers_for_role(0), &[0, 1, 2]);
245 assert_eq!(layers_for_role(255), &[0, 1, 2]);
246 }
247
248 #[test]
249 fn test_ingress_url_not_serialized_when_none() {
250 let node = make_node("0xabc", "100", 1);
251 let json = serde_json::to_string(&node).expect("serialize");
252 assert!(
253 !json.contains("ingress_url"),
254 "ingress_url should be skipped when None"
255 );
256 }
257}