diff --git a/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt new file mode 100644 index 0000000..34ebd7a --- /dev/null +++ b/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt @@ -0,0 +1,231 @@ +package org.ccoin.routes + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.apiRoutes() { + route("/api") { + /** Get all available API routes */ + get("/routes") { + try { + val routes = mapOf( + "wallet" to mapOf( + "POST /wallet/create" to "Create a new wallet", + "GET /wallet/{address}" to "Get wallet by address", + "GET /wallet/{address}/balance" to "Get wallet balance", + "PUT /wallet/{address}/label" to "Update wallet label", + "GET /wallet/list" to "Get all wallets with pagination", + "GET /wallet/rich" to "Get wallets with minimum balance", + "GET /wallet/{address}/exists" to "Check if wallet exists" + ), + "transaction" to mapOf( + "POST /transaction/send" to "Send a transaction", + "GET /transaction/{hash}" to "Get transaction by hash", + "GET /transaction/history/{address}" to "Get transaction history for address", + "GET /transaction/pending" to "Get pending transactions", + "GET /transaction/count/{address}" to "Get transaction count for address", + "GET /transaction/list" to "Get all transactions with pagination", + "GET /transaction/stats" to "Get network transaction statistics" + ), + "mining" to mapOf( + "POST /mining/start" to "Start a mining job", + "POST /mining/submit" to "Submit mining result", + "GET /mining/difficulty" to "Get current mining difficulty", + "GET /mining/stats/{address}" to "Get mining statistics for miner", + "GET /mining/network" to "Get network mining statistics", + "GET /mining/pending-transactions" to "Get pending transactions for mining", + "POST /mining/validate" to "Validate mining job", + "GET /mining/leaderboard" to "Get mining leaderboard" + ), + "block" to mapOf( + "GET /block/{hash}" to "Get block by hash", + "GET /block/height/{height}" to "Get block by height", + "GET /block/{hash}/exists" to "Check if block exists", + "GET /blocks/latest" to "Get latest blocks", + "GET /blocks/range" to "Get blocks in height range", + "GET /blocks/miner/{address}" to "Get blocks by miner address", + "GET /blocks/time-range" to "Get blocks by timestamp range", + "GET /blocks/difficulty/{difficulty}" to "Get blocks by difficulty", + "GET /blocks/stats" to "Get blockchain statistics" + ), + "health" to mapOf( + "GET /health" to "Basic health check", + "GET /health/detailed" to "Detailed health check with system metrics", + "GET /health/database" to "Database health check", + "GET /health/blockchain" to "Blockchain health check", + "GET /ready" to "Readiness probe (Kubernetes)", + "GET /live" to "Liveness probe (Kubernetes)", + "GET /version" to "Service version and build info" + ), + "api" to mapOf( + "GET /api/routes" to "Get all available API routes", + "GET /api/docs" to "Get API documentation", + "GET /api/examples" to "Get API usage examples" + ) + ) + + val summary = mapOf( + "totalEndpoints" to routes.values.sumOf { it.size }, + "categories" to routes.keys.toList(), + "baseUrl" to "http://localhost:8080", + "documentation" to "https://github.com/your-repo/ccoin-server/docs", + "version" to "1.0.0" + ) + + call.respond(mapOf( + "summary" to summary, + "routes" to routes + )) + + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf( + "error" to (e.message ?: "Failed to get routes information") + )) + } + } + + /** Get API documentation */ + get("/docs") { + try { + val documentation = mapOf( + "title" to "CCoin API Documentation", + "version" to "1.0.0", + "description" to "REST API for CCoin cryptocurrency server", + "baseUrl" to "http://localhost:8080", + "authentication" to "None required", + "contentType" to "application/json", + "rateLimit" to "100 requests per minute", + "sections" to mapOf( + "wallet" to mapOf( + "description" to "Wallet management endpoints", + "addressFormat" to "random_word:random_6_digits (e.g., phoenix:123456)" + ), + "transaction" to mapOf( + "description" to "Transaction management endpoints", + "fees" to "Optional, defaults to 0.0", + "memoMaxLength" to 256 + ), + "mining" to mapOf( + "description" to "Mining and block creation endpoints", + "defaultDifficulty" to 4, + "defaultReward" to 50.0 + ), + "block" to mapOf( + "description" to "Blockchain query endpoints", + "hashFormat" to "64-character hex string" + ) + ), + "errorCodes" to mapOf( + "400" to "Bad Request - Invalid parameters", + "404" to "Not Found - Resource not found", + "500" to "Internal Server Error - Server error" + ) + ) + + call.respond(documentation) + + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf( + "error" to (e.message ?: "Failed to get API documentation") + )) + } + } + + /** Get API usage examples */ + get("/examples") { + try { + val examples = mapOf( + "createWallet" to mapOf( + "method" to "POST", + "url" to "/wallet/create", + "body" to mapOf( + "label" to "My Wallet" + ), + "response" to mapOf( + "address" to "phoenix:123456", + "balance" to 0.0, + "label" to "My Wallet", + "createdAt" to 1703097600, + "lastActivity" to null + ) + ), + "sendTransaction" to mapOf( + "method" to "POST", + "url" to "/transaction/send", + "body" to mapOf( + "fromAddress" to "phoenix:123456", + "toAddress" to "dragon:789012", + "amount" to 10.5, + "fee" to 0.01, + "memo" to "Payment for services" + ), + "response" to mapOf( + "hash" to "abc123...", + "fromAddress" to "phoenix:123456", + "toAddress" to "dragon:789012", + "amount" to 10.5, + "fee" to 0.01, + "memo" to "Payment for services", + "timestamp" to 1703097600, + "status" to "CONFIRMED" + ) + ), + "startMining" to mapOf( + "method" to "POST", + "url" to "/mining/start", + "body" to mapOf( + "minerAddress" to "tiger:456789", + "difficulty" to 4 + ), + "response" to mapOf( + "jobId" to "job123", + "target" to "0000", + "difficulty" to 4, + "previousHash" to "def456...", + "height" to 100, + "timestamp" to 1703097600, + "expiresAt" to 1703097900 + ) + ), + "getBlock" to mapOf( + "method" to "GET", + "url" to "/block/abc123...", + "response" to mapOf( + "hash" to "abc123...", + "previousHash" to "def456...", + "merkleRoot" to "ghi789...", + "timestamp" to 1703097600, + "difficulty" to 4, + "nonce" to 12345, + "minerAddress" to "tiger:456789", + "reward" to 50.0, + "height" to 100, + "transactionCount" to 0, + "confirmations" to 6 + ) + ) + ) + + call.respond(mapOf( + "title" to "CCoin API Examples", + "description" to "Common usage examples for the CCoin API", + "examples" to examples, + "curlExamples" to mapOf( + "createWallet" to "curl -X POST http://localhost:8080/wallet/create -H 'Content-Type: application/json' -d '{\"label\":\"My Wallet\"}'", + "getBalance" to "curl http://localhost:8080/wallet/phoenix:123456/balance", + "sendTransaction" to "curl -X POST http://localhost:8080/transaction/send -H 'Content-Type: application/json' -d '{\"fromAddress\":\"phoenix:123456\",\"toAddress\":\"dragon:789012\",\"amount\":10.5}'", + "getLatestBlocks" to "curl http://localhost:8080/blocks/latest?limit=5" + ) + )) + + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf( + "error" to (e.message ?: "Failed to get API examples") + )) + } + } + } +} + diff --git a/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt index e69de29..14d3890 100644 --- a/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt @@ -0,0 +1,275 @@ +package org.ccoin.routes + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.ccoin.config.DatabaseConfig +import org.ccoin.config.ServerConfig +import org.ccoin.models.DatabaseHealth +import org.ccoin.models.BlockchainHealth +import org.ccoin.models.HealthResponse +import org.ccoin.services.BlockService +import org.ccoin.services.TransactionService +import org.ccoin.services.WalletService +import org.ccoin.services.MiningService +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.transaction +import java.lang.management.ManagementFactory + +fun Route.healthRoutes() { + /** Basic health check */ + get("/health") { + try { + val startTime = System.currentTimeMillis() + + // Check database connectivity + val dbHealth = checkDatabaseHealth() + + // Check blockchain health + val blockchainHealth = checkBlockchainHealth() + + val uptime = ManagementFactory.getRuntimeMXBean().uptime + + val health = HealthResponse( + status = if (dbHealth.connected) "healthy" else "unhealthy", + version = "1.0.0", + uptime = uptime, + database = dbHealth, + blockchain = blockchainHealth + ) + + val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable + call.respond(statusCode, health) + + } catch (e: Exception) { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf( + "status" to "unhealthy", + "error" to (e.message ?: "Health check failed") + )) + } + } + + /** Detailed health check */ + get("/health/detailed") { + try { + val dbHealth = checkDatabaseHealth() + val blockchainHealth = checkBlockchainHealth() + + // Additional checks + val memoryUsage = getMemoryUsage() + val diskSpace = getDiskSpace() + val networkStats = getNetworkStats() + + call.respond(mapOf( + "status" to if (dbHealth.connected) "healthy" else "unhealthy", + "version" to "1.0.0", + "uptime" to ManagementFactory.getRuntimeMXBean().uptime, + "timestamp" to System.currentTimeMillis(), + "database" to dbHealth, + "blockchain" to blockchainHealth, + "system" to mapOf( + "memory" to memoryUsage, + "diskSpace" to diskSpace + ), + "network" to networkStats, + "config" to ServerConfig.getServerInfo() + )) + + } catch (e: Exception) { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf( + "status" to "unhealthy", + "error" to (e.message ?: "Detailed health check failed") + )) + } + } + + /** Database health check */ + get("/health/database") { + try { + val dbHealth = checkDatabaseHealth() + val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable + call.respond(statusCode, dbHealth) + + } catch (e: Exception) { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf( + "connected" to false, + "error" to (e.message ?: "Database health check failed") + )) + } + } + + /** Blockchain health check */ + get("/health/blockchain") { + try { + val blockchainHealth = checkBlockchainHealth() + call.respond(blockchainHealth) + + } catch (e: Exception) { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf( + "error" to (e.message ?: "Blockchain health check failed") + )) + } + } + + /** Readiness probe (for Kubernetes) */ + get("/ready") { + try { + val dbHealth = checkDatabaseHealth() + + if (dbHealth.connected) { + call.respond(HttpStatusCode.OK, mapOf("status" to "ready")) + } else { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf("status" to "not ready")) + } + + } catch (e: Exception) { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf( + "status" to "not ready", + "error" to (e.message ?: "Readiness check failed") + )) + } + } + + /** Liveness probe (for Kubernetes) */ + get("/live") { + try { + // Simple liveness check - just return OK if the service is running + call.respond(HttpStatusCode.OK, mapOf("status" to "alive")) + + } catch (e: Exception) { + call.respond(HttpStatusCode.ServiceUnavailable, mapOf( + "status" to "dead", + "error" to (e.message ?: "Liveness check failed") + )) + } + } + + /** Service version and build info */ + get("/version") { + try { + call.respond(mapOf( + "version" to "1.0.0", + "buildTime" to System.getProperty("build.time", "unknown"), + "gitCommit" to System.getProperty("git.commit", "unknown"), + "javaVersion" to System.getProperty("java.version"), + "kotlinVersion" to "2.2.21" + )) + + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf( + "error" to (e.message ?: "Version check failed") + )) + } + } +} + +/** Check database health */ +private fun checkDatabaseHealth(): DatabaseHealth { + return try { + val startTime = System.currentTimeMillis() + + transaction { + // Simple query to test connectivity + WalletService.getTotalWalletCount() + } + + val responseTime = System.currentTimeMillis() - startTime + + DatabaseHealth( + connected = true, + responseTime = responseTime, + activeConnections = 0, // Would need HikariCP integration to get real numbers + maxConnections = 20 + ) + + } catch (e: Exception) { + DatabaseHealth( + connected = false, + responseTime = -1, + activeConnections = 0, + maxConnections = 20 + ) + } +} + +/** Check blockchain health */ +private fun checkBlockchainHealth(): BlockchainHealth { + return try { + val latestBlock = BlockService.getLatestBlockHeight() + val pendingTransactions = TransactionService.getPendingTransactions().size + val networkHashRate = MiningService.getNetworkHashRate() + val averageBlockTime = BlockService.getAverageBlockTime() + + BlockchainHealth( + latestBlock = latestBlock, + pendingTransactions = pendingTransactions, + networkHashRate = networkHashRate, + averageBlockTime = averageBlockTime + ) + + } catch (e: Exception) { + BlockchainHealth( + latestBlock = 0, + pendingTransactions = 0, + networkHashRate = 0.0, + averageBlockTime = 0L + ) + } +} + +/** Get memory usage information */ +private fun getMemoryUsage(): Map { + val runtime = Runtime.getRuntime() + val maxMemory = runtime.maxMemory() + val totalMemory = runtime.totalMemory() + val freeMemory = runtime.freeMemory() + val usedMemory = totalMemory - freeMemory + + return mapOf( + "maxMemory" to maxMemory, + "totalMemory" to totalMemory, + "usedMemory" to usedMemory, + "freeMemory" to freeMemory, + "usagePercentage" to ((usedMemory.toDouble() / maxMemory) * 100).toInt() + ) +} + +/** Get disk space information */ +private fun getDiskSpace(): Map { + return try { + val file = java.io.File(".") + val totalSpace = file.totalSpace + val freeSpace = file.freeSpace + val usedSpace = totalSpace - freeSpace + + mapOf( + "totalSpace" to totalSpace, + "freeSpace" to freeSpace, + "usedSpace" to usedSpace, + "usagePercentage" to ((usedSpace.toDouble() / totalSpace) * 100).toInt() + ) + } catch (e: Exception) { + mapOf("error" to "Unable to get disk space information") + } +} + +/** Get network statistics */ +private fun getNetworkStats(): Map { + return try { + val totalBlocks = BlockService.getTotalBlockCount() + val totalTransactions = TransactionService.getTotalTransactionCount() + val totalWallets = WalletService.getTotalWalletCount() + val totalSupply = WalletService.getTotalSupply() + + mapOf( + "totalBlocks" to totalBlocks, + "totalTransactions" to totalTransactions, + "totalWallets" to totalWallets, + "totalSupply" to totalSupply + ) + } catch (e: Exception) { + mapOf("error" to "Unable to get network statistics") + } +} +