diff --git a/server/build.gradle.kts b/server/build.gradle.kts index cb4e368..601725c 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -1,3 +1,4 @@ +import java.time.Instant val ktor_version: String by project val kotlin_version: String by project val logback_version: String by project @@ -13,12 +14,20 @@ plugins { id("io.ktor.plugin") version "3.3.3" id("com.gradleup.shadow") version "9.3.0" id("org.flywaydb.flyway") version "11.19.0" + id("com.github.gmazzo.buildconfig") version "6.0.6" application } group = "org.ccoin" version = "1.0.0" +buildConfig { + buildConfigField("String", "VERSION", "\"${project.version}\"") + buildConfigField("BASE_URL", "https://ccoin.darwincereska.dev") + buildConfigField("String", "BUILD_TIME", "\"${Instant.now()}\"") + packageName("org.ccoin") +} + application { mainClass.set("org.ccoin.ServerKt") diff --git a/server/gradle.properties b/server/gradle.properties index 795c5e1..278a6f6 100644 --- a/server/gradle.properties +++ b/server/gradle.properties @@ -19,7 +19,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.caching=true org.gradle.parallel=true -org.gradle.configuration-cache=true +org.gradle.configuration-cache=false # Application ccoin.version=1.0.0 diff --git a/server/src/main/kotlin/org/ccoin/Server.kt b/server/src/main/kotlin/org/ccoin/Server.kt index d10a9f6..b1d725f 100644 --- a/server/src/main/kotlin/org/ccoin/Server.kt +++ b/server/src/main/kotlin/org/ccoin/Server.kt @@ -1,5 +1,258 @@ package org.ccoin +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.calllogging.* +import io.ktor.server.plugins.compression.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.defaultheaders.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.json.Json +import org.ccoin.config.DatabaseConfig +import org.ccoin.config.ServerConfig +import org.ccoin.exceptions.CCoinException +import org.ccoin.exceptions.InsufficientFundsException +import org.ccoin.exceptions.InvalidTransactionException +import org.ccoin.exceptions.WalletNotFoundException +import org.ccoin.routes.* +import org.slf4j.LoggerFactory +import org.slf4j.event.Level +import kotlinx.serialization.Serializable + fun main() { - println("CCoin Server Started") + val logger = LoggerFactory.getLogger("CCoinServer") + + try { + logger.info("Starting CCoin Server...") + + // Validate configuration + ServerConfig.validateConfig() + + // Initialize database + DatabaseConfig.init() + + // Start server + embeddedServer( + Netty, + port = ServerConfig.port, + host = ServerConfig.host, + module = Application::module + ).start(wait = true) + + } catch (e: Exception) { + logger.error("Failed to start CCoin Server", e) + throw e + } } + +fun Application.module() { + val logger = LoggerFactory.getLogger("CCoinServer") + + logger.info("Configuring CCoin Server modules...") + + configureSerialization() + configureHTTP() + configureStatusPages() + configureRouting() + + logger.info("CCoin Server started successfully on ${ServerConfig.host}:${ServerConfig.port}") +} + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + encodeDefaults = false + }) + } +} + +fun Application.configureHTTP() { + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + allowHeader(HttpHeaders.ContentType) + allowHeader(HttpHeaders.AccessControlAllowOrigin) + allowCredentials = true + anyHost() // For development - restrict in production + } + + install(CallLogging) { + level = Level.INFO + filter { call -> call.request.uri.startsWith("/") } + format { call -> + val status = call.response.status() + val httpMethod = call.request.httpMethod.value + val userAgent = call.request.headers["User-Agent"] + val uri = call.request.uri + "$status: $httpMethod $uri - $userAgent" + } + } + + install(Compression) { + gzip { + priority = 1.0 + } + deflate { + priority = 10.0 + minimumSize(1024) + } + } + + install(DefaultHeaders) { + header("X-Engine", "Ktor") + header("X-Service", "CCoin") + header("X-Version", ServerConfig.version) + } +} + +fun Application.configureStatusPages() { + install(StatusPages) { + exception { call, cause -> + call.respond( + HttpStatusCode.NotFound, + mapOf( + "error" to cause.message, + "code" to "WALLET_NOT_FOUND", + "address" to cause.address + ) + ) + } + + exception { call, cause -> + call.respond( + HttpStatusCode.BadRequest, + mapOf( + "error" to cause.message, + "code" to "INSUFFICIENT_FUNDS", + "address" to cause.address, + "requested" to cause.requestedAmount, + "available" to cause.availableBalance + ) + ) + } + + exception { call, cause -> + call.respond( + HttpStatusCode.BadRequest, + mapOf( + "error" to cause.message, + "code" to "INVALID_TRANSACTION", + "transactionHash" to cause.transactionHash + ) + ) + } + + exception { call, cause -> + call.respond( + HttpStatusCode.BadRequest, + mapOf( + "error" to cause.message, + "code" to cause.errorCode + ) + ) + } + + exception { call, cause -> + call.respond( + HttpStatusCode.BadRequest, + mapOf( + "error" to (cause.message ?: "Invalid argument"), + "code" to "INVALID_ARGUMENT" + ) + ) + } + + exception { call, cause -> + val logger = LoggerFactory.getLogger("CCoinServer") + logger.error("Unhandled exception", cause) + + call.respond( + HttpStatusCode.InternalServerError, + mapOf( + "error" to if (ServerConfig.developmentMode) { + cause.message ?: "Internal server error" + } else { + "Internal server error" + }, + "code" to "INTERNAL_ERROR" + ) + ) + } + } +} + +fun Application.configureRouting() { + routing { + // Root endpoint + get("/") { + call.respond( + DefaultResponse( + service = "CCoin Server", + version = ServerConfig.version, + status = "running", + timestamp = System.currentTimeMillis(), + endpoints = listOf( + Endpoint("health", "/health"), + Endpoint("api", "/api/routes"), + Endpoint("wallet", "/wallet"), + Endpoint("transaction", "/transaction"), + Endpoint("mining", "/mining"), + Endpoint("block", "/block") + ) + ) + ) + } + + // API routes + walletRoutes() + transactionRoutes() + miningRoutes() + blockRoutes() + healthRoutes() + apiRoutes() + + // Catch-all for undefined routes + route("{...}") { + handle { + call.respond( + HttpStatusCode.NotFound, + mapOf( + "error" to "Endpoint not found", + "code" to "NOT_FOUND", + "path" to call.request.uri, + "method" to call.request.httpMethod.value, + "availableEndpoints" to "/api/routes" + ) + ) + } + } + } +} + +@Serializable +data class DefaultResponse( + val service: String, + val version: String, + val status: String, + val timestamp: Long, + val endpoints: List +) + +@Serializable +data class Endpoint( + val name: String, + val route: String +) diff --git a/server/src/main/kotlin/org/ccoin/config/DatabaseConfig.kt b/server/src/main/kotlin/org/ccoin/config/DatabaseConfig.kt index b03e3dd..992a365 100644 --- a/server/src/main/kotlin/org/ccoin/config/DatabaseConfig.kt +++ b/server/src/main/kotlin/org/ccoin/config/DatabaseConfig.kt @@ -16,7 +16,7 @@ object DatabaseConfig { val config = HikariConfig().apply { driverClassName = "org.postgresql.Driver" - jdbcUrl = System.getenv("DATABASE_URL") ?: "jbdc:postgresql://localhost:5432/ccoin" + jdbcUrl = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/ccoin" username = System.getenv("DATABASE_USER") ?: "ccoin" password = System.getenv("DATABASE_PASSWORD") ?: "ccoin" @@ -28,8 +28,7 @@ object DatabaseConfig { maxLifetime = 1800000 // Performance settings - isAutoCommit = false - transactionIsolation = "TRANSACTION_REPEATABLE_READ" + isAutoCommit = true // Connection validation connectionTestQuery = "SELECT 1" diff --git a/server/src/main/kotlin/org/ccoin/config/ServerConfig.kt b/server/src/main/kotlin/org/ccoin/config/ServerConfig.kt index b18faac..90d279a 100644 --- a/server/src/main/kotlin/org/ccoin/config/ServerConfig.kt +++ b/server/src/main/kotlin/org/ccoin/config/ServerConfig.kt @@ -1,14 +1,20 @@ package org.ccoin.config import org.slf4j.LoggerFactory +import org.ccoin.BuildConfig object ServerConfig { private val logger = LoggerFactory.getLogger(ServerConfig::class.java) + + // App Details + val version: String = BuildConfig.VERSION + val buildTime: String = BuildConfig.BUILD_TIME + val baseUrl: String = BuildConfig.BASE_URL // Server settings val host: String = System.getenv("SERVER_HOST") ?: "0.0.0.0" val port: Int = System.getenv("SERVER_PORT")?.toIntOrNull() ?: 8080 - val developmentMode: Boolean = System.getenv("DEVELOPMENT_MODE")?.toBoolean() ?: false + val developmentMode: Boolean = System.getenv("DEVELOPMENT_MODE")?.toBoolean() ?: true // Mining settings val miningDifficulty: Int = System.getenv("MINING_DIFFICULTY")?.toIntOrNull() ?: 4 @@ -59,7 +65,7 @@ object ServerConfig { "host" to host, "port" to port, "developmentMode" to developmentMode, - "version" to "1.0.0", + "version" to version, "miningDifficulty" to miningDifficulty, "miningReward" to miningReward, "blockTimeTarget" to blockTimeTarget, diff --git a/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt index 34ebd7a..df122b5 100644 --- a/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt @@ -2,11 +2,14 @@ package org.ccoin.routes import io.ktor.http.* import io.ktor.server.application.* +import org.ccoin.config.ServerConfig import io.ktor.server.response.* import io.ktor.server.routing.* +import kotlinx.serialization.Serializable fun Route.apiRoutes() { route("/api") { + /** Get all available API routes */ get("/routes") { try { @@ -66,18 +69,16 @@ fun Route.apiRoutes() { ) ) - 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" + val summary = ApiSummary( + totalEndpoints = routes.values.sumOf { it.size }, + categories = routes.keys.toList(), + baseUrl = ServerConfig.baseUrl, + documentation = "https://github.com/your-repo/ccoin-server/docs", + version = ServerConfig.version ) - call.respond(mapOf( - "summary" to summary, - "routes" to routes - )) + val response = ApiRoutesResponse(summary, routes) + call.respond(response) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf( @@ -89,42 +90,20 @@ fun Route.apiRoutes() { /** Get API documentation */ get("/docs") { try { - val documentation = mapOf( + call.respond(mapOf( "title" to "CCoin API Documentation", - "version" to "1.0.0", + "version" to ServerConfig.version, "description" to "REST API for CCoin cryptocurrency server", - "baseUrl" to "http://localhost:8080", + "baseUrl" to ServerConfig.baseUrl, "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) + "addressFormat" to "random_word:random_6_digits (e.g., phoenix:123456)", + "hashFormat" to "64-character hex string", + "defaultDifficulty" to "4", + "defaultReward" to "50.0", + "memoMaxLength" to "256" + )) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf( @@ -137,88 +116,46 @@ fun Route.apiRoutes() { 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 - ) + "createWallet" to ApiExample( + method = "POST", + url = "/wallet/create", + body = """{"label": "My Wallet"}""", + response = """{"address": "phoenix:123456", "balance": 0.0, "label": "My Wallet", "createdAt": 1703097600, "lastActivity": 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" - ) + "sendTransaction" to ApiExample( + method = "POST", + url = "/transaction/send", + body = """{"fromAddress": "phoenix:123456", "toAddress": "dragon:789012", "amount": 10.5, "fee": 0.01, "memo": "Payment for services"}""", + response = """{"hash": "abc123...", "fromAddress": "phoenix:123456", "toAddress": "dragon:789012", "amount": 10.5, "fee": 0.01, "memo": "Payment for services", "timestamp": 1703097600, "status": "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 - ) + "startMining" to ApiExample( + method = "POST", + url = "/mining/start", + body = """{"minerAddress": "tiger:456789", "difficulty": 4}""", + response = """{"jobId": "job123", "target": "0000", "difficulty": 4, "previousHash": "def456...", "height": 100, "timestamp": 1703097600, "expiresAt": 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 - ) + "getBlock" to ApiExample( + method = "GET", + url = "/block/abc123...", + response = """{"hash": "abc123...", "previousHash": "def456...", "merkleRoot": "ghi789...", "timestamp": 1703097600, "difficulty": 4, "nonce": 12345, "minerAddress": "tiger:456789", "reward": 50.0, "height": 100, "transactionCount": 0, "confirmations": 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" - ) - )) + val curlExamples = 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" + ) + + val response = ApiExamplesResponse( + title = "CCoin API Examples", + description = "Common usage examples for the CCoin API", + examples = examples, + curlExamples = curlExamples + ) + + call.respond(response) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf( @@ -229,3 +166,43 @@ fun Route.apiRoutes() { } } +@Serializable +data class ApiSummary( + val totalEndpoints: Int, + val categories: List, + val baseUrl: String, + val documentation: String, + val version: String +) + +@Serializable +data class ApiRoutesResponse( + val summary: ApiSummary, + val routes: Map> +) + +@Serializable +data class ApiExample( + val method: String, + val url: String, + val body: String? = null, + val response: String +) + +@Serializable +data class ApiExamplesResponse( + val title: String, + val description: String, + val examples: Map, + val curlExamples: Map +) + +@Serializable +data class PaginationInfo( + val currentPage: Int, + val pageSize: Int, + val totalItems: Long, + val totalPages: Long, + val hasNext: Boolean, + val hasPrevious: Boolean +) diff --git a/server/src/main/kotlin/org/ccoin/routes/BlockRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/BlockRoutes.kt index 98e1450..07a13b0 100644 --- a/server/src/main/kotlin/org/ccoin/routes/BlockRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/BlockRoutes.kt @@ -6,6 +6,8 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import org.ccoin.services.BlockService import org.ccoin.services.ValidationService +import org.ccoin.models.BlockResponse +import kotlinx.serialization.Serializable fun Route.blockRoutes() { route("/block") { @@ -75,10 +77,12 @@ fun Route.blockRoutes() { } val exists = BlockService.blockExists(hash) - call.respond(mapOf( - "hash" to hash, - "exists" to exists - )) + call.respond( + BlockExistsResponse( + hash = hash, + exists = exists + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check block existence"))) @@ -93,16 +97,18 @@ fun Route.blockRoutes() { try { val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10 - if (limit in 0..100) { + if (limit !in 1..100) { call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 100")) return@get } val blocks = BlockService.getLatestBlocks(limit) - call.respond(mapOf( - "blocks" to blocks, - "count" to blocks.size - )) + call.respond( + BlocksLatestResponse( + blocks = blocks, + count = blocks.size + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get latest blocks"))) @@ -172,16 +178,30 @@ fun Route.blockRoutes() { val offset = (page - 1) * pageSize val blocks = BlockService.getBlocksByMiner(address, pageSize, offset) - call.respond(mapOf( - "blocks" to blocks, - "minerAddress" to address, - "pagination" to mapOf( - "currentPage" to page, - "pageSize" to pageSize, - "hasNext" to (blocks.size == pageSize), - "hasPrevious" to (page > 1) + // call.respond(mapOf( + // "blocks" to blocks, + // "minerAddress" to address, + // "pagination" to mapOf( + // "currentPage" to page, + // "pageSize" to pageSize, + // "hasNext" to (blocks.size == pageSize), + // "hasPrevious" to (page > 1) + // ) + // )) + call.respond( + BlocksMinerResponse( + blocks = blocks, + minerAddress = address, + pagination = PaginationInfo( + currentPage = page, + pageSize = pageSize, + hasNext = (blocks.size == pageSize), + hasPrevious = (page > 1), + totalPages = blocks.size.toLong(), + totalItems = blocks.size.toLong() + ) ) - )) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by miner"))) @@ -218,12 +238,14 @@ fun Route.blockRoutes() { } val blocks = BlockService.getBlocksByTimeRange(fromTime, toTime) - call.respond(mapOf( - "blocks" to blocks, - "fromTime" to fromTime, - "toTime" to toTime, - "count" to blocks.size - )) + call.respond( + BlocksTimeRangeResponse( + blocks = blocks, + fromTime = fromTime, + toTime = toTime, + count = blocks.size + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by time range"))) @@ -244,11 +266,13 @@ fun Route.blockRoutes() { } val blocks = BlockService.getBlocksByDifficulty(difficulty) - call.respond(mapOf( - "blocks" to blocks, - "difficulty" to difficulty, - "count" to blocks.size - )) + call.respond( + BlocksDifficultyResponse( + blocks = blocks, + difficulty = difficulty, + count = blocks.size + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by difficulty"))) @@ -264,13 +288,15 @@ fun Route.blockRoutes() { val averageBlockTime = BlockService.getAverageBlockTime() val totalRewards = BlockService.getTotalRewardsDistributed() - call.respond(mapOf( - "totalBlocks" to totalBlocks, - "latestHeight" to latestHeight, - "latestHash" to latestHash, - "averageBlockTime" to averageBlockTime, - "totalRewardsDistributed" to totalRewards - )) + call.respond( + BlocksStatsResponse( + totalBlocks = totalBlocks, + latestHeight = latestHeight, + latestHash = latestHash, + averageBlockTime = averageBlockTime, + totalRewardsDistributed = totalRewards + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blockchain statistics"))) @@ -278,3 +304,46 @@ fun Route.blockRoutes() { } } } + +@Serializable +data class BlockExistsResponse( + val hash: String, + val exists: Boolean +) + +@Serializable +data class BlocksLatestResponse( + val blocks: List, + val count: Int +) + +@Serializable +data class BlocksMinerResponse( + val blocks: List, + val minerAddress: String, + val pagination: PaginationInfo +) + +@Serializable +data class BlocksTimeRangeResponse( + val blocks: List, + val fromTime: Long, + val toTime: Long, + val count: Int +) + +@Serializable +data class BlocksDifficultyResponse( + val blocks: List, + val difficulty: Int, + val count: Int +) + +@Serializable +data class BlocksStatsResponse( + val totalBlocks: Long, + val latestHeight: Int, + val latestHash: String, + val averageBlockTime: Long, + val totalRewardsDistributed: Double +) diff --git a/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt index 14d3890..e773db4 100644 --- a/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/HealthRoutes.kt @@ -16,6 +16,7 @@ import org.ccoin.services.MiningService import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.transactions.transaction import java.lang.management.ManagementFactory +import kotlinx.serialization.Serializable fun Route.healthRoutes() { /** Basic health check */ @@ -33,7 +34,7 @@ fun Route.healthRoutes() { val health = HealthResponse( status = if (dbHealth.connected) "healthy" else "unhealthy", - version = "1.0.0", + version = ServerConfig.version, uptime = uptime, database = dbHealth, blockchain = blockchainHealth @@ -61,19 +62,15 @@ fun Route.healthRoutes() { 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() + call.respond(HealthDetailedResponse( + status = if (dbHealth.connected) "healthy" else "unhealthy", + version = ServerConfig.version, + uptime = ManagementFactory.getRuntimeMXBean().uptime, + timestamp = System.currentTimeMillis(), + database = dbHealth, + blockchain = blockchainHealth, + system = SystemInfo(memoryUsage, diskSpace), + network = networkStats, )) } catch (e: Exception) { @@ -149,11 +146,11 @@ fun Route.healthRoutes() { get("/version") { try { call.respond(mapOf( - "version" to "1.0.0", - "buildTime" to System.getProperty("build.time", "unknown"), + "version" to ServerConfig.version, + "buildTime" to ServerConfig.buildTime, "gitCommit" to System.getProperty("git.commit", "unknown"), "javaVersion" to System.getProperty("java.version"), - "kotlinVersion" to "2.2.21" + // "kotlinVersion" to System.getProperty("kotlin_version").toString() )) } catch (e: Exception) { @@ -219,57 +216,100 @@ private fun checkBlockchainHealth(): BlockchainHealth { } /** Get memory usage information */ -private fun getMemoryUsage(): Map { +private fun getMemoryUsage(): MemoryInfo { 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() + return MemoryInfo( + maxMemory = maxMemory, + totalMemory = totalMemory, + usedMemory = usedMemory, + freeMemory = freeMemory, + usagePercentage = ((usedMemory.toDouble() / maxMemory) * 100).toInt() ) } /** Get disk space information */ -private fun getDiskSpace(): Map { +private fun getDiskSpace(): DiskInfo { 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() + DiskInfo( + totalSpace = totalSpace, + freeSpace = freeSpace, + usedSpace = usedSpace, + usagePercentage = ((usedSpace.toDouble() / totalSpace) * 100).toInt() ) } catch (e: Exception) { - mapOf("error" to "Unable to get disk space information") + DiskInfo(0,0,0,0) } } /** Get network statistics */ -private fun getNetworkStats(): Map { +private fun getNetworkStats(): NetworkStats { 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 + NetworkStats( + totalBlocks = totalBlocks, + totalTransactions = totalTransactions, + totalWallets = totalWallets, + totalSupply = totalSupply ) } catch (e: Exception) { - mapOf("error" to "Unable to get network statistics") + NetworkStats(0,0,0,0.0) } } +@Serializable +data class HealthDetailedResponse( + val status: String, + val version: String, + val uptime: Long, + val timestamp: Long, + val database: DatabaseHealth, + val blockchain: BlockchainHealth, + val system: SystemInfo, + val network: NetworkStats, + // val config: Map +) + +@Serializable +data class MemoryInfo( + val maxMemory: Long, + val totalMemory: Long, + val usedMemory: Long, + val freeMemory: Long, + val usagePercentage: Int +) + +@Serializable +data class DiskInfo( + val totalSpace: Long, + val freeSpace: Long, + val usedSpace: Long, + val usagePercentage: Int +) + +@Serializable +data class NetworkStats( + val totalBlocks: Long, + val totalTransactions: Long, + val totalWallets: Long, + val totalSupply: Double +) + +@Serializable +data class SystemInfo( + val memory: MemoryInfo, + val disk: DiskInfo +) diff --git a/server/src/main/kotlin/org/ccoin/routes/MiningRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/MiningRoutes.kt index fbf87b0..7b2bcba 100644 --- a/server/src/main/kotlin/org/ccoin/routes/MiningRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/MiningRoutes.kt @@ -9,6 +9,7 @@ import org.ccoin.models.StartMiningRequest import org.ccoin.models.SubmitMiningRequest import org.ccoin.services.MiningService import org.ccoin.services.ValidationService +import kotlinx.serialization.Serializable fun Route.miningRoutes() { route("/mining") { @@ -128,12 +129,14 @@ fun Route.miningRoutes() { val activeMiners = MiningService.getActiveMinersCount() val difficulty = MiningService.getCurrentDifficulty() - call.respond(mapOf( - "networkHashRate" to hashRate, - "averageBlockTime" to averageBlockTime, - "activeMiners" to activeMiners, - "currentDifficulty" to difficulty - )) + call.respond( + MiningNetworkResponse( + networkHashRate = hashRate, + averageBlockTime = averageBlockTime, + activeMiners = activeMiners, + currentDifficulty = difficulty + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get network statistics"))) @@ -145,16 +148,18 @@ fun Route.miningRoutes() { try { val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 - if (limit in 0..1000) { + if (limit !in 1..1000) { call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 1000")) return@get } val transactions = MiningService.getPendingTransactionsForMining(limit) - call.respond(mapOf( - "transactions" to transactions, - "count" to transactions.size - )) + call.respond( + MiningPendingTransactionsResponse( + transactions = transactions, + count = transactions.size + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get pending transactions"))) @@ -192,12 +197,14 @@ fun Route.miningRoutes() { } val isValid = MiningService.validateMiningJob(jobId, hash, nonce) - call.respond(mapOf( - "jobId" to jobId, - "hash" to hash, - "nonce" to nonce, - "isValid" to isValid - )) + call.respond( + MiningValidateResponse( + jobId = jobId, + hash = hash, + nonce = nonce, + isValid = isValid + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to validate mining job"))) @@ -209,17 +216,19 @@ fun Route.miningRoutes() { try { val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10 - if (limit in 0..100) { + if (limit !in 0..100) { call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 100")) return@get } // This would need a new service method to get top miners // For now, return a placeholder response - call.respond(mapOf( - "message" to "Leaderboard endpoint - implementation needed", - "limit" to limit - )) + call.respond( + MiningLeaderboardResponse( + message = "Not implemented", + limit = limit + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get mining leaderboard"))) @@ -227,3 +236,31 @@ fun Route.miningRoutes() { } } } + +@Serializable +data class MiningNetworkResponse( + val networkHashRate: Double, + val averageBlockTime: Long, + val activeMiners: Int, + val currentDifficulty: Int +) + +@Serializable +data class MiningLeaderboardResponse( + val message: String, + val limit: Int +) + +@Serializable +data class MiningValidateResponse( + val jobId: String, + val hash: String, + val nonce: Long, + val isValid: Boolean +) + +@Serializable +data class MiningPendingTransactionsResponse( + val transactions: List, + val count: Int +) diff --git a/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt index 2b17c84..fcbf900 100644 --- a/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt @@ -6,8 +6,11 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.ccoin.models.SendTransactionRequest +import org.ccoin.models.TransactionResponse import org.ccoin.services.TransactionService import org.ccoin.services.ValidationService +import kotlinx.serialization.Serializable +import org.ccoin.routes.PaginationInfo fun Route.transactionRoutes() { route("/transaction") { @@ -98,19 +101,21 @@ fun Route.transactionRoutes() { val offset = (page - 1) * pageSize val transactions = TransactionService.getTransactionHistory(address, pageSize, offset) val totalCount = TransactionService.getTransactionCountForAddress(address) - - call.respond(mapOf( - "transactions" to transactions, - "address" to address, - "pagination" to mapOf( - "currentPage" to page, - "pageSize" to pageSize, - "totalItems" to totalCount, - "totalPages" to ((totalCount + pageSize - 1) / pageSize), - "hasNext" to (offset + pageSize < totalCount), - "hasPrevious" to (page > 1) + + call.respond( + TransactionHistoryResponse( + transactions = transactions, + address = address, + pagination = PaginationInfo( + currentPage = page, + pageSize = pageSize, + totalItems = totalCount, + totalPages = ((totalCount + pageSize - 1) / pageSize), + hasNext = (offset + pageSize < totalCount), + hasPrevious = (page > 1) + ) ) - )) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction history"))) @@ -121,10 +126,13 @@ fun Route.transactionRoutes() { get("/pending") { try { val transactions = TransactionService.getPendingTransactions() - call.respond(mapOf( - "transactions" to transactions, - "count" to transactions.size - )) + + call.respond( + TransactionPendingResponse( + transactions = transactions, + count = transactions.size + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get pending transactions"))) @@ -208,3 +216,16 @@ fun Route.transactionRoutes() { } } } + +@Serializable +data class TransactionHistoryResponse( + val transactions: List, + val address: String, + val pagination: PaginationInfo +) + +@Serializable +data class TransactionPendingResponse( + val transactions: List, + val count: Int +) diff --git a/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt index 734cb1d..220f86a 100644 --- a/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt @@ -7,16 +7,24 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import org.ccoin.models.CreateWalletRequest import org.ccoin.models.UpdateWalletRequest +import org.ccoin.models.WalletResponse import org.ccoin.services.ValidationService import org.ccoin.services.WalletService +import kotlinx.serialization.Serializable +import org.ccoin.routes.PaginationInfo fun Route.walletRoutes() { route("/wallet") { /** Create a new wallet */ post("/create") { try { - val request = call.receive() - + val request = try { + call.receive() + } catch (e: Exception) { + // If no body or invalid JSON, create with null label + CreateWalletRequest(null) + } + // Validate input val validation = ValidationService.validateWalletCreation(request.label) if (!validation.isValid) { @@ -71,7 +79,7 @@ fun Route.walletRoutes() { } val balance = WalletService.getWalletBalance(address) - call.respond(mapOf("address" to address, "balance" to balance)) + call.respond(WalletBalanceResponse(address, balance)) } catch (e: Exception) { call.respond(HttpStatusCode.NotFound, mapOf("error" to (e.message ?: "Wallet not found"))) @@ -128,18 +136,16 @@ fun Route.walletRoutes() { val offset = (page - 1) * pageSize val wallets = WalletService.getAllWallets(pageSize, offset) val totalCount = WalletService.getTotalWalletCount() - - call.respond(mapOf( - "wallets" to wallets, - "pagination" to mapOf( - "currentPage" to page, - "pageSize" to pageSize, - "totalItems" to totalCount, - "totalPages" to ((totalCount + pageSize - 1) / pageSize), - "hasNext" to (offset + pageSize < totalCount), - "hasPrevious" to (page > 1) - ) - )) + val paginationInfo = PaginationInfo( + currentPage = page, + pageSize = pageSize, + totalItems = totalCount, + totalPages = (totalCount + pageSize - 1) / pageSize, + hasNext = offset + pageSize < totalCount, + hasPrevious = page > 1 + ) + + call.respond(WalletListResponse(wallets, paginationInfo)) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get wallets"))) @@ -157,7 +163,7 @@ fun Route.walletRoutes() { } val wallets = WalletService.getWalletsWithBalance(minBalance) - call.respond(mapOf("wallets" to wallets, "minBalance" to minBalance)) + call.respond(WalletRichResponse(wallets, minBalance)) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get rich wallets"))) @@ -179,7 +185,7 @@ fun Route.walletRoutes() { } val exists = WalletService.walletExists(address) - call.respond(mapOf("address" to address, "exists" to exists)) + call.respond(WalletExistsResponse(address, exists)) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check wallet existence"))) @@ -187,3 +193,27 @@ fun Route.walletRoutes() { } } } + +@Serializable +data class WalletListResponse( + val wallets: List, + val pagination: PaginationInfo +) + +@Serializable +data class WalletRichResponse( + val wallets: List, + val minBalance: Double +) + +@Serializable +data class WalletExistsResponse( + val address: String, + val exists: Boolean +) + +@Serializable +data class WalletBalanceResponse( + val address: String, + val balance: Double +) diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf index e69de29..3ad67f8 100644 --- a/server/src/main/resources/application.conf +++ b/server/src/main/resources/application.conf @@ -0,0 +1,9 @@ +ktor { + deployment { + port = 8080 + port = ${?PORT} + } + application { + modules = [ org.ccoin.ServerKt.module] + } +} diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index e69de29..89d89e0 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{MM-dd HH:mm:ss} %-5level - %msg%n + + + + + + + + + +