1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use utoipa::ToSchema;
9
10#[cfg(feature = "asic-rs-telemetry")]
11use super::miner_telemetry::MinerTelemetry;
12
13#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
15pub struct ExtendedChannelInfo {
16 pub channel_id: u32,
17 pub user_identity: String,
18 pub nominal_hashrate: f32,
19 pub stable_hashrate: bool,
20 pub target_hex: String,
21 pub requested_max_target_hex: String,
22 pub extranonce_prefix_hex: String,
23 pub full_extranonce_size: usize,
24 pub rollable_extranonce_size: u16,
25 pub expected_shares_per_minute: f32,
26 pub shares_accepted: u32,
27 pub shares_rejected: u32,
28 pub shares_rejected_by_reason: HashMap<String, u32>,
29 pub share_work_sum: f64,
30 pub last_share_sequence_number: u32,
31 pub best_diff: f64,
32 pub last_batch_accepted: u32,
33 pub last_batch_work_sum: u64,
34 pub share_batch_size: usize,
35 pub blocks_found: u32,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
40pub struct StandardChannelInfo {
41 pub channel_id: u32,
42 pub user_identity: String,
43 pub nominal_hashrate: f32,
44 pub stable_hashrate: bool,
45 pub target_hex: String,
46 pub requested_max_target_hex: String,
47 pub extranonce_prefix_hex: String,
48 pub expected_shares_per_minute: f32,
49 pub shares_accepted: u32,
50 pub shares_rejected: u32,
51 pub shares_rejected_by_reason: HashMap<String, u32>,
52 pub share_work_sum: f64,
53 pub last_share_sequence_number: u32,
54 pub best_diff: f64,
55 pub last_batch_accepted: u32,
56 pub last_batch_work_sum: u64,
57 pub share_batch_size: usize,
58 pub blocks_found: u32,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
63pub struct Sv2ClientInfo {
64 pub client_id: usize,
65 pub extended_channels: Vec<ExtendedChannelInfo>,
66 pub standard_channels: Vec<StandardChannelInfo>,
67 #[cfg(feature = "asic-rs-telemetry")]
68 pub miner_telemetry: Option<MinerTelemetry>,
69}
70
71impl Sv2ClientInfo {
72 pub fn new(
73 client_id: usize,
74 extended_channels: Vec<ExtendedChannelInfo>,
75 standard_channels: Vec<StandardChannelInfo>,
76 ) -> Self {
77 Self {
78 client_id,
79 extended_channels,
80 standard_channels,
81 #[cfg(feature = "asic-rs-telemetry")]
82 miner_telemetry: None,
83 }
84 }
85
86 pub fn total_channels(&self) -> usize {
88 self.extended_channels.len() + self.standard_channels.len()
89 }
90
91 pub fn total_hashrate(&self) -> f32 {
93 self.extended_channels
94 .iter()
95 .map(|c| c.nominal_hashrate)
96 .sum::<f32>()
97 + self
98 .standard_channels
99 .iter()
100 .map(|c| c.nominal_hashrate)
101 .sum::<f32>()
102 }
103
104 pub fn to_metadata(&self) -> Sv2ClientMetadata {
106 Sv2ClientMetadata {
107 client_id: self.client_id,
108 extended_channels_count: self.extended_channels.len(),
109 standard_channels_count: self.standard_channels.len(),
110 total_hashrate: self.total_hashrate(),
111 #[cfg(feature = "asic-rs-telemetry")]
112 miner_telemetry: self.miner_telemetry.clone(),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
119pub struct Sv2ClientMetadata {
120 pub client_id: usize,
121 pub extended_channels_count: usize,
122 pub standard_channels_count: usize,
123 pub total_hashrate: f32,
124 #[cfg(feature = "asic-rs-telemetry")]
125 pub miner_telemetry: Option<MinerTelemetry>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
130pub struct Sv2ClientsSummary {
131 pub total_clients: usize,
132 pub total_channels: usize,
133 pub extended_channels: usize,
134 pub standard_channels: usize,
135 pub total_hashrate: f32,
136}
137
138pub trait Sv2ClientsMonitoring: Send + Sync {
140 fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo>;
142
143 fn get_sv2_client_by_id(&self, client_id: usize) -> Option<Sv2ClientInfo> {
148 self.get_sv2_clients()
149 .into_iter()
150 .find(|c| c.client_id == client_id)
151 }
152
153 fn get_sv2_clients_summary(&self) -> Sv2ClientsSummary {
155 let clients = self.get_sv2_clients();
156 let extended: usize = clients.iter().map(|c| c.extended_channels.len()).sum();
157 let standard: usize = clients.iter().map(|c| c.standard_channels.len()).sum();
158
159 Sv2ClientsSummary {
160 total_clients: clients.len(),
161 total_channels: extended + standard,
162 extended_channels: extended,
163 standard_channels: standard,
164 total_hashrate: clients.iter().map(|c| c.total_hashrate()).sum(),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use stratum_core::mining_sv2::ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE;
173
174 fn create_extended_channel_info(channel_id: u32, hashrate: f32) -> ExtendedChannelInfo {
177 ExtendedChannelInfo {
178 channel_id,
179 user_identity: format!("user-ext-{}", channel_id),
180 nominal_hashrate: hashrate,
181 stable_hashrate: false,
182 target_hex: "00ff".into(),
183 requested_max_target_hex: "00ff".into(),
184 extranonce_prefix_hex: "aa".into(),
185 full_extranonce_size: 16,
186 rollable_extranonce_size: 4,
187 expected_shares_per_minute: 1.0,
188 shares_accepted: 10,
189 shares_rejected: 0,
190 shares_rejected_by_reason: HashMap::new(),
191 share_work_sum: 100.0,
192 last_share_sequence_number: 5,
193 best_diff: 50.0,
194 last_batch_accepted: 3,
195 last_batch_work_sum: 30,
196 share_batch_size: 10,
197 blocks_found: 0,
198 }
199 }
200
201 fn create_standard_channel_info(channel_id: u32, hashrate: f32) -> StandardChannelInfo {
202 StandardChannelInfo {
203 channel_id,
204 user_identity: format!("user-std-{}", channel_id),
205 nominal_hashrate: hashrate,
206 stable_hashrate: false,
207 target_hex: "00ff".into(),
208 requested_max_target_hex: "00ff".into(),
209 extranonce_prefix_hex: "bb".into(),
210 expected_shares_per_minute: 2.0,
211 shares_accepted: 20,
212 shares_rejected: 1,
213 shares_rejected_by_reason: HashMap::from([(
214 ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE.to_string(),
215 1,
216 )]),
217 share_work_sum: 200.0,
218 last_share_sequence_number: 8,
219 best_diff: 80.0,
220 last_batch_accepted: 5,
221 last_batch_work_sum: 50,
222 share_batch_size: 20,
223 blocks_found: 0,
224 }
225 }
226
227 fn create_sv2_client_info(
228 id: usize,
229 ext: Vec<ExtendedChannelInfo>,
230 std: Vec<StandardChannelInfo>,
231 ) -> Sv2ClientInfo {
232 Sv2ClientInfo {
233 client_id: id,
234 extended_channels: ext,
235 standard_channels: std,
236 #[cfg(feature = "asic-rs-telemetry")]
237 miner_telemetry: None,
238 }
239 }
240
241 #[test]
244 fn client_info_empty_channels() {
245 let client = create_sv2_client_info(1, vec![], vec![]);
246 assert_eq!(client.total_channels(), 0);
247 assert_eq!(client.total_hashrate(), 0.0);
248 }
249
250 #[test]
251 fn client_info_aggregates_both_channel_types() {
252 let client = create_sv2_client_info(
253 1,
254 vec![
255 create_extended_channel_info(1, 100.0),
256 create_extended_channel_info(2, 200.0),
257 ],
258 vec![create_standard_channel_info(3, 50.0)],
259 );
260 assert_eq!(client.total_channels(), 3);
261 assert_eq!(client.total_hashrate(), 350.0);
262 }
263
264 #[test]
265 fn client_info_to_metadata() {
266 let client = create_sv2_client_info(
267 42,
268 vec![create_extended_channel_info(1, 100.0)],
269 vec![
270 create_standard_channel_info(2, 50.0),
271 create_standard_channel_info(3, 75.0),
272 ],
273 );
274 let meta = client.to_metadata();
275
276 assert_eq!(meta.client_id, 42);
277 assert_eq!(meta.extended_channels_count, 1);
278 assert_eq!(meta.standard_channels_count, 2);
279 assert_eq!(meta.total_hashrate, 225.0);
280 #[cfg(feature = "asic-rs-telemetry")]
281 assert!(meta.miner_telemetry.is_none());
282 }
283
284 struct MockClients(Vec<Sv2ClientInfo>);
287 impl Sv2ClientsMonitoring for MockClients {
288 fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo> {
289 self.0.clone()
290 }
291 }
292
293 #[test]
294 fn clients_monitoring_get_client_by_id_found() {
295 let monitor = MockClients(vec![
296 create_sv2_client_info(1, vec![create_extended_channel_info(1, 10.0)], vec![]),
297 create_sv2_client_info(2, vec![], vec![create_standard_channel_info(1, 20.0)]),
298 ]);
299 let found = monitor.get_sv2_client_by_id(2);
300 assert!(found.is_some());
301 assert_eq!(found.unwrap().client_id, 2);
302 }
303
304 #[test]
305 fn clients_monitoring_get_client_by_id_not_found() {
306 let monitor = MockClients(vec![create_sv2_client_info(1, vec![], vec![])]);
307 assert!(monitor.get_sv2_client_by_id(999).is_none());
308 }
309
310 #[test]
311 fn clients_monitoring_summary_empty() {
312 let monitor = MockClients(vec![]);
313 let summary = monitor.get_sv2_clients_summary();
314
315 assert_eq!(summary.total_clients, 0);
316 assert_eq!(summary.total_channels, 0);
317 assert_eq!(summary.extended_channels, 0);
318 assert_eq!(summary.standard_channels, 0);
319 assert_eq!(summary.total_hashrate, 0.0);
320 }
321
322 #[test]
323 fn clients_monitoring_summary_aggregates_correctly() {
324 let monitor = MockClients(vec![
325 create_sv2_client_info(
326 1,
327 vec![create_extended_channel_info(1, 100.0)],
328 vec![create_standard_channel_info(2, 50.0)],
329 ),
330 create_sv2_client_info(
331 2,
332 vec![
333 create_extended_channel_info(3, 200.0),
334 create_extended_channel_info(4, 300.0),
335 ],
336 vec![],
337 ),
338 ]);
339 let summary = monitor.get_sv2_clients_summary();
340
341 assert_eq!(summary.total_clients, 2);
342 assert_eq!(summary.extended_channels, 3);
343 assert_eq!(summary.standard_channels, 1);
344 assert_eq!(summary.total_channels, 4);
345 assert_eq!(summary.total_hashrate, 650.0);
346 }
347}