1use std::sync::Arc;
12
13use axum::{
14 body::Body,
15 extract::{Path, State},
16 http::{Request, StatusCode},
17 middleware::{self, Next},
18 response::IntoResponse,
19 routing::{get, post},
20 Json, Router,
21};
22use chrono::{DateTime, Utc};
23use pot_o_core::TokenType;
24use pot_o_extensions::pool_strategy::ProofRecord;
25use pot_o_extensions::{tx::TransferTransaction, Mempool};
26use serde::{Deserialize, Serialize};
27use serde_json::json;
28use sha2::{Digest, Sha256};
29use tokio::sync::RwLock;
30
31use crate::peer_store::PeerStore;
32
33#[derive(Clone)]
35pub struct InternalApiState {
36 #[allow(dead_code)]
38 pub node_id: String,
39 pub peers: Arc<RwLock<Vec<PeerInfo>>>,
41 pub current_challenge: Arc<RwLock<Option<serde_json::Value>>>,
43 pub mempool: Option<Arc<Mempool>>,
45 pub ledger: Arc<RwLock<pot_o_extensions::Ledger>>,
47 pub tribechain_enabled: bool,
49 pub internal_mint_secret: Option<String>,
51 pub peer_store: Option<Arc<PeerStore>>,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
57pub struct PeerInfo {
58 pub node_id: String,
60 pub url: String,
62 pub last_seen: DateTime<Utc>,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68pub struct RegisterPeerRequest {
69 pub node_id: String,
70 pub url: String,
71}
72
73#[derive(Debug, Serialize, Deserialize)]
75pub struct BroadcastChallengeRequest {
76 #[serde(flatten)]
77 pub challenge: serde_json::Value,
78}
79
80#[derive(Debug, Serialize)]
82pub struct ApiResponse {
83 pub message: String,
84}
85
86pub fn internal_router(state: InternalApiState) -> Router {
88 Router::new()
89 .route("/internal/peer/register", post(handle_peer_register))
90 .route("/internal/peers", get(handle_list_peers))
91 .route(
92 "/internal/challenge/broadcast",
93 post(handle_challenge_broadcast),
94 )
95 .route("/internal/tx/broadcast", post(handle_tx_broadcast))
96 .route("/internal/mempool/hashes", get(handle_mempool_hashes))
97 .route("/internal/tx/:hash", get(handle_get_tx_by_hash))
98 .route("/internal/mint", post(handle_internal_mint))
99 .route("/api/pool/submit-batch", post(handle_submit_batch))
100 .layer(middleware::from_fn(verify_peer_signature_middleware))
101 .with_state(state)
102}
103
104async fn verify_peer_signature_middleware(req: Request<Body>, next: Next) -> impl IntoResponse {
110 let pubkey_hex = req
111 .headers()
112 .get("x-node-pubkey")
113 .and_then(|v| v.to_str().ok())
114 .map(|s| s.to_string());
115 let signature_hex = req
116 .headers()
117 .get("x-node-signature")
118 .and_then(|v| v.to_str().ok())
119 .map(|s| s.to_string());
120 let timestamp = req
121 .headers()
122 .get("x-node-timestamp")
123 .and_then(|v| v.to_str().ok())
124 .map(|s| s.to_string());
125
126 let has_auth_headers = pubkey_hex.is_some() && signature_hex.is_some() && timestamp.is_some();
127
128 if !has_auth_headers {
129 return next.run(req).await.into_response();
130 }
131
132 let pubkey_hex = pubkey_hex.unwrap();
133 let signature_hex = signature_hex.unwrap();
134 let timestamp = timestamp.unwrap();
135
136 let pubkey_bytes = match hex::decode(&pubkey_hex) {
137 Ok(b) if b.len() == 32 => {
138 let mut arr = [0u8; 32];
139 arr.copy_from_slice(&b);
140 arr
141 }
142 _ => {
143 return (
144 StatusCode::UNAUTHORIZED,
145 Json(json!({"error": "invalid pubkey format"})),
146 )
147 .into_response();
148 }
149 };
150
151 let signature_bytes = match hex::decode(&signature_hex) {
152 Ok(b) if b.len() == 64 => b,
153 _ => {
154 return (
155 StatusCode::UNAUTHORIZED,
156 Json(json!({"error": "invalid signature format"})),
157 )
158 .into_response();
159 }
160 };
161
162 let method = req.method().as_str().to_string();
163 let path = req.uri().path().to_string();
164
165 let body_bytes = match axum::body::to_bytes(req.into_body(), 1024 * 1024).await {
166 Ok(b) => b,
167 Err(_) => {
168 return (
169 StatusCode::BAD_REQUEST,
170 Json(json!({"error": "failed to read body"})),
171 )
172 .into_response();
173 }
174 };
175
176 let body_hash = hex::encode(Sha256::digest(&body_bytes));
177 let message = format!("{}:{}:{}:{}", timestamp, method, path, body_hash);
178
179 if !pot_o_extensions::peer_auth::verify_peer_signature(
180 &pubkey_bytes,
181 message.as_bytes(),
182 &signature_bytes,
183 ) {
184 return (
185 StatusCode::UNAUTHORIZED,
186 Json(json!({"error": "invalid signature"})),
187 )
188 .into_response();
189 }
190
191 let new_req = Request::builder()
192 .method(method.as_str())
193 .uri(path.as_str())
194 .body(Body::from(body_bytes))
195 .unwrap();
196 next.run(new_req).await.into_response()
197}
198
199async fn handle_peer_register(
202 State(state): State<InternalApiState>,
203 Json(req): Json<RegisterPeerRequest>,
204) -> impl IntoResponse {
205 if req.node_id.is_empty() || req.url.is_empty() {
206 return (
207 StatusCode::BAD_REQUEST,
208 Json(json!({"error": "node_id and url are required"})),
209 )
210 .into_response();
211 }
212
213 let peer_info = PeerInfo {
214 node_id: req.node_id.clone(),
215 url: req.url,
216 last_seen: Utc::now(),
217 };
218
219 let mut peers = state.peers.write().await;
220
221 if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
223 existing.last_seen = Utc::now();
224 existing.url = peer_info.url;
225 } else {
226 peers.push(peer_info);
227 }
228
229 drop(peers);
230
231 if let Some(ref store) = state.peer_store {
232 let store = store.clone();
233 tokio::spawn(async move {
234 if let Err(e) = store.save().await {
235 tracing::warn!(error = %e, "Failed to persist peers after registration");
236 }
237 });
238 }
239
240 (
241 StatusCode::OK,
242 Json(ApiResponse {
243 message: "Peer registered".to_string(),
244 }),
245 )
246 .into_response()
247}
248
249async fn handle_list_peers(State(state): State<InternalApiState>) -> impl IntoResponse {
252 let peers = state.peers.read().await.clone();
253 (StatusCode::OK, Json(peers)).into_response()
254}
255
256async fn handle_challenge_broadcast(
259 State(state): State<InternalApiState>,
260 Json(req): Json<BroadcastChallengeRequest>,
261) -> impl IntoResponse {
262 let mut challenge = state.current_challenge.write().await;
263 *challenge = Some(req.challenge);
264
265 (
266 StatusCode::OK,
267 Json(ApiResponse {
268 message: "Challenge broadcast received".to_string(),
269 }),
270 )
271 .into_response()
272}
273
274async fn handle_tx_broadcast(
277 State(state): State<InternalApiState>,
278 Json(tx_val): Json<serde_json::Value>,
279) -> impl IntoResponse {
280 if !state.tribechain_enabled {
281 return (
282 StatusCode::BAD_REQUEST,
283 Json(json!({"error": "tribechain not enabled"})),
284 )
285 .into_response();
286 }
287
288 let Some(mempool) = &state.mempool else {
289 return (
290 StatusCode::SERVICE_UNAVAILABLE,
291 Json(json!({"error": "mempool not available"})),
292 )
293 .into_response();
294 };
295
296 let tx: TransferTransaction = match serde_json::from_value(tx_val.clone()) {
297 Ok(tx) => tx,
298 Err(e) => {
299 tracing::warn!(error = %e, "Invalid transaction from peer");
300 return (
301 StatusCode::BAD_REQUEST,
302 Json(json!({"error": "invalid transaction"})),
303 )
304 .into_response();
305 }
306 };
307
308 match mempool.submit(tx, &state.ledger).await {
309 Ok(tx_hash) => {
310 tracing::debug!(tx_hash = %hex::encode(tx_hash), "Received tx from peer, added to mempool");
311 let peers = state.peers.read().await.clone();
312 let tx_val_clone = tx_val.clone();
313 tokio::spawn(async move {
314 for peer in &peers {
315 let url = format!("{}/internal/tx/broadcast", peer.url.trim_end_matches('/'));
316 let _ = reqwest::Client::new()
317 .post(&url)
318 .timeout(std::time::Duration::from_secs(5))
319 .json(&tx_val_clone)
320 .send()
321 .await;
322 }
323 });
324 (
325 StatusCode::OK,
326 Json(json!({
327 "accepted": true,
328 "tx_hash": hex::encode(tx_hash),
329 })),
330 )
331 .into_response()
332 }
333 Err(e) => {
334 tracing::warn!(error = %e, "Rejected tx from peer");
335 (
336 StatusCode::BAD_REQUEST,
337 Json(json!({"error": "transaction rejected"})),
338 )
339 .into_response()
340 }
341 }
342}
343
344async fn handle_mempool_hashes(State(state): State<InternalApiState>) -> impl IntoResponse {
347 if !state.tribechain_enabled {
348 return (
349 StatusCode::BAD_REQUEST,
350 Json(json!({"error": "tribechain not enabled"})),
351 )
352 .into_response();
353 }
354
355 let Some(mempool) = &state.mempool else {
356 return (
357 StatusCode::SERVICE_UNAVAILABLE,
358 Json(json!({"error": "mempool not available"})),
359 )
360 .into_response();
361 };
362
363 let hashes: Vec<String> = mempool.hashes().iter().map(hex::encode).collect();
364 (StatusCode::OK, Json(json!({"hashes": hashes}))).into_response()
365}
366
367async fn handle_get_tx_by_hash(
370 State(state): State<InternalApiState>,
371 Path(hash_hex): Path<String>,
372) -> impl IntoResponse {
373 if !state.tribechain_enabled {
374 return (
375 StatusCode::BAD_REQUEST,
376 Json(json!({"error": "tribechain not enabled"})),
377 )
378 .into_response();
379 }
380
381 let Some(mempool) = &state.mempool else {
382 return (
383 StatusCode::SERVICE_UNAVAILABLE,
384 Json(json!({"error": "mempool not available"})),
385 )
386 .into_response();
387 };
388
389 let hash_bytes = match hex::decode(&hash_hex) {
390 Ok(b) if b.len() == 32 => {
391 let mut arr = [0u8; 32];
392 arr.copy_from_slice(&b);
393 arr
394 }
395 _ => {
396 return (
397 StatusCode::BAD_REQUEST,
398 Json(json!({"error": "invalid hash format"})),
399 )
400 .into_response();
401 }
402 };
403
404 match mempool.get_tx(&hash_bytes) {
405 Some(tx) => match serde_json::to_value(&tx) {
406 Ok(val) => (StatusCode::OK, Json(val)).into_response(),
407 Err(_) => (
408 StatusCode::INTERNAL_SERVER_ERROR,
409 Json(json!({"error": "failed to serialize transaction"})),
410 )
411 .into_response(),
412 },
413 None => (
414 StatusCode::NOT_FOUND,
415 Json(json!({"error": "transaction not found"})),
416 )
417 .into_response(),
418 }
419}
420
421#[derive(Debug, Serialize, Deserialize)]
423pub struct SubmitBatchRequest {
424 pub node_id: String,
425 pub pool_strategy: String,
426 pub batch: Vec<ProofRecord>,
427}
428
429async fn handle_submit_batch(
433 State(_state): State<InternalApiState>,
434 Json(req): Json<SubmitBatchRequest>,
435) -> impl IntoResponse {
436 let batch_size = req.batch.len();
437
438 if batch_size == 0 {
439 return (
440 StatusCode::BAD_REQUEST,
441 Json(json!({"error": "batch cannot be empty"})),
442 )
443 .into_response();
444 }
445
446 let mut hasher = Sha256::new();
448 hasher.update(b"pot-o-batch:");
449 hasher.update(req.node_id.as_bytes());
450 hasher.update(b":");
451 for record in &req.batch {
452 hasher.update(record.miner_pubkey.as_bytes());
453 hasher.update(b":");
454 hasher.update(record.challenge_id.as_bytes());
455 hasher.update(b":");
456 hasher.update(record.reward.to_le_bytes());
457 hasher.update(b":");
458 hasher.update(record.timestamp.timestamp().to_le_bytes());
459 hasher.update(b"|");
460 }
461 let hash = hex::encode(hasher.finalize());
462 let submission_id = format!("pool-batch-{}", &hash[..40]);
463
464 tracing::info!(
465 from_node = %req.node_id,
466 batch_size,
467 submission_id = %submission_id,
468 "Received batch submission from peer validator"
469 );
470
471 (
472 StatusCode::OK,
473 Json(json!({
474 "submission_id": submission_id,
475 "accepted": true,
476 "batch_size": batch_size,
477 })),
478 )
479 .into_response()
480}
481
482#[derive(Debug, Deserialize)]
483struct InternalMintRequest {
484 to: String,
485 token_type: String,
486 amount: u64,
487}
488
489async fn handle_internal_mint(
490 State(state): State<InternalApiState>,
491 headers: axum::http::HeaderMap,
492 Json(body): Json<InternalMintRequest>,
493) -> impl IntoResponse {
494 if let Some(ref expected) = state.internal_mint_secret {
495 let got = headers
496 .get("x-internal-secret")
497 .and_then(|v| v.to_str().ok());
498 if got != Some(expected.as_str()) {
499 return (
500 StatusCode::UNAUTHORIZED,
501 Json(json!({"error": "invalid internal secret"})),
502 )
503 .into_response();
504 }
505 }
506
507 if !state.tribechain_enabled {
508 return (
509 StatusCode::SERVICE_UNAVAILABLE,
510 Json(json!({"error": "tribechain not enabled"})),
511 )
512 .into_response();
513 }
514
515 let token = match token_type_from_str(&body.token_type) {
516 Some(t) => t,
517 None => {
518 return (
519 StatusCode::BAD_REQUEST,
520 Json(json!({"error": format!("Unknown token: {}", body.token_type)})),
521 )
522 .into_response();
523 }
524 };
525
526 let mut ledger = state.ledger.write().await;
527 if let Err(e) = ledger.try_issue(&body.to, &token, body.amount) {
528 return (StatusCode::BAD_REQUEST, Json(json!({"error": e}))).into_response();
529 }
530
531 let receipt = json!({
532 "status": "ok",
533 "to": body.to,
534 "token": body.token_type,
535 "amount": body.amount,
536 "tx_hash": format!("mint-{}-{}", body.to, body.token_type),
537 });
538 (StatusCode::OK, Json(receipt)).into_response()
539}
540
541fn token_type_from_str(s: &str) -> Option<TokenType> {
542 match s.to_lowercase().as_str() {
543 "tribechain" | "native" | "tribe" => Some(TokenType::TribeChain),
544 "pttc" => Some(TokenType::PTtC),
545 "nmtc" => Some(TokenType::NMTC),
546 "stomp" => Some(TokenType::STOMP),
547 "aum" => Some(TokenType::AUM),
548 "ai3" => Some(TokenType::AI3),
549 "ravecoin" => Some(TokenType::RAVECOIN),
550 _ => None,
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[tokio::test]
559 async fn test_internal_api_state_creation() {
560 let state = InternalApiState {
561 node_id: "validator-1".to_string(),
562 peers: Arc::new(RwLock::new(vec![])),
563 current_challenge: Arc::new(RwLock::new(None)),
564 mempool: None,
565 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
566 "test".to_string(),
567 ))),
568 tribechain_enabled: false,
569 internal_mint_secret: None,
570 peer_store: None,
571 };
572
573 assert_eq!(state.node_id, "validator-1");
574 let peers = state.peers.read().await;
575 assert_eq!(peers.len(), 0);
576 drop(peers);
577
578 let challenge = state.current_challenge.read().await;
579 assert!(challenge.is_none());
580 }
581
582 #[tokio::test]
583 async fn test_peer_info_creation() {
584 let now = Utc::now();
585 let peer = PeerInfo {
586 node_id: "validator-2".to_string(),
587 url: "http://validator-2:8900".to_string(),
588 last_seen: now,
589 };
590
591 assert_eq!(peer.node_id, "validator-2");
592 assert_eq!(peer.url, "http://validator-2:8900");
593 assert_eq!(peer.last_seen, now);
594 }
595
596 #[tokio::test]
597 async fn test_peer_registration_creates_new_entry() {
598 let state = InternalApiState {
599 node_id: "validator-1".to_string(),
600 peers: Arc::new(RwLock::new(vec![])),
601 current_challenge: Arc::new(RwLock::new(None)),
602 mempool: None,
603 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
604 "test".to_string(),
605 ))),
606 tribechain_enabled: false,
607 internal_mint_secret: None,
608 peer_store: None,
609 };
610
611 let req = RegisterPeerRequest {
612 node_id: "validator-2".to_string(),
613 url: "http://validator-2:8900".to_string(),
614 };
615
616 let mut peers = state.peers.write().await;
617 if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
618 existing.last_seen = Utc::now();
619 existing.url = req.url;
620 } else {
621 peers.push(PeerInfo {
622 node_id: req.node_id,
623 url: req.url,
624 last_seen: Utc::now(),
625 });
626 }
627 drop(peers);
628
629 let peers = state.peers.read().await;
630 assert_eq!(peers.len(), 1);
631 assert_eq!(peers[0].node_id, "validator-2");
632 }
633
634 #[tokio::test]
635 async fn test_peer_registration_updates_existing_peer() {
636 let initial_peer = PeerInfo {
637 node_id: "validator-2".to_string(),
638 url: "http://validator-2:8900".to_string(),
639 last_seen: Utc::now(),
640 };
641
642 let state = InternalApiState {
643 node_id: "validator-1".to_string(),
644 peers: Arc::new(RwLock::new(vec![initial_peer.clone()])),
645 current_challenge: Arc::new(RwLock::new(None)),
646 mempool: None,
647 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
648 "test".to_string(),
649 ))),
650 tribechain_enabled: false,
651 internal_mint_secret: None,
652 peer_store: None,
653 };
654
655 let req = RegisterPeerRequest {
656 node_id: "validator-2".to_string(),
657 url: "http://validator-2:9000".to_string(), };
659
660 let mut peers = state.peers.write().await;
661 if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
662 existing.last_seen = Utc::now();
663 existing.url = req.url;
664 } else {
665 peers.push(PeerInfo {
666 node_id: req.node_id,
667 url: req.url,
668 last_seen: Utc::now(),
669 });
670 }
671 drop(peers);
672
673 let peers = state.peers.read().await;
674 assert_eq!(peers.len(), 1); assert_eq!(peers[0].url, "http://validator-2:9000"); }
677
678 #[tokio::test]
679 async fn test_list_peers_empty_by_default() {
680 let state = InternalApiState {
681 node_id: "validator-1".to_string(),
682 peers: Arc::new(RwLock::new(vec![])),
683 current_challenge: Arc::new(RwLock::new(None)),
684 mempool: None,
685 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
686 "test".to_string(),
687 ))),
688 tribechain_enabled: false,
689 internal_mint_secret: None,
690 peer_store: None,
691 };
692
693 let peers = state.peers.read().await;
694 assert_eq!(peers.len(), 0);
695 }
696
697 #[tokio::test]
698 async fn test_list_peers_returns_all_registered_peers() {
699 let peer1 = PeerInfo {
700 node_id: "validator-2".to_string(),
701 url: "http://validator-2:8900".to_string(),
702 last_seen: Utc::now(),
703 };
704
705 let peer2 = PeerInfo {
706 node_id: "validator-3".to_string(),
707 url: "http://validator-3:8900".to_string(),
708 last_seen: Utc::now(),
709 };
710
711 let state = InternalApiState {
712 node_id: "validator-1".to_string(),
713 peers: Arc::new(RwLock::new(vec![peer1.clone(), peer2.clone()])),
714 current_challenge: Arc::new(RwLock::new(None)),
715 mempool: None,
716 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
717 "test".to_string(),
718 ))),
719 tribechain_enabled: false,
720 internal_mint_secret: None,
721 peer_store: None,
722 };
723
724 let peers = state.peers.read().await;
725 assert_eq!(peers.len(), 2);
726 assert_eq!(peers[0], peer1);
727 assert_eq!(peers[1], peer2);
728 }
729
730 #[tokio::test]
731 async fn test_challenge_broadcast_stores_challenge() {
732 let state = InternalApiState {
733 node_id: "validator-1".to_string(),
734 peers: Arc::new(RwLock::new(vec![])),
735 current_challenge: Arc::new(RwLock::new(None)),
736 mempool: None,
737 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
738 "test".to_string(),
739 ))),
740 tribechain_enabled: false,
741 internal_mint_secret: None,
742 peer_store: None,
743 };
744
745 let challenge_data = json!({"id": "challenge-1", "difficulty": 2});
746 let mut challenge = state.current_challenge.write().await;
747 *challenge = Some(challenge_data.clone());
748 drop(challenge);
749
750 let challenge = state.current_challenge.read().await;
751 assert!(challenge.is_some());
752 assert_eq!(*challenge, Some(challenge_data));
753 }
754
755 #[tokio::test]
756 async fn test_challenge_broadcast_replaces_old_challenge() {
757 let old_challenge = json!({"id": "challenge-1", "difficulty": 1});
758 let new_challenge = json!({"id": "challenge-2", "difficulty": 2});
759
760 let state = InternalApiState {
761 node_id: "validator-1".to_string(),
762 peers: Arc::new(RwLock::new(vec![])),
763 current_challenge: Arc::new(RwLock::new(Some(old_challenge))),
764 mempool: None,
765 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
766 "test".to_string(),
767 ))),
768 tribechain_enabled: false,
769 internal_mint_secret: None,
770 peer_store: None,
771 };
772
773 let mut challenge = state.current_challenge.write().await;
774 *challenge = Some(new_challenge.clone());
775 drop(challenge);
776
777 let challenge = state.current_challenge.read().await;
778 assert_eq!(*challenge, Some(new_challenge));
779 }
780
781 #[tokio::test]
782 async fn test_peer_deduplication_by_node_id() {
783 let peer1 = PeerInfo {
784 node_id: "validator-2".to_string(),
785 url: "http://validator-2:8900".to_string(),
786 last_seen: Utc::now(),
787 };
788
789 let peer2 = PeerInfo {
790 node_id: "validator-3".to_string(),
791 url: "http://validator-3:8900".to_string(),
792 last_seen: Utc::now(),
793 };
794
795 let state = InternalApiState {
796 node_id: "validator-1".to_string(),
797 peers: Arc::new(RwLock::new(vec![peer1, peer2])),
798 current_challenge: Arc::new(RwLock::new(None)),
799 mempool: None,
800 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
801 "test".to_string(),
802 ))),
803 tribechain_enabled: false,
804 internal_mint_secret: None,
805 peer_store: None,
806 };
807
808 let req = RegisterPeerRequest {
810 node_id: "validator-2".to_string(),
811 url: "http://validator-2:9000".to_string(),
812 };
813
814 let mut peers = state.peers.write().await;
815 if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
816 existing.url = req.url;
817 existing.last_seen = Utc::now();
818 } else {
819 peers.push(PeerInfo {
820 node_id: req.node_id,
821 url: req.url,
822 last_seen: Utc::now(),
823 });
824 }
825 drop(peers);
826
827 let peers = state.peers.read().await;
828 assert_eq!(peers.len(), 2); assert_eq!(
830 peers.iter().filter(|p| p.node_id == "validator-2").count(),
831 1
832 );
833 }
834
835 #[tokio::test]
836 async fn test_last_seen_update_on_re_registration() {
837 let initial_time = Utc::now();
838 let peer = PeerInfo {
839 node_id: "validator-2".to_string(),
840 url: "http://validator-2:8900".to_string(),
841 last_seen: initial_time,
842 };
843
844 let state = InternalApiState {
845 node_id: "validator-1".to_string(),
846 peers: Arc::new(RwLock::new(vec![peer])),
847 current_challenge: Arc::new(RwLock::new(None)),
848 mempool: None,
849 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
850 "test".to_string(),
851 ))),
852 tribechain_enabled: false,
853 internal_mint_secret: None,
854 peer_store: None,
855 };
856
857 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
859
860 let req = RegisterPeerRequest {
861 node_id: "validator-2".to_string(),
862 url: "http://validator-2:8900".to_string(),
863 };
864
865 let mut peers = state.peers.write().await;
866 if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
867 existing.last_seen = Utc::now();
868 existing.url = req.url;
869 } else {
870 peers.push(PeerInfo {
871 node_id: req.node_id,
872 url: req.url,
873 last_seen: Utc::now(),
874 });
875 }
876 drop(peers);
877
878 let peers = state.peers.read().await;
879 assert!(peers[0].last_seen > initial_time);
880 }
881
882 #[tokio::test]
883 async fn test_handler_peer_registration_success() {
884 let state = InternalApiState {
885 node_id: "validator-1".to_string(),
886 peers: Arc::new(RwLock::new(vec![])),
887 current_challenge: Arc::new(RwLock::new(None)),
888 mempool: None,
889 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
890 "test".to_string(),
891 ))),
892 tribechain_enabled: false,
893 internal_mint_secret: None,
894 peer_store: None,
895 };
896
897 let peers = state.peers.read().await;
899 assert_eq!(peers.len(), 0);
900 }
901
902 #[tokio::test]
903 async fn test_handler_challenge_broadcast_success() {
904 let state = InternalApiState {
905 node_id: "validator-1".to_string(),
906 peers: Arc::new(RwLock::new(vec![])),
907 current_challenge: Arc::new(RwLock::new(None)),
908 mempool: None,
909 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
910 "test".to_string(),
911 ))),
912 tribechain_enabled: false,
913 internal_mint_secret: None,
914 peer_store: None,
915 };
916
917 let challenge = json!({"id": "c1", "difficulty": 2});
918 let mut challenge_lock = state.current_challenge.write().await;
919 *challenge_lock = Some(challenge.clone());
920 drop(challenge_lock);
921
922 let stored = state.current_challenge.read().await;
923 assert_eq!(*stored, Some(challenge));
924 }
925
926 #[tokio::test]
927 async fn test_multiple_peers_can_coexist() {
928 let peers_vec = vec![
929 PeerInfo {
930 node_id: "v2".to_string(),
931 url: "http://v2:8900".to_string(),
932 last_seen: Utc::now(),
933 },
934 PeerInfo {
935 node_id: "v3".to_string(),
936 url: "http://v3:8900".to_string(),
937 last_seen: Utc::now(),
938 },
939 PeerInfo {
940 node_id: "v4".to_string(),
941 url: "http://v4:8900".to_string(),
942 last_seen: Utc::now(),
943 },
944 ];
945
946 let state = InternalApiState {
947 node_id: "validator-1".to_string(),
948 peers: Arc::new(RwLock::new(peers_vec)),
949 current_challenge: Arc::new(RwLock::new(None)),
950 mempool: None,
951 ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
952 "test".to_string(),
953 ))),
954 tribechain_enabled: false,
955 internal_mint_secret: None,
956 peer_store: None,
957 };
958
959 let peers = state.peers.read().await;
960 assert_eq!(peers.len(), 3);
961 assert_eq!(peers[0].node_id, "v2");
962 assert_eq!(peers[1].node_id, "v3");
963 assert_eq!(peers[2].node_id, "v4");
964 }
965}