Skip to main content

pot_o_validator/
http_api.rs

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
28/// Forward a confirmed mining reward to the wallet service ledger.
29/// Errors are logged but never propagated — the wallet is a best-effort side-effect.
30async 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
143// ---------------------------------------------------------------------------
144// Handlers
145// ---------------------------------------------------------------------------
146
147async 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    // No Bearer token required: the cryptographic proof is self-authenticating.
309
310    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                // Record proof trace
339                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                    // Forward rewards to wallet service (non-blocking, best-effort)
429                    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    // No Bearer token required: device registration is a prerequisite for mining.
545
546    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    // No Bearer token required: progress reporting is low-stakes telemetry.
643
644    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
800// ---------------------------------------------------------------------------
801// Token ledger handlers
802// ---------------------------------------------------------------------------
803
804async 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// ---------------------------------------------------------------------------
937// Tribechain public API handlers
938// ---------------------------------------------------------------------------
939
940#[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
1077// Get all token supplies across the network
1078async 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    // Get supplies for all known token types
1085    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
1112// Get transaction details by hash
1113async 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    // Search for the transaction in history
1121    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
1153// ---------------------------------------------------------------------------
1154// Proof trace handler
1155// ---------------------------------------------------------------------------
1156
1157async 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
1173// ---------------------------------------------------------------------------
1174// SPV proof handler
1175// ---------------------------------------------------------------------------
1176
1177async 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
1239// ---------------------------------------------------------------------------
1240// Mempool handler
1241// ---------------------------------------------------------------------------
1242
1243async 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// ---------------------------------------------------------------------------
1286// Marketplace handlers
1287// ---------------------------------------------------------------------------
1288
1289#[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// ---------------------------------------------------------------------------
1516// Auth handlers
1517// ---------------------------------------------------------------------------
1518
1519#[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
1587// ---------------------------------------------------------------------------
1588// WebSocket — Messaging Protocol
1589// ---------------------------------------------------------------------------
1590
1591async 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// ---------------------------------------------------------------------------
1695// Tests
1696// ---------------------------------------------------------------------------
1697
1698#[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, // wallet_url — not needed in tests
1765        );
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, // wallet_url — not needed in tests
1797        );
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, // wallet_url — not needed in tests
1826        );
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, // wallet_url — not needed in tests
1858        );
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, // wallet_url — not needed in tests
1901        );
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, // wallet_url — not needed in tests
1937        );
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}