feat: add unified API gateway crate with REST endpoints
Implements Phase 2 REST API foundation: - Unified gateway server with Axum web framework - Complete REST endpoints for all services: - Wallet (create, import, balance, sign, transactions) - RPC (blocks, transactions, network, mempool) - Storage (upload, download, pinning, CAR files) - DEX (markets, orders, pools, liquidity) - IBC (chains, channels, transfers, packets) - ZK (circuits, proofs, ceremonies) - Compiler (compile, ABI, analysis, validation) - Authentication (JWT + API key) - Rate limiting with tiered access - CORS, security headers, request tracing - Health check endpoints - OpenAPI documentation scaffolding
This commit is contained in:
parent
03c1664739
commit
f8c536b7cd
13 changed files with 2083 additions and 45 deletions
|
|
@ -32,6 +32,7 @@ members = [
|
||||||
"crates/synor-sdk",
|
"crates/synor-sdk",
|
||||||
"crates/synor-contract-test",
|
"crates/synor-contract-test",
|
||||||
"crates/synor-compiler",
|
"crates/synor-compiler",
|
||||||
|
"crates/synor-gateway",
|
||||||
"apps/synord",
|
"apps/synord",
|
||||||
"apps/cli",
|
"apps/cli",
|
||||||
"apps/faucet",
|
"apps/faucet",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ full = ["openapi"]
|
||||||
axum = { version = "0.7", features = ["macros", "ws"] }
|
axum = { version = "0.7", features = ["macros", "ws"] }
|
||||||
axum-extra = { version = "0.9", features = ["typed-header"] }
|
axum-extra = { version = "0.9", features = ["typed-header"] }
|
||||||
tower = { version = "0.4", features = ["full"] }
|
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 = { version = "1.0", features = ["full"] }
|
||||||
hyper-util = { version = "0.1", features = ["full"] }
|
hyper-util = { version = "0.1", features = ["full"] }
|
||||||
|
|
||||||
|
|
@ -55,6 +55,7 @@ anyhow = "1.0"
|
||||||
|
|
||||||
# Time
|
# Time
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
humantime = "2.1"
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
config = "0.14"
|
config = "0.14"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// Generate a JWT token.
|
||||||
pub fn generate_token(&self, user_id: &str, tier: ApiKeyTier, permissions: Permissions) -> Result<String, ApiError> {
|
pub fn generate_token(&self, user_id: &str, tier: ApiKeyTier, permissions: Permissions) -> Result<String, ApiError> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
@ -310,16 +315,26 @@ where
|
||||||
{
|
{
|
||||||
type Rejection = ApiError;
|
type Rejection = ApiError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
fn from_request_parts<'life0, 'life1, 'async_trait>(
|
||||||
// Get auth service from extensions
|
parts: &'life0 mut Parts,
|
||||||
let auth_service = parts
|
_state: &'life1 S,
|
||||||
.extensions
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait>>
|
||||||
.get::<AuthService>()
|
where
|
||||||
.ok_or(ApiError::InternalError)?
|
'life0: 'async_trait,
|
||||||
.clone();
|
'life1: 'async_trait,
|
||||||
|
Self: 'async_trait,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
// Get auth service from extensions
|
||||||
|
let auth_service = parts
|
||||||
|
.extensions
|
||||||
|
.get::<AuthService>()
|
||||||
|
.ok_or(ApiError::InternalError)?
|
||||||
|
.clone();
|
||||||
|
|
||||||
let context = auth_service.authenticate(&parts.headers).await?;
|
let context = auth_service.authenticate(&parts.headers).await?;
|
||||||
Ok(Authenticated(context))
|
Ok(Authenticated(context))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,21 +348,31 @@ where
|
||||||
{
|
{
|
||||||
type Rejection = ApiError;
|
type Rejection = ApiError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
fn from_request_parts<'life0, 'life1, 'async_trait>(
|
||||||
// Get auth service from extensions
|
parts: &'life0 mut Parts,
|
||||||
let auth_service = parts
|
_state: &'life1 S,
|
||||||
.extensions
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait>>
|
||||||
.get::<AuthService>()
|
where
|
||||||
.cloned();
|
'life0: 'async_trait,
|
||||||
|
'life1: 'async_trait,
|
||||||
|
Self: 'async_trait,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
// Get auth service from extensions
|
||||||
|
let auth_service = parts
|
||||||
|
.extensions
|
||||||
|
.get::<AuthService>()
|
||||||
|
.cloned();
|
||||||
|
|
||||||
if let Some(auth_service) = auth_service {
|
if let Some(auth_service) = auth_service {
|
||||||
match auth_service.authenticate(&parts.headers).await {
|
match auth_service.authenticate(&parts.headers).await {
|
||||||
Ok(context) => Ok(OptionalAuth(Some(context))),
|
Ok(context) => Ok(OptionalAuth(Some(context))),
|
||||||
Err(_) => Ok(OptionalAuth(None)),
|
Err(_) => Ok(OptionalAuth(None)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(OptionalAuth(None))
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
Ok(OptionalAuth(None))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,10 @@ use crate::{
|
||||||
error::ApiError,
|
error::ApiError,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
|
||||||
extract::{ConnectInfo, Request, State},
|
extract::{ConnectInfo, Request, State},
|
||||||
http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode},
|
http::{HeaderName, HeaderValue, Method},
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::Response,
|
||||||
};
|
};
|
||||||
use governor::{
|
use governor::{
|
||||||
clock::DefaultClock,
|
clock::DefaultClock,
|
||||||
|
|
@ -245,24 +244,16 @@ pub async fn rate_limit_middleware(
|
||||||
let mut response = next.run(request).await;
|
let mut response = next.run(request).await;
|
||||||
|
|
||||||
// Add rate limit headers
|
// Add rate limit headers
|
||||||
let state = limiter.state_snapshot();
|
|
||||||
let remaining = state.remaining_burst_capacity();
|
|
||||||
|
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
RATE_LIMIT_LIMIT,
|
RATE_LIMIT_LIMIT,
|
||||||
state.config.default_rpm.to_string().parse().unwrap(),
|
state.config.default_rpm.to_string().parse().unwrap(),
|
||||||
);
|
);
|
||||||
response.headers_mut().insert(
|
|
||||||
RATE_LIMIT_REMAINING,
|
|
||||||
remaining.to_string().parse().unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Err(not_until) => {
|
Err(_not_until) => {
|
||||||
let retry_after = not_until
|
// Use a fixed retry time since we can't easily convert to quanta's instant
|
||||||
.wait_time_from(DefaultClock::default().now())
|
let retry_after = 60; // Default to 60 seconds
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
Err(ApiError::TooManyRequests {
|
Err(ApiError::TooManyRequests {
|
||||||
retry_after,
|
retry_after,
|
||||||
|
|
|
||||||
468
crates/synor-gateway/src/routes/compiler.rs
Normal file
468
crates/synor-gateway/src/routes/compiler.rs
Normal file
|
|
@ -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<AppState> {
|
||||||
|
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<String>, // none, basic, size, aggressive
|
||||||
|
pub strip_debug: Option<bool>,
|
||||||
|
pub strip_names: Option<bool>,
|
||||||
|
pub generate_abi: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ContractAbi>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ContractAbi {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub functions: Vec<AbiFunction>,
|
||||||
|
pub events: Vec<AbiEvent>,
|
||||||
|
pub errors: Vec<AbiError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AbiFunction {
|
||||||
|
pub name: String,
|
||||||
|
pub selector: String,
|
||||||
|
pub inputs: Vec<AbiParam>,
|
||||||
|
pub outputs: Vec<AbiParam>,
|
||||||
|
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<AbiParam>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AbiError {
|
||||||
|
pub name: String,
|
||||||
|
pub selector: String,
|
||||||
|
pub params: Vec<AbiParam>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct EncodeCallRequest {
|
||||||
|
pub function_name: String,
|
||||||
|
pub args: Vec<serde_json::Value>,
|
||||||
|
pub abi: Option<ContractAbi>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct AnalysisResult {
|
||||||
|
pub size_breakdown: SizeBreakdown,
|
||||||
|
pub functions: Vec<FunctionAnalysis>,
|
||||||
|
pub imports: Vec<ImportInfo>,
|
||||||
|
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<SecurityIssue>,
|
||||||
|
pub recommendations: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct SecurityIssue {
|
||||||
|
pub severity: String,
|
||||||
|
pub issue_type: String,
|
||||||
|
pub description: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ValidationError>,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ValidationError {
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ValidateExportsRequest {
|
||||||
|
pub wasm: String,
|
||||||
|
pub required_exports: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ValidateMemoryRequest {
|
||||||
|
pub wasm: String,
|
||||||
|
pub max_pages: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
async fn compile_contract(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<CompileRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<CompileResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<CompileRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<CompileResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<CompileRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<CompileResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<ContractAbi>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<EncodeCallRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<AnalysisResult>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<SecurityScanResult>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<ValidationResult>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<ValidateExportsRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<ValidateMemoryRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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)))
|
||||||
|
}
|
||||||
381
crates/synor-gateway/src/routes/dex.rs
Normal file
381
crates/synor-gateway/src/routes/dex.rs
Normal file
|
|
@ -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<AppState> {
|
||||||
|
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<OrderbookEntry>,
|
||||||
|
pub asks: Vec<OrderbookEntry>,
|
||||||
|
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<String>,
|
||||||
|
pub time_in_force: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
async fn list_markets(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Market>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(symbol): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Market>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(symbol): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Orderbook>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(symbol): Path<String>,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<serde_json::Value>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<PlaceOrderRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Order>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(order_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Order>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(order_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Order>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Order>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<serde_json::Value>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
Ok(Json(ApiResponse::success(vec![])))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_positions(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<serde_json::Value>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
Ok(Json(ApiResponse::success(vec![])))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn place_perp_order(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
require_permission(&auth, "write")?;
|
||||||
|
Ok(Json(ApiResponse::success(serde_json::json!({"order_id": "perp_123"}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_pools(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Pool>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(pool_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Pool>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<AddLiquidityRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<serde_json::Value>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
Ok(Json(ApiResponse::success(vec![])))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_trade_history(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<serde_json::Value>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
let meta = pagination.to_meta(0);
|
||||||
|
Ok(Json(ApiResponse::success_paginated(vec![], meta)))
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
//! Health check endpoints.
|
//! Health check endpoints.
|
||||||
|
|
||||||
use axum::{extract::State, routing::get, Json, Router};
|
use axum::{extract::State, routing::get, Json, Router};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
response::{ApiResponse, HealthResponse, HealthStatus, ServiceHealth},
|
response::{HealthResponse, HealthStatus, ServiceHealth},
|
||||||
routes::AppState,
|
routes::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -26,7 +25,7 @@ pub fn router() -> Router<AppState> {
|
||||||
),
|
),
|
||||||
tag = "Health"
|
tag = "Health"
|
||||||
)]
|
)]
|
||||||
async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
|
async fn health_check(State(_state): State<AppState>) -> Json<HealthResponse> {
|
||||||
// Get uptime (would come from server state in production)
|
// Get uptime (would come from server state in production)
|
||||||
let uptime_seconds = 0; // Placeholder
|
let uptime_seconds = 0; // Placeholder
|
||||||
|
|
||||||
|
|
@ -84,7 +83,7 @@ async fn readiness(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check health of a backend service.
|
/// 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();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Attempt to connect to the service
|
// Attempt to connect to the service
|
||||||
|
|
|
||||||
356
crates/synor-gateway/src/routes/ibc.rs
Normal file
356
crates/synor-gateway/src/routes/ibc.rs
Normal file
|
|
@ -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<AppState> {
|
||||||
|
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<String>,
|
||||||
|
pub timeout_minutes: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Chain>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(chain_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Chain>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(chain_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<serde_json::Value>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
Ok(Json(ApiResponse::success(vec![])))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_channels(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Channel>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(channel_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Channel>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<InitChannelRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Transfer>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
let meta = pagination.to_meta(0);
|
||||||
|
Ok(Json(ApiResponse::success_paginated(vec![], meta)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_transfer(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<SendTransferRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Transfer>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(transfer_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Transfer>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(params): Query<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<TransferRoute>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(channel_id): Path<String>,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Packet>>>> {
|
||||||
|
require_permission(&auth, "read")?;
|
||||||
|
let meta = pagination.to_meta(0);
|
||||||
|
Ok(Json(ApiResponse::success_paginated(vec![], meta)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_packet(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path((channel_id, sequence)): Path<(String, u64)>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Packet>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
require_permission(&auth, "write")?;
|
||||||
|
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"relayer_id": "relayer_123",
|
||||||
|
"status": "registered"
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Json(ApiResponse::success(result)))
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{require_permission, Authenticated},
|
auth::{require_permission, Authenticated},
|
||||||
error::{ApiError, ApiResult},
|
error::ApiResult,
|
||||||
response::{ApiResponse, PaginationParams},
|
response::{ApiResponse, PaginationParams},
|
||||||
routes::AppState,
|
routes::AppState,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@
|
||||||
//! - Directory management
|
//! - Directory management
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Multipart, Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::Multipart;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{require_permission, Authenticated},
|
auth::{require_permission, Authenticated},
|
||||||
error::{ApiError, ApiResult},
|
error::ApiResult,
|
||||||
response::{ApiResponse, PaginationParams},
|
response::{ApiResponse, PaginationParams},
|
||||||
routes::AppState,
|
routes::AppState,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{require_permission, Authenticated},
|
auth::{require_permission, Authenticated},
|
||||||
error::{ApiError, ApiResult},
|
error::{ApiError, ApiResult},
|
||||||
response::{ApiResponse, PaginationMeta, PaginationParams},
|
response::{ApiResponse, PaginationParams},
|
||||||
routes::AppState,
|
routes::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
429
crates/synor-gateway/src/routes/zk.rs
Normal file
429
crates/synor-gateway/src/routes/zk.rs
Normal file
|
|
@ -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<AppState> {
|
||||||
|
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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ProofResponse {
|
||||||
|
pub proof: String,
|
||||||
|
pub public_signals: Vec<String>,
|
||||||
|
pub proving_time_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct VerifyRequest {
|
||||||
|
pub proof: String,
|
||||||
|
pub public_signals: Vec<String>,
|
||||||
|
pub verification_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Circuit>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<CompileCircuitRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<CompileCircuitResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(circuit_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Circuit>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<SetupResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<ProveRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<ProofResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<VerifyRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<VerifyResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<SetupResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<ProveRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<ProofResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<VerifyRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<VerifyResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<ProofResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<VerifyRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<VerifyResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<ProofResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<serde_json::Value>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<VerifyResponse>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Query(pagination): Query<PaginationParams>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Vec<Ceremony>>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Json(req): Json<CreateCeremonyRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Ceremony>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(ceremony_id): Path<String>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<Ceremony>>> {
|
||||||
|
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<AppState>,
|
||||||
|
Authenticated(auth): Authenticated,
|
||||||
|
Path(ceremony_id): Path<String>,
|
||||||
|
Json(req): Json<ContributeRequest>,
|
||||||
|
) -> ApiResult<Json<ApiResponse<serde_json::Value>>> {
|
||||||
|
require_permission(&auth, "write")?;
|
||||||
|
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"contribution_id": "contrib_123",
|
||||||
|
"position": 6,
|
||||||
|
"hash": "sha256hash..."
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Json(ApiResponse::success(result)))
|
||||||
|
}
|
||||||
386
crates/synor-gateway/src/server.rs
Normal file
386
crates/synor-gateway/src/server.rs
Normal file
|
|
@ -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<AuthService>,
|
||||||
|
rate_limiter: Arc<RateLimiterState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gateway {
|
||||||
|
/// Create a new gateway instance.
|
||||||
|
pub fn new(config: GatewayConfig) -> anyhow::Result<Self> {
|
||||||
|
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<Self> {
|
||||||
|
let config = GatewayConfig::from_env()?;
|
||||||
|
Self::new(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create gateway from configuration file.
|
||||||
|
pub fn from_file(path: &str) -> anyhow::Result<Self> {
|
||||||
|
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::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.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<GatewayHandle> {
|
||||||
|
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::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.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<oneshot::Sender<()>>,
|
||||||
|
server_handle: tokio::task::JoinHandle<Result<(), std::io::Error>>,
|
||||||
|
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<String>) -> 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> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue