diff --git a/Cargo.toml b/Cargo.toml index 1d9fc09..4fd3cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "crates/synor-sdk", "crates/synor-contract-test", "crates/synor-compiler", + "crates/synor-gateway", "apps/synord", "apps/cli", "apps/faucet", diff --git a/crates/synor-gateway/Cargo.toml b/crates/synor-gateway/Cargo.toml index f4f63d7..febfeb1 100644 --- a/crates/synor-gateway/Cargo.toml +++ b/crates/synor-gateway/Cargo.toml @@ -18,7 +18,7 @@ full = ["openapi"] axum = { version = "0.7", features = ["macros", "ws"] } axum-extra = { version = "0.9", features = ["typed-header"] } tower = { version = "0.4", features = ["full"] } -tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip", "limit", "request-id"] } +tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip", "limit", "request-id", "timeout"] } hyper = { version = "1.0", features = ["full"] } hyper-util = { version = "0.1", features = ["full"] } @@ -55,6 +55,7 @@ anyhow = "1.0" # Time chrono = { version = "0.4", features = ["serde"] } +humantime = "2.1" # Configuration config = "0.14" diff --git a/crates/synor-gateway/src/auth.rs b/crates/synor-gateway/src/auth.rs index e3dc2b9..7de0a05 100644 --- a/crates/synor-gateway/src/auth.rs +++ b/crates/synor-gateway/src/auth.rs @@ -175,6 +175,11 @@ impl AuthService { } } + /// Create from configuration. + pub fn from_config(config: crate::config::AuthConfig) -> Self { + Self::new(config.jwt_secret, config.jwt_expiration.as_secs() as i64) + } + /// Generate a JWT token. pub fn generate_token(&self, user_id: &str, tier: ApiKeyTier, permissions: Permissions) -> Result { let now = Utc::now(); @@ -310,16 +315,26 @@ where { type Rejection = ApiError; - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - // Get auth service from extensions - let auth_service = parts - .extensions - .get::() - .ok_or(ApiError::InternalError)? - .clone(); + fn from_request_parts<'life0, 'life1, 'async_trait>( + parts: &'life0 mut Parts, + _state: &'life1 S, + ) -> std::pin::Pin> + Send + 'async_trait>> + where + 'life0: 'async_trait, + 'life1: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + // Get auth service from extensions + let auth_service = parts + .extensions + .get::() + .ok_or(ApiError::InternalError)? + .clone(); - let context = auth_service.authenticate(&parts.headers).await?; - Ok(Authenticated(context)) + let context = auth_service.authenticate(&parts.headers).await?; + Ok(Authenticated(context)) + }) } } @@ -333,21 +348,31 @@ where { type Rejection = ApiError; - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - // Get auth service from extensions - let auth_service = parts - .extensions - .get::() - .cloned(); + fn from_request_parts<'life0, 'life1, 'async_trait>( + parts: &'life0 mut Parts, + _state: &'life1 S, + ) -> std::pin::Pin> + Send + 'async_trait>> + where + 'life0: 'async_trait, + 'life1: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + // Get auth service from extensions + let auth_service = parts + .extensions + .get::() + .cloned(); - if let Some(auth_service) = auth_service { - match auth_service.authenticate(&parts.headers).await { - Ok(context) => Ok(OptionalAuth(Some(context))), - Err(_) => Ok(OptionalAuth(None)), + if let Some(auth_service) = auth_service { + match auth_service.authenticate(&parts.headers).await { + Ok(context) => Ok(OptionalAuth(Some(context))), + Err(_) => Ok(OptionalAuth(None)), + } + } else { + Ok(OptionalAuth(None)) } - } else { - Ok(OptionalAuth(None)) - } + }) } } diff --git a/crates/synor-gateway/src/middleware.rs b/crates/synor-gateway/src/middleware.rs index bb47414..04b42d9 100644 --- a/crates/synor-gateway/src/middleware.rs +++ b/crates/synor-gateway/src/middleware.rs @@ -6,11 +6,10 @@ use crate::{ error::ApiError, }; use axum::{ - body::Body, extract::{ConnectInfo, Request, State}, - http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode}, + http::{HeaderName, HeaderValue, Method}, middleware::Next, - response::{IntoResponse, Response}, + response::Response, }; use governor::{ clock::DefaultClock, @@ -245,24 +244,16 @@ pub async fn rate_limit_middleware( let mut response = next.run(request).await; // Add rate limit headers - let state = limiter.state_snapshot(); - let remaining = state.remaining_burst_capacity(); - response.headers_mut().insert( RATE_LIMIT_LIMIT, state.config.default_rpm.to_string().parse().unwrap(), ); - response.headers_mut().insert( - RATE_LIMIT_REMAINING, - remaining.to_string().parse().unwrap(), - ); Ok(response) } - Err(not_until) => { - let retry_after = not_until - .wait_time_from(DefaultClock::default().now()) - .as_secs(); + Err(_not_until) => { + // Use a fixed retry time since we can't easily convert to quanta's instant + let retry_after = 60; // Default to 60 seconds Err(ApiError::TooManyRequests { retry_after, diff --git a/crates/synor-gateway/src/routes/compiler.rs b/crates/synor-gateway/src/routes/compiler.rs new file mode 100644 index 0000000..19ed54b --- /dev/null +++ b/crates/synor-gateway/src/routes/compiler.rs @@ -0,0 +1,468 @@ +//! Compiler API endpoints. +//! +//! REST endpoints for smart contract compilation: +//! - WASM compilation and optimization +//! - ABI extraction and encoding +//! - Contract analysis and validation +//! - Security scanning + +use axum::{ + extract::State, + routing::post, + Json, Router, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + auth::{require_permission, Authenticated}, + error::ApiResult, + response::ApiResponse, + routes::AppState, +}; + +/// Build compiler routes. +pub fn router() -> Router { + Router::new() + // Compilation + .route("/compile", post(compile_contract)) + .route("/compile/dev", post(compile_dev)) + .route("/compile/production", post(compile_production)) + // ABI + .route("/abi/extract", post(extract_abi)) + .route("/abi/encode", post(encode_call)) + .route("/abi/decode", post(decode_result)) + // Analysis + .route("/analyze", post(analyze_contract)) + .route("/analyze/security", post(security_scan)) + .route("/analyze/gas", post(estimate_gas)) + // Validation + .route("/validate", post(validate_contract)) + .route("/validate/exports", post(validate_exports)) + .route("/validate/memory", post(validate_memory)) +} + +// Types +#[derive(Debug, Deserialize)] +pub struct CompileRequest { + pub wasm: String, // base64 encoded WASM + pub optimization_level: Option, // none, basic, size, aggressive + pub strip_debug: Option, + pub strip_names: Option, + pub generate_abi: Option, +} + +#[derive(Debug, Serialize)] +pub struct CompileResponse { + pub contract_id: String, + pub code_hash: String, + pub original_size: u64, + pub optimized_size: u64, + pub size_reduction: f64, + pub estimated_deploy_gas: u64, + pub wasm: String, // base64 encoded optimized WASM + pub abi: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ContractAbi { + pub name: String, + pub version: String, + pub functions: Vec, + pub events: Vec, + pub errors: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AbiFunction { + pub name: String, + pub selector: String, + pub inputs: Vec, + pub outputs: Vec, + pub view: bool, + pub payable: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AbiParam { + pub name: String, + pub param_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AbiEvent { + pub name: String, + pub topic: String, + pub params: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AbiError { + pub name: String, + pub selector: String, + pub params: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct EncodeCallRequest { + pub function_name: String, + pub args: Vec, + pub abi: Option, +} + +#[derive(Debug, Serialize)] +pub struct AnalysisResult { + pub size_breakdown: SizeBreakdown, + pub functions: Vec, + pub imports: Vec, + pub gas_analysis: GasAnalysis, +} + +#[derive(Debug, Serialize)] +pub struct SizeBreakdown { + pub code: u64, + pub data: u64, + pub functions: u64, + pub memory: u64, + pub exports: u64, + pub imports: u64, + pub total: u64, +} + +#[derive(Debug, Serialize)] +pub struct FunctionAnalysis { + pub name: String, + pub size: u64, + pub instruction_count: u32, + pub local_count: u32, + pub exported: bool, + pub estimated_gas: u64, +} + +#[derive(Debug, Serialize)] +pub struct ImportInfo { + pub module: String, + pub name: String, + pub kind: String, +} + +#[derive(Debug, Serialize)] +pub struct GasAnalysis { + pub deployment_gas: u64, + pub memory_init_gas: u64, + pub data_section_gas: u64, +} + +#[derive(Debug, Serialize)] +pub struct SecurityScanResult { + pub score: u32, + pub issues: Vec, + pub recommendations: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SecurityIssue { + pub severity: String, + pub issue_type: String, + pub description: String, + pub location: Option, +} + +#[derive(Debug, Serialize)] +pub struct ValidationResult { + pub valid: bool, + pub export_count: u32, + pub import_count: u32, + pub function_count: u32, + pub memory_pages: u32, + pub errors: Vec, + pub warnings: Vec, +} + +#[derive(Debug, Serialize)] +pub struct ValidationError { + pub code: String, + pub message: String, + pub location: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ValidateExportsRequest { + pub wasm: String, + pub required_exports: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct ValidateMemoryRequest { + pub wasm: String, + pub max_pages: u32, +} + +// Handlers +async fn compile_contract( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = CompileResponse { + contract_id: "contract_abc123".to_string(), + code_hash: "0xhash...".to_string(), + original_size: 10000, + optimized_size: 5000, + size_reduction: 50.0, + estimated_deploy_gas: 100000, + wasm: "optimized_wasm_base64...".to_string(), + abi: Some(ContractAbi { + name: "MyContract".to_string(), + version: "1.0.0".to_string(), + functions: vec![ + AbiFunction { + name: "init".to_string(), + selector: "0x12345678".to_string(), + inputs: vec![], + outputs: vec![], + view: false, + payable: false, + }, + ], + events: vec![], + errors: vec![], + }), + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn compile_dev( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + // Development mode: fast, no optimization + let response = CompileResponse { + contract_id: "contract_dev".to_string(), + code_hash: "0xhash...".to_string(), + original_size: 10000, + optimized_size: 10000, + size_reduction: 0.0, + estimated_deploy_gas: 200000, + wasm: "dev_wasm_base64...".to_string(), + abi: None, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn compile_production( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + // Production mode: aggressive optimization + let response = CompileResponse { + contract_id: "contract_prod".to_string(), + code_hash: "0xhash...".to_string(), + original_size: 10000, + optimized_size: 3000, + size_reduction: 70.0, + estimated_deploy_gas: 60000, + wasm: "prod_wasm_base64...".to_string(), + abi: None, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn extract_abi( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let abi = ContractAbi { + name: "MyContract".to_string(), + version: "1.0.0".to_string(), + functions: vec![], + events: vec![], + errors: vec![], + }; + + Ok(Json(ApiResponse::success(abi))) +} + +async fn encode_call( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = serde_json::json!({ + "encoded": "0x12345678abcdef...", + "selector": "0x12345678" + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn decode_result( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = serde_json::json!({ + "decoded": ["value1", 42, true] + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn analyze_contract( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = AnalysisResult { + size_breakdown: SizeBreakdown { + code: 3000, + data: 500, + functions: 2500, + memory: 100, + exports: 50, + imports: 100, + total: 5000, + }, + functions: vec![ + FunctionAnalysis { + name: "init".to_string(), + size: 500, + instruction_count: 50, + local_count: 3, + exported: true, + estimated_gas: 10000, + }, + ], + imports: vec![ + ImportInfo { + module: "env".to_string(), + name: "memory".to_string(), + kind: "memory".to_string(), + }, + ], + gas_analysis: GasAnalysis { + deployment_gas: 100000, + memory_init_gas: 5000, + data_section_gas: 2000, + }, + }; + + Ok(Json(ApiResponse::success(result))) +} + +async fn security_scan( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = SecurityScanResult { + score: 85, + issues: vec![ + SecurityIssue { + severity: "low".to_string(), + issue_type: "unbounded_loop".to_string(), + description: "Potential unbounded loop detected".to_string(), + location: Some("function:process".to_string()), + }, + ], + recommendations: vec![ + "Add loop iteration limits".to_string(), + "Consider using checked arithmetic".to_string(), + ], + }; + + Ok(Json(ApiResponse::success(result))) +} + +async fn estimate_gas( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = serde_json::json!({ + "deployment_gas": 100000, + "per_function": { + "init": 10000, + "execute": 5000, + "query": 2000 + } + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn validate_contract( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = ValidationResult { + valid: true, + export_count: 5, + import_count: 3, + function_count: 10, + memory_pages: 1, + errors: vec![], + warnings: vec!["Consider adding explicit error handling".to_string()], + }; + + Ok(Json(ApiResponse::success(result))) +} + +async fn validate_exports( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = serde_json::json!({ + "valid": true, + "missing_exports": [], + "extra_exports": ["helper"] + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn validate_memory( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let result = serde_json::json!({ + "valid": true, + "current_pages": 1, + "max_pages": req.max_pages, + "within_limit": true + }); + + Ok(Json(ApiResponse::success(result))) +} diff --git a/crates/synor-gateway/src/routes/dex.rs b/crates/synor-gateway/src/routes/dex.rs new file mode 100644 index 0000000..bf1b558 --- /dev/null +++ b/crates/synor-gateway/src/routes/dex.rs @@ -0,0 +1,381 @@ +//! DEX API endpoints. +//! +//! REST endpoints for decentralized exchange operations: +//! - Market data and orderbook +//! - Spot trading +//! - Perpetual futures +//! - Liquidity provision + +use axum::{ + extract::{Path, Query, State}, + routing::{delete, get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + auth::{require_permission, Authenticated}, + error::ApiResult, + response::{ApiResponse, PaginationParams}, + routes::AppState, +}; + +/// Build DEX routes. +pub fn router() -> Router { + Router::new() + // Markets + .route("/markets", get(list_markets)) + .route("/markets/:symbol", get(get_market)) + .route("/markets/:symbol/orderbook", get(get_orderbook)) + .route("/markets/:symbol/trades", get(get_trades)) + // Spot trading + .route("/spot/order", post(place_order)) + .route("/spot/order/:order_id", get(get_order)) + .route("/spot/order/:order_id", delete(cancel_order)) + .route("/spot/orders", get(list_orders)) + // Perpetuals + .route("/perps/markets", get(list_perp_markets)) + .route("/perps/positions", get(list_positions)) + .route("/perps/order", post(place_perp_order)) + // Liquidity + .route("/liquidity/pools", get(list_pools)) + .route("/liquidity/pools/:pool_id", get(get_pool)) + .route("/liquidity/add", post(add_liquidity)) + .route("/liquidity/remove", post(remove_liquidity)) + // Account + .route("/account/balances", get(get_balances)) + .route("/account/history", get(get_trade_history)) +} + +// Types +#[derive(Debug, Serialize)] +pub struct Market { + pub symbol: String, + pub base_asset: String, + pub quote_asset: String, + pub last_price: String, + pub change_24h: String, + pub volume_24h: String, + pub status: String, +} + +#[derive(Debug, Serialize)] +pub struct OrderbookEntry { + pub price: String, + pub quantity: String, +} + +#[derive(Debug, Serialize)] +pub struct Orderbook { + pub bids: Vec, + pub asks: Vec, + pub spread: String, +} + +#[derive(Debug, Deserialize)] +pub struct PlaceOrderRequest { + pub symbol: String, + pub side: String, + pub order_type: String, + pub quantity: String, + pub price: Option, + pub time_in_force: Option, +} + +#[derive(Debug, Serialize)] +pub struct Order { + pub order_id: String, + pub symbol: String, + pub side: String, + pub order_type: String, + pub price: String, + pub quantity: String, + pub filled: String, + pub status: String, + pub created_at: String, +} + +#[derive(Debug, Serialize)] +pub struct Pool { + pub pool_id: String, + pub name: String, + pub token_a: String, + pub token_b: String, + pub reserve_a: String, + pub reserve_b: String, + pub tvl: String, + pub apr: String, +} + +#[derive(Debug, Deserialize)] +pub struct AddLiquidityRequest { + pub pool_id: String, + pub amount_a: String, + pub amount_b: String, + pub slippage: Option, +} + +// Handlers +async fn list_markets( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let markets = vec![ + Market { + symbol: "ETH-USDC".to_string(), + base_asset: "ETH".to_string(), + quote_asset: "USDC".to_string(), + last_price: "3000.00".to_string(), + change_24h: "2.5".to_string(), + volume_24h: "10000000".to_string(), + status: "active".to_string(), + }, + ]; + + Ok(Json(ApiResponse::success(markets))) +} + +async fn get_market( + State(state): State, + Authenticated(auth): Authenticated, + Path(symbol): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let market = Market { + symbol, + base_asset: "ETH".to_string(), + quote_asset: "USDC".to_string(), + last_price: "3000.00".to_string(), + change_24h: "2.5".to_string(), + volume_24h: "10000000".to_string(), + status: "active".to_string(), + }; + + Ok(Json(ApiResponse::success(market))) +} + +async fn get_orderbook( + State(state): State, + Authenticated(auth): Authenticated, + Path(symbol): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let orderbook = Orderbook { + bids: vec![OrderbookEntry { price: "2999.00".to_string(), quantity: "1.5".to_string() }], + asks: vec![OrderbookEntry { price: "3001.00".to_string(), quantity: "2.0".to_string() }], + spread: "2.00".to_string(), + }; + + Ok(Json(ApiResponse::success(orderbook))) +} + +async fn get_trades( + State(state): State, + Authenticated(auth): Authenticated, + Path(symbol): Path, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let trades = vec![]; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(trades, meta))) +} + +async fn place_order( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let order = Order { + order_id: "ord_123...".to_string(), + symbol: req.symbol, + side: req.side, + order_type: req.order_type, + price: req.price.unwrap_or_default(), + quantity: req.quantity, + filled: "0".to_string(), + status: "open".to_string(), + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(order))) +} + +async fn get_order( + State(state): State, + Authenticated(auth): Authenticated, + Path(order_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let order = Order { + order_id, + symbol: "ETH-USDC".to_string(), + side: "buy".to_string(), + order_type: "limit".to_string(), + price: "3000.00".to_string(), + quantity: "1.0".to_string(), + filled: "0.5".to_string(), + status: "partially_filled".to_string(), + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(order))) +} + +async fn cancel_order( + State(state): State, + Authenticated(auth): Authenticated, + Path(order_id): Path, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let order = Order { + order_id, + symbol: "ETH-USDC".to_string(), + side: "buy".to_string(), + order_type: "limit".to_string(), + price: "3000.00".to_string(), + quantity: "1.0".to_string(), + filled: "0.0".to_string(), + status: "cancelled".to_string(), + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(order))) +} + +async fn list_orders( + State(state): State, + Authenticated(auth): Authenticated, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let orders = vec![]; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(orders, meta))) +} + +async fn list_perp_markets( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + Ok(Json(ApiResponse::success(vec![]))) +} + +async fn list_positions( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + Ok(Json(ApiResponse::success(vec![]))) +} + +async fn place_perp_order( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + Ok(Json(ApiResponse::success(serde_json::json!({"order_id": "perp_123"})))) +} + +async fn list_pools( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let pools = vec![ + Pool { + pool_id: "ETH-USDC".to_string(), + name: "ETH/USDC".to_string(), + token_a: "ETH".to_string(), + token_b: "USDC".to_string(), + reserve_a: "1000".to_string(), + reserve_b: "3000000".to_string(), + tvl: "6000000".to_string(), + apr: "15.5".to_string(), + }, + ]; + + Ok(Json(ApiResponse::success(pools))) +} + +async fn get_pool( + State(state): State, + Authenticated(auth): Authenticated, + Path(pool_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let pool = Pool { + pool_id, + name: "ETH/USDC".to_string(), + token_a: "ETH".to_string(), + token_b: "USDC".to_string(), + reserve_a: "1000".to_string(), + reserve_b: "3000000".to_string(), + tvl: "6000000".to_string(), + apr: "15.5".to_string(), + }; + + Ok(Json(ApiResponse::success(pool))) +} + +async fn add_liquidity( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let result = serde_json::json!({ + "lp_tokens": "100.0", + "share_of_pool": "0.01" + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn remove_liquidity( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let result = serde_json::json!({ + "amount_a": "1.0", + "amount_b": "3000.0" + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn get_balances( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + Ok(Json(ApiResponse::success(vec![]))) +} + +async fn get_trade_history( + State(state): State, + Authenticated(auth): Authenticated, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(vec![], meta))) +} diff --git a/crates/synor-gateway/src/routes/health.rs b/crates/synor-gateway/src/routes/health.rs index b252d6d..b75fe3f 100644 --- a/crates/synor-gateway/src/routes/health.rs +++ b/crates/synor-gateway/src/routes/health.rs @@ -1,11 +1,10 @@ //! Health check endpoints. use axum::{extract::State, routing::get, Json, Router}; -use serde::{Deserialize, Serialize}; use std::time::Instant; use crate::{ - response::{ApiResponse, HealthResponse, HealthStatus, ServiceHealth}, + response::{HealthResponse, HealthStatus, ServiceHealth}, routes::AppState, }; @@ -26,7 +25,7 @@ pub fn router() -> Router { ), tag = "Health" )] -async fn health_check(State(state): State) -> Json { +async fn health_check(State(_state): State) -> Json { // Get uptime (would come from server state in production) let uptime_seconds = 0; // Placeholder @@ -84,7 +83,7 @@ async fn readiness(State(state): State) -> Json { } /// Check health of a backend service. -async fn check_service_health(name: &str, endpoint: &str) -> ServiceHealth { +async fn check_service_health(name: &str, _endpoint: &str) -> ServiceHealth { let start = Instant::now(); // Attempt to connect to the service diff --git a/crates/synor-gateway/src/routes/ibc.rs b/crates/synor-gateway/src/routes/ibc.rs new file mode 100644 index 0000000..c949bba --- /dev/null +++ b/crates/synor-gateway/src/routes/ibc.rs @@ -0,0 +1,356 @@ +//! IBC API endpoints. +//! +//! REST endpoints for Inter-Blockchain Communication: +//! - Chain information +//! - Channel management +//! - Cross-chain transfers +//! - Packet operations + +use axum::{ + extract::{Path, Query, State}, + routing::{get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + auth::{require_permission, Authenticated}, + error::ApiResult, + response::{ApiResponse, PaginationParams}, + routes::AppState, +}; + +/// Build IBC routes. +pub fn router() -> Router { + Router::new() + // Chains + .route("/chains", get(list_chains)) + .route("/chains/:chain_id", get(get_chain)) + .route("/chains/:chain_id/clients", get(get_clients)) + // Channels + .route("/channels", get(list_channels)) + .route("/channels/:channel_id", get(get_channel)) + .route("/channels/init", post(init_channel)) + // Transfers + .route("/transfers", get(list_transfers)) + .route("/transfers/send", post(send_transfer)) + .route("/transfers/:transfer_id", get(get_transfer)) + .route("/transfers/routes", get(get_routes)) + // Packets + .route("/packets/:channel_id", get(list_packets)) + .route("/packets/:channel_id/:sequence", get(get_packet)) + // Relayer + .route("/relayer/status", get(relayer_status)) + .route("/relayer/register", post(register_relayer)) +} + +// Types +#[derive(Debug, Serialize)] +pub struct Chain { + pub chain_id: String, + pub name: String, + pub status: String, + pub rpc_endpoint: String, + pub latest_height: u64, + pub active_channels: u32, +} + +#[derive(Debug, Serialize)] +pub struct Channel { + pub channel_id: String, + pub state: String, + pub port_id: String, + pub counterparty_channel_id: String, + pub connection_id: String, + pub ordering: String, + pub version: String, +} + +#[derive(Debug, Deserialize)] +pub struct InitChannelRequest { + pub source_chain: String, + pub dest_chain: String, + pub port_id: String, + pub version: String, +} + +#[derive(Debug, Serialize)] +pub struct Transfer { + pub transfer_id: String, + pub source_chain: String, + pub dest_chain: String, + pub channel_id: String, + pub asset: String, + pub amount: String, + pub sender: String, + pub receiver: String, + pub status: String, + pub created_at: String, +} + +#[derive(Debug, Deserialize)] +pub struct SendTransferRequest { + pub source_chain: String, + pub dest_chain: String, + pub channel_id: String, + pub asset: String, + pub amount: String, + pub receiver: String, + pub memo: Option, + pub timeout_minutes: Option, +} + +#[derive(Debug, Serialize)] +pub struct TransferRoute { + pub source_chain: String, + pub dest_chain: String, + pub channel_id: String, + pub estimated_time: String, + pub fee: String, +} + +#[derive(Debug, Serialize)] +pub struct Packet { + pub sequence: u64, + pub source_port: String, + pub source_channel: String, + pub dest_port: String, + pub dest_channel: String, + pub timeout_height: u64, + pub timeout_timestamp: u64, + pub data: String, +} + +// Handlers +async fn list_chains( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let chains = vec![ + Chain { + chain_id: "cosmoshub-4".to_string(), + name: "Cosmos Hub".to_string(), + status: "active".to_string(), + rpc_endpoint: "https://rpc.cosmos.network".to_string(), + latest_height: 18000000, + active_channels: 50, + }, + ]; + + Ok(Json(ApiResponse::success(chains))) +} + +async fn get_chain( + State(state): State, + Authenticated(auth): Authenticated, + Path(chain_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let chain = Chain { + chain_id, + name: "Cosmos Hub".to_string(), + status: "active".to_string(), + rpc_endpoint: "https://rpc.cosmos.network".to_string(), + latest_height: 18000000, + active_channels: 50, + }; + + Ok(Json(ApiResponse::success(chain))) +} + +async fn get_clients( + State(state): State, + Authenticated(auth): Authenticated, + Path(chain_id): Path, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + Ok(Json(ApiResponse::success(vec![]))) +} + +async fn list_channels( + State(state): State, + Authenticated(auth): Authenticated, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let channels = vec![]; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(channels, meta))) +} + +async fn get_channel( + State(state): State, + Authenticated(auth): Authenticated, + Path(channel_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let channel = Channel { + channel_id, + state: "OPEN".to_string(), + port_id: "transfer".to_string(), + counterparty_channel_id: "channel-141".to_string(), + connection_id: "connection-0".to_string(), + ordering: "UNORDERED".to_string(), + version: "ics20-1".to_string(), + }; + + Ok(Json(ApiResponse::success(channel))) +} + +async fn init_channel( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let result = serde_json::json!({ + "channel_id": "channel-new", + "status": "init_sent" + }); + + Ok(Json(ApiResponse::success(result))) +} + +async fn list_transfers( + State(state): State, + Authenticated(auth): Authenticated, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(vec![], meta))) +} + +async fn send_transfer( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let transfer = Transfer { + transfer_id: "transfer_123".to_string(), + source_chain: req.source_chain, + dest_chain: req.dest_chain, + channel_id: req.channel_id, + asset: req.asset, + amount: req.amount, + sender: "synor1...".to_string(), + receiver: req.receiver, + status: "pending".to_string(), + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(transfer))) +} + +async fn get_transfer( + State(state): State, + Authenticated(auth): Authenticated, + Path(transfer_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let transfer = Transfer { + transfer_id, + source_chain: "cosmoshub-4".to_string(), + dest_chain: "synor-mainnet".to_string(), + channel_id: "channel-0".to_string(), + asset: "uatom".to_string(), + amount: "1000000".to_string(), + sender: "cosmos1...".to_string(), + receiver: "synor1...".to_string(), + status: "completed".to_string(), + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(transfer))) +} + +async fn get_routes( + State(state): State, + Authenticated(auth): Authenticated, + Query(params): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let routes = vec![ + TransferRoute { + source_chain: "cosmoshub-4".to_string(), + dest_chain: "synor-mainnet".to_string(), + channel_id: "channel-0".to_string(), + estimated_time: "30s".to_string(), + fee: "0.001 ATOM".to_string(), + }, + ]; + + Ok(Json(ApiResponse::success(routes))) +} + +async fn list_packets( + State(state): State, + Authenticated(auth): Authenticated, + Path(channel_id): Path, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(vec![], meta))) +} + +async fn get_packet( + State(state): State, + Authenticated(auth): Authenticated, + Path((channel_id, sequence)): Path<(String, u64)>, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let packet = Packet { + sequence, + source_port: "transfer".to_string(), + source_channel: channel_id.clone(), + dest_port: "transfer".to_string(), + dest_channel: "channel-141".to_string(), + timeout_height: 0, + timeout_timestamp: 1705400000, + data: "base64data...".to_string(), + }; + + Ok(Json(ApiResponse::success(packet))) +} + +async fn relayer_status( + State(state): State, + Authenticated(auth): Authenticated, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let status = serde_json::json!({ + "active_relayers": 10, + "pending_packets": 5, + "total_relayed": 10000 + }); + + Ok(Json(ApiResponse::success(status))) +} + +async fn register_relayer( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let result = serde_json::json!({ + "relayer_id": "relayer_123", + "status": "registered" + }); + + Ok(Json(ApiResponse::success(result))) +} diff --git a/crates/synor-gateway/src/routes/rpc.rs b/crates/synor-gateway/src/routes/rpc.rs index d67603b..d823553 100644 --- a/crates/synor-gateway/src/routes/rpc.rs +++ b/crates/synor-gateway/src/routes/rpc.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use crate::{ auth::{require_permission, Authenticated}, - error::{ApiError, ApiResult}, + error::ApiResult, response::{ApiResponse, PaginationParams}, routes::AppState, }; diff --git a/crates/synor-gateway/src/routes/storage.rs b/crates/synor-gateway/src/routes/storage.rs index 81beb15..77c1496 100644 --- a/crates/synor-gateway/src/routes/storage.rs +++ b/crates/synor-gateway/src/routes/storage.rs @@ -7,15 +7,16 @@ //! - Directory management use axum::{ - extract::{Multipart, Path, Query, State}, + extract::{Path, Query, State}, routing::{delete, get, post}, Json, Router, }; +use axum_extra::extract::Multipart; use serde::{Deserialize, Serialize}; use crate::{ auth::{require_permission, Authenticated}, - error::{ApiError, ApiResult}, + error::ApiResult, response::{ApiResponse, PaginationParams}, routes::AppState, }; diff --git a/crates/synor-gateway/src/routes/wallet.rs b/crates/synor-gateway/src/routes/wallet.rs index acd8ca5..f33ae5a 100644 --- a/crates/synor-gateway/src/routes/wallet.rs +++ b/crates/synor-gateway/src/routes/wallet.rs @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; use crate::{ auth::{require_permission, Authenticated}, error::{ApiError, ApiResult}, - response::{ApiResponse, PaginationMeta, PaginationParams}, + response::{ApiResponse, PaginationParams}, routes::AppState, }; diff --git a/crates/synor-gateway/src/routes/zk.rs b/crates/synor-gateway/src/routes/zk.rs new file mode 100644 index 0000000..e83099a --- /dev/null +++ b/crates/synor-gateway/src/routes/zk.rs @@ -0,0 +1,429 @@ +//! ZK API endpoints. +//! +//! REST endpoints for zero-knowledge proof operations: +//! - Circuit compilation +//! - Proof generation +//! - Proof verification +//! - Trusted setup ceremonies + +use axum::{ + extract::{Path, Query, State}, + routing::{get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + auth::{require_permission, Authenticated}, + error::ApiResult, + response::{ApiResponse, PaginationParams}, + routes::AppState, +}; + +/// Build ZK routes. +pub fn router() -> Router { + Router::new() + // Circuits + .route("/circuits", get(list_circuits)) + .route("/circuits/compile", post(compile_circuit)) + .route("/circuits/:circuit_id", get(get_circuit)) + // Proofs - Groth16 + .route("/groth16/setup", post(groth16_setup)) + .route("/groth16/prove", post(groth16_prove)) + .route("/groth16/verify", post(groth16_verify)) + // Proofs - PLONK + .route("/plonk/setup", post(plonk_setup)) + .route("/plonk/prove", post(plonk_prove)) + .route("/plonk/verify", post(plonk_verify)) + // Proofs - STARK + .route("/stark/prove", post(stark_prove)) + .route("/stark/verify", post(stark_verify)) + // Recursive proofs + .route("/recursive/prove", post(recursive_prove)) + .route("/recursive/aggregate", post(recursive_aggregate)) + .route("/recursive/verify", post(recursive_verify)) + // Ceremonies + .route("/ceremonies", get(list_ceremonies)) + .route("/ceremonies", post(create_ceremony)) + .route("/ceremonies/:ceremony_id", get(get_ceremony)) + .route("/ceremonies/:ceremony_id/contribute", post(contribute)) +} + +// Types +#[derive(Debug, Serialize)] +pub struct Circuit { + pub circuit_id: String, + pub name: String, + pub constraints: u64, + pub public_inputs: u32, + pub private_inputs: u32, + pub outputs: u32, +} + +#[derive(Debug, Deserialize)] +pub struct CompileCircuitRequest { + pub code: String, + pub language: String, // circom, noir, etc. +} + +#[derive(Debug, Serialize)] +pub struct CompileCircuitResponse { + pub circuit_id: String, + pub constraints: u64, + pub public_inputs: u32, + pub private_inputs: u32, + pub outputs: u32, +} + +#[derive(Debug, Deserialize)] +pub struct ProveRequest { + pub circuit_id: String, + pub witness: serde_json::Value, + pub proving_key: Option, +} + +#[derive(Debug, Serialize)] +pub struct ProofResponse { + pub proof: String, + pub public_signals: Vec, + pub proving_time_ms: u64, +} + +#[derive(Debug, Deserialize)] +pub struct VerifyRequest { + pub proof: String, + pub public_signals: Vec, + pub verification_key: Option, +} + +#[derive(Debug, Serialize)] +pub struct VerifyResponse { + pub valid: bool, + pub verification_time_ms: u64, +} + +#[derive(Debug, Serialize)] +pub struct SetupResponse { + pub proving_key: String, + pub verification_key: String, +} + +#[derive(Debug, Serialize)] +pub struct Ceremony { + pub ceremony_id: String, + pub circuit_id: String, + pub status: String, + pub participant_count: u32, + pub current_round: u32, + pub created_at: String, +} + +#[derive(Debug, Deserialize)] +pub struct CreateCeremonyRequest { + pub circuit_id: String, + pub min_participants: u32, + pub max_participants: u32, + pub round_duration: u32, +} + +#[derive(Debug, Deserialize)] +pub struct ContributeRequest { + pub entropy: String, // base64 encoded random bytes +} + +// Handlers +async fn list_circuits( + State(state): State, + Authenticated(auth): Authenticated, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let circuits = vec![ + Circuit { + circuit_id: "multiplier-v1".to_string(), + name: "Multiplier".to_string(), + constraints: 1, + public_inputs: 1, + private_inputs: 2, + outputs: 1, + }, + ]; + + let meta = pagination.to_meta(circuits.len() as u64); + Ok(Json(ApiResponse::success_paginated(circuits, meta))) +} + +async fn compile_circuit( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = CompileCircuitResponse { + circuit_id: "new-circuit-123".to_string(), + constraints: 100, + public_inputs: 1, + private_inputs: 2, + outputs: 1, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn get_circuit( + State(state): State, + Authenticated(auth): Authenticated, + Path(circuit_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let circuit = Circuit { + circuit_id, + name: "Multiplier".to_string(), + constraints: 1, + public_inputs: 1, + private_inputs: 2, + outputs: 1, + }; + + Ok(Json(ApiResponse::success(circuit))) +} + +async fn groth16_setup( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = SetupResponse { + proving_key: "pk_base64...".to_string(), + verification_key: "vk_base64...".to_string(), + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn groth16_prove( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = ProofResponse { + proof: "proof_base64...".to_string(), + public_signals: vec!["21".to_string()], + proving_time_ms: 500, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn groth16_verify( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let response = VerifyResponse { + valid: true, + verification_time_ms: 10, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn plonk_setup( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = SetupResponse { + proving_key: "plonk_pk_base64...".to_string(), + verification_key: "plonk_vk_base64...".to_string(), + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn plonk_prove( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = ProofResponse { + proof: "plonk_proof_base64...".to_string(), + public_signals: vec!["21".to_string()], + proving_time_ms: 300, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn plonk_verify( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let response = VerifyResponse { + valid: true, + verification_time_ms: 15, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn stark_prove( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = ProofResponse { + proof: "stark_proof_base64...".to_string(), + public_signals: vec!["21".to_string()], + proving_time_ms: 1000, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn stark_verify( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let response = VerifyResponse { + valid: true, + verification_time_ms: 50, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn recursive_prove( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = ProofResponse { + proof: "recursive_proof...".to_string(), + public_signals: vec![], + proving_time_ms: 200, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn recursive_aggregate( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let response = serde_json::json!({ + "proof": "aggregated_proof...", + "proofs_aggregated": 3, + "recursion_depth": 1 + }); + + Ok(Json(ApiResponse::success(response))) +} + +async fn recursive_verify( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let response = VerifyResponse { + valid: true, + verification_time_ms: 5, + }; + + Ok(Json(ApiResponse::success(response))) +} + +async fn list_ceremonies( + State(state): State, + Authenticated(auth): Authenticated, + Query(pagination): Query, +) -> ApiResult>>> { + require_permission(&auth, "read")?; + + let ceremonies = vec![]; + let meta = pagination.to_meta(0); + Ok(Json(ApiResponse::success_paginated(ceremonies, meta))) +} + +async fn create_ceremony( + State(state): State, + Authenticated(auth): Authenticated, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let ceremony = Ceremony { + ceremony_id: "ceremony_123".to_string(), + circuit_id: req.circuit_id, + status: "active".to_string(), + participant_count: 0, + current_round: 1, + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(ceremony))) +} + +async fn get_ceremony( + State(state): State, + Authenticated(auth): Authenticated, + Path(ceremony_id): Path, +) -> ApiResult>> { + require_permission(&auth, "read")?; + + let ceremony = Ceremony { + ceremony_id, + circuit_id: "multiplier-v1".to_string(), + status: "active".to_string(), + participant_count: 5, + current_round: 1, + created_at: "2024-01-15T10:30:00Z".to_string(), + }; + + Ok(Json(ApiResponse::success(ceremony))) +} + +async fn contribute( + State(state): State, + Authenticated(auth): Authenticated, + Path(ceremony_id): Path, + Json(req): Json, +) -> ApiResult>> { + require_permission(&auth, "write")?; + + let result = serde_json::json!({ + "contribution_id": "contrib_123", + "position": 6, + "hash": "sha256hash..." + }); + + Ok(Json(ApiResponse::success(result))) +} diff --git a/crates/synor-gateway/src/server.rs b/crates/synor-gateway/src/server.rs new file mode 100644 index 0000000..1d959e3 --- /dev/null +++ b/crates/synor-gateway/src/server.rs @@ -0,0 +1,386 @@ +//! Gateway server implementation. +//! +//! This module provides the main Gateway server that runs the REST API, +//! WebSocket, and metrics endpoints. + +use crate::{ + auth::AuthService, + config::GatewayConfig, + middleware::{ + auth_middleware, build_cors_layer, rate_limit_middleware, request_id_middleware, + security_headers_middleware, timing_middleware, version_middleware, RateLimiterState, + }, + routes::{self, AppState}, +}; +use axum::{ + middleware::{from_fn, from_fn_with_state}, + Router, +}; +use std::{net::SocketAddr, sync::Arc}; +use tokio::{ + net::TcpListener, + signal, + sync::oneshot, +}; +use tower_http::{ + compression::CompressionLayer, + limit::RequestBodyLimitLayer, + timeout::TimeoutLayer, + trace::TraceLayer, +}; +use tracing::info; + +/// Synor API Gateway server. +pub struct Gateway { + config: GatewayConfig, + auth_service: Arc, + rate_limiter: Arc, +} + +impl Gateway { + /// Create a new gateway instance. + pub fn new(config: GatewayConfig) -> anyhow::Result { + let auth_service = Arc::new(AuthService::from_config(config.auth.clone())); + let rate_limiter = Arc::new(RateLimiterState::new(config.rate_limit.clone())); + + Ok(Self { + config, + auth_service, + rate_limiter, + }) + } + + /// Create gateway from environment configuration. + pub fn from_env() -> anyhow::Result { + let config = GatewayConfig::from_env()?; + Self::new(config) + } + + /// Create gateway from configuration file. + pub fn from_file(path: &str) -> anyhow::Result { + let config = GatewayConfig::from_file(path)?; + Self::new(config) + } + + /// Build the router with all middleware and routes. + fn build_router(&self) -> Router { + let app_state = AppState::new(self.config.clone()); + + // Build base router with all service routes + let router = routes::build_router(app_state.clone()); + + // Apply middleware stack (order matters - applied bottom to top) + let router = router + // Innermost: service routes (already applied) + // Security headers + .layer(from_fn(security_headers_middleware)) + // Version checking + .layer(from_fn(version_middleware)) + // Authentication context injection + .layer(from_fn_with_state( + self.auth_service.clone(), + auth_middleware, + )) + // Rate limiting + .layer(from_fn_with_state( + self.rate_limiter.clone(), + |state, connect_info, req, next| async move { + rate_limit_middleware(state, connect_info, req, next).await + }, + )) + // Request timing + .layer(from_fn(timing_middleware)) + // Request ID + .layer(from_fn(request_id_middleware)); + + // Optional layers based on configuration + let router = if self.config.cors.enabled { + router.layer(build_cors_layer(&self.config.cors)) + } else { + router + }; + + let router = if self.config.server.compression { + router.layer(CompressionLayer::new()) + } else { + router + }; + + // Request body limit + let router = router.layer(RequestBodyLimitLayer::new(self.config.server.max_body_size)); + + // Request timeout + let router = router.layer(TimeoutLayer::new(self.config.server.request_timeout)); + + // Tracing + let router = router.layer(TraceLayer::new_for_http()); + + router + } + + /// Start the gateway server. + pub async fn serve(self) -> anyhow::Result<()> { + let listen_addr = self.config.server.listen_addr; + let shutdown_timeout = self.config.server.shutdown_timeout; + + info!( + listen_addr = %listen_addr, + "Starting Synor API Gateway" + ); + + let router = self.build_router(); + + // Create TCP listener + let listener = TcpListener::bind(listen_addr).await?; + + info!( + listen_addr = %listen_addr, + "Gateway listening for connections" + ); + + // Graceful shutdown handling + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + + // Spawn shutdown signal handler + tokio::spawn(async move { + shutdown_signal().await; + let _ = shutdown_tx.send(()); + }); + + // Serve with graceful shutdown + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async { + let _ = shutdown_rx.await; + info!("Shutdown signal received, initiating graceful shutdown"); + }) + .await?; + + info!("Gateway shutdown complete"); + Ok(()) + } + + /// Start the gateway server and return a handle for programmatic shutdown. + pub async fn serve_with_shutdown(self) -> anyhow::Result { + let listen_addr = self.config.server.listen_addr; + + info!( + listen_addr = %listen_addr, + "Starting Synor API Gateway" + ); + + let router = self.build_router(); + + // Create TCP listener + let listener = TcpListener::bind(listen_addr).await?; + let local_addr = listener.local_addr()?; + + info!( + listen_addr = %local_addr, + "Gateway listening for connections" + ); + + // Create shutdown channel + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + + // Spawn server in background + let server_handle = tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async { + let _ = shutdown_rx.await; + info!("Shutdown signal received"); + }) + .await + }); + + Ok(GatewayHandle { + shutdown_tx: Some(shutdown_tx), + server_handle, + local_addr, + }) + } + + /// Get the configured listen address. + pub fn listen_addr(&self) -> SocketAddr { + self.config.server.listen_addr + } + + /// Get the configured WebSocket address. + pub fn ws_addr(&self) -> SocketAddr { + self.config.server.ws_addr + } +} + +/// Handle for controlling a running gateway. +pub struct GatewayHandle { + shutdown_tx: Option>, + server_handle: tokio::task::JoinHandle>, + local_addr: SocketAddr, +} + +impl GatewayHandle { + /// Get the local address the server is bound to. + pub fn local_addr(&self) -> SocketAddr { + self.local_addr + } + + /// Trigger graceful shutdown. + pub fn shutdown(&mut self) { + if let Some(tx) = self.shutdown_tx.take() { + let _ = tx.send(()); + } + } + + /// Wait for the server to finish. + pub async fn wait(self) -> anyhow::Result<()> { + self.server_handle.await??; + Ok(()) + } + + /// Shutdown and wait for completion. + pub async fn shutdown_and_wait(mut self) -> anyhow::Result<()> { + self.shutdown(); + self.wait().await + } +} + +/// Wait for shutdown signal (Ctrl+C or SIGTERM). +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("Failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => { + info!("Received Ctrl+C signal"); + } + _ = terminate => { + info!("Received SIGTERM signal"); + } + } +} + +/// Builder for Gateway configuration. +pub struct GatewayBuilder { + config: GatewayConfig, +} + +impl GatewayBuilder { + /// Create a new gateway builder with default configuration. + pub fn new() -> Self { + Self { + config: GatewayConfig::default(), + } + } + + /// Set the listen address. + pub fn listen_addr(mut self, addr: SocketAddr) -> Self { + self.config.server.listen_addr = addr; + self + } + + /// Set the WebSocket address. + pub fn ws_addr(mut self, addr: SocketAddr) -> Self { + self.config.server.ws_addr = addr; + self + } + + /// Set the JWT secret. + pub fn jwt_secret(mut self, secret: impl Into) -> Self { + self.config.auth.jwt_secret = secret.into(); + self + } + + /// Disable authentication (for development). + pub fn disable_auth(mut self) -> Self { + self.config.auth.enabled = false; + self + } + + /// Disable rate limiting. + pub fn disable_rate_limit(mut self) -> Self { + self.config.rate_limit.enabled = false; + self + } + + /// Set maximum request body size. + pub fn max_body_size(mut self, size: usize) -> Self { + self.config.server.max_body_size = size; + self + } + + /// Enable or disable compression. + pub fn compression(mut self, enabled: bool) -> Self { + self.config.server.compression = enabled; + self + } + + /// Build the Gateway. + pub fn build(self) -> anyhow::Result { + Gateway::new(self.config) + } +} + +impl Default for GatewayBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_gateway_builder() { + let gateway = GatewayBuilder::new() + .listen_addr("127.0.0.1:0".parse().unwrap()) + .disable_auth() + .disable_rate_limit() + .build() + .unwrap(); + + assert!(!gateway.config.auth.enabled); + assert!(!gateway.config.rate_limit.enabled); + } + + #[tokio::test] + async fn test_gateway_start_stop() { + let gateway = GatewayBuilder::new() + .listen_addr("127.0.0.1:0".parse().unwrap()) + .disable_auth() + .disable_rate_limit() + .build() + .unwrap(); + + let mut handle = gateway.serve_with_shutdown().await.unwrap(); + + // Server should be running + let addr = handle.local_addr(); + assert!(addr.port() > 0); + + // Trigger shutdown + handle.shutdown(); + + // Wait for server to stop + handle.wait().await.unwrap(); + } +}