feat: fixed some route serialization
This commit is contained in:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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