249 lines
8.0 KiB
Rust
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();
|
|
}
|