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/// Information about an extended channel
11#[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/// Information about a standard channel
36#[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/// Full information about a single Sv2 client including all channels
59#[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    /// Get total number of channels for this client
68    pub fn total_channels(&self) -> usize {
69        self.extended_channels.len() + self.standard_channels.len()
70    }
71
72    /// Get total hashrate for this client
73    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    /// Convert to metadata (without channel arrays)
86    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/// Sv2 client metadata without channel arrays (for listings)
97#[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/// Aggregate information about all Sv2 clients
106#[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
115/// Trait for monitoring Sv2 clients (downstream connections)
116pub trait Sv2ClientsMonitoring: Send + Sync {
117    /// Get all Sv2 clients with their channels
118    fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo>;
119
120    /// Get a single Sv2 client by client_id
121    ///
122    /// Default implementation does O(n) scan. Override for O(1) lookup
123    /// if your implementation uses a HashMap internally.
124    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    /// Get summary of all Sv2 clients
131    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    // ── helpers ──────────────────────────────────────────────────────
152
153    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    // ── ClientInfo unit tests ───────────────────────────────────────
217
218    #[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    // ── ClientsMonitoring trait default implementations ─────────────
258
259    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}