1use std::collections::HashMap;
2use std::sync::Arc;
3
4use axum::{
5 extract::{
6 ws::{Message, WebSocket, WebSocketUpgrade},
7 Path, Query, State,
8 },
9 http::StatusCode,
10 middleware,
11 response::{IntoResponse, Redirect},
12 routing::{delete, get, post},
13 Json, Router,
14};
15use pot_o_core::TokenType;
16use pot_o_extensions::marketplace::{parse_market_asset, OrderSide};
17use pot_o_extensions::{calculate_mining_reward, ProofRecord};
18use pot_o_mining::{PotOProof, ProofPayload};
19use serde::Deserialize;
20
21use crate::consensus::AppState;
22use crate::device_registry::{
23 normalize_device_type, prune_stale_devices, spawn_persist_registry, CurrentCalculation,
24 RegisteredDevice, DEVICE_TYPE_KEYS,
25};
26use crate::rate_limit::{rate_limit_middleware, RateLimiter};
27
28async fn record_reward_in_wallet(wallet_url: &str, miner: &str, amount: u64, challenge_id: &str) {
31 let client = reqwest::Client::builder()
32 .timeout(std::time::Duration::from_secs(5))
33 .build()
34 .unwrap_or_default();
35
36 let payload = serde_json::json!({
37 "address": miner,
38 "tx_hash": format!("reward-{}-{}", challenge_id, miner),
39 "token": "TRIBE",
40 "amount": amount,
41 "timestamp": chrono::Utc::now().timestamp_millis(),
42 });
43
44 match client
45 .post(format!("{}/api/record-reward", wallet_url))
46 .json(&payload)
47 .send()
48 .await
49 {
50 Ok(res) if res.status().is_success() || res.status() == reqwest::StatusCode::CONFLICT => {
51 tracing::debug!(miner, amount, "Reward forwarded to wallet service");
52 }
53 Ok(res) => {
54 tracing::warn!(
55 miner,
56 amount,
57 status = %res.status(),
58 "Wallet service returned error for reward"
59 );
60 }
61 Err(e) => {
62 tracing::warn!(miner, amount, error = %e, "Failed to contact wallet service");
63 }
64 }
65}
66
67async fn auth_middleware(
68 state: &Arc<AppState>,
69 bearer: Option<&str>,
70) -> Result<String, StatusCode> {
71 let token = bearer
72 .and_then(|b| b.strip_prefix("Bearer "))
73 .ok_or(StatusCode::UNAUTHORIZED)?;
74 state
75 .auth
76 .validate_token(token)
77 .await
78 .ok_or(StatusCode::UNAUTHORIZED)
79}
80
81pub fn build_router(state: Arc<AppState>) -> Router {
82 let challenge_limiter = RateLimiter::new(5, 1);
83 let submit_limiter = RateLimiter::new(3, 1);
84 let tx_limiter = RateLimiter::new(10, 1);
85
86 let challenge_route = Router::new()
87 .route("/challenge", post(get_challenge))
88 .layer(middleware::from_fn(rate_limit_middleware))
89 .layer(axum::Extension(challenge_limiter));
90
91 let submit_route = Router::new()
92 .route("/submit", post(submit_proof))
93 .layer(middleware::from_fn(rate_limit_middleware))
94 .layer(axum::Extension(submit_limiter));
95
96 let tx_route = Router::new()
97 .route("/api/tx", post(post_tribechain_tx))
98 .layer(middleware::from_fn(rate_limit_middleware))
99 .layer(axum::Extension(tx_limiter));
100
101 Router::new()
102 .route("/", get(|| async { Redirect::permanent("/status") }))
103 .route("/health", get(health))
104 .route("/status", get(status))
105 .route("/miners/:pubkey", get(get_miner))
106 .route("/pool", get(pool_info))
107 .route("/devices/register", post(register_device))
108 .route("/devices/progress", post(device_progress))
109 .route("/devices", get(get_devices))
110 .route("/network/peers", get(get_peers))
111 .route(
112 "/token/balance/:address/:token_type",
113 get(get_token_balance),
114 )
115 .route("/token/transfer", post(post_token_transfer))
116 .route("/token/tx/:address", get(get_token_tx_history))
117 .route("/token/tribe/supply", get(get_tribe_supply))
118 .route("/api/nonce/:address", get(get_tribechain_nonce))
119 .route("/api/blocks", get(get_tribechain_blocks))
120 .route("/api/supply", get(get_token_supply))
121 .route("/api/tx/:hash", get(get_tx_by_hash))
122 .route("/api/proof-trace", get(get_proof_trace))
123 .route("/mempool", get(get_mempool))
124 .route("/marketplace/order", post(post_marketplace_order))
125 .route("/marketplace/order/{id}", delete(delete_marketplace_order))
126 .route("/marketplace/order/{id}", get(get_marketplace_order))
127 .route(
128 "/marketplace/orderbook/{sell_asset}/{buy_asset}",
129 get(get_marketplace_orderbook),
130 )
131 .route("/marketplace/orders/{maker}", get(get_marketplace_orders))
132 .route("/marketplace/trades", get(get_marketplace_trades))
133 .route("/api/proof/tx/{block_height}/{tx_index}", get(get_tx_proof))
134 .route("/ws", get(ws_handler))
135 .route("/auth/challenge", post(post_auth_challenge))
136 .route("/auth/verify", post(post_auth_verify))
137 .merge(challenge_route)
138 .merge(submit_route)
139 .merge(tx_route)
140 .with_state(state)
141}
142
143async fn health() -> impl IntoResponse {
148 Json(serde_json::json!({
149 "status": "ok",
150 "service": "pot-o-validator",
151 "version": crate::VERSION,
152 }))
153}
154
155async fn status(State(state): State<Arc<AppState>>) -> impl IntoResponse {
156 tracing::debug!("GET /status");
157 let stats = state.stats.read().await.clone();
158 let engine_stats = state.consensus.engine_stats();
159 let network = state.extensions.network.sync_state().await.ok();
160 let peers = tokio::time::timeout(
161 std::time::Duration::from_secs(2),
162 state.extensions.network.discover_peers(),
163 )
164 .await
165 .ok()
166 .and_then(|r| r.ok())
167 .unwrap_or_default();
168 let current_challenge = state.current_challenge.read().await.as_ref().map(|c| {
169 let (expected_paths, expected_calcs) = state.consensus.expected_paths_and_calcs(c);
170 serde_json::json!({
171 "id": c.id,
172 "slot": c.slot,
173 "operation_type": c.operation_type,
174 "difficulty": c.difficulty,
175 "mml_threshold": c.mml_threshold,
176 "path_distance_max": c.path_distance_max,
177 "expires_at": c.expires_at.to_rfc3339(),
178 "expected_paths": expected_paths,
179 "expected_calcs": expected_calcs,
180 })
181 });
182 let block_height = *state.canonical_tip_height.read().await;
183 let resp = Json(serde_json::json!({
184 "node_id": state.config.node_id,
185 "difficulty": state.config.difficulty,
186 "max_tensor_dim": state.config.max_tensor_dim,
187 "peer_network_mode": state.config.peer_network_mode,
188 "pool_strategy": state.config.pool_strategy,
189 "stats": stats,
190 "engine": {
191 "tasks_processed": engine_stats.total_tasks_processed,
192 "successful": engine_stats.successful_tasks,
193 "failed": engine_stats.failed_tasks,
194 },
195 "network": network,
196 "current_challenge": current_challenge,
197 "connected_peers": peers,
198 "block_height": block_height,
199 }));
200 tracing::info!(
201 challenges_issued = stats.total_challenges_issued,
202 proofs_valid = stats.total_proofs_valid,
203 paths_in_block = stats.paths_in_block,
204 calcs_in_block = stats.calcs_in_block,
205 peers = peers.len(),
206 has_challenge = current_challenge.is_some(),
207 "GET /status response"
208 );
209 resp
210}
211
212#[derive(Deserialize)]
213struct ChallengeRequest {
214 slot: Option<u64>,
215 slot_hash: Option<String>,
216 device_type: Option<String>,
217}
218
219async fn get_challenge(
220 State(state): State<Arc<AppState>>,
221 Json(body): Json<ChallengeRequest>,
222) -> impl IntoResponse {
223 let slot = match body.slot {
224 Some(s) => s,
225 None => {
226 let stats = state.stats.read().await;
227 stats.total_challenges_issued + 1
228 }
229 };
230 let slot_hash = body
231 .slot_hash
232 .unwrap_or_else(|| format!("{:0>64}", hex::encode(slot.to_le_bytes())));
233 tracing::debug!(slot, device_type = ?body.device_type, "POST /challenge request");
234
235 match state.consensus.generate_challenge(slot, &slot_hash) {
236 Ok(challenge) => {
237 {
238 let mut s = state.stats.write().await;
239 s.total_challenges_issued += 1;
240 s.paths_in_block = 0;
241 s.calcs_in_block = 0;
242 }
243 let mut current = state.current_challenge.write().await;
244 *current = Some(challenge.clone());
245 tracing::info!(
246 challenge_id = %challenge.id,
247 slot = challenge.slot,
248 operation_type = %challenge.operation_type,
249 difficulty = challenge.difficulty,
250 "POST /challenge issued"
251 );
252
253 let challenge_clone = challenge.clone();
254 let state_clone = state.clone();
255 tokio::spawn(async move {
256 match state_clone
257 .extensions
258 .network
259 .broadcast_challenge(&challenge_clone)
260 .await
261 {
262 Ok(()) => {
263 tracing::debug!(challenge_id = %challenge_clone.id, "Challenge broadcast to peers completed");
264 }
265 Err(e) => {
266 tracing::warn!(challenge_id = %challenge_clone.id, error = %e, "Challenge broadcast to peers failed (non-fatal)");
267 }
268 }
269 });
270
271 let state_clone = state.clone();
272 let challenge_json = serde_json::to_string(&challenge).unwrap_or_default();
273 tokio::spawn(async move {
274 state_clone
275 .extensions
276 .messaging
277 .broadcast(&pot_o_extensions::ValidatorMessage::Challenge { challenge_json })
278 .await;
279 });
280
281 (
282 StatusCode::OK,
283 Json(serde_json::to_value(&challenge).unwrap_or_default()),
284 )
285 }
286 Err(e) => {
287 tracing::warn!(error = %e, "POST /challenge failed");
288 (
289 StatusCode::INTERNAL_SERVER_ERROR,
290 Json(serde_json::json!({ "error": "internal error" })),
291 )
292 }
293 }
294}
295
296#[derive(Deserialize)]
297struct SubmitRequest {
298 proof: PotOProof,
299 signature: Option<Vec<u8>>,
300 device_id: Option<String>,
301 device_type: Option<String>,
302}
303
304async fn submit_proof(
305 State(state): State<Arc<AppState>>,
306 Json(body): Json<SubmitRequest>,
307) -> impl IntoResponse {
308 tracing::debug!(
311 challenge_id = %body.proof.challenge_id,
312 miner = %body.proof.miner_pubkey,
313 device_id = ?body.device_id,
314 device_type = ?body.device_type,
315 "POST /submit received"
316 );
317 {
318 let mut s = state.stats.write().await;
319 s.total_proofs_received += 1;
320 }
321
322 let challenge = state.current_challenge.read().await;
323 if let Some(ref chal) = *challenge {
324 match state.consensus.verify_proof(&body.proof, chal) {
325 Ok(true) => {
326 {
327 let mut s = state.stats.write().await;
328 s.total_proofs_valid += 1;
329 s.paths_in_block += 1;
330 s.calcs_in_block += 1;
331 }
332 let now = chrono::Utc::now();
333 let device_type_normalized = body
334 .device_type
335 .as_deref()
336 .map(normalize_device_type)
337 .unwrap_or_else(|| "native".to_string());
338 state
340 .proof_traces
341 .record(pot_o_extensions::proof_trace::ProofTrace {
342 challenge_id: body.proof.challenge_id.clone(),
343 miner_pubkey: body.proof.miner_pubkey.clone(),
344 device_type: device_type_normalized.clone(),
345 accepted: true,
346 path_distance: body.proof.path_distance,
347 mml_score: body.proof.mml_score,
348 neural_paths_tested: 0,
349 successful_paths: 0,
350 failed_paths: 0,
351 timestamp: now,
352 });
353 let registry_key: String = match &body.device_id {
354 Some(id) => id.clone(),
355 None => format!("{}:{}", body.proof.miner_pubkey, device_type_normalized),
356 };
357 const MAX_MINER_PUBKEYS_PER_DEVICE: usize = 100;
358 {
359 let mut reg = state.device_registry.write().await;
360 prune_stale_devices(&mut reg);
361 let entry = reg.entry(registry_key).or_insert_with(|| RegisteredDevice {
362 device_type: device_type_normalized.clone(),
363 node_id: state.config.node_id.clone(),
364 last_activity: now,
365 proofs_valid: 0,
366 tasks_processed: 0,
367 miner_pubkeys: Vec::new(),
368 current_calculation: None,
369 });
370 entry.last_activity = now;
371 entry.proofs_valid += 1;
372 entry.tasks_processed += 1;
373 if body.device_id.is_some() {
374 entry.device_type = device_type_normalized;
375 let pk = body.proof.miner_pubkey.as_str();
376 if !entry.miner_pubkeys.iter().any(|p| p.as_str() == pk)
377 && entry.miner_pubkeys.len() < MAX_MINER_PUBKEYS_PER_DEVICE
378 {
379 entry.miner_pubkeys.push(body.proof.miner_pubkey.clone());
380 }
381 }
382 }
383 {
384 let reg = state.device_registry.read().await.clone();
385 spawn_persist_registry(reg, state.registry_path.clone());
386 }
387
388 let path_distance = body.proof.path_distance;
389 let reward_amount = calculate_mining_reward(chal.difficulty, path_distance);
390 let proof_record = ProofRecord {
391 miner_pubkey: body.proof.miner_pubkey.clone(),
392 challenge_id: body.proof.challenge_id.clone(),
393 reward: reward_amount,
394 timestamp: chrono::Utc::now(),
395 };
396 if let Ok(shares) = state
397 .extensions
398 .pool
399 .calculate_shares(&[proof_record], reward_amount)
400 {
401 {
402 let mut ledger = state.extensions.ledger.write().await;
403 for share in &shares {
404 if let Err(e) = ledger.try_issue(
405 &share.miner_pubkey,
406 &TokenType::TribeChain,
407 share.reward_amount,
408 ) {
409 tracing::warn!(
410 miner = %share.miner_pubkey,
411 error = %e,
412 "Mining reward capped — supply limit reached"
413 );
414 }
415 }
416 }
417 let mut stats = state.stats.write().await;
418 stats.total_tribe_minted =
419 stats.total_tribe_minted.saturating_add(reward_amount);
420 stats.total_rewards_paid = stats.total_rewards_paid.saturating_add(1);
421 tracing::debug!(
422 miner = %body.proof.miner_pubkey,
423 reward = reward_amount,
424 shares = shares.len(),
425 "TRIBE mining reward distributed"
426 );
427
428 if let Some(ref wallet_url) = state.wallet_url {
430 let wallet_url = wallet_url.clone();
431 for share in shares {
432 let url = wallet_url.clone();
433 let miner = share.miner_pubkey.clone();
434 let amount = share.reward_amount;
435 let challenge_id = body.proof.challenge_id.clone();
436 tokio::spawn(async move {
437 record_reward_in_wallet(&url, &miner, amount, &challenge_id).await;
438 });
439 }
440 }
441 }
442
443 let payload = ProofPayload {
444 proof: body.proof.clone(),
445 signature: body.signature.unwrap_or_default(),
446 };
447
448 match state.extensions.chain.submit_proof(&payload).await {
449 Ok(tx) => {
450 tracing::info!(
451 challenge_id = %body.proof.challenge_id,
452 tx_signature = %tx.0,
453 device_id = ?body.device_id,
454 "POST /submit accepted"
455 );
456 (
457 StatusCode::OK,
458 Json(serde_json::json!({
459 "accepted": true,
460 "tx_signature": tx.0,
461 })),
462 )
463 }
464 Err(e) => {
465 tracing::warn!(error = %e, "POST /submit chain submit failed");
466 (
467 StatusCode::INTERNAL_SERVER_ERROR,
468 Json(
469 serde_json::json!({ "accepted": false, "error": "internal error" }),
470 ),
471 )
472 }
473 }
474 }
475 Ok(false) => {
476 tracing::info!(challenge_id = %body.proof.challenge_id, "POST /submit rejected (validation failed)");
477 state.stats.write().await.total_proofs_rejected += 1;
478 (
479 StatusCode::BAD_REQUEST,
480 Json(
481 serde_json::json!({ "accepted": false, "error": "Proof validation failed" }),
482 ),
483 )
484 }
485 Err(e) => {
486 tracing::warn!(error = %e, "POST /submit verify error");
487 state.stats.write().await.total_proofs_rejected += 1;
488 (
489 StatusCode::INTERNAL_SERVER_ERROR,
490 Json(serde_json::json!({ "accepted": false, "error": "internal error" })),
491 )
492 }
493 }
494 } else {
495 tracing::debug!("POST /submit rejected (no active challenge)");
496 state.stats.write().await.total_proofs_rejected += 1;
497 (
498 StatusCode::BAD_REQUEST,
499 Json(serde_json::json!({ "accepted": false, "error": "No active challenge" })),
500 )
501 }
502}
503
504async fn get_miner(
505 State(state): State<Arc<AppState>>,
506 Path(pubkey): Path<String>,
507) -> impl IntoResponse {
508 tracing::debug!(pubkey = %pubkey, "GET /miners/:pubkey");
509 let balance = state
510 .extensions
511 .ledger
512 .read()
513 .await
514 .balance_of(&pubkey, &TokenType::TribeChain);
515 let nonce = state.extensions.ledger.read().await.current_nonce(&pubkey);
516 tracing::debug!(pubkey = %pubkey, balance, "GET /miners/:pubkey");
517 (
518 StatusCode::OK,
519 Json(serde_json::json!({
520 "pubkey": pubkey,
521 "balance": balance,
522 "nonce": nonce,
523 })),
524 )
525}
526
527async fn pool_info(State(state): State<Arc<AppState>>) -> impl IntoResponse {
528 tracing::debug!("GET /pool");
529 let info = state.extensions.pool.pool_info(0, 0);
530 Json(serde_json::to_value(&info).unwrap_or_default())
531}
532
533#[derive(Deserialize)]
534struct DeviceRegisterRequest {
535 device_type: String,
536 device_id: Option<String>,
537 miner_pubkey: Option<String>,
538}
539
540async fn register_device(
541 State(state): State<Arc<AppState>>,
542 Json(body): Json<DeviceRegisterRequest>,
543) -> impl IntoResponse {
544 let device_id = body
547 .device_id
548 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
549 let device_type_normalized = normalize_device_type(&body.device_type);
550
551 let our_type = state.extensions.device.device_type();
552 let our_type_str = format!("{:?}", our_type).to_lowercase();
553 if device_type_normalized != our_type_str && body.device_type != "any" {
554 tracing::warn!(
555 requested = %device_type_normalized,
556 configured = %our_type_str,
557 "Device type mismatch — requested device type differs from configured protocol"
558 );
559 }
560 let now = chrono::Utc::now();
561 let is_new = {
562 let mut reg = state.device_registry.write().await;
563 prune_stale_devices(&mut reg);
564 if let Some(prev) = reg.get_mut(&device_id) {
565 prev.last_activity = now;
566 prev.device_type = device_type_normalized.clone();
567 false
568 } else {
569 reg.insert(
570 device_id.clone(),
571 RegisteredDevice {
572 device_type: device_type_normalized,
573 node_id: state.config.node_id.clone(),
574 last_activity: now,
575 proofs_valid: 0,
576 tasks_processed: 0,
577 miner_pubkeys: Vec::new(),
578 current_calculation: None,
579 },
580 );
581 true
582 }
583 };
584
585 let miner_registered = if let Some(ref miner_pubkey) = body.miner_pubkey {
586 match state.extensions.chain.register_miner(miner_pubkey).await {
587 Ok(_) => {
588 tracing::info!(
589 device_id = %device_id,
590 miner_pubkey = %miner_pubkey,
591 "Registered miner at device registration"
592 );
593 true
594 }
595 Err(e) => {
596 tracing::warn!(
597 device_id = %device_id,
598 miner_pubkey = %miner_pubkey,
599 error = %e,
600 "Register miner at registration failed"
601 );
602 false
603 }
604 }
605 } else {
606 false
607 };
608
609 tracing::info!(
610 device_id = %device_id,
611 device_type = %body.device_type,
612 is_new = is_new,
613 miner_registered = miner_registered,
614 "POST /devices/register"
615 );
616 let reg = state.device_registry.read().await.clone();
617 spawn_persist_registry(reg, state.registry_path.clone());
618 (
619 StatusCode::OK,
620 Json(serde_json::json!({
621 "registered": true,
622 "device_type": body.device_type,
623 "device_id": device_id,
624 "miner_registered": miner_registered,
625 })),
626 )
627}
628
629#[derive(Deserialize)]
630struct DeviceProgressRequest {
631 device_id: Option<String>,
632 miner_pubkey: Option<String>,
633 device_type: Option<String>,
634 challenge_id: String,
635 hash: String,
636}
637
638async fn device_progress(
639 State(state): State<Arc<AppState>>,
640 Json(body): Json<DeviceProgressRequest>,
641) -> impl IntoResponse {
642 let device_type_normalized = body
645 .device_type
646 .as_deref()
647 .map(normalize_device_type)
648 .unwrap_or_else(|| "native".to_string());
649 let registry_key: Option<String> = match &body.device_id {
650 Some(id) => Some(id.clone()),
651 None => body
652 .miner_pubkey
653 .as_ref()
654 .map(|pk| format!("{}:{}", pk, device_type_normalized)),
655 };
656 let Some(registry_key) = registry_key else {
657 return (
658 StatusCode::BAD_REQUEST,
659 Json(serde_json::json!({
660 "ok": false,
661 "error": "Either device_id or miner_pubkey must be set",
662 })),
663 );
664 };
665 let now = chrono::Utc::now();
666 let current = CurrentCalculation {
667 challenge_id: body.challenge_id,
668 hash: body.hash,
669 updated_at: now,
670 };
671 let updated = {
672 let mut reg = state.device_registry.write().await;
673 prune_stale_devices(&mut reg);
674 let entry = reg
675 .entry(registry_key.clone())
676 .or_insert_with(|| RegisteredDevice {
677 device_type: device_type_normalized.clone(),
678 node_id: state.config.node_id.clone(),
679 last_activity: now,
680 proofs_valid: 0,
681 tasks_processed: 0,
682 miner_pubkeys: Vec::new(),
683 current_calculation: None,
684 });
685 entry.last_activity = now;
686 entry.current_calculation = Some(current);
687 true
688 };
689 let reg = state.device_registry.read().await.clone();
690 spawn_persist_registry(reg, state.registry_path.clone());
691 tracing::debug!(registry_key = %registry_key, "POST /devices/progress");
692 (
693 StatusCode::OK,
694 Json(serde_json::json!({
695 "ok": true,
696 "updated": updated,
697 })),
698 )
699}
700
701#[allow(clippy::type_complexity)]
702async fn get_devices(State(state): State<Arc<AppState>>) -> impl IntoResponse {
703 tracing::debug!("GET /devices");
704 {
705 let mut reg = state.device_registry.write().await;
706 prune_stale_devices(&mut reg);
707 }
708 let reg = state.device_registry.read().await.clone();
709 let mut by_type: HashMap<String, (u64, u64, u64, Option<chrono::DateTime<chrono::Utc>>)> =
710 HashMap::new();
711 for key in DEVICE_TYPE_KEYS {
712 by_type.insert((*key).to_string(), (0, 0, 0, None));
713 }
714 for d in reg.values() {
715 let key = &d.device_type;
716 if !DEVICE_TYPE_KEYS.contains(&key.as_str()) {
717 continue;
718 }
719 let entry = by_type.get_mut(key).unwrap();
720 entry.0 += 1;
721 entry.1 += d.proofs_valid;
722 entry.2 += d.tasks_processed;
723 if entry.3.is_none() || d.last_activity > entry.3.unwrap() {
724 entry.3 = Some(d.last_activity);
725 }
726 }
727 let mut miners_map = serde_json::Map::new();
728 for (k, (count, proofs_valid, tasks_processed, last_activity)) in by_type {
729 let last_activity_val = last_activity.map(|t| serde_json::Value::String(t.to_rfc3339()));
730 let proofs_val = if count > 0 {
731 serde_json::Value::Number(serde_json::Number::from(proofs_valid))
732 } else {
733 serde_json::Value::Null
734 };
735 let tasks_val = if count > 0 {
736 serde_json::Value::Number(serde_json::Number::from(tasks_processed))
737 } else {
738 serde_json::Value::Null
739 };
740 miners_map.insert(
741 k,
742 serde_json::json!({
743 "count": count,
744 "proofs_valid": proofs_val,
745 "tasks_processed": tasks_val,
746 "last_activity": last_activity_val,
747 }),
748 );
749 }
750 let devices_detail: serde_json::Map<String, serde_json::Value> = reg
751 .iter()
752 .map(|(id, d)| {
753 let current_calculation = d.current_calculation.as_ref().map(|c| {
754 serde_json::json!({
755 "challenge_id": c.challenge_id,
756 "hash": c.hash,
757 "updated_at": c.updated_at.to_rfc3339(),
758 })
759 });
760 (
761 id.clone(),
762 serde_json::json!({
763 "device_type": d.device_type,
764 "proofs_valid": d.proofs_valid,
765 "tasks_processed": d.tasks_processed,
766 "last_activity": d.last_activity.to_rfc3339(),
767 "miner_pubkeys": d.miner_pubkeys,
768 "current_calculation": current_calculation,
769 }),
770 )
771 })
772 .collect();
773
774 tracing::debug!(device_count = reg.len(), "GET /devices response");
775 Json(serde_json::json!({
776 "miners_by_device": miners_map,
777 "devices_detail": devices_detail,
778 }))
779}
780
781async fn get_peers(State(state): State<Arc<AppState>>) -> impl IntoResponse {
782 tracing::debug!("GET /network/peers");
783 match state.extensions.network.discover_peers().await {
784 Ok(peers) => {
785 tracing::debug!(peer_count = peers.len(), "GET /network/peers");
786 Json(serde_json::json!({
787 "node_id": state.extensions.network.node_id(),
788 "peers": peers,
789 }))
790 }
791 Err(e) => {
792 tracing::warn!(error = %e, "GET /network/peers failed");
793 Json(serde_json::json!({
794 "error": "internal error",
795 }))
796 }
797 }
798}
799
800async fn get_token_balance(
805 State(state): State<Arc<AppState>>,
806 Path((address, token_type_str)): Path<(String, String)>,
807) -> impl IntoResponse {
808 tracing::debug!(address = %address, token = %token_type_str, "GET /token/balance");
809
810 let token = match token_type_from_str(&token_type_str) {
811 Some(t) => t,
812 None => {
813 return (
814 StatusCode::BAD_REQUEST,
815 Json(
816 serde_json::json!({ "error": format!("Unknown token type: {}", token_type_str) }),
817 ),
818 );
819 }
820 };
821
822 let balance = state
823 .extensions
824 .ledger
825 .read()
826 .await
827 .balance_of(&address, &token);
828 (
829 StatusCode::OK,
830 Json(serde_json::json!({
831 "address": address,
832 "token": token_type_str,
833 "balance": balance,
834 })),
835 )
836}
837
838#[derive(Debug, Deserialize)]
839struct TransferRequest {
840 from: String,
841 to: String,
842 token_type: String,
843 amount: u64,
844 fee: u64,
845}
846
847async fn post_token_transfer(
848 State(state): State<Arc<AppState>>,
849 headers: axum::http::HeaderMap,
850 Json(body): Json<TransferRequest>,
851) -> impl IntoResponse {
852 if let Err(status) = auth_middleware(
853 &state,
854 headers.get("authorization").and_then(|v| v.to_str().ok()),
855 )
856 .await
857 {
858 return (status, Json(serde_json::json!({ "error": "unauthorized" })));
859 }
860
861 tracing::debug!(
862 from = %body.from, to = %body.to,
863 token = %body.token_type, amount = body.amount, fee = body.fee,
864 "POST /token/transfer"
865 );
866
867 let token = match token_type_from_str(&body.token_type) {
868 Some(t) => t,
869 None => {
870 return (
871 StatusCode::BAD_REQUEST,
872 Json(
873 serde_json::json!({ "error": format!("Unknown token type: {}", body.token_type) }),
874 ),
875 );
876 }
877 };
878
879 let receipt = {
880 let mut ledger = state.extensions.ledger.write().await;
881 ledger.transfer(&body.from, &body.to, &token, body.amount, body.fee)
882 };
883
884 match receipt {
885 Ok(r) => (
886 StatusCode::OK,
887 Json(serde_json::to_value(&r).unwrap_or_default()),
888 ),
889 Err(e) => (
890 StatusCode::BAD_REQUEST,
891 Json(serde_json::json!({ "error": e })),
892 ),
893 }
894}
895
896async fn get_token_tx_history(
897 State(state): State<Arc<AppState>>,
898 Path(address): Path<String>,
899) -> impl IntoResponse {
900 tracing::debug!(address = %address, "GET /token/tx");
901
902 let txs = state
903 .extensions
904 .ledger
905 .read()
906 .await
907 .tx_history_for(&address);
908 (
909 StatusCode::OK,
910 Json(serde_json::json!({ "address": address, "transactions": txs })),
911 )
912}
913
914async fn get_tribe_supply(State(state): State<Arc<AppState>>) -> impl IntoResponse {
915 tracing::debug!("GET /token/tribe/supply");
916 let supply = state
917 .extensions
918 .ledger
919 .read()
920 .await
921 .total_supply(&TokenType::TribeChain);
922 let minted = {
923 let stats = state.stats.read().await;
924 stats.total_tribe_minted
925 };
926 (
927 StatusCode::OK,
928 Json(serde_json::json!({
929 "supply": supply,
930 "total_minted": minted,
931 "token": "TribeChain",
932 })),
933 )
934}
935
936#[derive(Debug, Deserialize)]
941struct TribechainTxRequest {
942 tx: pot_o_extensions::tx::TransferTransaction,
943}
944
945async fn post_tribechain_tx(
946 State(state): State<Arc<AppState>>,
947 Json(body): Json<TribechainTxRequest>,
948) -> impl IntoResponse {
949 tracing::info!("POST /api/tx received");
950 tracing::debug!(from = %body.tx.from, "POST /api/tx");
951
952 if !state.extensions.tribechain_enabled {
953 tracing::warn!("tribechain not enabled");
954 return (
955 StatusCode::BAD_REQUEST,
956 Json(serde_json::json!({"error": "tribechain not enabled"})),
957 );
958 }
959
960 let Some(mempool) = &state.extensions.mempool else {
961 tracing::warn!("mempool not available");
962 return (
963 StatusCode::SERVICE_UNAVAILABLE,
964 Json(serde_json::json!({"error": "mempool not available"})),
965 );
966 };
967
968 if body.tx.signature.len() != 64 {
969 return (
970 StatusCode::BAD_REQUEST,
971 Json(serde_json::json!({"error": "invalid signature length"})),
972 );
973 }
974
975 if body.tx.tx_hash.len() != 32 {
976 return (
977 StatusCode::BAD_REQUEST,
978 Json(serde_json::json!({"error": "invalid tx_hash length"})),
979 );
980 }
981
982 match mempool
983 .submit(body.tx.clone(), &state.extensions.ledger)
984 .await
985 {
986 Ok(tx_hash) => {
987 let tx_json = serde_json::to_value(&body.tx).unwrap_or_default();
988 let _ = state
989 .extensions
990 .network
991 .broadcast_transaction(&tx_json)
992 .await;
993 (
994 StatusCode::OK,
995 Json(serde_json::json!({
996 "accepted": true,
997 "tx_hash": hex::encode(tx_hash),
998 })),
999 )
1000 }
1001 Err(e) => {
1002 tracing::warn!(error = %e, "POST /api/tx mempool submit failed");
1003 (
1004 StatusCode::BAD_REQUEST,
1005 Json(serde_json::json!({"error": "internal error"})),
1006 )
1007 }
1008 }
1009}
1010
1011async fn get_tribechain_nonce(
1012 State(state): State<Arc<AppState>>,
1013 Path(address): Path<String>,
1014) -> impl IntoResponse {
1015 tracing::debug!(address = %address, "GET /api/nonce");
1016 let nonce = state.extensions.ledger.read().await.current_nonce(&address);
1017 (
1018 StatusCode::OK,
1019 Json(serde_json::json!({
1020 "address": address,
1021 "nonce": nonce,
1022 })),
1023 )
1024}
1025
1026#[derive(Debug, Deserialize)]
1027struct BlocksQuery {
1028 from_height: Option<u64>,
1029 limit: Option<usize>,
1030}
1031
1032async fn get_tribechain_blocks(
1033 State(state): State<Arc<AppState>>,
1034 Query(query): Query<BlocksQuery>,
1035) -> impl IntoResponse {
1036 tracing::debug!("GET /api/blocks");
1037 let Some(block_store) = &state.extensions.block_store else {
1038 return (
1039 StatusCode::SERVICE_UNAVAILABLE,
1040 Json(serde_json::json!({"error": "block store not available"})),
1041 );
1042 };
1043
1044 let from_height = query.from_height.unwrap_or(0);
1045 let limit = query.limit.unwrap_or(100).min(1000);
1046 let latest = block_store.latest_height();
1047
1048 let mut blocks = Vec::new();
1049 for h in from_height..=latest {
1050 if blocks.len() >= limit {
1051 break;
1052 }
1053 if let Some(stored) = block_store.at_height(h) {
1054 if let Ok(block) = serde_json::from_value::<hexchain_p2p::block::HexBlock>(
1055 serde_json::from_str(&stored.block_json).unwrap_or_default(),
1056 ) {
1057 blocks.push(serde_json::json!({
1058 "height": block.height,
1059 "hash": hex::encode(stored.hash),
1060 "coord": block.coord,
1061 "tx_count": block.transactions.as_ref().map(|t| t.len()).unwrap_or(0),
1062 "timestamp": block.timestamp,
1063 }));
1064 }
1065 }
1066 }
1067
1068 (
1069 StatusCode::OK,
1070 Json(serde_json::json!({
1071 "blocks": blocks,
1072 "latest_height": latest,
1073 })),
1074 )
1075}
1076
1077async fn get_token_supply(State(state): State<Arc<AppState>>) -> impl IntoResponse {
1079 tracing::debug!("GET /api/supply");
1080 let ledger = state.extensions.ledger.read().await;
1081
1082 let mut supplies = serde_json::json!({});
1083
1084 let token_types = vec![
1086 TokenType::TribeChain,
1087 TokenType::PTtC,
1088 TokenType::NMTC,
1089 TokenType::STOMP,
1090 TokenType::AUM,
1091 TokenType::AI3,
1092 TokenType::RAVECOIN,
1093 ];
1094
1095 for token_type in token_types {
1096 let supply = ledger.total_supply(&token_type);
1097 let token_name = match token_type {
1098 TokenType::TribeChain => "TRIBE",
1099 TokenType::PTtC => "PTtC",
1100 TokenType::NMTC => "NMTC",
1101 TokenType::STOMP => "STOMP",
1102 TokenType::AUM => "AUM",
1103 TokenType::AI3 => "AI3",
1104 TokenType::RAVECOIN => "RAVECOIN",
1105 };
1106 supplies[token_name] = supply.into();
1107 }
1108
1109 (StatusCode::OK, Json(supplies))
1110}
1111
1112async fn get_tx_by_hash(
1114 State(state): State<Arc<AppState>>,
1115 Path(hash): Path<String>,
1116) -> impl IntoResponse {
1117 tracing::debug!("GET /api/tx/{}", hash);
1118 let ledger = state.extensions.ledger.read().await;
1119
1120 for tx in ledger.tx_history() {
1122 if tx.tx_hash == hash {
1123 return (
1124 StatusCode::OK,
1125 Json(serde_json::json!({
1126 "tx_hash": tx.tx_hash,
1127 "from": tx.from,
1128 "to": tx.to,
1129 "token": match &tx.token {
1130 TokenType::TribeChain => "TRIBE",
1131 TokenType::PTtC => "PTtC",
1132 TokenType::NMTC => "NMTC",
1133 TokenType::STOMP => "STOMP",
1134 TokenType::AUM => "AUM",
1135 TokenType::AI3 => "AI3",
1136 TokenType::RAVECOIN => "RAVECOIN",
1137 },
1138 "amount": tx.amount,
1139 "fee": tx.fee,
1140 "block_height": tx.block_height,
1141 "timestamp": tx.timestamp,
1142 })),
1143 );
1144 }
1145 }
1146
1147 (
1148 StatusCode::NOT_FOUND,
1149 Json(serde_json::json!({ "error": "transaction not found" })),
1150 )
1151}
1152
1153async fn get_proof_trace(
1158 State(state): State<Arc<AppState>>,
1159 Query(query): Query<HashMap<String, usize>>,
1160) -> impl IntoResponse {
1161 tracing::debug!("GET /api/proof-trace");
1162 let limit = query.get("limit").copied().unwrap_or(50).min(200);
1163 let traces = state.proof_traces.recent(limit);
1164 (
1165 StatusCode::OK,
1166 Json(serde_json::json!({
1167 "traces": traces,
1168 "count": traces.len(),
1169 })),
1170 )
1171}
1172
1173async fn get_tx_proof(
1178 State(state): State<Arc<AppState>>,
1179 Path((block_height, tx_index)): Path<(u64, usize)>,
1180) -> impl IntoResponse {
1181 let block_store = match &state.extensions.block_store {
1182 Some(bs) => bs,
1183 None => {
1184 return (
1185 StatusCode::SERVICE_UNAVAILABLE,
1186 Json(serde_json::json!({"error": "block store not available"})),
1187 )
1188 }
1189 };
1190
1191 let stored = match block_store.at_height(block_height) {
1192 Some(s) => s,
1193 None => {
1194 return (
1195 StatusCode::NOT_FOUND,
1196 Json(serde_json::json!({"error": "block not found"})),
1197 )
1198 }
1199 };
1200
1201 let block: hexchain_p2p::block::HexBlock = match serde_json::from_str(&stored.block_json) {
1202 Ok(b) => b,
1203 Err(_) => {
1204 return (
1205 StatusCode::INTERNAL_SERVER_ERROR,
1206 Json(serde_json::json!({"error": "invalid block"})),
1207 )
1208 }
1209 };
1210
1211 let txs = match block.transactions {
1212 Some(t) => t,
1213 None => {
1214 return (
1215 StatusCode::NOT_FOUND,
1216 Json(serde_json::json!({"error": "no transactions"})),
1217 )
1218 }
1219 };
1220
1221 match crate::spv::generate_merkle_proof(&txs, tx_index) {
1222 Some(proof) => (
1223 StatusCode::OK,
1224 Json(serde_json::json!({
1225 "tx_hash": hex::encode(proof.tx_hash),
1226 "merkle_root": hex::encode(proof.merkle_root),
1227 "proof": proof.proof.iter().map(hex::encode).collect::<Vec<_>>(),
1228 "index": proof.index,
1229 "valid": crate::spv::verify_merkle_proof(&proof),
1230 })),
1231 ),
1232 None => (
1233 StatusCode::NOT_FOUND,
1234 Json(serde_json::json!({"error": "transaction not found"})),
1235 ),
1236 }
1237}
1238
1239async fn get_mempool(State(state): State<Arc<AppState>>) -> impl IntoResponse {
1244 tracing::debug!("GET /mempool");
1245
1246 let pending_txs = match &state.extensions.mempool {
1247 Some(mp) => {
1248 let txs = mp.pending();
1249 txs.iter()
1250 .map(|tx| {
1251 serde_json::json!({
1252 "tx_hash": hex::encode(tx.tx_hash),
1253 "from": tx.from,
1254 "to": tx.to,
1255 "amount": tx.amount,
1256 "fee": tx.fee,
1257 "nonce": tx.nonce,
1258 "timestamp": tx.timestamp,
1259 })
1260 })
1261 .collect::<Vec<_>>()
1262 }
1263 None => vec![],
1264 };
1265
1266 let stats = state.stats.read().await;
1267 let pending_or_failed = stats
1268 .total_proofs_received
1269 .saturating_sub(stats.total_proofs_valid);
1270
1271 (
1272 StatusCode::OK,
1273 Json(serde_json::json!({
1274 "pending_transactions": pending_txs,
1275 "pending_or_failed": pending_or_failed,
1276 "pending_proofs": 0,
1277 "total_proofs_received": stats.total_proofs_received,
1278 "total_proofs_valid": stats.total_proofs_valid,
1279 "total_proofs_rejected": stats.total_proofs_rejected,
1280 "total_challenges_issued": stats.total_challenges_issued,
1281 })),
1282 )
1283}
1284
1285#[derive(Debug, Deserialize)]
1290struct PlaceOrderRequest {
1291 maker: String,
1292 side: String,
1293 sell_asset: String,
1294 buy_asset: String,
1295 amount: u64,
1296 price: u64,
1297}
1298
1299async fn post_marketplace_order(
1300 State(state): State<Arc<AppState>>,
1301 headers: axum::http::HeaderMap,
1302 Json(body): Json<PlaceOrderRequest>,
1303) -> impl IntoResponse {
1304 if let Err(status) = auth_middleware(
1305 &state,
1306 headers.get("authorization").and_then(|v| v.to_str().ok()),
1307 )
1308 .await
1309 {
1310 return (status, Json(serde_json::json!({ "error": "unauthorized" })));
1311 }
1312
1313 let side = match body.side.to_lowercase().as_str() {
1314 "buy" => OrderSide::Buy,
1315 "sell" => OrderSide::Sell,
1316 _ => {
1317 return (
1318 StatusCode::BAD_REQUEST,
1319 Json(serde_json::json!({ "error": "side must be 'buy' or 'sell'" })),
1320 )
1321 }
1322 };
1323
1324 let sell_asset = match parse_market_asset(&body.sell_asset) {
1325 Ok(a) => a,
1326 Err(e) => {
1327 return (
1328 StatusCode::BAD_REQUEST,
1329 Json(serde_json::json!({ "error": e })),
1330 )
1331 }
1332 };
1333 let buy_asset = match parse_market_asset(&body.buy_asset) {
1334 Ok(a) => a,
1335 Err(e) => {
1336 return (
1337 StatusCode::BAD_REQUEST,
1338 Json(serde_json::json!({ "error": e })),
1339 )
1340 }
1341 };
1342
1343 let (order_id, trades) = {
1344 let mut mp = state.extensions.marketplace.write().await;
1345 mp.place_and_match(
1346 &body.maker,
1347 side,
1348 sell_asset,
1349 buy_asset,
1350 body.amount,
1351 body.price,
1352 )
1353 };
1354
1355 (
1356 StatusCode::OK,
1357 Json(serde_json::json!({
1358 "order_id": order_id,
1359 "trades": trades,
1360 })),
1361 )
1362}
1363
1364async fn delete_marketplace_order(
1365 State(state): State<Arc<AppState>>,
1366 headers: axum::http::HeaderMap,
1367 Path(id): Path<String>,
1368) -> impl IntoResponse {
1369 let auth_pubkey = match auth_middleware(
1370 &state,
1371 headers.get("authorization").and_then(|v| v.to_str().ok()),
1372 )
1373 .await
1374 {
1375 Ok(pk) => pk,
1376 Err(status) => return (status, Json(serde_json::json!({ "error": "unauthorized" }))),
1377 };
1378
1379 tracing::debug!(order_id = %id, maker = %auth_pubkey, "DELETE /marketplace/order");
1380
1381 let order = {
1382 let mp = state.extensions.marketplace.read().await;
1383 mp.get_order(&id).cloned()
1384 };
1385 let Some(order) = order else {
1386 return (
1387 StatusCode::NOT_FOUND,
1388 Json(serde_json::json!({ "error": "Order not found or already filled/cancelled" })),
1389 );
1390 };
1391 if order.maker != auth_pubkey {
1392 return (
1393 StatusCode::FORBIDDEN,
1394 Json(serde_json::json!({ "error": "unauthorized" })),
1395 );
1396 }
1397
1398 let cancelled = {
1399 let mut mp = state.extensions.marketplace.write().await;
1400 mp.cancel_order(&id)
1401 };
1402 if cancelled {
1403 (
1404 StatusCode::OK,
1405 Json(serde_json::json!({ "status": "cancelled", "order_id": id })),
1406 )
1407 } else {
1408 (
1409 StatusCode::NOT_FOUND,
1410 Json(serde_json::json!({ "error": "Order not found or already filled/cancelled" })),
1411 )
1412 }
1413}
1414
1415async fn get_marketplace_order(
1416 State(state): State<Arc<AppState>>,
1417 Path(id): Path<String>,
1418) -> impl IntoResponse {
1419 tracing::debug!(order_id = %id, "GET /marketplace/order");
1420 let order = {
1421 let mp = state.extensions.marketplace.read().await;
1422 mp.get_order(&id).cloned()
1423 };
1424 match order {
1425 Some(o) => (
1426 StatusCode::OK,
1427 Json(serde_json::to_value(&o).unwrap_or_default()),
1428 ),
1429 None => (
1430 StatusCode::NOT_FOUND,
1431 Json(serde_json::json!({ "error": "Order not found" })),
1432 ),
1433 }
1434}
1435
1436async fn get_marketplace_orderbook(
1437 State(state): State<Arc<AppState>>,
1438 Path((sell_asset_str, buy_asset_str)): Path<(String, String)>,
1439) -> impl IntoResponse {
1440 let sell_asset = match parse_market_asset(&sell_asset_str) {
1441 Ok(a) => a,
1442 Err(e) => {
1443 return (
1444 StatusCode::BAD_REQUEST,
1445 Json(serde_json::json!({ "error": e })),
1446 )
1447 }
1448 };
1449 let buy_asset = match parse_market_asset(&buy_asset_str) {
1450 Ok(a) => a,
1451 Err(e) => {
1452 return (
1453 StatusCode::BAD_REQUEST,
1454 Json(serde_json::json!({ "error": e })),
1455 )
1456 }
1457 };
1458
1459 let ob = {
1460 let mp = state.extensions.marketplace.read().await;
1461 mp.order_book(&sell_asset, &buy_asset)
1462 };
1463
1464 (
1465 StatusCode::OK,
1466 Json(serde_json::json!({
1467 "sell_asset": sell_asset_str,
1468 "buy_asset": buy_asset_str,
1469 "bids": ob.bids,
1470 "asks": ob.asks,
1471 })),
1472 )
1473}
1474
1475async fn get_marketplace_orders(
1476 State(state): State<Arc<AppState>>,
1477 Path(maker): Path<String>,
1478) -> impl IntoResponse {
1479 tracing::debug!(maker = %maker, "GET /marketplace/orders");
1480 let orders: Vec<_> = {
1481 let mp = state.extensions.marketplace.read().await;
1482 mp.orders_for_maker(&maker).into_iter().cloned().collect()
1483 };
1484 (
1485 StatusCode::OK,
1486 Json(serde_json::json!({ "maker": maker, "orders": orders })),
1487 )
1488}
1489
1490async fn get_marketplace_trades(State(state): State<Arc<AppState>>) -> impl IntoResponse {
1491 tracing::debug!("GET /marketplace/trades");
1492 let trades = {
1493 let mp = state.extensions.marketplace.read().await;
1494 mp.trades().to_vec()
1495 };
1496 (
1497 StatusCode::OK,
1498 Json(serde_json::json!({ "trades": trades })),
1499 )
1500}
1501
1502fn token_type_from_str(s: &str) -> Option<TokenType> {
1503 match s.to_lowercase().as_str() {
1504 "tribechain" | "native" | "tribe" => Some(TokenType::TribeChain),
1505 "pttc" => Some(TokenType::PTtC),
1506 "nmtc" => Some(TokenType::NMTC),
1507 "stomp" => Some(TokenType::STOMP),
1508 "aum" => Some(TokenType::AUM),
1509 "ai3" => Some(TokenType::AI3),
1510 "ravecoin" => Some(TokenType::RAVECOIN),
1511 _ => None,
1512 }
1513}
1514
1515#[derive(Deserialize)]
1520struct AuthChallengeRequest {
1521 pubkey: String,
1522}
1523
1524async fn post_auth_challenge(
1525 State(state): State<Arc<AppState>>,
1526 Json(body): Json<AuthChallengeRequest>,
1527) -> impl IntoResponse {
1528 let nonce = state.auth.create_challenge(&body.pubkey).await;
1529 (
1530 StatusCode::OK,
1531 Json(serde_json::json!({
1532 "pubkey": body.pubkey,
1533 "nonce": hex::encode(nonce),
1534 })),
1535 )
1536}
1537
1538#[derive(Deserialize)]
1539struct AuthVerifyRequest {
1540 pubkey: String,
1541 signature: String,
1542 message: String,
1543}
1544
1545async fn post_auth_verify(
1546 State(state): State<Arc<AppState>>,
1547 Json(body): Json<AuthVerifyRequest>,
1548) -> impl IntoResponse {
1549 let signature = match hex::decode(&body.signature) {
1550 Ok(s) => s,
1551 Err(e) => {
1552 return (
1553 StatusCode::BAD_REQUEST,
1554 Json(serde_json::json!({ "error": format!("Invalid signature hex: {}", e) })),
1555 )
1556 }
1557 };
1558 let message = match hex::decode(&body.message) {
1559 Ok(m) => m,
1560 Err(e) => {
1561 return (
1562 StatusCode::BAD_REQUEST,
1563 Json(serde_json::json!({ "error": format!("Invalid message hex: {}", e) })),
1564 )
1565 }
1566 };
1567
1568 match state
1569 .auth
1570 .verify_challenge(&body.pubkey, &signature, &message)
1571 .await
1572 {
1573 Ok(token) => (
1574 StatusCode::OK,
1575 Json(serde_json::json!({
1576 "token": token,
1577 "pubkey": body.pubkey,
1578 })),
1579 ),
1580 Err(e) => (
1581 StatusCode::UNAUTHORIZED,
1582 Json(serde_json::json!({ "error": e })),
1583 ),
1584 }
1585}
1586
1587async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> impl IntoResponse {
1592 tracing::debug!("WS upgrade requested");
1593 ws.on_upgrade(move |socket| handle_ws_socket(socket, state))
1594}
1595
1596async fn handle_ws_socket(mut socket: WebSocket, state: Arc<AppState>) {
1597 let (msg_tx, mut msg_rx) = tokio::sync::mpsc::unbounded_channel();
1598
1599 let mut device_id: Option<String> = None;
1600
1601 loop {
1602 tokio::select! {
1603 ws_msg = socket.recv() => {
1604 match ws_msg {
1605 Some(Ok(Message::Text(text)))
1606 if handle_ws_text(
1607 &text, &state, &msg_tx, &mut device_id,
1608 ).await.is_err() => {
1609 break;
1610 }
1611 Some(Ok(Message::Close(_))) | None => break,
1612 _ => {}
1613 }
1614 }
1615 json = msg_rx.recv() => {
1616 match json {
1617 Some(json) => {
1618 if socket.send(Message::Text(json)).await.is_err() {
1619 break;
1620 }
1621 }
1622 None => break,
1623 }
1624 }
1625 }
1626 }
1627
1628 if let Some(did) = device_id {
1629 state.extensions.messaging.unregister(&did).await;
1630 }
1631}
1632
1633async fn handle_ws_text(
1634 text: &str,
1635 state: &Arc<AppState>,
1636 msg_tx: &tokio::sync::mpsc::UnboundedSender<String>,
1637 device_id: &mut Option<String>,
1638) -> Result<(), ()> {
1639 match serde_json::from_str::<pot_o_extensions::MinerMessage>(text) {
1640 Ok(miner_msg) => match miner_msg {
1641 pot_o_extensions::MinerMessage::Subscribe {
1642 device_id: did,
1643 device_type: _,
1644 } => {
1645 let did_clone = did.clone();
1646 state
1647 .extensions
1648 .messaging
1649 .register(did.clone(), msg_tx.clone())
1650 .await;
1651 *device_id = Some(did_clone.clone());
1652 let _ = msg_tx.send(
1653 pot_o_extensions::ValidatorMessage::Subscribed {
1654 device_id: did_clone,
1655 }
1656 .to_json(),
1657 );
1658 Ok(())
1659 }
1660 pot_o_extensions::MinerMessage::Unsubscribe { device_id: did } => {
1661 state.extensions.messaging.unregister(&did).await;
1662 *device_id = None;
1663 Err(())
1664 }
1665 pot_o_extensions::MinerMessage::Heartbeat { device_id: _ } => {
1666 let _ = msg_tx.send(pot_o_extensions::ValidatorMessage::HeartbeatAck.to_json());
1667 Ok(())
1668 }
1669 pot_o_extensions::MinerMessage::SubmitProof { .. } => {
1670 let _ = msg_tx.send(
1671 pot_o_extensions::ValidatorMessage::Error {
1672 code: "use_http_submit".into(),
1673 message: "Use POST /submit for proof submission".into(),
1674 }
1675 .to_json(),
1676 );
1677 Ok(())
1678 }
1679 pot_o_extensions::MinerMessage::Progress { .. } => Ok(()),
1680 },
1681 Err(e) => {
1682 let _ = msg_tx.send(
1683 pot_o_extensions::ValidatorMessage::Error {
1684 code: "parse_error".into(),
1685 message: format!("Invalid message: {e}"),
1686 }
1687 .to_json(),
1688 );
1689 Ok(())
1690 }
1691 }
1692}
1693
1694#[cfg(test)]
1699mod tests {
1700 use super::*;
1701
1702 fn create_test_config() -> crate::config::ValidatorConfig {
1703 crate::config::ValidatorConfig {
1704 node_id: "test-node".to_string(),
1705 listen_addr: "127.0.0.1".to_string(),
1706 port: 8900,
1707 pot_program_id: String::new(),
1708 difficulty: 2,
1709 max_tensor_dim: 4,
1710 max_mine_iterations: 1000,
1711 peer_network_mode: "local_only".to_string(),
1712 pool_strategy: "solo".to_string(),
1713 device_protocol: "native".to_string(),
1714 bootstrap_urls: vec![],
1715 enable_mdns: false,
1716 mdns_service_name: "pot-o-validator".to_string(),
1717 internal_api_port: 8901,
1718 peer_timeout_secs: 30,
1719 challenge_relay_enabled: true,
1720 maturity_depth: 10,
1721 symmetry_num: 1,
1722 symmetry_den: 1,
1723 base_target: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1724 .to_string(),
1725 protocol_fee_address: String::new(),
1726 marketplace_fee_bps: 25,
1727 tribechain_enabled: false,
1728 tribechain_min_fee: 0,
1729 tribechain_max_pool_size: 10_000,
1730 tribechain_max_txs_per_block: 1000,
1731 tribechain_genesis_path: String::new(),
1732 tribechain_miner_address: String::new(),
1733 tribechain_miner_keypair_path: "keys/miner.json".to_string(),
1734 tribechain_blockstore_path: "blockstore.json".to_string(),
1735 }
1736 }
1737
1738 fn create_test_hex_consensus() -> hexchain_p2p::hex_consensus::HexConsensus {
1739 use hexchain_p2p::types::{ConsensusParams, MmlParams};
1740 let base_target_bytes: [u8; 32] = [0xFFu8; 32];
1741 let params = ConsensusParams {
1742 maturity_depth: 10,
1743 symmetry_num: 1,
1744 symmetry_den: 1,
1745 base_target: base_target_bytes,
1746 mml: MmlParams::default(),
1747 };
1748 hexchain_p2p::hex_consensus::HexConsensus::new(params)
1749 }
1750
1751 #[tokio::test]
1752 async fn test_challenge_generation_with_local_only_network() {
1753 let cfg = create_test_config();
1754 let consensus = pot_o_mining::PotOConsensus::new(cfg.difficulty, cfg.max_tensor_dim);
1755 let extensions = pot_o_extensions::ExtensionRegistry::local_defaults("", 25);
1756
1757 let state = crate::consensus::create_app_state(
1758 cfg,
1759 consensus,
1760 extensions,
1761 "/tmp/registry.json".to_string(),
1762 std::collections::HashMap::new(),
1763 create_test_hex_consensus(),
1764 None, );
1766
1767 let req = ChallengeRequest {
1768 slot: Some(100),
1769 slot_hash: Some("0".repeat(64)),
1770 device_type: None,
1771 };
1772
1773 let body = Json(req);
1774 let response = get_challenge(State(state.clone()), body).await;
1775
1776 let (status, _) = response.into_response().into_parts();
1777 assert_eq!(status.status.as_u16(), 200);
1778
1779 let current = state.current_challenge.read().await;
1780 assert!(current.is_some());
1781 }
1782
1783 #[tokio::test]
1784 async fn test_challenge_generation_broadcasts() {
1785 let cfg = create_test_config();
1786 let consensus = pot_o_mining::PotOConsensus::new(cfg.difficulty, cfg.max_tensor_dim);
1787 let extensions = pot_o_extensions::ExtensionRegistry::local_defaults("", 25);
1788
1789 let state = crate::consensus::create_app_state(
1790 cfg,
1791 consensus,
1792 extensions,
1793 "/tmp/registry.json".to_string(),
1794 std::collections::HashMap::new(),
1795 create_test_hex_consensus(),
1796 None, );
1798
1799 let req = ChallengeRequest {
1800 slot: Some(100),
1801 slot_hash: Some("0".repeat(64)),
1802 device_type: None,
1803 };
1804
1805 let body = Json(req);
1806 let response = get_challenge(State(state), body).await;
1807
1808 let (status, _) = response.into_response().into_parts();
1809 assert_eq!(status.status.as_u16(), 200);
1810 }
1811
1812 #[tokio::test]
1813 async fn test_challenge_generation_with_defaults() {
1814 let cfg = create_test_config();
1815 let consensus = pot_o_mining::PotOConsensus::new(cfg.difficulty, cfg.max_tensor_dim);
1816 let extensions = pot_o_extensions::ExtensionRegistry::local_defaults("", 25);
1817
1818 let state = crate::consensus::create_app_state(
1819 cfg,
1820 consensus,
1821 extensions,
1822 "/tmp/registry.json".to_string(),
1823 std::collections::HashMap::new(),
1824 create_test_hex_consensus(),
1825 None, );
1827
1828 let req = ChallengeRequest {
1829 slot: None,
1830 slot_hash: None,
1831 device_type: None,
1832 };
1833
1834 let body = Json(req);
1835 let response = get_challenge(State(state.clone()), body).await;
1836
1837 let (status, _) = response.into_response().into_parts();
1838 assert_eq!(status.status.as_u16(), 200);
1839
1840 let current = state.current_challenge.read().await;
1841 assert!(current.is_some());
1842 }
1843
1844 #[tokio::test]
1845 async fn test_challenge_updates_stats() {
1846 let cfg = create_test_config();
1847 let consensus = pot_o_mining::PotOConsensus::new(cfg.difficulty, cfg.max_tensor_dim);
1848 let extensions = pot_o_extensions::ExtensionRegistry::local_defaults("", 25);
1849
1850 let state = crate::consensus::create_app_state(
1851 cfg,
1852 consensus,
1853 extensions,
1854 "/tmp/registry.json".to_string(),
1855 std::collections::HashMap::new(),
1856 create_test_hex_consensus(),
1857 None, );
1859
1860 {
1861 let stats = state.stats.read().await;
1862 assert_eq!(stats.total_challenges_issued, 0);
1863 assert_eq!(stats.paths_in_block, 0);
1864 assert_eq!(stats.calcs_in_block, 0);
1865 }
1866
1867 let req = ChallengeRequest {
1868 slot: Some(100),
1869 slot_hash: Some("0".repeat(64)),
1870 device_type: None,
1871 };
1872
1873 let body = Json(req);
1874 let response = get_challenge(State(state.clone()), body).await;
1875
1876 let (status, _) = response.into_response().into_parts();
1877 assert_eq!(status.status.as_u16(), 200);
1878
1879 {
1880 let stats = state.stats.read().await;
1881 assert_eq!(stats.total_challenges_issued, 1);
1882 assert_eq!(stats.paths_in_block, 0);
1883 assert_eq!(stats.calcs_in_block, 0);
1884 }
1885 }
1886
1887 #[tokio::test]
1888 async fn test_multiple_challenges() {
1889 let cfg = create_test_config();
1890 let consensus = pot_o_mining::PotOConsensus::new(cfg.difficulty, cfg.max_tensor_dim);
1891 let extensions = pot_o_extensions::ExtensionRegistry::local_defaults("", 25);
1892
1893 let state = crate::consensus::create_app_state(
1894 cfg,
1895 consensus,
1896 extensions,
1897 "/tmp/registry.json".to_string(),
1898 std::collections::HashMap::new(),
1899 create_test_hex_consensus(),
1900 None, );
1902
1903 for i in 0..3 {
1904 let req = ChallengeRequest {
1905 slot: Some(100 + i),
1906 slot_hash: Some(format!("{:0>64}", i)),
1907 device_type: None,
1908 };
1909
1910 let body = Json(req);
1911 let response = get_challenge(State(state.clone()), body).await;
1912
1913 let (status, _) = response.into_response().into_parts();
1914 assert_eq!(status.status.as_u16(), 200);
1915 }
1916
1917 {
1918 let stats = state.stats.read().await;
1919 assert_eq!(stats.total_challenges_issued, 3);
1920 }
1921 }
1922
1923 #[tokio::test]
1924 async fn test_challenge_generated_with_async_broadcast() {
1925 let cfg = create_test_config();
1926 let consensus = pot_o_mining::PotOConsensus::new(cfg.difficulty, cfg.max_tensor_dim);
1927 let extensions = pot_o_extensions::ExtensionRegistry::local_defaults("", 25);
1928
1929 let state = crate::consensus::create_app_state(
1930 cfg,
1931 consensus,
1932 extensions,
1933 "/tmp/registry.json".to_string(),
1934 std::collections::HashMap::new(),
1935 create_test_hex_consensus(),
1936 None, );
1938
1939 let req = ChallengeRequest {
1940 slot: Some(100),
1941 slot_hash: Some("0".repeat(64)),
1942 device_type: None,
1943 };
1944
1945 let body = Json(req);
1946 let response = get_challenge(State(state), body).await;
1947
1948 let (status, _) = response.into_response().into_parts();
1949 assert_eq!(status.status.as_u16(), 200);
1950
1951 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1952 }
1953}