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 ktor_version: String by project
val kotlin_version: String by project val kotlin_version: String by project
val logback_version: String by project val logback_version: String by project
@@ -13,12 +14,20 @@ plugins {
id("io.ktor.plugin") version "3.3.3" id("io.ktor.plugin") version "3.3.3"
id("com.gradleup.shadow") version "9.3.0" id("com.gradleup.shadow") version "9.3.0"
id("org.flywaydb.flyway") version "11.19.0" id("org.flywaydb.flyway") version "11.19.0"
id("com.github.gmazzo.buildconfig") version "6.0.6"
application application
} }
group = "org.ccoin" group = "org.ccoin"
version = "1.0.0" 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 { application {
mainClass.set("org.ccoin.ServerKt") 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.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.configuration-cache=true org.gradle.configuration-cache=false
# Application # Application
ccoin.version=1.0.0 ccoin.version=1.0.0

View File

@@ -1,5 +1,258 @@
package org.ccoin 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() { 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 { val config = HikariConfig().apply {
driverClassName = "org.postgresql.Driver" 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" username = System.getenv("DATABASE_USER") ?: "ccoin"
password = System.getenv("DATABASE_PASSWORD") ?: "ccoin" password = System.getenv("DATABASE_PASSWORD") ?: "ccoin"
@@ -28,8 +28,7 @@ object DatabaseConfig {
maxLifetime = 1800000 maxLifetime = 1800000
// Performance settings // Performance settings
isAutoCommit = false isAutoCommit = true
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
// Connection validation // Connection validation
connectionTestQuery = "SELECT 1" connectionTestQuery = "SELECT 1"

View File

@@ -1,14 +1,20 @@
package org.ccoin.config package org.ccoin.config
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.ccoin.BuildConfig
object ServerConfig { object ServerConfig {
private val logger = LoggerFactory.getLogger(ServerConfig::class.java) 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 // Server settings
val host: String = System.getenv("SERVER_HOST") ?: "0.0.0.0" val host: String = System.getenv("SERVER_HOST") ?: "0.0.0.0"
val port: Int = System.getenv("SERVER_PORT")?.toIntOrNull() ?: 8080 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 // Mining settings
val miningDifficulty: Int = System.getenv("MINING_DIFFICULTY")?.toIntOrNull() ?: 4 val miningDifficulty: Int = System.getenv("MINING_DIFFICULTY")?.toIntOrNull() ?: 4
@@ -59,7 +65,7 @@ object ServerConfig {
"host" to host, "host" to host,
"port" to port, "port" to port,
"developmentMode" to developmentMode, "developmentMode" to developmentMode,
"version" to "1.0.0", "version" to version,
"miningDifficulty" to miningDifficulty, "miningDifficulty" to miningDifficulty,
"miningReward" to miningReward, "miningReward" to miningReward,
"blockTimeTarget" to blockTimeTarget, "blockTimeTarget" to blockTimeTarget,

View File

@@ -2,11 +2,14 @@ package org.ccoin.routes
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import org.ccoin.config.ServerConfig
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
fun Route.apiRoutes() { fun Route.apiRoutes() {
route("/api") { route("/api") {
/** Get all available API routes */ /** Get all available API routes */
get("/routes") { get("/routes") {
try { try {
@@ -66,18 +69,16 @@ fun Route.apiRoutes() {
) )
) )
val summary = mapOf( val summary = ApiSummary(
"totalEndpoints" to routes.values.sumOf { it.size }, totalEndpoints = routes.values.sumOf { it.size },
"categories" to routes.keys.toList(), categories = routes.keys.toList(),
"baseUrl" to "http://localhost:8080", baseUrl = ServerConfig.baseUrl,
"documentation" to "https://github.com/your-repo/ccoin-server/docs", documentation = "https://github.com/your-repo/ccoin-server/docs",
"version" to "1.0.0" version = ServerConfig.version
) )
call.respond(mapOf( val response = ApiRoutesResponse(summary, routes)
"summary" to summary, call.respond(response)
"routes" to routes
))
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf( call.respond(HttpStatusCode.InternalServerError, mapOf(
@@ -89,42 +90,20 @@ fun Route.apiRoutes() {
/** Get API documentation */ /** Get API documentation */
get("/docs") { get("/docs") {
try { try {
val documentation = mapOf( call.respond(mapOf(
"title" to "CCoin API Documentation", "title" to "CCoin API Documentation",
"version" to "1.0.0", "version" to ServerConfig.version,
"description" to "REST API for CCoin cryptocurrency server", "description" to "REST API for CCoin cryptocurrency server",
"baseUrl" to "http://localhost:8080", "baseUrl" to ServerConfig.baseUrl,
"authentication" to "None required", "authentication" to "None required",
"contentType" to "application/json", "contentType" to "application/json",
"rateLimit" to "100 requests per minute", "rateLimit" to "100 requests per minute",
"sections" to mapOf( "addressFormat" to "random_word:random_6_digits (e.g., phoenix:123456)",
"wallet" to mapOf( "hashFormat" to "64-character hex string",
"description" to "Wallet management endpoints", "defaultDifficulty" to "4",
"addressFormat" to "random_word:random_6_digits (e.g., phoenix:123456)" "defaultReward" to "50.0",
), "memoMaxLength" to "256"
"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) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf( call.respond(HttpStatusCode.InternalServerError, mapOf(
@@ -137,88 +116,46 @@ fun Route.apiRoutes() {
get("/examples") { get("/examples") {
try { try {
val examples = mapOf( val examples = mapOf(
"createWallet" to mapOf( "createWallet" to ApiExample(
"method" to "POST", method = "POST",
"url" to "/wallet/create", url = "/wallet/create",
"body" to mapOf( body = """{"label": "My Wallet"}""",
"label" to "My Wallet" response = """{"address": "phoenix:123456", "balance": 0.0, "label": "My Wallet", "createdAt": 1703097600, "lastActivity": null}"""
), ),
"response" to mapOf( "sendTransaction" to ApiExample(
"address" to "phoenix:123456", method = "POST",
"balance" to 0.0, url = "/transaction/send",
"label" to "My Wallet", body = """{"fromAddress": "phoenix:123456", "toAddress": "dragon:789012", "amount": 10.5, "fee": 0.01, "memo": "Payment for services"}""",
"createdAt" to 1703097600, response = """{"hash": "abc123...", "fromAddress": "phoenix:123456", "toAddress": "dragon:789012", "amount": 10.5, "fee": 0.01, "memo": "Payment for services", "timestamp": 1703097600, "status": "CONFIRMED"}"""
"lastActivity" to null
)
), ),
"sendTransaction" to mapOf( "startMining" to ApiExample(
"method" to "POST", method = "POST",
"url" to "/transaction/send", url = "/mining/start",
"body" to mapOf( body = """{"minerAddress": "tiger:456789", "difficulty": 4}""",
"fromAddress" to "phoenix:123456", response = """{"jobId": "job123", "target": "0000", "difficulty": 4, "previousHash": "def456...", "height": 100, "timestamp": 1703097600, "expiresAt": 1703097900}"""
"toAddress" to "dragon:789012",
"amount" to 10.5,
"fee" to 0.01,
"memo" to "Payment for services"
), ),
"response" to mapOf( "getBlock" to ApiExample(
"hash" to "abc123...", method = "GET",
"fromAddress" to "phoenix:123456", url = "/block/abc123...",
"toAddress" to "dragon:789012", 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}"""
"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( val curlExamples = mapOf(
"title" to "CCoin API Examples", "createWallet" to """curl -X POST http://localhost:8080/wallet/create -H 'Content-Type: application/json' -d '{"label":"My Wallet"}'""",
"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", "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" "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) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf( 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 io.ktor.server.routing.*
import org.ccoin.services.BlockService import org.ccoin.services.BlockService
import org.ccoin.services.ValidationService import org.ccoin.services.ValidationService
import org.ccoin.models.BlockResponse
import kotlinx.serialization.Serializable
fun Route.blockRoutes() { fun Route.blockRoutes() {
route("/block") { route("/block") {
@@ -75,10 +77,12 @@ fun Route.blockRoutes() {
} }
val exists = BlockService.blockExists(hash) val exists = BlockService.blockExists(hash)
call.respond(mapOf( call.respond(
"hash" to hash, BlockExistsResponse(
"exists" to exists hash = hash,
)) exists = exists
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check block existence"))) call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check block existence")))
@@ -93,16 +97,18 @@ fun Route.blockRoutes() {
try { try {
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10 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")) call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 100"))
return@get return@get
} }
val blocks = BlockService.getLatestBlocks(limit) val blocks = BlockService.getLatestBlocks(limit)
call.respond(mapOf( call.respond(
"blocks" to blocks, BlocksLatestResponse(
"count" to blocks.size blocks = blocks,
)) count = blocks.size
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get latest blocks"))) 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 offset = (page - 1) * pageSize
val blocks = BlockService.getBlocksByMiner(address, pageSize, offset) val blocks = BlockService.getBlocksByMiner(address, pageSize, offset)
call.respond(mapOf( // call.respond(mapOf(
"blocks" to blocks, // "blocks" to blocks,
"minerAddress" to address, // "minerAddress" to address,
"pagination" to mapOf( // "pagination" to mapOf(
"currentPage" to page, // "currentPage" to page,
"pageSize" to pageSize, // "pageSize" to pageSize,
"hasNext" to (blocks.size == pageSize), // "hasNext" to (blocks.size == pageSize),
"hasPrevious" to (page > 1) // "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) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by miner"))) 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) val blocks = BlockService.getBlocksByTimeRange(fromTime, toTime)
call.respond(mapOf( call.respond(
"blocks" to blocks, BlocksTimeRangeResponse(
"fromTime" to fromTime, blocks = blocks,
"toTime" to toTime, fromTime = fromTime,
"count" to blocks.size toTime = toTime,
)) count = blocks.size
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by time range"))) 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) val blocks = BlockService.getBlocksByDifficulty(difficulty)
call.respond(mapOf( call.respond(
"blocks" to blocks, BlocksDifficultyResponse(
"difficulty" to difficulty, blocks = blocks,
"count" to blocks.size difficulty = difficulty,
)) count = blocks.size
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by difficulty"))) 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 averageBlockTime = BlockService.getAverageBlockTime()
val totalRewards = BlockService.getTotalRewardsDistributed() val totalRewards = BlockService.getTotalRewardsDistributed()
call.respond(mapOf( call.respond(
"totalBlocks" to totalBlocks, BlocksStatsResponse(
"latestHeight" to latestHeight, totalBlocks = totalBlocks,
"latestHash" to latestHash, latestHeight = latestHeight,
"averageBlockTime" to averageBlockTime, latestHash = latestHash,
"totalRewardsDistributed" to totalRewards averageBlockTime = averageBlockTime,
)) totalRewardsDistributed = totalRewards
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blockchain statistics"))) 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.Database
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import kotlinx.serialization.Serializable
fun Route.healthRoutes() { fun Route.healthRoutes() {
/** Basic health check */ /** Basic health check */
@@ -33,7 +34,7 @@ fun Route.healthRoutes() {
val health = HealthResponse( val health = HealthResponse(
status = if (dbHealth.connected) "healthy" else "unhealthy", status = if (dbHealth.connected) "healthy" else "unhealthy",
version = "1.0.0", version = ServerConfig.version,
uptime = uptime, uptime = uptime,
database = dbHealth, database = dbHealth,
blockchain = blockchainHealth blockchain = blockchainHealth
@@ -61,19 +62,15 @@ fun Route.healthRoutes() {
val diskSpace = getDiskSpace() val diskSpace = getDiskSpace()
val networkStats = getNetworkStats() val networkStats = getNetworkStats()
call.respond(mapOf( call.respond(HealthDetailedResponse(
"status" to if (dbHealth.connected) "healthy" else "unhealthy", status = if (dbHealth.connected) "healthy" else "unhealthy",
"version" to "1.0.0", version = ServerConfig.version,
"uptime" to ManagementFactory.getRuntimeMXBean().uptime, uptime = ManagementFactory.getRuntimeMXBean().uptime,
"timestamp" to System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
"database" to dbHealth, database = dbHealth,
"blockchain" to blockchainHealth, blockchain = blockchainHealth,
"system" to mapOf( system = SystemInfo(memoryUsage, diskSpace),
"memory" to memoryUsage, network = networkStats,
"diskSpace" to diskSpace
),
"network" to networkStats,
"config" to ServerConfig.getServerInfo()
)) ))
} catch (e: Exception) { } catch (e: Exception) {
@@ -149,11 +146,11 @@ fun Route.healthRoutes() {
get("/version") { get("/version") {
try { try {
call.respond(mapOf( call.respond(mapOf(
"version" to "1.0.0", "version" to ServerConfig.version,
"buildTime" to System.getProperty("build.time", "unknown"), "buildTime" to ServerConfig.buildTime,
"gitCommit" to System.getProperty("git.commit", "unknown"), "gitCommit" to System.getProperty("git.commit", "unknown"),
"javaVersion" to System.getProperty("java.version"), "javaVersion" to System.getProperty("java.version"),
"kotlinVersion" to "2.2.21" // "kotlinVersion" to System.getProperty("kotlin_version").toString()
)) ))
} catch (e: Exception) { } catch (e: Exception) {
@@ -219,57 +216,100 @@ private fun checkBlockchainHealth(): BlockchainHealth {
} }
/** Get memory usage information */ /** Get memory usage information */
private fun getMemoryUsage(): Map<String, Any> { private fun getMemoryUsage(): MemoryInfo {
val runtime = Runtime.getRuntime() val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory() val maxMemory = runtime.maxMemory()
val totalMemory = runtime.totalMemory() val totalMemory = runtime.totalMemory()
val freeMemory = runtime.freeMemory() val freeMemory = runtime.freeMemory()
val usedMemory = totalMemory - freeMemory val usedMemory = totalMemory - freeMemory
return mapOf( return MemoryInfo(
"maxMemory" to maxMemory, maxMemory = maxMemory,
"totalMemory" to totalMemory, totalMemory = totalMemory,
"usedMemory" to usedMemory, usedMemory = usedMemory,
"freeMemory" to freeMemory, freeMemory = freeMemory,
"usagePercentage" to ((usedMemory.toDouble() / maxMemory) * 100).toInt() usagePercentage = ((usedMemory.toDouble() / maxMemory) * 100).toInt()
) )
} }
/** Get disk space information */ /** Get disk space information */
private fun getDiskSpace(): Map<String, Any> { private fun getDiskSpace(): DiskInfo {
return try { return try {
val file = java.io.File(".") val file = java.io.File(".")
val totalSpace = file.totalSpace val totalSpace = file.totalSpace
val freeSpace = file.freeSpace val freeSpace = file.freeSpace
val usedSpace = totalSpace - freeSpace val usedSpace = totalSpace - freeSpace
mapOf( DiskInfo(
"totalSpace" to totalSpace, totalSpace = totalSpace,
"freeSpace" to freeSpace, freeSpace = freeSpace,
"usedSpace" to usedSpace, usedSpace = usedSpace,
"usagePercentage" to ((usedSpace.toDouble() / totalSpace) * 100).toInt() usagePercentage = ((usedSpace.toDouble() / totalSpace) * 100).toInt()
) )
} catch (e: Exception) { } catch (e: Exception) {
mapOf("error" to "Unable to get disk space information") DiskInfo(0,0,0,0)
} }
} }
/** Get network statistics */ /** Get network statistics */
private fun getNetworkStats(): Map<String, Any> { private fun getNetworkStats(): NetworkStats {
return try { return try {
val totalBlocks = BlockService.getTotalBlockCount() val totalBlocks = BlockService.getTotalBlockCount()
val totalTransactions = TransactionService.getTotalTransactionCount() val totalTransactions = TransactionService.getTotalTransactionCount()
val totalWallets = WalletService.getTotalWalletCount() val totalWallets = WalletService.getTotalWalletCount()
val totalSupply = WalletService.getTotalSupply() val totalSupply = WalletService.getTotalSupply()
mapOf( NetworkStats(
"totalBlocks" to totalBlocks, totalBlocks = totalBlocks,
"totalTransactions" to totalTransactions, totalTransactions = totalTransactions,
"totalWallets" to totalWallets, totalWallets = totalWallets,
"totalSupply" to totalSupply totalSupply = totalSupply
) )
} catch (e: Exception) { } 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.models.SubmitMiningRequest
import org.ccoin.services.MiningService import org.ccoin.services.MiningService
import org.ccoin.services.ValidationService import org.ccoin.services.ValidationService
import kotlinx.serialization.Serializable
fun Route.miningRoutes() { fun Route.miningRoutes() {
route("/mining") { route("/mining") {
@@ -128,12 +129,14 @@ fun Route.miningRoutes() {
val activeMiners = MiningService.getActiveMinersCount() val activeMiners = MiningService.getActiveMinersCount()
val difficulty = MiningService.getCurrentDifficulty() val difficulty = MiningService.getCurrentDifficulty()
call.respond(mapOf( call.respond(
"networkHashRate" to hashRate, MiningNetworkResponse(
"averageBlockTime" to averageBlockTime, networkHashRate = hashRate,
"activeMiners" to activeMiners, averageBlockTime = averageBlockTime,
"currentDifficulty" to difficulty activeMiners = activeMiners,
)) currentDifficulty = difficulty
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get network statistics"))) call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get network statistics")))
@@ -145,16 +148,18 @@ fun Route.miningRoutes() {
try { try {
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 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")) call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 1000"))
return@get return@get
} }
val transactions = MiningService.getPendingTransactionsForMining(limit) val transactions = MiningService.getPendingTransactionsForMining(limit)
call.respond(mapOf( call.respond(
"transactions" to transactions, MiningPendingTransactionsResponse(
"count" to transactions.size transactions = transactions,
)) count = transactions.size
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get pending transactions"))) 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) val isValid = MiningService.validateMiningJob(jobId, hash, nonce)
call.respond(mapOf( call.respond(
"jobId" to jobId, MiningValidateResponse(
"hash" to hash, jobId = jobId,
"nonce" to nonce, hash = hash,
"isValid" to isValid nonce = nonce,
)) isValid = isValid
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to validate mining job"))) call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to validate mining job")))
@@ -209,17 +216,19 @@ fun Route.miningRoutes() {
try { try {
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10 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")) call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 100"))
return@get return@get
} }
// This would need a new service method to get top miners // This would need a new service method to get top miners
// For now, return a placeholder response // For now, return a placeholder response
call.respond(mapOf( call.respond(
"message" to "Leaderboard endpoint - implementation needed", MiningLeaderboardResponse(
"limit" to limit message = "Not implemented",
)) limit = limit
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get mining leaderboard"))) 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.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.ccoin.models.SendTransactionRequest import org.ccoin.models.SendTransactionRequest
import org.ccoin.models.TransactionResponse
import org.ccoin.services.TransactionService import org.ccoin.services.TransactionService
import org.ccoin.services.ValidationService import org.ccoin.services.ValidationService
import kotlinx.serialization.Serializable
import org.ccoin.routes.PaginationInfo
fun Route.transactionRoutes() { fun Route.transactionRoutes() {
route("/transaction") { route("/transaction") {
@@ -99,18 +102,20 @@ fun Route.transactionRoutes() {
val transactions = TransactionService.getTransactionHistory(address, pageSize, offset) val transactions = TransactionService.getTransactionHistory(address, pageSize, offset)
val totalCount = TransactionService.getTransactionCountForAddress(address) val totalCount = TransactionService.getTransactionCountForAddress(address)
call.respond(mapOf( call.respond(
"transactions" to transactions, TransactionHistoryResponse(
"address" to address, transactions = transactions,
"pagination" to mapOf( address = address,
"currentPage" to page, pagination = PaginationInfo(
"pageSize" to pageSize, currentPage = page,
"totalItems" to totalCount, pageSize = pageSize,
"totalPages" to ((totalCount + pageSize - 1) / pageSize), totalItems = totalCount,
"hasNext" to (offset + pageSize < totalCount), totalPages = ((totalCount + pageSize - 1) / pageSize),
"hasPrevious" to (page > 1) hasNext = (offset + pageSize < totalCount),
hasPrevious = (page > 1)
)
)
) )
))
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction history"))) call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction history")))
@@ -121,10 +126,13 @@ fun Route.transactionRoutes() {
get("/pending") { get("/pending") {
try { try {
val transactions = TransactionService.getPendingTransactions() val transactions = TransactionService.getPendingTransactions()
call.respond(mapOf(
"transactions" to transactions, call.respond(
"count" to transactions.size TransactionPendingResponse(
)) transactions = transactions,
count = transactions.size
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get pending transactions"))) 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 io.ktor.server.routing.*
import org.ccoin.models.CreateWalletRequest import org.ccoin.models.CreateWalletRequest
import org.ccoin.models.UpdateWalletRequest import org.ccoin.models.UpdateWalletRequest
import org.ccoin.models.WalletResponse
import org.ccoin.services.ValidationService import org.ccoin.services.ValidationService
import org.ccoin.services.WalletService import org.ccoin.services.WalletService
import kotlinx.serialization.Serializable
import org.ccoin.routes.PaginationInfo
fun Route.walletRoutes() { fun Route.walletRoutes() {
route("/wallet") { route("/wallet") {
/** Create a new wallet */ /** Create a new wallet */
post("/create") { post("/create") {
try { 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 // Validate input
val validation = ValidationService.validateWalletCreation(request.label) val validation = ValidationService.validateWalletCreation(request.label)
@@ -71,7 +79,7 @@ fun Route.walletRoutes() {
} }
val balance = WalletService.getWalletBalance(address) val balance = WalletService.getWalletBalance(address)
call.respond(mapOf("address" to address, "balance" to balance)) call.respond(WalletBalanceResponse(address, balance))
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.NotFound, mapOf("error" to (e.message ?: "Wallet not found"))) 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 offset = (page - 1) * pageSize
val wallets = WalletService.getAllWallets(pageSize, offset) val wallets = WalletService.getAllWallets(pageSize, offset)
val totalCount = WalletService.getTotalWalletCount() val totalCount = WalletService.getTotalWalletCount()
val paginationInfo = PaginationInfo(
call.respond(mapOf( currentPage = page,
"wallets" to wallets, pageSize = pageSize,
"pagination" to mapOf( totalItems = totalCount,
"currentPage" to page, totalPages = (totalCount + pageSize - 1) / pageSize,
"pageSize" to pageSize, hasNext = offset + pageSize < totalCount,
"totalItems" to totalCount, hasPrevious = page > 1
"totalPages" to ((totalCount + pageSize - 1) / pageSize),
"hasNext" to (offset + pageSize < totalCount),
"hasPrevious" to (page > 1)
) )
))
call.respond(WalletListResponse(wallets, paginationInfo))
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get wallets"))) 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) val wallets = WalletService.getWalletsWithBalance(minBalance)
call.respond(mapOf("wallets" to wallets, "minBalance" to minBalance)) call.respond(WalletRichResponse(wallets, minBalance))
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get rich wallets"))) 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) val exists = WalletService.walletExists(address)
call.respond(mapOf("address" to address, "exists" to exists)) call.respond(WalletExistsResponse(address, exists))
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check wallet existence"))) 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>