Skip to main content

stratum_apps/monitoring/
client.rs

1//! Sv2 client monitoring types
2//!
3//! These types are for monitoring **Sv2 clients** (downstream connections).
4//! Each client can have multiple channels opened with the app.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use utoipa::ToSchema;
9
10#[cfg(feature = "asic-rs-telemetry")]
11use super::miner_telemetry::MinerTelemetry;
12
13/// Information about an extended channel
14#[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/// Information about a standard channel
39#[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/// Full information about a single Sv2 client including all channels
62#[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    /// Get total number of channels for this client
87    pub fn total_channels(&self) -> usize {
88        self.extended_channels.len() + self.standard_channels.len()
89    }
90
91    /// Get total hashrate for this client
92    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    /// Convert to metadata (without channel arrays)
105    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/// Sv2 client metadata without channel arrays (for listings)
118#[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/// Aggregate information about all Sv2 clients
129#[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
138/// Trait for monitoring Sv2 clients (downstream connections)
139pub trait Sv2ClientsMonitoring: Send + Sync {
140    /// Get all Sv2 clients with their channels
141    fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo>;
142
143    /// Get a single Sv2 client by client_id
144    ///
145    /// Default implementation does O(n) scan. Override for O(1) lookup
146    /// if your implementation uses a HashMap internally.
147    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    /// Get summary of all Sv2 clients
154    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    // ── helpers ──────────────────────────────────────────────────────
175
176    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    // ── ClientInfo unit tests ───────────────────────────────────────
242
243    #[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    // ── ClientsMonitoring trait default implementations ─────────────
285
286    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}