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}