Compare commits

8 Commits

Author SHA1 Message Date
darwincereska
3f4349c9d2 feat: first release 2025-12-19 10:14:05 -05:00
darwincereska
194fd7357c feat: fixed some route serialization 2025-12-18 19:48:50 -05:00
darwincereska
9a644b689a feat: added health and api routes 2025-12-18 09:48:54 -05:00
darwincereska
9bc861f1d1 feat: added block routes 2025-12-18 09:42:28 -05:00
darwincereska
89e45128b6 feat: added mining routes 2025-12-18 09:40:05 -05:00
darwincereska
3c097af03d feat: added transaction routes 2025-12-18 09:37:43 -05:00
darwincereska
35a73c340c feat: added wallet route 2025-12-18 09:34:36 -05:00
darwincereska
1c8fe77a43 feat: added rest of services 2025-12-18 09:22:54 -05:00
22 changed files with 2359 additions and 14 deletions

View File

@@ -1,3 +1,4 @@
import java.time.Instant
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
@@ -13,12 +14,20 @@ plugins {
id("io.ktor.plugin") version "3.3.3"
id("com.gradleup.shadow") version "9.3.0"
id("org.flywaydb.flyway") version "11.19.0"
id("com.github.gmazzo.buildconfig") version "6.0.6"
application
}
group = "org.ccoin"
version = "1.0.0"
buildConfig {
buildConfigField("String", "VERSION", "\"${project.version}\"")
buildConfigField("BASE_URL", "https://ccoin.darwincereska.dev")
buildConfigField("String", "BUILD_TIME", "\"${Instant.now()}\"")
packageName("org.ccoin")
}
application {
mainClass.set("org.ccoin.ServerKt")

View File

@@ -19,6 +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=false
# Application
ccoin.version=1.0.0

View File

@@ -1,5 +1,258 @@
package org.ccoin
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.json.Json
import org.ccoin.config.DatabaseConfig
import org.ccoin.config.ServerConfig
import org.ccoin.exceptions.CCoinException
import org.ccoin.exceptions.InsufficientFundsException
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.exceptions.WalletNotFoundException
import org.ccoin.routes.*
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import kotlinx.serialization.Serializable
fun main() {
println("CCoin Server Started")
val logger = LoggerFactory.getLogger("CCoinServer")
try {
logger.info("Starting CCoin Server...")
// Validate configuration
ServerConfig.validateConfig()
// Initialize database
DatabaseConfig.init()
// Start server
embeddedServer(
Netty,
port = ServerConfig.port,
host = ServerConfig.host,
module = Application::module
).start(wait = true)
} catch (e: Exception) {
logger.error("Failed to start CCoin Server", e)
throw e
}
}
fun Application.module() {
val logger = LoggerFactory.getLogger("CCoinServer")
logger.info("Configuring CCoin Server modules...")
configureSerialization()
configureHTTP()
configureStatusPages()
configureRouting()
logger.info("CCoin Server started successfully on ${ServerConfig.host}:${ServerConfig.port}")
}
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = false
})
}
}
fun Application.configureHTTP() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.AccessControlAllowOrigin)
allowCredentials = true
anyHost() // For development - restrict in production
}
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.uri.startsWith("/") }
format { call ->
val status = call.response.status()
val httpMethod = call.request.httpMethod.value
val userAgent = call.request.headers["User-Agent"]
val uri = call.request.uri
"$status: $httpMethod $uri - $userAgent"
}
}
install(Compression) {
gzip {
priority = 1.0
}
deflate {
priority = 10.0
minimumSize(1024)
}
}
install(DefaultHeaders) {
header("X-Engine", "Ktor")
header("X-Service", "CCoin")
header("X-Version", ServerConfig.version)
}
}
fun Application.configureStatusPages() {
install(StatusPages) {
exception<WalletNotFoundException> { call, cause ->
call.respond(
HttpStatusCode.NotFound,
mapOf(
"error" to cause.message,
"code" to "WALLET_NOT_FOUND",
"address" to cause.address
)
)
}
exception<InsufficientFundsException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to cause.message,
"code" to "INSUFFICIENT_FUNDS",
"address" to cause.address,
"requested" to cause.requestedAmount,
"available" to cause.availableBalance
)
)
}
exception<InvalidTransactionException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to cause.message,
"code" to "INVALID_TRANSACTION",
"transactionHash" to cause.transactionHash
)
)
}
exception<CCoinException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to cause.message,
"code" to cause.errorCode
)
)
}
exception<IllegalArgumentException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to (cause.message ?: "Invalid argument"),
"code" to "INVALID_ARGUMENT"
)
)
}
exception<Throwable> { call, cause ->
val logger = LoggerFactory.getLogger("CCoinServer")
logger.error("Unhandled exception", cause)
call.respond(
HttpStatusCode.InternalServerError,
mapOf(
"error" to if (ServerConfig.developmentMode) {
cause.message ?: "Internal server error"
} else {
"Internal server error"
},
"code" to "INTERNAL_ERROR"
)
)
}
}
}
fun Application.configureRouting() {
routing {
// Root endpoint
get("/") {
call.respond(
DefaultResponse(
service = "CCoin Server",
version = ServerConfig.version,
status = "running",
timestamp = System.currentTimeMillis(),
endpoints = listOf(
Endpoint("health", "/health"),
Endpoint("api", "/api/routes"),
Endpoint("wallet", "/wallet"),
Endpoint("transaction", "/transaction"),
Endpoint("mining", "/mining"),
Endpoint("block", "/block")
)
)
)
}
// API routes
walletRoutes()
transactionRoutes()
miningRoutes()
blockRoutes()
healthRoutes()
apiRoutes()
// Catch-all for undefined routes
route("{...}") {
handle {
call.respond(
HttpStatusCode.NotFound,
mapOf(
"error" to "Endpoint not found",
"code" to "NOT_FOUND",
"path" to call.request.uri,
"method" to call.request.httpMethod.value,
"availableEndpoints" to "/api/routes"
)
)
}
}
}
}
@Serializable
data class DefaultResponse(
val service: String,
val version: String,
val status: String,
val timestamp: Long,
val endpoints: List<Endpoint>
)
@Serializable
data class Endpoint(
val name: String,
val route: String
)

View File

@@ -16,7 +16,7 @@ object DatabaseConfig {
val config = HikariConfig().apply {
driverClassName = "org.postgresql.Driver"
jdbcUrl = System.getenv("DATABASE_URL") ?: "jbdc:postgresql://localhost:5432/ccoin"
jdbcUrl = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/ccoin"
username = System.getenv("DATABASE_USER") ?: "ccoin"
password = System.getenv("DATABASE_PASSWORD") ?: "ccoin"
@@ -28,8 +28,7 @@ object DatabaseConfig {
maxLifetime = 1800000
// Performance settings
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
isAutoCommit = true
// Connection validation
connectionTestQuery = "SELECT 1"

View File

@@ -1,14 +1,20 @@
package org.ccoin.config
import org.slf4j.LoggerFactory
import org.ccoin.BuildConfig
object ServerConfig {
private val logger = LoggerFactory.getLogger(ServerConfig::class.java)
// App Details
val version: String = BuildConfig.VERSION
val buildTime: String = BuildConfig.BUILD_TIME
val baseUrl: String = BuildConfig.BASE_URL
// Server settings
val host: String = System.getenv("SERVER_HOST") ?: "0.0.0.0"
val port: Int = System.getenv("SERVER_PORT")?.toIntOrNull() ?: 8080
val developmentMode: Boolean = System.getenv("DEVELOPMENT_MODE")?.toBoolean() ?: false
val developmentMode: Boolean = System.getenv("DEVELOPMENT_MODE")?.toBoolean() ?: true
// Mining settings
val miningDifficulty: Int = System.getenv("MINING_DIFFICULTY")?.toIntOrNull() ?: 4
@@ -59,7 +65,7 @@ object ServerConfig {
"host" to host,
"port" to port,
"developmentMode" to developmentMode,
"version" to "1.0.0",
"version" to version,
"miningDifficulty" to miningDifficulty,
"miningReward" to miningReward,
"blockTimeTarget" to blockTimeTarget,

View File

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

View File

@@ -25,7 +25,7 @@ data class BlockResponse(
val timestamp: Long,
val difficulty: Int,
val nonce: Long,
val minerAddress: String
val minerAddress: String,
val reward: Double,
val height: Int,
val transactionCount: Int,
@@ -51,3 +51,11 @@ data class MiningStatsResponse(
val lastBlockMined: Long?,
val currentDifficulty: Int
)
@Serializable
data class BlockRangeResponse(
val blocks: List<BlockResponse>,
val totalCount: Int,
val fromHeight: Int,
val toHeight: Int
)

View File

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

View File

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

View 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
)

View File

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

View File

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

View File

@@ -0,0 +1,266 @@
package org.ccoin.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
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") {
/** Start a mining job */
post("/start") {
try {
val request = call.receive<StartMiningRequest>()
// Validate miner address
if (!ValidationService.validateWalletAddress(request.minerAddress)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid miner address format"))
return@post
}
// Validate difficulty if provided
if (request.difficulty != null && !ValidationService.validateMiningDifficulty(request.difficulty)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid mining difficulty"))
return@post
}
val job = MiningService.startMining(request.minerAddress, request.difficulty)
call.respond(HttpStatusCode.Created, job)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to start mining job")))
}
}
/** Sumit mining result */
post("/submit") {
try {
val request = call.receive<SubmitMiningRequest>()
// Validate miner address
if (!ValidationService.validateWalletAddress(request.minerAddress)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid miner address format"))
return@post
}
// Validate hash format
if (!ValidationService.validateBlockHash(request.hash)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid hash format"))
return@post
}
// Validate previous hash format
if (!ValidationService.validateBlockHash(request.previousHash)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid previous hash format"))
return@post
}
// Validate nonce
if (!ValidationService.validateMiningNonce(request.nonce)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid nonce"))
return@post
}
// Validate timestamp
if (!ValidationService.validateTimestamp(request.timestamp)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid timestamp"))
return@post
}
val block = MiningService.submitMiningResult(
request.minerAddress,
request.nonce,
request.hash,
request.previousHash,
request.timestamp
)
call.respond(HttpStatusCode.Created, block)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to (e.message ?: "Mining submission failed")))
}
}
/** Get current mining difficulty */
get("/difficulty") {
try {
val difficulty = MiningService.getCurrentDifficulty()
call.respond(mapOf("difficulty" to difficulty))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get difficulty")))
}
}
/** Get mining statistics for a miner */
get("/stats/{address}") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@get
}
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
return@get
}
val stats = MiningService.getMinerStats(address)
call.respond(stats)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get miner statistics")))
}
}
/** Get network mining statistics */
get("/network") {
try {
val hashRate = MiningService.getNetworkHashRate()
val averageBlockTime = MiningService.getAverageBlockTime()
val activeMiners = MiningService.getActiveMinersCount()
val difficulty = MiningService.getCurrentDifficulty()
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")))
}
}
/** Get pending transactions available for mining */
get("/pending-transactions") {
try {
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
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(
MiningPendingTransactionsResponse(
transactions = transactions,
count = transactions.size
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get pending transactions")))
}
}
/** Validate mining job */
post("/validate") {
try {
val jobId = call.request.queryParameters["jobId"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Job ID parameter required"))
return@post
}
val hash = call.request.queryParameters["hash"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Hash parameter required"))
return@post
}
val nonce = call.request.queryParameters["nonce"]?.toLongOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid nonce parameter required"))
return@post
}
// Validate hash format
if (!ValidationService.validateBlockHash(hash)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid hash format"))
return@post
}
// Validate nonce
if (!ValidationService.validateMiningNonce(nonce)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid nonce"))
return@post
}
val isValid = MiningService.validateMiningJob(jobId, hash, nonce)
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")))
}
}
/** Get mining leaderboard */
get("/leaderboard") {
try {
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10
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(
MiningLeaderboardResponse(
message = "Not implemented",
limit = limit
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get mining leaderboard")))
}
}
}
}
@Serializable
data class MiningNetworkResponse(
val networkHashRate: Double,
val averageBlockTime: Long,
val activeMiners: Int,
val currentDifficulty: Int
)
@Serializable
data class MiningLeaderboardResponse(
val message: String,
val limit: Int
)
@Serializable
data class MiningValidateResponse(
val jobId: String,
val hash: String,
val nonce: Long,
val isValid: Boolean
)
@Serializable
data class MiningPendingTransactionsResponse(
val transactions: List<String>,
val count: Int
)

View File

@@ -0,0 +1,242 @@
package org.ccoin.routes
import io.ktor.http.*
import io.ktor.server.application.*
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") {
/** Send a transaction */
post("/send") {
try {
val request = call.receive<SendTransactionRequest>()
// Validate transaction data
val validation = ValidationService.validateTransaction(
request.fromAddress,
request.toAddress,
request.amount,
request.password,
request.fee,
request.memo
)
if (!validation.isValid) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to validation.getErrorMessage()))
return@post
}
val transaction = TransactionService.sendTransaction(
request.fromAddress,
request.toAddress,
request.amount,
request.password,
request.fee,
request.memo,
)
call.respond(HttpStatusCode.Created, transaction)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to (e.message ?: "Transaction failed")))
}
}
/** Get transaction by hash */
get("/{hash}") {
try {
val hash = call.parameters["hash"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Transaction hash parameter required"))
return@get
}
// Validate hash format
if (!ValidationService.validateTransactionHash(hash)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid transaction hash format"))
return@get
}
val transaction = TransactionService.getTransaction(hash)
if (transaction != null) {
call.respond(transaction)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Transaction not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction")))
}
}
/** Get transaction history for an address */
get("/history/{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 transactions = TransactionService.getTransactionHistory(address, pageSize, offset)
val totalCount = TransactionService.getTransactionCountForAddress(address)
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")))
}
}
/** Get pending transactions */
get("/pending") {
try {
val transactions = TransactionService.getPendingTransactions()
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")))
}
}
/** Get transaction count for address */
get("/count/{address}") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@get
}
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
return@get
}
val count = TransactionService.getTransactionCountForAddress(address)
call.respond(mapOf(
"address" to address,
"transactionCount" to count
))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction count")))
}
}
/** Get all transactions with pagination */
get("/list") {
try {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 50
val status = call.request.queryParameters["status"] // Optional filter
// Validate pagination
val validation = ValidationService.validatePagination(page, pageSize)
if (!validation.isValid) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to validation.getErrorMessage()))
return@get
}
// For now, just get pending transactions as an example
// You could extend this to support status filtering
val transactions = if (status == "pending") {
TransactionService.getPendingTransactions()
} else {
// This would need a new service method for all transactions
TransactionService.getPendingTransactions() // Placeholder
}
call.respond(mapOf(
"transactions" to transactions,
"count" to transactions.size,
"status" to status
))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transactions")))
}
}
/** Get network transaction statistics */
get("/stats") {
try {
val totalCount = TransactionService.getTotalTransactionCount()
val pendingCount = TransactionService.getPendingTransactions().size
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")))
}
}
}
}
@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
)

View File

@@ -0,0 +1,214 @@
package org.ccoin.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.ccoin.models.CreateWalletRequest
import org.ccoin.models.UpdateWalletRequest
import org.ccoin.models.WalletResponse
import org.ccoin.services.ValidationService
import org.ccoin.services.WalletService
import kotlinx.serialization.Serializable
import org.ccoin.routes.PaginationInfo
fun Route.walletRoutes() {
route("/wallet") {
/** Create a new wallet */
post("/create") {
try {
val request = call.receive<CreateWalletRequest>()
// Validate input
val validation = ValidationService.validateWalletCreation(request.label)
if (!validation.isValid) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to validation.getErrorMessage()))
return@post
}
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")))
}
}
/** Get wallet by address */
get("/{address}") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@get
}
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid adress format"))
return@get
}
val wallet = WalletService.getWallet(address)
if (wallet != null) {
call.respond(wallet)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Wallet not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get wallet")))
}
}
/** Get wallet balance */
get("/{address}/balance") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@get
}
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
return@get
}
val balance = WalletService.getWalletBalance(address)
call.respond(WalletBalanceResponse(address, balance))
} catch (e: Exception) {
call.respond(HttpStatusCode.NotFound, mapOf("error" to (e.message ?: "Wallet not found")))
}
}
/** Update wallet label */
put("/{address}/label") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@put
}
val request = call.receive<UpdateWalletRequest>()
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
return@put
}
// Validate label
if (!ValidationService.validateWalletLabel(request.label)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid label"))
return@put
}
val updated = WalletService.updateLabel(address, request.label)
if (updated) {
call.respond(mapOf("message" to "Label updated successfully"))
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Wallet not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to update label")))
}
}
/** Get all wallets with pagination */
get("/list") {
try {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 50
// 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 wallets = WalletService.getAllWallets(pageSize, offset)
val totalCount = WalletService.getTotalWalletCount()
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")))
}
}
/** Get wallets with minimum balance */
get("/rich") {
try {
val minBalance = call.request.queryParameters["minBalance"]?.toDoubleOrNull() ?: 1.0
if (!ValidationService.validateTransactionAmount(minBalance)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid minimum balance"))
return@get
}
val wallets = WalletService.getWalletsWithBalance(minBalance)
call.respond(WalletRichResponse(wallets, minBalance))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get rich wallets")))
}
}
/** Check if wallet exists */
get("/{address}/exists") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@get
}
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
return@get
}
val exists = WalletService.walletExists(address)
call.respond(WalletExistsResponse(address, exists))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check wallet existence")))
}
}
}
}
@Serializable
data class WalletListResponse(
val wallets: List<WalletResponse>,
val pagination: PaginationInfo
)
@Serializable
data class WalletRichResponse(
val wallets: List<WalletResponse>,
val minBalance: Double
)
@Serializable
data class WalletExistsResponse(
val address: String,
val exists: Boolean
)
@Serializable
data class WalletBalanceResponse(
val address: String,
val balance: Double
)

View File

@@ -0,0 +1,218 @@
package org.ccoin.services
import org.ccoin.database.Tables
import org.ccoin.models.BlockResponse
import org.ccoin.models.BlockRangeResponse
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object BlockService {
/** Gets block by hash */
fun getBlock(hash: String): BlockResponse? = transaction {
Tables.Blocks.selectAll().where { Tables.Blocks.hash eq hash }
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}.singleOrNull()
}
/** Gets block by height */
fun getBlockByHeight(height: Int): BlockResponse? = transaction {
Tables.Blocks.selectAll().where { Tables.Blocks.height eq height }
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}.singleOrNull()
}
/** Gets latest blocks */
fun getLatestBlocks(limit: Int = 10): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(limit)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
/** Gets blocks in height range */
fun getBlocksInRange(fromHeight: Int, toHeight: Int): BlockRangeResponse = transaction {
val blocks = Tables.Blocks.selectAll()
.where { Tables.Blocks.height.between(fromHeight, toHeight) }
.orderBy(Tables.Blocks.height, SortOrder.ASC)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
BlockRangeResponse(blocks, blocks.size, fromHeight, toHeight)
}
/** Gets blocks mined by specific address */
fun getBlocksByMiner(minerAddress: String, limit: Int = 50, offset: Int = 0): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.where { Tables.Blocks.minerAddress eq minerAddress }
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(limit)
.offset(offset.toLong())
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
/** Gets total block count */
fun getTotalBlockCount(): Long = transaction {
Tables.Blocks.selectAll().count()
}
/** Gets latest block height */
fun getLatestBlockHeight(): Int = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.height] }
.singleOrNull() ?: 0
}
/** Gets latest block hash */
fun getLatestBlockHash(): String = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.hash] }
.singleOrNull() ?: "0".repeat(64)
}
/** Checks if block exists */
fun blockExists(hash: String): Boolean = transaction {
Tables.Blocks.selectAll().where { Tables.Blocks.hash eq hash }.count() > 0
}
/** Gets blocks by timestamp range */
fun getBlocksByTimeRange(fromTime: Long, toTime: Long): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.where { Tables.Blocks.timestamp.between(fromTime, toTime) }
.orderBy(Tables.Blocks.timestamp, SortOrder.ASC)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
/** Updates block confirmations */
fun updateBlockConfirmations(hash: String, confirmations: Int): Boolean = transaction {
val updated = Tables.Blocks.update({ Tables.Blocks.hash eq hash }) {
it[Tables.Blocks.confirmations] = confirmations
}
updated > 0
}
/** Gets average block time over last N blocks */
fun getAverageBlockTime(blockCount: Int = 100): Long = transaction {
val blocks = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
.limit(blockCount)
.map { it[Tables.Blocks.timestamp] }
if (blocks.size < 2) return@transaction 0L
val timeDiffs = blocks.zipWithNext { newer, older -> newer - older }
timeDiffs.average().toLong()
}
/** Gets total rewards distributed */
fun getTotalRewardsDistributed(): Double = transaction {
Tables.Blocks.select(Tables.Blocks.reward.sum())
.single()[Tables.Blocks.reward.sum()]?.toDouble() ?: 0.0
}
/** Gets blocks with specific difficulty */
fun getBlocksByDifficulty(difficulty: Int): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.where { Tables.Blocks.difficulty eq difficulty }
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
}

View File

@@ -19,6 +19,7 @@ object TransactionService {
fromAddress: String,
toAddress: String,
amount: Double,
password: String,
fee: Double = 0.0,
memo: String? = null
): TransactionResponse {
@@ -27,12 +28,21 @@ 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())
}

View File

@@ -0,0 +1,201 @@
package org.ccoin.services
import org.ccoin.config.ServerConfig
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.utils.CryptoUtils
import org.ccoin.utils.HashUtils
object ValidationService {
/** Validates wallet address format */
fun validateWalletAddress(address: String): Boolean = CryptoUtils.isValidAddress(address)
/** Validates transaction amount */
fun validateTransactionAmount(amount: Double): Boolean = amount > 0 && amount <= Double.MAX_VALUE && !amount.isNaN() && !amount.isInfinite()
/** Validates transaction fee */
fun validateTransactionFee(fee: Double): Boolean = fee >= 0 && fee <= Double.MAX_VALUE && !fee.isNaN() && !fee.isInfinite()
/** Validates memo length */
fun validateMemo(memo: String?): Boolean = memo == null || memo.length <= ServerConfig.maxMemoLength
/** Validates transaction hash format */
fun validateTransactionHash(hash: String): Boolean = HashUtils.isValidSha256Hash(hash)
/** Validates block hash format */
fun validateBlockHash(hash: String): Boolean = HashUtils.isValidSha256Hash(hash)
/** Validates mining difficulty */
fun validateMiningDifficulty(difficulty: Int): Boolean = difficulty in 1..32
/** Validates mining nonce */
fun validateMiningNonce(nonce: Long): Boolean = nonce >= 0
/** Validates timestamp */
fun validateTimestamp(timestamp: Long): Boolean {
val now = System.currentTimeMillis() / 1000
val oneHourAgo = now - 3600
val oneHourFromNow = now + 3600
return timestamp in oneHourAgo..oneHourFromNow
}
/** Validates wallet label */
fun validateWalletLabel(label: String?): Boolean = label == null || (label.isNotBlank() && label.length <= 255)
/** Validates page number for pagination */
fun validatePageNumber(page: Int): Boolean = page >= 1
/** Validates page size for pagination */
fun validatePageSize(pageSize: Int): Boolean = pageSize in 1..ServerConfig.maxPageSize
/** Validates mining hash meets difficulty requirement */
fun validateMiningHash(hash: String, difficulty: Int): Boolean = validateBlockHash(hash) && CryptoUtils.isValidHash(hash, difficulty)
/** Validates complete transaction data */
fun validateTransaction(
fromAddress: String?,
toAddress: String,
amount: Double,
password: String,
fee: Double,
memo: String?
): ValidationResult {
val errors = mutableListOf<String>()
// Validate addresses
if (fromAddress != null && !validateWalletAddress(fromAddress)) {
errors.add("Invalid from address format")
}
if (!validateWalletAddress(toAddress)) {
errors.add("Invalid to address format")
}
if (fromAddress == toAddress) {
errors.add("Cannot send to same address")
}
// Validate amounts
if (!validateTransactionAmount(amount)) {
errors.add("Invalid transaction amount")
}
if (!validateTransactionFee(fee)) {
errors.add("Invalid transaction fee")
}
// Validate memo
if (!validateMemo(memo)) {
errors.add("Memo too long (max ${ServerConfig.maxMemoLength} characters)")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Validates complete block data */
fun validateBlock(
hash: String,
previousHash: String?,
merkleRoot: String,
timestamp: Long,
difficulty: Int,
nonce: Long,
minerAddress: String
): ValidationResult {
val errors = mutableListOf<String>()
// Validate hash
if (!validateBlockHash(hash)) {
errors.add("Invalid block hash format")
}
// Validate previous hash
if (previousHash != null && !validateBlockHash(previousHash)) {
errors.add("Invalid previous hash format")
}
// Validate merkle root
if (!validateBlockHash(merkleRoot)) {
errors.add("Invalid merkle root format")
}
// Validate timestamp
if (!validateTimestamp(timestamp)) {
errors.add("Invalid timestamp (must be within 1 hour of current time)")
}
// Validate difficulty
if (!validateMiningDifficulty(difficulty)) {
errors.add("Invalid mining difficulty (must be between 1 and 32)")
}
// Validate nonce
if (!validateMiningNonce(nonce)) {
errors.add("Invalid nonce (must be non-negative)")
}
// Validate miner address
if (!validateWalletAddress(minerAddress)) {
errors.add("Invalid miner address format")
}
// Validate hash meets difficulty
if (!validateMiningHash(hash, difficulty)) {
errors.add("Hash does not meet difficulty requirements")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Validates wallet creation data */
fun validateWalletCreation(label: String?): ValidationResult {
val errors = mutableListOf<String>()
if (!validateWalletLabel(label)) {
errors.add("Invalid wallet label")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Validates pagination parameters */
fun validatePagination(page: Int, pageSize: Int): ValidationResult {
val errors = mutableListOf<String>()
if (!validatePageNumber(page)) {
errors.add("Page number must be >= 1")
}
if (!validatePageSize(pageSize)) {
errors.add("Page size must be between 1 and ${ServerConfig.maxPageSize}")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Sanitizes user input */
fun sanitizeInput(input: String): String {
return input.trim()
.replace(Regex("[\\r\\n\\t]"), " ")
.replace(Regex("\\s+"), " ")
}
/** Validates hex string */
fun validateHexString(hex: String, expectedLength: Int? = null): Boolean {
if (!HashUtils.isValidHex(hex)) return false
return expectedLength == null || hex.length == expectedLength
}
}
/** Result of validation with success status and error messages */
data class ValidationResult(
val isValid: Boolean,
val errors: List<String> = emptyList()
) {
fun getErrorMessage(): String = errors.joinToString(", ")
fun throwIfInvalid() {
if (!isValid) {
throw InvalidTransactionException(getErrorMessage())
}
}
}

View File

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

View File

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

View File

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

View File

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