Files
sim-package/sim-engine/src/main.rs
T

249 lines
8.0 KiB
Rust

mod types;
mod ledger;
mod blockchain;
mod contracts;
mod engine;
mod error;
use std::sync::{Arc, Mutex};
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use tower_http::cors::CorsLayer;
use tracing_subscriber;
use types::*;
use ledger::Ledger;
use engine::Engine;
// ─── Shared state ────────────────────────────────────────────────────────────
struct AppState {
ledger: Ledger,
config: WorldConfig,
current_turn: u64,
}
type SharedState = Arc<Mutex<AppState>>;
// ─── API response wrapper ────────────────────────────────────────────────────
#[derive(Serialize)]
struct ApiResponse<T: Serialize> {
ok: bool,
data: Option<T>,
error: Option<String>,
}
impl<T: Serialize> ApiResponse<T> {
fn ok(data: T) -> Json<Self> {
Json(Self { ok: true, data: Some(data), error: None })
}
fn err_t(msg: &str) -> Json<Self> {
Json(Self { ok: false, data: None, error: Some(msg.to_string()) })
}
}
// ─── Request bodies ───────────────────────────────────────────────────────────
#[derive(Deserialize)]
struct InitRequest {
config: Option<WorldConfig>,
agent_ids: Vec<AgentId>,
}
#[derive(Deserialize)]
struct TurnRequest {
inputs: Vec<AgentTurnInput>,
}
// ─── Handlers ─────────────────────────────────────────────────────────────────
/// POST /init — initialize world, create genesis block and agents
async fn init_world(
State(state): State<SharedState>,
Json(req): Json<InitRequest>,
) -> (StatusCode, Json<ApiResponse<String>>) {
let mut app = state.lock().unwrap();
let config = req.config.unwrap_or_default();
// save config
if let Err(e) = app.ledger.save_config(&config) {
return (StatusCode::INTERNAL_SERVER_ERROR,
ApiResponse::err_t(&e.to_string()));
}
// create genesis agents with equal endowments
for agent_id in &req.agent_ids {
let agent = AgentState {
agent_id: agent_id.clone(),
balance: config.genesis_tokens_per_agent,
staked: 0,
burn_score: 0.0,
total_burned: 0,
core_shares: std::collections::HashMap::new(),
study_level: 0,
has_taken_job: false,
nonce: 0,
unstake_pending: None,
arbitration_wins: 0,
arbitration_losses: 0,
arbitration_active: 0,
};
if let Err(e) = app.ledger.upsert_agent(&agent) {
return (StatusCode::INTERNAL_SERVER_ERROR,
ApiResponse::err_t(&e.to_string()));
}
}
// create genesis block
let genesis = Block {
block_id: blockchain::hash("genesis:0"),
prev_block_id: blockchain::hash("null"),
turn: 0,
height: 0,
validator_id: "system".to_string(),
validator_type: ValidatorType::Genesis,
effective_score: 0.0,
transactions: Vec::new(),
merkle_root: blockchain::hash("empty"),
fees_collected: 0,
burn_proof_tx_id: None,
};
if let Err(e) = app.ledger.insert_block(&genesis, 0.0) {
return (StatusCode::INTERNAL_SERVER_ERROR,
ApiResponse::err_t(&e.to_string()));
}
app.config = config;
app.current_turn = 1;
(StatusCode::OK, ApiResponse::ok("world initialized".to_string()))
}
/// POST /turn — submit all agent actions for the current turn
async fn process_turn(
State(state): State<SharedState>,
Json(req): Json<TurnRequest>,
) -> (StatusCode, Json<ApiResponse<TurnResult>>) {
let mut app = state.lock().unwrap();
let turn = app.current_turn;
let eng = Engine::new(&app.ledger, &app.config);
match eng.process_turn(turn, req.inputs) {
Ok(result) => {
app.current_turn += 1;
(StatusCode::OK, ApiResponse::ok(result))
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::err_t(&e.to_string()))
}
}
/// GET /state — full world state snapshot
async fn get_state(
State(state): State<SharedState>,
) -> Json<serde_json::Value> {
let app = state.lock().unwrap();
let agents = app.ledger.get_all_agents().unwrap_or_default();
let tip = app.ledger.get_chain_tip().unwrap_or(None);
let supply = app.ledger.get_token_supply().unwrap_or(0);
let mempool = app.ledger.get_mempool().unwrap_or_default();
let speech = app.ledger.get_speech_log(app.current_turn - 1).unwrap_or_default();
Json(serde_json::json!({
"turn": app.current_turn,
"agents": agents,
"chain_tip": tip,
"token_supply": supply,
"mempool_size": mempool.len(),
"last_speech": speech,
}))
}
/// GET /agent/:id
async fn get_agent(
State(state): State<SharedState>,
Path(agent_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
let app = state.lock().unwrap();
match app.ledger.get_agent(&agent_id) {
Ok(Some(agent)) => (StatusCode::OK, Json(serde_json::to_value(agent).unwrap())),
Ok(None) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "not found"}))),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()}))),
}
}
/// GET /contract/:id
async fn get_contract(
State(state): State<SharedState>,
Path(contract_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
let app = state.lock().unwrap();
match app.ledger.get_contract(&contract_id) {
Ok(Some(c)) => (StatusCode::OK, Json(serde_json::to_value(c).unwrap())),
Ok(None) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "not found"}))),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()}))),
}
}
/// GET /speech/:turn
async fn get_speech(
State(state): State<SharedState>,
Path(turn): Path<u64>,
) -> Json<serde_json::Value> {
let app = state.lock().unwrap();
let log = app.ledger.get_speech_log(turn).unwrap_or_default();
Json(serde_json::json!({ "turn": turn, "speech": log }))
}
/// GET /config
async fn get_config(
State(state): State<SharedState>,
) -> Json<serde_json::Value> {
let app = state.lock().unwrap();
Json(serde_json::to_value(&app.config).unwrap())
}
// ─── Main ────────────────────────────────────────────────────────────────────
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let db_path = std::env::var("DB_PATH").unwrap_or_else(|_| "sim.db".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
let ledger = Ledger::open(&db_path).expect("failed to open ledger");
let config = ledger.load_config().unwrap().unwrap_or_default();
let state = Arc::new(Mutex::new(AppState {
ledger,
config,
current_turn: 1,
}));
let app = Router::new()
.route("/init", post(init_world))
.route("/turn", post(process_turn))
.route("/state", get(get_state))
.route("/agent/:id", get(get_agent))
.route("/contract/:id", get(get_contract))
.route("/speech/:turn", get(get_speech))
.route("/config", get(get_config))
.layer(CorsLayer::permissive())
.with_state(state);
let addr = format!("0.0.0.0:{}", port);
tracing::info!("sim-engine listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}