Compare commits
4 Commits
89e45128b6
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f4349c9d2 | ||
|
|
194fd7357c | ||
|
|
9a644b689a | ||
|
|
9bc861f1d1 |
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,7 @@ object Tables {
|
||||
val address = varchar("address", 64) // Format random_word:random_6_digits
|
||||
val balance = decimal("balance", 20, 8).default(BigDecimal.ZERO)
|
||||
val label = varchar("label", 255).nullable()
|
||||
val passwordHash = varchar("password_hash", 64).nullable()
|
||||
val createdAt = long("created_at")
|
||||
val lastActivity = long("last_activity").nullable()
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ data class SendTransactionRequest(
|
||||
val toAddress: String, // Format random_word:random_6_digits
|
||||
val amount: Double,
|
||||
val fee: Double = 0.0,
|
||||
val memo: String? = null
|
||||
val memo: String? = null,
|
||||
val password: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -4,7 +4,8 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CreateWalletRequest(
|
||||
val label: String? = null
|
||||
val label: String? = null,
|
||||
val password: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -12,6 +13,7 @@ data class WalletResponse(
|
||||
val address: String, // Format random_word:random_6_digits (e.g. "phoenix:123456")
|
||||
val balance: Double,
|
||||
val label: String?,
|
||||
val passwordHash: String,
|
||||
val createdAt: Long,
|
||||
val lastActivity: Long?
|
||||
)
|
||||
|
||||
208
server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt
Normal file
208
server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt
Normal file
@@ -0,0 +1,208 @@
|
||||
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 {
|
||||
val routes = mapOf(
|
||||
"wallet" to mapOf(
|
||||
"POST /wallet/create" to "Create a new wallet",
|
||||
"GET /wallet/{address}" to "Get wallet by address",
|
||||
"GET /wallet/{address}/balance" to "Get wallet balance",
|
||||
"PUT /wallet/{address}/label" to "Update wallet label",
|
||||
"GET /wallet/list" to "Get all wallets with pagination",
|
||||
"GET /wallet/rich" to "Get wallets with minimum balance",
|
||||
"GET /wallet/{address}/exists" to "Check if wallet exists"
|
||||
),
|
||||
"transaction" to mapOf(
|
||||
"POST /transaction/send" to "Send a transaction",
|
||||
"GET /transaction/{hash}" to "Get transaction by hash",
|
||||
"GET /transaction/history/{address}" to "Get transaction history for address",
|
||||
"GET /transaction/pending" to "Get pending transactions",
|
||||
"GET /transaction/count/{address}" to "Get transaction count for address",
|
||||
"GET /transaction/list" to "Get all transactions with pagination",
|
||||
"GET /transaction/stats" to "Get network transaction statistics"
|
||||
),
|
||||
"mining" to mapOf(
|
||||
"POST /mining/start" to "Start a mining job",
|
||||
"POST /mining/submit" to "Submit mining result",
|
||||
"GET /mining/difficulty" to "Get current mining difficulty",
|
||||
"GET /mining/stats/{address}" to "Get mining statistics for miner",
|
||||
"GET /mining/network" to "Get network mining statistics",
|
||||
"GET /mining/pending-transactions" to "Get pending transactions for mining",
|
||||
"POST /mining/validate" to "Validate mining job",
|
||||
"GET /mining/leaderboard" to "Get mining leaderboard"
|
||||
),
|
||||
"block" to mapOf(
|
||||
"GET /block/{hash}" to "Get block by hash",
|
||||
"GET /block/height/{height}" to "Get block by height",
|
||||
"GET /block/{hash}/exists" to "Check if block exists",
|
||||
"GET /blocks/latest" to "Get latest blocks",
|
||||
"GET /blocks/range" to "Get blocks in height range",
|
||||
"GET /blocks/miner/{address}" to "Get blocks by miner address",
|
||||
"GET /blocks/time-range" to "Get blocks by timestamp range",
|
||||
"GET /blocks/difficulty/{difficulty}" to "Get blocks by difficulty",
|
||||
"GET /blocks/stats" to "Get blockchain statistics"
|
||||
),
|
||||
"health" to mapOf(
|
||||
"GET /health" to "Basic health check",
|
||||
"GET /health/detailed" to "Detailed health check with system metrics",
|
||||
"GET /health/database" to "Database health check",
|
||||
"GET /health/blockchain" to "Blockchain health check",
|
||||
"GET /ready" to "Readiness probe (Kubernetes)",
|
||||
"GET /live" to "Liveness probe (Kubernetes)",
|
||||
"GET /version" to "Service version and build info"
|
||||
),
|
||||
"api" to mapOf(
|
||||
"GET /api/routes" to "Get all available API routes",
|
||||
"GET /api/docs" to "Get API documentation",
|
||||
"GET /api/examples" to "Get API usage examples"
|
||||
)
|
||||
)
|
||||
|
||||
val summary = 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
|
||||
)
|
||||
|
||||
val response = ApiRoutesResponse(summary, routes)
|
||||
call.respond(response)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf(
|
||||
"error" to (e.message ?: "Failed to get routes information")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get API documentation */
|
||||
get("/docs") {
|
||||
try {
|
||||
call.respond(mapOf(
|
||||
"title" to "CCoin API Documentation",
|
||||
"version" to ServerConfig.version,
|
||||
"description" to "REST API for CCoin cryptocurrency server",
|
||||
"baseUrl" to ServerConfig.baseUrl,
|
||||
"authentication" to "None required",
|
||||
"contentType" to "application/json",
|
||||
"rateLimit" to "100 requests per minute",
|
||||
"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(
|
||||
"error" to (e.message ?: "Failed to get API documentation")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get API usage examples */
|
||||
get("/examples") {
|
||||
try {
|
||||
val examples = mapOf(
|
||||
"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 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 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 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}"""
|
||||
)
|
||||
)
|
||||
|
||||
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(
|
||||
"error" to (e.message ?: "Failed to get API examples")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
@@ -0,0 +1,349 @@
|
||||
package org.ccoin.routes
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.ccoin.services.BlockService
|
||||
import org.ccoin.services.ValidationService
|
||||
import org.ccoin.models.BlockResponse
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
fun Route.blockRoutes() {
|
||||
route("/block") {
|
||||
/** Get block by hash */
|
||||
get("/{hash}") {
|
||||
try {
|
||||
val hash = call.parameters["hash"] ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Block hash parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
// Validate hash format
|
||||
if (!ValidationService.validateBlockHash(hash)) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid block hash format"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val block = BlockService.getBlock(hash)
|
||||
if (block != null) {
|
||||
call.respond(block)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Block not found"))
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get block")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get block by height */
|
||||
get("/height/{height}") {
|
||||
try {
|
||||
val height = call.parameters["height"]?.toIntOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid height parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (height < 0) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Height must be non-negative"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val block = BlockService.getBlockByHeight(height)
|
||||
if (block != null) {
|
||||
call.respond(block)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Block not found at height $height"))
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get block by height")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if block exists */
|
||||
get("/{hash}/exists") {
|
||||
try {
|
||||
val hash = call.parameters["hash"] ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Block hash parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
// Validate hash format
|
||||
if (!ValidationService.validateBlockHash(hash)) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid block hash format"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val exists = BlockService.blockExists(hash)
|
||||
call.respond(
|
||||
BlockExistsResponse(
|
||||
hash = hash,
|
||||
exists = exists
|
||||
)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check block existence")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route("/blocks") {
|
||||
|
||||
/** Get latest blocks */
|
||||
get("/latest") {
|
||||
try {
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10
|
||||
|
||||
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(
|
||||
BlocksLatestResponse(
|
||||
blocks = blocks,
|
||||
count = blocks.size
|
||||
)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get latest blocks")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get blocks in height range */
|
||||
get("/range") {
|
||||
try {
|
||||
val fromHeight = call.request.queryParameters["from"]?.toIntOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'from' height parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val toHeight = call.request.queryParameters["to"]?.toIntOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'to' height parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (fromHeight < 0 || toHeight < 0) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Heights must be non-negative"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (fromHeight > toHeight) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "From height must be less than or equal to to height"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (toHeight - fromHeight > 1000) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Range too large (max 1000 blocks)"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val blockRange = BlockService.getBlocksInRange(fromHeight, toHeight)
|
||||
call.respond(blockRange)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks in range")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get blocks by miner address */
|
||||
get("/miner/{address}") {
|
||||
try {
|
||||
val address = call.parameters["address"] ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
|
||||
val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 50
|
||||
|
||||
// Validate address format
|
||||
if (!ValidationService.validateWalletAddress(address)) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
|
||||
return@get
|
||||
}
|
||||
|
||||
// Validate pagination
|
||||
val validation = ValidationService.validatePagination(page, pageSize)
|
||||
if (!validation.isValid) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to validation.getErrorMessage()))
|
||||
return@get
|
||||
}
|
||||
|
||||
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(
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get blocks by timestamp range */
|
||||
get("/time-range") {
|
||||
try {
|
||||
val fromTime = call.request.queryParameters["from"]?.toLongOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'from' timestamp parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val toTime = call.request.queryParameters["to"]?.toLongOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'to' timestamp parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (fromTime < 0 || toTime < 0) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Timestamps must be non-negative"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (fromTime > toTime) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "From time must be less than or equal to to time"))
|
||||
return@get
|
||||
}
|
||||
|
||||
// Limit to 30 days max
|
||||
if (toTime - fromTime > 30 * 24 * 60 * 60) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Time range too large (max 30 days)"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val blocks = BlockService.getBlocksByTimeRange(fromTime, toTime)
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get blocks by difficulty */
|
||||
get("/difficulty/{difficulty}") {
|
||||
try {
|
||||
val difficulty = call.parameters["difficulty"]?.toIntOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid difficulty parameter required"))
|
||||
return@get
|
||||
}
|
||||
|
||||
if (!ValidationService.validateMiningDifficulty(difficulty)) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid mining difficulty"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val blocks = BlockService.getBlocksByDifficulty(difficulty)
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
/** Get blockchain statistics */
|
||||
get("/stats") {
|
||||
try {
|
||||
val totalBlocks = BlockService.getTotalBlockCount()
|
||||
val latestHeight = BlockService.getLatestBlockHeight()
|
||||
val latestHash = BlockService.getLatestBlockHash()
|
||||
val averageBlockTime = BlockService.getAverageBlockTime()
|
||||
val totalRewards = BlockService.getTotalRewardsDistributed()
|
||||
|
||||
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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
package org.ccoin.routes
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.ccoin.config.DatabaseConfig
|
||||
import org.ccoin.config.ServerConfig
|
||||
import org.ccoin.models.DatabaseHealth
|
||||
import org.ccoin.models.BlockchainHealth
|
||||
import org.ccoin.models.HealthResponse
|
||||
import org.ccoin.services.BlockService
|
||||
import org.ccoin.services.TransactionService
|
||||
import org.ccoin.services.WalletService
|
||||
import org.ccoin.services.MiningService
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.lang.management.ManagementFactory
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
fun Route.healthRoutes() {
|
||||
/** Basic health check */
|
||||
get("/health") {
|
||||
try {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
// Check database connectivity
|
||||
val dbHealth = checkDatabaseHealth()
|
||||
|
||||
// Check blockchain health
|
||||
val blockchainHealth = checkBlockchainHealth()
|
||||
|
||||
val uptime = ManagementFactory.getRuntimeMXBean().uptime
|
||||
|
||||
val health = HealthResponse(
|
||||
status = if (dbHealth.connected) "healthy" else "unhealthy",
|
||||
version = ServerConfig.version,
|
||||
uptime = uptime,
|
||||
database = dbHealth,
|
||||
blockchain = blockchainHealth
|
||||
)
|
||||
|
||||
val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable
|
||||
call.respond(statusCode, health)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||
"status" to "unhealthy",
|
||||
"error" to (e.message ?: "Health check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Detailed health check */
|
||||
get("/health/detailed") {
|
||||
try {
|
||||
val dbHealth = checkDatabaseHealth()
|
||||
val blockchainHealth = checkBlockchainHealth()
|
||||
|
||||
// Additional checks
|
||||
val memoryUsage = getMemoryUsage()
|
||||
val diskSpace = getDiskSpace()
|
||||
val networkStats = getNetworkStats()
|
||||
|
||||
call.respond(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) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||
"status" to "unhealthy",
|
||||
"error" to (e.message ?: "Detailed health check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Database health check */
|
||||
get("/health/database") {
|
||||
try {
|
||||
val dbHealth = checkDatabaseHealth()
|
||||
val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable
|
||||
call.respond(statusCode, dbHealth)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||
"connected" to false,
|
||||
"error" to (e.message ?: "Database health check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Blockchain health check */
|
||||
get("/health/blockchain") {
|
||||
try {
|
||||
val blockchainHealth = checkBlockchainHealth()
|
||||
call.respond(blockchainHealth)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||
"error" to (e.message ?: "Blockchain health check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Readiness probe (for Kubernetes) */
|
||||
get("/ready") {
|
||||
try {
|
||||
val dbHealth = checkDatabaseHealth()
|
||||
|
||||
if (dbHealth.connected) {
|
||||
call.respond(HttpStatusCode.OK, mapOf("status" to "ready"))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf("status" to "not ready"))
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||
"status" to "not ready",
|
||||
"error" to (e.message ?: "Readiness check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Liveness probe (for Kubernetes) */
|
||||
get("/live") {
|
||||
try {
|
||||
// Simple liveness check - just return OK if the service is running
|
||||
call.respond(HttpStatusCode.OK, mapOf("status" to "alive"))
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||
"status" to "dead",
|
||||
"error" to (e.message ?: "Liveness check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** Service version and build info */
|
||||
get("/version") {
|
||||
try {
|
||||
call.respond(mapOf(
|
||||
"version" to ServerConfig.version,
|
||||
"buildTime" to ServerConfig.buildTime,
|
||||
"gitCommit" to System.getProperty("git.commit", "unknown"),
|
||||
"javaVersion" to System.getProperty("java.version"),
|
||||
// "kotlinVersion" to System.getProperty("kotlin_version").toString()
|
||||
))
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf(
|
||||
"error" to (e.message ?: "Version check failed")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Check database health */
|
||||
private fun checkDatabaseHealth(): DatabaseHealth {
|
||||
return try {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
transaction {
|
||||
// Simple query to test connectivity
|
||||
WalletService.getTotalWalletCount()
|
||||
}
|
||||
|
||||
val responseTime = System.currentTimeMillis() - startTime
|
||||
|
||||
DatabaseHealth(
|
||||
connected = true,
|
||||
responseTime = responseTime,
|
||||
activeConnections = 0, // Would need HikariCP integration to get real numbers
|
||||
maxConnections = 20
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
DatabaseHealth(
|
||||
connected = false,
|
||||
responseTime = -1,
|
||||
activeConnections = 0,
|
||||
maxConnections = 20
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Check blockchain health */
|
||||
private fun checkBlockchainHealth(): BlockchainHealth {
|
||||
return try {
|
||||
val latestBlock = BlockService.getLatestBlockHeight()
|
||||
val pendingTransactions = TransactionService.getPendingTransactions().size
|
||||
val networkHashRate = MiningService.getNetworkHashRate()
|
||||
val averageBlockTime = BlockService.getAverageBlockTime()
|
||||
|
||||
BlockchainHealth(
|
||||
latestBlock = latestBlock,
|
||||
pendingTransactions = pendingTransactions,
|
||||
networkHashRate = networkHashRate,
|
||||
averageBlockTime = averageBlockTime
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
BlockchainHealth(
|
||||
latestBlock = 0,
|
||||
pendingTransactions = 0,
|
||||
networkHashRate = 0.0,
|
||||
averageBlockTime = 0L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get memory usage information */
|
||||
private fun getMemoryUsage(): MemoryInfo {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val maxMemory = runtime.maxMemory()
|
||||
val totalMemory = runtime.totalMemory()
|
||||
val freeMemory = runtime.freeMemory()
|
||||
val usedMemory = totalMemory - freeMemory
|
||||
|
||||
return MemoryInfo(
|
||||
maxMemory = maxMemory,
|
||||
totalMemory = totalMemory,
|
||||
usedMemory = usedMemory,
|
||||
freeMemory = freeMemory,
|
||||
usagePercentage = ((usedMemory.toDouble() / maxMemory) * 100).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
/** Get disk space information */
|
||||
private fun getDiskSpace(): DiskInfo {
|
||||
return try {
|
||||
val file = java.io.File(".")
|
||||
val totalSpace = file.totalSpace
|
||||
val freeSpace = file.freeSpace
|
||||
val usedSpace = totalSpace - freeSpace
|
||||
|
||||
DiskInfo(
|
||||
totalSpace = totalSpace,
|
||||
freeSpace = freeSpace,
|
||||
usedSpace = usedSpace,
|
||||
usagePercentage = ((usedSpace.toDouble() / totalSpace) * 100).toInt()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
DiskInfo(0,0,0,0)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get network statistics */
|
||||
private fun getNetworkStats(): NetworkStats {
|
||||
return try {
|
||||
val totalBlocks = BlockService.getTotalBlockCount()
|
||||
val totalTransactions = TransactionService.getTotalTransactionCount()
|
||||
val totalWallets = WalletService.getTotalWalletCount()
|
||||
val totalSupply = WalletService.getTotalSupply()
|
||||
|
||||
NetworkStats(
|
||||
totalBlocks = totalBlocks,
|
||||
totalTransactions = totalTransactions,
|
||||
totalWallets = totalWallets,
|
||||
totalSupply = totalSupply
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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") {
|
||||
@@ -21,6 +24,7 @@ fun Route.transactionRoutes() {
|
||||
request.fromAddress,
|
||||
request.toAddress,
|
||||
request.amount,
|
||||
request.password,
|
||||
request.fee,
|
||||
request.memo
|
||||
)
|
||||
@@ -34,8 +38,9 @@ fun Route.transactionRoutes() {
|
||||
request.fromAddress,
|
||||
request.toAddress,
|
||||
request.amount,
|
||||
request.password,
|
||||
request.fee,
|
||||
request.memo
|
||||
request.memo,
|
||||
)
|
||||
|
||||
call.respond(HttpStatusCode.Created, transaction)
|
||||
@@ -98,19 +103,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 +128,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")))
|
||||
@@ -196,11 +206,13 @@ fun Route.transactionRoutes() {
|
||||
val totalCount = TransactionService.getTotalTransactionCount()
|
||||
val pendingCount = TransactionService.getPendingTransactions().size
|
||||
|
||||
call.respond(mapOf(
|
||||
"totalTransactions" to totalCount,
|
||||
"pendingTransactions" to pendingCount,
|
||||
"confirmedTransactions" to (totalCount - pendingCount)
|
||||
))
|
||||
call.respond(
|
||||
TransactionStatsResponse(
|
||||
totalTransactions = totalCount,
|
||||
pendingTransactions = pendingCount,
|
||||
confirmedTransactions = (totalCount - pendingCount)
|
||||
)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction statistics")))
|
||||
@@ -208,3 +220,23 @@ 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
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TransactionStatsResponse(
|
||||
val totalTransactions: Long,
|
||||
val pendingTransactions: Int,
|
||||
val confirmedTransactions: Long
|
||||
)
|
||||
|
||||
@@ -7,8 +7,11 @@ 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") {
|
||||
@@ -16,7 +19,7 @@ fun Route.walletRoutes() {
|
||||
post("/create") {
|
||||
try {
|
||||
val request = call.receive<CreateWalletRequest>()
|
||||
|
||||
|
||||
// Validate input
|
||||
val validation = ValidationService.validateWalletCreation(request.label)
|
||||
if (!validation.isValid) {
|
||||
@@ -24,7 +27,7 @@ fun Route.walletRoutes() {
|
||||
return@post
|
||||
}
|
||||
|
||||
val wallet = WalletService.createWallet(request.label)
|
||||
val wallet = WalletService.createWallet(request.label, request.password)
|
||||
call.respond(HttpStatusCode.Created, wallet)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to create wallet")))
|
||||
@@ -71,7 +74,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 +131,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 +158,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 +180,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 +188,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
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ object TransactionService {
|
||||
fromAddress: String,
|
||||
toAddress: String,
|
||||
amount: Double,
|
||||
password: String,
|
||||
fee: Double = 0.0,
|
||||
memo: String? = null
|
||||
): TransactionResponse {
|
||||
@@ -27,11 +28,20 @@ object TransactionService {
|
||||
val totalAmount = BigDecimal.valueOf(amount + fee)
|
||||
|
||||
return transaction {
|
||||
// Check if sender wallet exists and has sufficient balance
|
||||
val fromBalance = Tables.Wallets.selectAll()
|
||||
// Check if sender wallet exists
|
||||
val wallet = Tables.Wallets.selectAll()
|
||||
.where { Tables.Wallets.address eq fromAddress }
|
||||
.map { it[Tables.Wallets.balance] }
|
||||
.singleOrNull() ?: throw WalletNotFoundException(fromAddress)
|
||||
|
||||
val storedHash = wallet[Tables.Wallets.passwordHash]
|
||||
?: throw InvalidTransactionException("Wallet has no password set")
|
||||
|
||||
if (!CryptoUtils.verifyPassword(password, storedHash)) {
|
||||
throw InvalidTransactionException("Invalid password")
|
||||
}
|
||||
|
||||
// Check if sender wallet exists and has sufficient balance
|
||||
val fromBalance = wallet[Tables.Wallets.balance]
|
||||
|
||||
if (fromBalance < totalAmount) {
|
||||
throw InsufficientFundsException(fromAddress, amount + fee, fromBalance.toDouble())
|
||||
|
||||
@@ -55,6 +55,7 @@ object ValidationService {
|
||||
fromAddress: String?,
|
||||
toAddress: String,
|
||||
amount: Double,
|
||||
password: String,
|
||||
fee: Double,
|
||||
memo: String?
|
||||
): ValidationResult {
|
||||
|
||||
@@ -12,18 +12,27 @@ import java.time.Instant
|
||||
object WalletService {
|
||||
|
||||
/** Creates a new wallet with optional label */
|
||||
fun createWallet(label: String? = null): WalletResponse {
|
||||
fun createWallet(label: String? = null, password: String): WalletResponse {
|
||||
val address = CryptoUtils.generateWalletAddress()
|
||||
val timestamp = Instant.now().epochSecond
|
||||
val passwordHash = CryptoUtils.hashPassword(password)
|
||||
|
||||
return transaction {
|
||||
Tables.Wallets.insert {
|
||||
it[Tables.Wallets.address] = address
|
||||
it[Tables.Wallets.label] = label
|
||||
it[Tables.Wallets.passwordHash] = passwordHash
|
||||
it[createdAt] = timestamp
|
||||
}
|
||||
|
||||
WalletResponse(address, 0.0, label, timestamp, null)
|
||||
WalletResponse(
|
||||
address = address,
|
||||
balance = 0.0,
|
||||
label = label,
|
||||
passwordHash = passwordHash,
|
||||
createdAt = timestamp,
|
||||
lastActivity = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +44,7 @@ object WalletService {
|
||||
it[Tables.Wallets.address],
|
||||
it[Tables.Wallets.balance].toDouble(),
|
||||
it[Tables.Wallets.label],
|
||||
it[Tables.Wallets.passwordHash].toString(),
|
||||
it[Tables.Wallets.createdAt],
|
||||
it[Tables.Wallets.lastActivity]
|
||||
)
|
||||
@@ -95,6 +105,7 @@ object WalletService {
|
||||
it[Tables.Wallets.address],
|
||||
it[Tables.Wallets.balance].toDouble(),
|
||||
it[Tables.Wallets.label],
|
||||
it[Tables.Wallets.passwordHash].toString(),
|
||||
it[Tables.Wallets.createdAt],
|
||||
it[Tables.Wallets.lastActivity]
|
||||
)
|
||||
@@ -115,6 +126,7 @@ object WalletService {
|
||||
it[Tables.Wallets.address],
|
||||
it[Tables.Wallets.balance].toDouble(),
|
||||
it[Tables.Wallets.label],
|
||||
it[Tables.Wallets.passwordHash].toString(),
|
||||
it[Tables.Wallets.createdAt],
|
||||
it[Tables.Wallets.lastActivity]
|
||||
)
|
||||
|
||||
@@ -52,6 +52,9 @@ object CryptoUtils {
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
/** Hashes password */
|
||||
fun hashPassword(password: String): String = sha256("ccoin_password_$password")
|
||||
|
||||
/** Generates a transaction hash */
|
||||
fun generateTransactionHash(
|
||||
fromAddress: String?,
|
||||
@@ -120,4 +123,7 @@ object CryptoUtils {
|
||||
|
||||
/** Validates block has format */
|
||||
fun isValidBlockHash(hash: String): Boolean = hash.length == 64 && hash.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
|
||||
|
||||
/** Verifies password hash */
|
||||
fun verifyPassword(password: String, storedHash: String): Boolean = hashPassword(password) == storedHash
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
ktor {
|
||||
deployment {
|
||||
port = 8080
|
||||
port = ${?PORT}
|
||||
}
|
||||
application {
|
||||
modules = [ org.ccoin.ServerKt.module]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user