1#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use super::{BandwidthProof, ContentCid, PeerIdString, Points};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(JsonSchema))]
15pub struct BatchProofSubmission {
16 pub proofs: Vec<BandwidthProof>,
18 pub batch_id: uuid::Uuid,
20 pub peer_id: PeerIdString,
22 pub timestamp_ms: i64,
24}
25
26impl BatchProofSubmission {
27 #[must_use]
78 pub fn new(proofs: Vec<BandwidthProof>, peer_id: impl Into<String>) -> Self {
79 Self {
80 proofs,
81 batch_id: uuid::Uuid::new_v4(),
82 peer_id: peer_id.into(),
83 timestamp_ms: crate::now_ms(),
84 }
85 }
86
87 #[must_use]
89 pub fn proof_count(&self) -> usize {
90 self.proofs.len()
91 }
92
93 #[must_use]
95 pub fn is_empty(&self) -> bool {
96 self.proofs.is_empty()
97 }
98
99 #[must_use]
101 pub fn total_bytes_transferred(&self) -> u64 {
102 self.proofs.iter().map(|p| p.bytes_transferred).sum()
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108#[cfg_attr(feature = "schema", derive(JsonSchema))]
109pub struct BatchProofResponse {
110 pub batch_id: uuid::Uuid,
112 pub accepted_count: usize,
114 pub rejected_count: usize,
116 pub total_reward_points: Points,
118 pub results: Vec<ProofResult>,
120}
121
122impl BatchProofResponse {
123 #[must_use]
149 pub fn new(batch_id: uuid::Uuid) -> Self {
150 Self {
151 batch_id,
152 accepted_count: 0,
153 rejected_count: 0,
154 total_reward_points: 0,
155 results: Vec::new(),
156 }
157 }
158
159 #[must_use]
161 pub fn total_count(&self) -> usize {
162 self.accepted_count + self.rejected_count
163 }
164
165 #[must_use]
167 #[allow(clippy::cast_precision_loss)]
168 pub fn acceptance_rate(&self) -> f64 {
169 let total = self.total_count();
170 if total == 0 {
171 0.0
172 } else {
173 self.accepted_count as f64 / total as f64
174 }
175 }
176
177 #[must_use]
179 pub fn all_accepted(&self) -> bool {
180 self.rejected_count == 0 && self.accepted_count > 0
181 }
182
183 #[must_use]
185 pub fn all_rejected(&self) -> bool {
186 self.accepted_count == 0 && self.rejected_count > 0
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192#[cfg_attr(feature = "schema", derive(JsonSchema))]
193pub struct ProofResult {
194 pub index: usize,
196 pub accepted: bool,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub proof_id: Option<uuid::Uuid>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub reward_points: Option<Points>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub rejection_reason: Option<String>,
207}
208
209impl ProofResult {
210 #[must_use]
228 pub fn accepted(index: usize, proof_id: uuid::Uuid, reward_points: Points) -> Self {
229 Self {
230 index,
231 accepted: true,
232 proof_id: Some(proof_id),
233 reward_points: Some(reward_points),
234 rejection_reason: None,
235 }
236 }
237
238 #[must_use]
254 pub fn rejected(index: usize, reason: impl Into<String>) -> Self {
255 Self {
256 index,
257 accepted: false,
258 proof_id: None,
259 reward_points: None,
260 rejection_reason: Some(reason.into()),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267#[cfg_attr(feature = "schema", derive(JsonSchema))]
268pub struct BatchContentAnnouncement {
269 pub content_cids: Vec<ContentCid>,
271 pub peer_id: PeerIdString,
273 pub batch_id: uuid::Uuid,
275 pub timestamp_ms: i64,
277}
278
279impl BatchContentAnnouncement {
280 #[must_use]
282 pub fn new(content_cids: Vec<ContentCid>, peer_id: impl Into<String>) -> Self {
283 Self {
284 content_cids,
285 peer_id: peer_id.into(),
286 batch_id: uuid::Uuid::new_v4(),
287 timestamp_ms: crate::now_ms(),
288 }
289 }
290
291 #[must_use]
293 pub fn content_count(&self) -> usize {
294 self.content_cids.len()
295 }
296
297 #[must_use]
299 pub fn is_empty(&self) -> bool {
300 self.content_cids.is_empty()
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306#[cfg_attr(feature = "schema", derive(JsonSchema))]
307pub struct BatchStatsUpdate {
308 pub updates: Vec<StatUpdate>,
310 pub batch_id: uuid::Uuid,
312 pub timestamp_ms: i64,
314}
315
316impl BatchStatsUpdate {
317 #[must_use]
319 pub fn new(updates: Vec<StatUpdate>) -> Self {
320 Self {
321 updates,
322 batch_id: uuid::Uuid::new_v4(),
323 timestamp_ms: crate::now_ms(),
324 }
325 }
326
327 #[must_use]
329 pub fn update_count(&self) -> usize {
330 self.updates.len()
331 }
332
333 #[must_use]
335 pub fn is_empty(&self) -> bool {
336 self.updates.is_empty()
337 }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342#[cfg_attr(feature = "schema", derive(JsonSchema))]
343pub struct StatUpdate {
344 pub metric: String,
346 pub value: f64,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub entity: Option<String>,
351}
352
353impl StatUpdate {
354 #[must_use]
356 pub fn new(metric: impl Into<String>, value: f64) -> Self {
357 Self {
358 metric: metric.into(),
359 value,
360 entity: None,
361 }
362 }
363
364 #[must_use]
366 pub fn with_entity(metric: impl Into<String>, value: f64, entity: impl Into<String>) -> Self {
367 Self {
368 metric: metric.into(),
369 value,
370 entity: Some(entity.into()),
371 }
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn test_batch_proof_submission() {
381 let proof1 = crate::test_helpers::create_test_proof();
382 let proof2 = crate::test_helpers::create_test_proof();
383
384 let batch = BatchProofSubmission::new(vec![proof1, proof2], "12D3KooTest");
385
386 assert_eq!(batch.proof_count(), 2);
387 assert!(!batch.is_empty());
388 assert!(batch.total_bytes_transferred() > 0);
389 }
390
391 #[test]
392 fn test_batch_proof_response() {
393 let batch_id = uuid::Uuid::new_v4();
394 let mut response = BatchProofResponse::new(batch_id);
395
396 response.accepted_count = 8;
397 response.rejected_count = 2;
398 response.total_reward_points = 1000;
399
400 assert_eq!(response.total_count(), 10);
401 assert_eq!(response.acceptance_rate(), 0.8);
402 assert!(!response.all_accepted());
403 assert!(!response.all_rejected());
404 }
405
406 #[test]
407 fn test_batch_proof_response_all_accepted() {
408 let batch_id = uuid::Uuid::new_v4();
409 let mut response = BatchProofResponse::new(batch_id);
410
411 response.accepted_count = 10;
412 response.rejected_count = 0;
413
414 assert!(response.all_accepted());
415 assert_eq!(response.acceptance_rate(), 1.0);
416 }
417
418 #[test]
419 fn test_batch_proof_response_all_rejected() {
420 let batch_id = uuid::Uuid::new_v4();
421 let mut response = BatchProofResponse::new(batch_id);
422
423 response.accepted_count = 0;
424 response.rejected_count = 10;
425
426 assert!(response.all_rejected());
427 assert_eq!(response.acceptance_rate(), 0.0);
428 }
429
430 #[test]
431 fn test_proof_result_accepted() {
432 let proof_id = uuid::Uuid::new_v4();
433 let result = ProofResult::accepted(0, proof_id, 100);
434
435 assert!(result.accepted);
436 assert_eq!(result.proof_id, Some(proof_id));
437 assert_eq!(result.reward_points, Some(100));
438 assert!(result.rejection_reason.is_none());
439 }
440
441 #[test]
442 fn test_proof_result_rejected() {
443 let result = ProofResult::rejected(1, "Invalid signature");
444
445 assert!(!result.accepted);
446 assert!(result.proof_id.is_none());
447 assert!(result.reward_points.is_none());
448 assert_eq!(
449 result.rejection_reason,
450 Some("Invalid signature".to_string())
451 );
452 }
453
454 #[test]
455 fn test_batch_content_announcement() {
456 let cids = vec![
457 "QmTest1".to_string(),
458 "QmTest2".to_string(),
459 "QmTest3".to_string(),
460 ];
461 let batch = BatchContentAnnouncement::new(cids, "12D3KooTest");
462
463 assert_eq!(batch.content_count(), 3);
464 assert!(!batch.is_empty());
465 }
466
467 #[test]
468 fn test_batch_stats_update() {
469 let updates = vec![
470 StatUpdate::new("bandwidth_total", 1_000_000.0),
471 StatUpdate::with_entity("chunks_served", 50.0, "12D3KooTest"),
472 ];
473
474 let batch = BatchStatsUpdate::new(updates);
475
476 assert_eq!(batch.update_count(), 2);
477 assert!(!batch.is_empty());
478 }
479
480 #[test]
481 fn test_stat_update() {
482 let update1 = StatUpdate::new("test_metric", 42.0);
483 assert_eq!(update1.metric, "test_metric");
484 assert_eq!(update1.value, 42.0);
485 assert!(update1.entity.is_none());
486
487 let update2 = StatUpdate::with_entity("peer_metric", 100.0, "12D3Koo");
488 assert_eq!(update2.metric, "peer_metric");
489 assert_eq!(update2.value, 100.0);
490 assert_eq!(update2.entity, Some("12D3Koo".to_string()));
491 }
492
493 #[test]
494 fn test_batch_proof_submission_serialization() {
495 let proof = crate::test_helpers::create_test_proof();
496 let batch = BatchProofSubmission::new(vec![proof], "12D3KooTest");
497
498 let json = serde_json::to_string(&batch).unwrap();
499 let deserialized: BatchProofSubmission = serde_json::from_str(&json).unwrap();
500
501 assert_eq!(batch.batch_id, deserialized.batch_id);
502 assert_eq!(batch.proof_count(), deserialized.proof_count());
503 }
504}