pot_o_extensions/
messaging.rs1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use tokio::sync::{mpsc, RwLock};
6
7#[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#[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
73pub 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 pub async fn register(&self, device_id: String, tx: mpsc::UnboundedSender<String>) {
88 self.connections.write().await.insert(device_id, tx);
89 }
90
91 pub async fn unregister(&self, device_id: &str) {
93 self.connections.write().await.remove(device_id);
94 }
95
96 pub async fn connected_count(&self) -> usize {
98 self.connections.read().await.len()
99 }
100
101 pub async fn active_devices(&self) -> Vec<String> {
103 self.connections.read().await.keys().cloned().collect()
104 }
105
106 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 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); 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}