Skip to main content

pot_o_extensions/
messaging.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use tokio::sync::{mpsc, RwLock};
6
7/// Message sent from a miner to the validator over WebSocket.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum MinerMessage {
11    Subscribe {
12        device_id: String,
13        device_type: String,
14    },
15    Unsubscribe {
16        device_id: String,
17    },
18    Heartbeat {
19        device_id: String,
20    },
21    SubmitProof {
22        device_id: String,
23        challenge_id: String,
24        proof_hex: String,
25        signature: String,
26        device_type: String,
27    },
28    Progress {
29        device_id: String,
30        challenge_id: String,
31        hash: String,
32        progress_pct: f64,
33    },
34}
35
36/// Message sent from the validator to a miner over WebSocket.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(tag = "type", rename_all = "snake_case")]
39pub enum ValidatorMessage {
40    Challenge {
41        challenge_json: String,
42    },
43    ProofAccepted {
44        tx_signature: String,
45    },
46    ProofRejected {
47        reason: String,
48    },
49    HeartbeatAck,
50    Error {
51        code: String,
52        message: String,
53    },
54    Subscribed {
55        device_id: String,
56    },
57    NewBlock {
58        height: u64,
59        hash: String,
60        tx_count: usize,
61        timestamp: u64,
62    },
63}
64
65impl ValidatorMessage {
66    pub fn to_json(&self) -> String {
67        serde_json::to_string(self).unwrap_or_else(|_| {
68            r#"{"type":"error","code":"serialization_failed","message":"internal error"}"#.into()
69        })
70    }
71}
72
73/// Connection manager for miner WebSocket sessions.
74/// Each connected miner gets an `mpsc::UnboundedSender` that the WS task polls.
75pub struct Messaging {
76    connections: RwLock<HashMap<String, mpsc::UnboundedSender<String>>>,
77}
78
79impl Messaging {
80    pub fn new() -> Arc<Self> {
81        Arc::new(Self {
82            connections: RwLock::new(HashMap::new()),
83        })
84    }
85
86    /// Register a connected miner.
87    pub async fn register(&self, device_id: String, tx: mpsc::UnboundedSender<String>) {
88        self.connections.write().await.insert(device_id, tx);
89    }
90
91    /// Remove a disconnected miner.
92    pub async fn unregister(&self, device_id: &str) {
93        self.connections.write().await.remove(device_id);
94    }
95
96    /// Number of currently connected miners.
97    pub async fn connected_count(&self) -> usize {
98        self.connections.read().await.len()
99    }
100
101    /// List all connected device IDs.
102    pub async fn active_devices(&self) -> Vec<String> {
103        self.connections.read().await.keys().cloned().collect()
104    }
105
106    /// Push a message to every connected miner.
107    pub async fn broadcast(&self, msg: &ValidatorMessage) {
108        let json = msg.to_json();
109        let mut dead = Vec::new();
110        for (id, tx) in self.connections.read().await.iter() {
111            if tx.send(json.clone()).is_err() {
112                dead.push(id.clone());
113            }
114        }
115        if !dead.is_empty() {
116            let mut w = self.connections.write().await;
117            for id in dead {
118                w.remove(&id);
119            }
120        }
121    }
122
123    /// Push a message to a specific miner.
124    pub async fn send_to(&self, device_id: &str, msg: &ValidatorMessage) {
125        let json = msg.to_json();
126        if let Some(tx) = self.connections.read().await.get(device_id) {
127            if tx.send(json).is_err() {
128                self.unregister(device_id).await;
129            }
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[tokio::test]
139    async fn test_messaging_register_and_count() {
140        let m = Messaging::new();
141        let (tx, _) = mpsc::unbounded_channel();
142        m.register("miner-1".into(), tx).await;
143        assert_eq!(m.connected_count().await, 1);
144        assert_eq!(m.active_devices().await, vec!["miner-1".to_string()]);
145    }
146
147    #[tokio::test]
148    async fn test_messaging_unregister() {
149        let m = Messaging::new();
150        let (tx, _) = mpsc::unbounded_channel();
151        m.register("miner-1".into(), tx).await;
152        m.unregister("miner-1").await;
153        assert_eq!(m.connected_count().await, 0);
154    }
155
156    #[tokio::test]
157    async fn test_messaging_broadcast() {
158        let m = Messaging::new();
159        let (tx, mut rx) = mpsc::unbounded_channel();
160        m.register("miner-1".into(), tx).await;
161
162        m.broadcast(&ValidatorMessage::HeartbeatAck).await;
163        let received: String = rx.recv().await.unwrap();
164        assert!(received.contains("\"type\":\"heartbeat_ack\""));
165    }
166
167    #[tokio::test]
168    async fn test_messaging_send_to() {
169        let m = Messaging::new();
170        let (tx, mut rx) = mpsc::unbounded_channel();
171        m.register("miner-1".into(), tx).await;
172
173        m.send_to("miner-1", &ValidatorMessage::HeartbeatAck).await;
174        let received: String = rx.recv().await.unwrap();
175        assert!(received.contains("\"type\":\"heartbeat_ack\""));
176    }
177
178    #[tokio::test]
179    async fn test_messaging_cleanup_dead() {
180        let m = Messaging::new();
181        let (tx, rx) = mpsc::unbounded_channel();
182        m.register("miner-1".into(), tx).await;
183        drop(rx); // drop receiver to simulate disconnect
184
185        // Broadcast should detect dead connection and clean up
186        m.broadcast(&ValidatorMessage::HeartbeatAck).await;
187        assert_eq!(m.connected_count().await, 0);
188    }
189
190    #[tokio::test]
191    async fn test_validator_message_to_json() {
192        let msg = ValidatorMessage::HeartbeatAck;
193        let json = msg.to_json();
194        assert!(json.contains("\"type\":\"heartbeat_ack\""));
195
196        let msg = ValidatorMessage::ProofAccepted {
197            tx_signature: "sig123".into(),
198        };
199        let json = msg.to_json();
200        assert!(json.contains("proof_accepted"));
201        assert!(json.contains("sig123"));
202    }
203
204    #[test]
205    fn test_miner_message_serialization() {
206        let msg = MinerMessage::Subscribe {
207            device_id: "esp-01".into(),
208            device_type: "esp32s".into(),
209        };
210        let json = serde_json::to_string(&msg).unwrap();
211        assert!(json.contains("\"type\":\"subscribe\""));
212        assert!(json.contains("esp-01"));
213
214        let msg = MinerMessage::Heartbeat {
215            device_id: "esp-01".into(),
216        };
217        let json = serde_json::to_string(&msg).unwrap();
218        assert!(json.contains("\"type\":\"heartbeat\""));
219    }
220}