feat: fixed some route serialization

This commit is contained in:
darwincereska
2025-12-18 19:48:50 -05:00
parent 9a644b689a
commit 194fd7357c
13 changed files with 718 additions and 253 deletions

View File

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

View File

@@ -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

View File

@@ -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<WalletNotFoundException> { call, cause ->
call.respond(
HttpStatusCode.NotFound,
mapOf(
"error" to cause.message,
"code" to "WALLET_NOT_FOUND",
"address" to cause.address
)
)
}
exception<InsufficientFundsException> { 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<InvalidTransactionException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to cause.message,
"code" to "INVALID_TRANSACTION",
"transactionHash" to cause.transactionHash
)
)
}
exception<CCoinException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to cause.message,
"code" to cause.errorCode
)
)
}
exception<IllegalArgumentException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to (cause.message ?: "Invalid argument"),
"code" to "INVALID_ARGUMENT"
)
)
}
exception<Throwable> { 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<Endpoint>
)
@Serializable
data class Endpoint(
val name: String,
val route: String
)

View File

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

View File

@@ -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,

View File

@@ -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"
"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}"""
),
"response" to mapOf(
"address" to "phoenix:123456",
"balance" to 0.0,
"label" to "My Wallet",
"createdAt" to 1703097600,
"lastActivity" to null
)
"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"}"""
),
"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"
"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}"""
),
"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
)
"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\"}'",
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}'",
"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<String>,
val baseUrl: String,
val documentation: String,
val version: String
)
@Serializable
data class ApiRoutesResponse(
val summary: ApiSummary,
val routes: Map<String, Map<String, String>>
)
@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<String, ApiExample>,
val curlExamples: Map<String, String>
)
@Serializable
data class PaginationInfo(
val currentPage: Int,
val pageSize: Int,
val totalItems: Long,
val totalPages: Long,
val hasNext: Boolean,
val hasPrevious: Boolean
)

View File

@@ -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<BlockResponse>,
val count: Int
)
@Serializable
data class BlocksMinerResponse(
val blocks: List<BlockResponse>,
val minerAddress: String,
val pagination: PaginationInfo
)
@Serializable
data class BlocksTimeRangeResponse(
val blocks: List<BlockResponse>,
val fromTime: Long,
val toTime: Long,
val count: Int
)
@Serializable
data class BlocksDifficultyResponse(
val blocks: List<BlockResponse>,
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
)

View File

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

View File

@@ -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<String>,
val count: Int
)

View File

@@ -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") {
@@ -99,18 +102,20 @@ fun Route.transactionRoutes() {
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<TransactionResponse>,
val address: String,
val pagination: PaginationInfo
)
@Serializable
data class TransactionPendingResponse(
val transactions: List<TransactionResponse>,
val count: Int
)

View File

@@ -7,15 +7,23 @@ 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<CreateWalletRequest>()
val request = try {
call.receive<CreateWalletRequest>()
} catch (e: Exception) {
// If no body or invalid JSON, create with null label
CreateWalletRequest(null)
}
// Validate input
val validation = ValidationService.validateWalletCreation(request.label)
@@ -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<WalletResponse>,
val pagination: PaginationInfo
)
@Serializable
data class WalletRichResponse(
val wallets: List<WalletResponse>,
val minBalance: Double
)
@Serializable
data class WalletExistsResponse(
val address: String,
val exists: Boolean
)
@Serializable
data class WalletBalanceResponse(
val address: String,
val balance: Double
)

View File

@@ -0,0 +1,9 @@
ktor {
deployment {
port = 8080
port = ${?PORT}
}
application {
modules = [ org.ccoin.ServerKt.module]
}
}

View File

@@ -0,0 +1,15 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> -->
<pattern>%d{MM-dd HH:mm:ss} %-5level - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
<logger name="Exposed" level="INFO"/>
</configuration>