feat: added health and api routes

This commit is contained in:
darwincereska
2025-12-18 09:48:54 -05:00
parent 9bc861f1d1
commit 9a644b689a
2 changed files with 506 additions and 0 deletions

View File

@@ -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")
))
}
}
}
}

View File

@@ -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<String, Any> {
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<String, Any> {
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<String, Any> {
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")
}
}