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