chie_shared/messages.rs
1//! Message types for inter-component communication.
2
3use crate::{BandwidthProof, Bytes, ContentCid, PeerIdString, Points};
4use serde::{Deserialize, Serialize};
5
6/// Request to submit a bandwidth proof to the coordinator.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SubmitProofRequest {
9 /// The bandwidth proof to submit.
10 pub proof: BandwidthProof,
11 /// Optional metadata about the submission.
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub metadata: Option<ProofMetadata>,
14}
15
16/// Additional metadata for proof submission.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProofMetadata {
19 /// Client version that generated the proof.
20 pub client_version: String,
21 /// Node region/location.
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub region: Option<String>,
24 /// Connection type (e.g., "direct", "relay").
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub connection_type: Option<String>,
27}
28
29/// Response from proof submission.
30///
31/// # Examples
32///
33/// ```
34/// use chie_shared::SubmitProofResponse;
35/// use uuid::Uuid;
36///
37/// // Accepted proof with rewards
38/// let accepted = SubmitProofResponse {
39/// accepted: true,
40/// proof_id: Some(Uuid::new_v4()),
41/// reward_points: Some(100),
42/// rejection_reason: None,
43/// };
44/// assert!(accepted.accepted);
45/// assert!(accepted.reward_points.is_some());
46///
47/// // Rejected proof with reason
48/// let rejected = SubmitProofResponse {
49/// accepted: false,
50/// proof_id: None,
51/// reward_points: None,
52/// rejection_reason: Some("Invalid signature".to_string()),
53/// };
54/// assert!(!rejected.accepted);
55/// assert!(rejected.rejection_reason.is_some());
56/// ```
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SubmitProofResponse {
59 /// Whether the proof was accepted.
60 pub accepted: bool,
61 /// Proof ID if accepted.
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub proof_id: Option<uuid::Uuid>,
64 /// Reward points if accepted.
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub reward_points: Option<Points>,
67 /// Rejection reason if not accepted.
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub rejection_reason: Option<String>,
70}
71
72/// Request to announce new content to the network.
73///
74/// # Examples
75///
76/// ```
77/// use chie_shared::AnnounceContentRequest;
78///
79/// // Announce a 25MB content with 100 chunks
80/// let request = AnnounceContentRequest {
81/// content_cid: "QmExampleContent123".to_string(),
82/// peer_id: "12D3KooWProvider".to_string(),
83/// chunk_count: 100,
84/// size_bytes: 25 * 1024 * 1024, // 25 MB
85/// ttl_seconds: 7200, // 2 hours
86/// };
87///
88/// assert_eq!(request.chunk_count, 100);
89/// assert_eq!(request.size_bytes, 26_214_400);
90/// assert_eq!(request.ttl_seconds, 7200);
91/// ```
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct AnnounceContentRequest {
94 /// Content CID being announced.
95 pub content_cid: ContentCid,
96 /// Peer ID of the announcing node.
97 pub peer_id: PeerIdString,
98 /// Number of chunks available.
99 pub chunk_count: u64,
100 /// Total size in bytes.
101 pub size_bytes: Bytes,
102 /// TTL for the announcement (seconds).
103 #[serde(default = "default_announcement_ttl")]
104 pub ttl_seconds: u32,
105}
106
107fn default_announcement_ttl() -> u32 {
108 3600 // 1 hour
109}
110
111/// Request to query content availability.
112///
113/// # Examples
114///
115/// ```
116/// use chie_shared::QueryContentRequest;
117///
118/// // Query for content with default max providers (20)
119/// let request = QueryContentRequest {
120/// content_cid: "QmExampleContent".to_string(),
121/// max_providers: 20,
122/// };
123///
124/// assert_eq!(request.content_cid, "QmExampleContent");
125/// assert_eq!(request.max_providers, 20);
126///
127/// // Query with custom limit
128/// let limited = QueryContentRequest {
129/// content_cid: "QmAnotherContent".to_string(),
130/// max_providers: 5,
131/// };
132/// assert_eq!(limited.max_providers, 5);
133/// ```
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct QueryContentRequest {
136 /// Content CID to query.
137 pub content_cid: ContentCid,
138 /// Maximum number of providers to return.
139 #[serde(default = "default_max_providers")]
140 pub max_providers: usize,
141}
142
143fn default_max_providers() -> usize {
144 20
145}
146
147/// Response to content query.
148///
149/// # Examples
150///
151/// ```
152/// use chie_shared::{QueryContentResponse, ContentProvider};
153///
154/// // Response with multiple providers
155/// let response = QueryContentResponse {
156/// content_cid: "QmExample".to_string(),
157/// providers: vec![
158/// ContentProvider {
159/// peer_id: "12D3Koo1".to_string(),
160/// addresses: vec!["/ip4/1.2.3.4/tcp/4001".to_string()],
161/// available_chunks: Some(vec![0, 1, 2, 3]),
162/// reputation: Some(98.5),
163/// last_seen: None,
164/// },
165/// ContentProvider {
166/// peer_id: "12D3Koo2".to_string(),
167/// addresses: vec!["/ip4/5.6.7.8/tcp/4001".to_string()],
168/// available_chunks: None,
169/// reputation: Some(87.0),
170/// last_seen: None,
171/// },
172/// ],
173/// total_providers: 5,
174/// };
175///
176/// assert_eq!(response.providers.len(), 2);
177/// assert_eq!(response.total_providers, 5);
178/// ```
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct QueryContentResponse {
181 /// Content CID that was queried.
182 pub content_cid: ContentCid,
183 /// List of providers.
184 pub providers: Vec<ContentProvider>,
185 /// Total number of providers available.
186 pub total_providers: usize,
187}
188
189/// Information about a content provider.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ContentProvider {
192 /// Provider's peer ID.
193 pub peer_id: PeerIdString,
194 /// Provider's multiaddresses.
195 pub addresses: Vec<String>,
196 /// Chunks available from this provider.
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub available_chunks: Option<Vec<u64>>,
199 /// Provider reputation score.
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub reputation: Option<f32>,
202 /// Last seen timestamp.
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub last_seen: Option<chrono::DateTime<chrono::Utc>>,
205}
206
207/// Request to update node statistics.
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct UpdateNodeStatsRequest {
210 /// Node's peer ID.
211 pub peer_id: PeerIdString,
212 /// Bandwidth uploaded (bytes).
213 pub bandwidth_uploaded: Bytes,
214 /// Bandwidth downloaded (bytes).
215 pub bandwidth_downloaded: Bytes,
216 /// Number of chunks served.
217 pub chunks_served: u64,
218 /// Storage used (bytes).
219 pub storage_used: Bytes,
220 /// Uptime (seconds).
221 pub uptime_seconds: u64,
222}
223
224/// Heartbeat message from node to coordinator.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct NodeHeartbeat {
227 /// Node's peer ID.
228 pub peer_id: PeerIdString,
229 /// Current node status.
230 pub status: crate::NodeStatus,
231 /// Available storage (bytes).
232 pub available_storage: Bytes,
233 /// Available bandwidth (bps).
234 pub available_bandwidth: u64,
235 /// Number of active connections.
236 pub active_connections: u32,
237 /// Timestamp of the heartbeat.
238 pub timestamp: chrono::DateTime<chrono::Utc>,
239}
240
241impl NodeHeartbeat {
242 /// Create a new heartbeat with current timestamp.
243 ///
244 /// # Examples
245 ///
246 /// ```
247 /// use chie_shared::{NodeHeartbeat, NodeStatus};
248 ///
249 /// // Create online heartbeat
250 /// let heartbeat = NodeHeartbeat::new("12D3KooWNode", NodeStatus::Online);
251 /// assert_eq!(heartbeat.peer_id, "12D3KooWNode");
252 /// assert_eq!(heartbeat.status, NodeStatus::Online);
253 /// assert_eq!(heartbeat.available_storage, 0);
254 ///
255 /// // Create custom heartbeat with resources
256 /// let mut custom = NodeHeartbeat::new("12D3KooWOther", NodeStatus::Online);
257 /// custom.available_storage = 100 * 1024 * 1024 * 1024; // 100 GB
258 /// custom.available_bandwidth = 10_000_000; // 10 Mbps
259 /// custom.active_connections = 42;
260 /// assert_eq!(custom.active_connections, 42);
261 /// ```
262 pub fn new(peer_id: impl Into<String>, status: crate::NodeStatus) -> Self {
263 Self {
264 peer_id: peer_id.into(),
265 status,
266 available_storage: 0,
267 available_bandwidth: 0,
268 active_connections: 0,
269 timestamp: chrono::Utc::now(),
270 }
271 }
272}
273
274/// Request to get earnings information.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct GetEarningsRequest {
277 /// Peer ID to get earnings for.
278 pub peer_id: PeerIdString,
279 /// Start of time range (optional).
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub start_time: Option<chrono::DateTime<chrono::Utc>>,
282 /// End of time range (optional).
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
285}
286
287/// Response with earnings information.
288///
289/// # Examples
290///
291/// ```
292/// use chie_shared::{GetEarningsResponse, ContentEarnings};
293///
294/// // Earnings summary with breakdown
295/// let earnings = GetEarningsResponse {
296/// total_points: 50_000,
297/// total_bandwidth: 100 * 1024 * 1024 * 1024, // 100 GB
298/// proof_count: 500,
299/// avg_per_proof: 100,
300/// by_content: Some(vec![
301/// ContentEarnings {
302/// content_cid: "QmPopular".to_string(),
303/// points_earned: 30_000,
304/// bandwidth_served: 60 * 1024 * 1024 * 1024,
305/// chunks_served: 300,
306/// },
307/// ContentEarnings {
308/// content_cid: "QmRare".to_string(),
309/// points_earned: 20_000,
310/// bandwidth_served: 40 * 1024 * 1024 * 1024,
311/// chunks_served: 200,
312/// },
313/// ]),
314/// };
315///
316/// assert_eq!(earnings.total_points, 50_000);
317/// assert_eq!(earnings.proof_count, 500);
318/// assert_eq!(earnings.avg_per_proof, 100);
319/// assert!(earnings.by_content.is_some());
320/// assert_eq!(earnings.by_content.as_ref().unwrap().len(), 2);
321/// ```
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct GetEarningsResponse {
324 /// Total points earned.
325 pub total_points: Points,
326 /// Total bandwidth served (bytes).
327 pub total_bandwidth: Bytes,
328 /// Number of successful proofs.
329 pub proof_count: u64,
330 /// Average earnings per proof.
331 pub avg_per_proof: Points,
332 /// Earnings breakdown by content (optional).
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub by_content: Option<Vec<ContentEarnings>>,
335}
336
337/// Earnings for a specific content item.
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct ContentEarnings {
340 /// Content CID.
341 pub content_cid: ContentCid,
342 /// Points earned from this content.
343 pub points_earned: Points,
344 /// Bandwidth served for this content (bytes).
345 pub bandwidth_served: Bytes,
346 /// Number of chunks served.
347 pub chunks_served: u64,
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_submit_proof_request_serialization() {
356 let proof = crate::test_helpers::create_test_proof();
357 let request = SubmitProofRequest {
358 proof,
359 metadata: Some(ProofMetadata {
360 client_version: "1.0.0".to_string(),
361 region: Some("us-west".to_string()),
362 connection_type: Some("direct".to_string()),
363 }),
364 };
365
366 let json = serde_json::to_string(&request).unwrap();
367 let deserialized: SubmitProofRequest = serde_json::from_str(&json).unwrap();
368 assert_eq!(request.proof.content_cid, deserialized.proof.content_cid);
369 }
370
371 #[test]
372 fn test_announce_content_request() {
373 let request = AnnounceContentRequest {
374 content_cid: "QmTest123".to_string(),
375 peer_id: "12D3KooTest".to_string(),
376 chunk_count: 100,
377 size_bytes: 26_214_400,
378 ttl_seconds: 7200,
379 };
380
381 let json = serde_json::to_string(&request).unwrap();
382 let deserialized: AnnounceContentRequest = serde_json::from_str(&json).unwrap();
383 assert_eq!(request.content_cid, deserialized.content_cid);
384 assert_eq!(request.chunk_count, deserialized.chunk_count);
385 }
386
387 #[test]
388 fn test_node_heartbeat() {
389 let heartbeat = NodeHeartbeat::new("12D3KooTest", crate::NodeStatus::Online);
390 assert_eq!(heartbeat.peer_id, "12D3KooTest");
391 assert_eq!(heartbeat.status, crate::NodeStatus::Online);
392
393 let json = serde_json::to_string(&heartbeat).unwrap();
394 let deserialized: NodeHeartbeat = serde_json::from_str(&json).unwrap();
395 assert_eq!(heartbeat.peer_id, deserialized.peer_id);
396 }
397
398 #[test]
399 fn test_query_content_response() {
400 let response = QueryContentResponse {
401 content_cid: "QmTest".to_string(),
402 providers: vec![ContentProvider {
403 peer_id: "12D3Koo1".to_string(),
404 addresses: vec!["/ip4/127.0.0.1/tcp/4001".to_string()],
405 available_chunks: Some(vec![0, 1, 2]),
406 reputation: Some(95.5),
407 last_seen: Some(chrono::Utc::now()),
408 }],
409 total_providers: 5,
410 };
411
412 let json = serde_json::to_string(&response).unwrap();
413 let deserialized: QueryContentResponse = serde_json::from_str(&json).unwrap();
414 assert_eq!(response.providers.len(), deserialized.providers.len());
415 }
416}