Compare commits

7 Commits

Author SHA1 Message Date
darwincereska
f5f40eb79c reset: wiped and rewriting in go 2025-12-20 03:03:41 -05:00
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
43 changed files with 0 additions and 2140 deletions

View File

@@ -1,67 +0,0 @@
# Multi-stage build for Java 24
FROM openjdk:24-jdk-slim AS builder
# Install required packages
RUN apt-get update && apt-get install -y \
curl \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy gradle wrapper and build files
COPY gradle/ gradle/
COPY gradlew .
COPY gradle.properties .
COPY settings.gradle.kts .
COPY build.gradle.kts .
# Make gradlew executable
RUN chmod +x gradlew
# Download dependencies (for better caching)
RUN ./gradlew dependencies --no-daemon
# Copy source code
COPY src/ src/
# Build the application
RUN ./gradlew shadowJar --no-daemon
# Runtime stage
FROM openjdk:24-jdk-slim
# Install required runtime packages
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create app user
RUN groupadd -r ccoin && useradd -r -g ccoin ccoin
# Set working directory
WORKDIR /app
# Copy the built JAR from builder stage
COPY --from=builder /app/build/libs/*.jar app.jar
# Change ownership to app user
RUN chown -R ccoin:ccoin /app
# Switch to app user
USER ccoin
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# JVM options for production
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UseStringDeduplication"
# Run the application
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

View File

View File

@@ -1,121 +0,0 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val exposed_version: String by project
val postgresql_version: String by project
val hikari_version: String by project
val flyway_version: String by project
val bouncycastle_version: String by project
plugins {
kotlin("jvm") version "2.2.21"
kotlin("plugin.serialization") version "2.2.21"
id("io.ktor.plugin") version "3.3.3"
id("com.gradleup.shadow") version "9.3.0"
id("org.flywaydb.flyway") version "11.19.0"
application
}
group = "org.ccoin"
version = "1.0.0"
application {
mainClass.set("org.ccoin.ServerKt")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}
repositories {
mavenCentral()
}
dependencies {
// Ktor server
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cors-jvm:$ktor_version")
implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-status-pages-jvm:$ktor_version")
implementation("io.ktor:ktor-server-compression-jvm:$ktor_version")
implementation("io.ktor:ktor-server-default-headers-jvm:$ktor_version")
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
implementation("io.ktor:ktor-server-config-yaml:$ktor_version")
// Database
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version")
implementation("org.postgresql:postgresql:$postgresql_version")
implementation("com.zaxxer:HikariCP:$hikari_version")
implementation("org.flywaydb:flyway-core:$flyway_version")
implementation("org.flywaydb:flyway-database-postgresql:$flyway_version")
// Logging
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.0")
// Crypto utilities
implementation("org.bouncycastle:bcprov-jdk18on:$bouncycastle_version")
implementation("org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version")
// JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// Testing
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
testImplementation("io.mockk:mockk:1.13.13")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
testImplementation("org.testcontainers:testcontainers:1.20.3")
testImplementation("org.testcontainers:postgresql:1.20.3")
testImplementation("org.testcontainers:junit-jupiter:1.20.3")
}
tasks.withType<Test> {
useJUnitPlatform()
}
// Fat JAR configuration for Docker
tasks.jar {
enabled = false
}
tasks.shadowJar {
archiveClassifier.set("")
manifest {
attributes["Main-Class"] = "org.ccoin.ServerKt"
}
mergeServiceFiles()
}
// Flyway configuration
flyway {
url = "jdbc:postgresql://localhost:5432/ccoin"
user = "ccoin"
password = "ccoin"
locations = arrayOf("classpath:db/migration")
baselineOnMigrate = true
validateOnMigrate = true
}
// Docker build task
tasks.register<Exec>("buildDocker") {
dependsOn("shadowJar")
commandLine("docker", "build", "-t", "ccoin-server:latest", ".")
}
// Development task
tasks.register("dev") {
dependsOn("run")
doFirst {
project.ext.set("development", true)
}
}

View File

@@ -1,82 +0,0 @@
services:
# Postgres Database
postgres:
image: postgres:17-alpine
container_name: ccoin-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ccoin
POSTGRES_USER: ccoin
POSTGRES_PASSWORD: ccoin
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init:/docker-entrypoint-initdb.d
networks:
- ccoin-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ccoin -d ccoin"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# CCoin server
ccoin-server:
build:
context: .
dockerfile: Dockerfile
container_name: ccoin-server
restart: unless-stopped
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
environment:
# Database configuration
DATABASE_URL: jdbc:postgresql://postgres:5432/ccoin
DATABASE_USER: ccoin
DATABASE_PASSWORD: ccoin
DATABASE_POOL_SIZE: 20
# Server configuration
SERVER_HOST: 0.0.0.0
SERVER_PORT: 8080
# Mining configuration
MINING_DIFFICULTY: 4
MINING_REWARD: 50.0
BLOCK_TIME_TARGET: 600000
# Security
JWT_SECRET: your-super-secret-jwt-key-change-this-in-production
# Logging
LOG_LEVEL: INFO
# Development
DEVELOPMENT_MODE: false
volumes:
- ./logs:/app/logs
networks:
- ccoin-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
volumes:
postgres_data:
driver: local
networks:
ccoin-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16

View File

@@ -1,25 +0,0 @@
# Ktor
ktor_version=3.3.3
kotlin_version=2.2.21
# Database
exposed_version=0.56.0
postgresql_version=42.7.4
hikari_version=6.0.0
flyway_version=11.19.0
# Crypto
bouncycastle_version=1.78.1
# Logging
logback_version=1.5.12
# Gradle
kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configuration-cache=true
# Application
ccoin.version=1.0.0

View File

@@ -1,5 +0,0 @@
package org.ccoin
fun main() {
println("CCoin Server Started")
}

View File

@@ -1,77 +0,0 @@
package org.ccoin.config
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.ccoin.database.Tables
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
object DatabaseConfig {
private val logger = LoggerFactory.getLogger(DatabaseConfig::class.java)
fun init() {
logger.info("Initializing database connection...")
val config = HikariConfig().apply {
driverClassName = "org.postgresql.Driver"
jdbcUrl = System.getenv("DATABASE_URL") ?: "jbdc:postgresql://localhost:5432/ccoin"
username = System.getenv("DATABASE_USER") ?: "ccoin"
password = System.getenv("DATABASE_PASSWORD") ?: "ccoin"
// Connection pool settings
maximumPoolSize = (System.getenv("DATABASE_POOL_SIZE")?.toIntOrNull() ?: 20)
minimumIdle = 5
connectionTimeout = 30000
idleTimeout = 600000
maxLifetime = 1800000
// Performance settings
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
// Connection validation
connectionTestQuery = "SELECT 1"
validationTimeout = 5000
// Pool name for monitoring
poolName = "CCoinPool"
}
try {
val dataSource = HikariDataSource(config)
Database.connect(dataSource)
logger.info("Database connection established successfully")
// Create tables if they don't exist
createTables()
} catch(e: Exception) {
logger.error("Failed to initialize database connection", e)
throw e
}
}
private fun createTables() {
logger.info("Creating database tables if they don't exist...")
transaction {
SchemaUtils.create(
Tables.Wallets,
Tables.Transactions,
Tables.Blocks
)
}
logger.info("Database tables created/verified successfully")
}
fun getConnectionInfo(): Map<String, Any> {
return mapOf(
"url" to (System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/ccoin"),
"user" to (System.getenv("DATABASE_USER") ?: "ccoin_user"),
"poolSize" to (System.getenv("DATABASE_POOL_SIZE")?.toIntOrNull() ?: 20)
)
}
}

View File

@@ -1,82 +0,0 @@
package org.ccoin.config
import org.slf4j.LoggerFactory
object ServerConfig {
private val logger = LoggerFactory.getLogger(ServerConfig::class.java)
// 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
// Mining settings
val miningDifficulty: Int = System.getenv("MINING_DIFFICULTY")?.toIntOrNull() ?: 4
val miningReward: Double = System.getenv("MINING_REWARD")?.toDoubleOrNull() ?: 50.0
val blockTimeTarget: Long = System.getenv("BLOCK_TIME_TARGET")?.toLongOrNull() ?: 600000L // 10 minutes
// Transaction settings
val defaultTransactionFee: Double = System.getenv("DEFAULT_TRANSACTION_FEE")?.toDoubleOrNull() ?: 0.01
val maxTransactionSize: Int = System.getenv("MAX_TRANSACTION_SIZE")?.toIntOrNull() ?: (1024 * 1024) // 1MB
val maxMemoLength: Int = System.getenv("MAX_MEMO_LENGTH")?.toIntOrNull() ?: 256
// Security settings
val jwtSecret: String = System.getenv("JWT_SECRET") ?: "change-this-in-production"
val rateLimitRequests: Int = System.getenv("RATE_LIMIT_REQUESTS")?.toIntOrNull() ?: 100
val rateLimitWindow: Long = System.getenv("RATE_LIMIT_WINDOW")?.toLongOrNull() ?: 60000L // 1 minute
// Blockchain settings
val maxBlockSize: Int = System.getenv("MAX_BLOCK_SIZE")?.toIntOrNull() ?: 1024 * 1024 // 1MB
val maxTransactionsPerBlock: Int = System.getenv("MAX_TRANSACTIONS_PER_BLOCK")?.toIntOrNull() ?: 1000
val confirmationsRequired: Int = System.getenv("CONFIRMATIONS_REQUIRED")?.toIntOrNull() ?: 6
// API Settings
val maxPageSize: Int = System.getenv("MAX_PAGE_SIZE")?.toIntOrNull() ?: 100
val defaultPageSize: Int = System.getenv("DEFAULT_PAGE_SIZE")?.toIntOrNull() ?: 50
val apiTimeout: Long = System.getenv("API_TIMEOUT")?.toLongOrNull() ?: 30000L // 30 seconds
// Logging settings
val logLevel: String = System.getenv("LOG_LEVEL") ?: "INFO"
init {
logger.info("Server configuration loaded:")
logger.info("Host: $host")
logger.info("Port: $port")
logger.info("Development mode: $developmentMode")
logger.info("Mining difficulty: $miningDifficulty")
logger.info("Mining reward: $miningReward")
logger.info("Block time target: ${blockTimeTarget}ms")
logger.info("Default transaction fee: $defaultTransactionFee")
logger.info("Confirmations required: $confirmationsRequired")
if (jwtSecret == "change-this-in-production" && !developmentMode) {
logger.warn("WARNING: Using default JWT secret in production mode!")
}
}
fun getServerInfo(): Map<String, Any> {
return mapOf(
"host" to host,
"port" to port,
"developmentMode" to developmentMode,
"version" to "1.0.0",
"miningDifficulty" to miningDifficulty,
"miningReward" to miningReward,
"blockTimeTarget" to blockTimeTarget,
"confirmationsRequired" to confirmationsRequired
)
}
fun validateConfig() {
require(port in 1..65535) { "Port must be between 1 and 65535" }
require(miningDifficulty in 1..32) { "Mining difficulty must be between 1 and 32" }
require(miningReward > 0) { "Mining reward must be positive" }
require(blockTimeTarget > 0) { "Block time target must be positive" }
require(defaultTransactionFee >= 0) { "Default transaction fee cannot be negative" }
require(confirmationsRequired > 0) { "Confirmations required must be positive" }
require(maxPageSize > 0) { "Max page size must be positive" }
require(defaultPageSize > 0) { "Default page size must be positive" }
logger.info("Server configuration validation passed")
}
}

View File

@@ -1,69 +0,0 @@
package org.ccoin.database
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.timestamp
import java.math.BigDecimal
object Tables {
object Wallets : Table("wallets") {
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 createdAt = long("created_at")
val lastActivity = long("last_activity").nullable()
override val primaryKey = PrimaryKey(address)
init {
index(false, createdAt)
index(false, lastActivity)
}
}
object Transactions : Table("transactions") {
val hash = varchar("hash", 64)
val fromAddress = varchar("from_address", 64).nullable()
val toAddress = varchar("to_address", 64)
val amount = decimal("amount", 20, 8)
val fee = decimal("fee", 20, 8).default(BigDecimal.ZERO)
val memo = text("memo").nullable()
val blockHash = varchar("block_hash", 64).nullable()
val timestamp = long("timestamp")
val status = varchar("status", 20).default("pending")
val confirmations = integer("confirmations").default(0)
override val primaryKey = PrimaryKey(hash)
init {
index(false, fromAddress)
index(false, toAddress)
index(false, blockHash)
index(false, timestamp)
index(false, status)
}
}
object Blocks : Table("blocks") {
val hash = varchar("hash", 64)
val previousHash = varchar("previous_hash", 64).nullable()
val merkleRoot = varchar("merkle_root", 64)
val timestamp = long("timestamp")
val difficulty = integer("difficulty")
val nonce = long("nonce")
val minerAddress = varchar("miner_address", 64)
val reward = decimal("reward", 20, 8)
val height = integer("height").autoIncrement()
val transactionCount = integer("transaction_count").default(0)
val confirmations = integer("confirmations").default(0)
override val primaryKey = PrimaryKey(hash)
init {
index(false, height)
index(false, minerAddress)
index(false, timestamp)
index(false, previousHash)
}
}
}

View File

@@ -1,8 +0,0 @@
package org.ccoin.exceptions
/** Base exception class for all CCoin-related exceptions */
open class CCoinException(
message: String,
cause: Throwable? = null,
val errorCode: String? = null
) : Exception(message, cause)

View File

@@ -1,10 +0,0 @@
package org.ccoin.exceptions
class InsufficientFundsException(
val address: String,
val requestedAmount: Double,
val availableBalance: Double
) : CCoinException(
message = "Insufficient funds in wallet $address. Requested: $requestedAmount, Available: $availableBalance",
errorCode = "INSUFFICIENT_FUNDS"
)

View File

@@ -1,9 +0,0 @@
package org.ccoin.exceptions
class InvalidTransactionException(
message: String,
val transactionHash: String? = null
) : CCoinException(
message = message,
errorCode = "INVALID_TRANSACTION"
)

View File

@@ -1,8 +0,0 @@
package org.ccoin.exceptions
class WalletNotFoundException(
val address: String
) : CCoinException(
message = "Wallet with address '$address' not found",
errorCode = "WALLET_NOT_FOUND"
)

View File

@@ -1,80 +0,0 @@
package org.ccoin.models
import kotlinx.serialization.Serializable
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: String? = null,
val timestamp: Long = System.currentTimeMillis()
)
@Serializable
data class ErrorResponse(
val error: String,
val code: String? = null,
val details: Map<String, String>? = null,
val timestamp: Long = System.currentTimeMillis()
)
@Serializable
data class SuccessResponse(
val message: String,
val timestamp: Long = System.currentTimeMillis()
)
@Serializable
data class HealthResponse(
val status: String,
val version: String,
val uptime: Long,
val database: DatabaseHealth,
val blockchain: BlockchainHealth
)
@Serializable
data class DatabaseHealth(
val connected: Boolean,
val responseTime: Long,
val activeConnections: Int,
val maxConnections: Int
)
@Serializable
data class BlockchainHealth(
val latestBlock: Int,
val pendingTransactions: Int,
val networkHashRate: Double,
val averageBlockTime: Long
)
@Serializable
data class PaginationRequest(
val page: Int = 1,
val pageSize: Int = 50,
val sortBy: String? = null,
val sortOrder: SortOrder = SortOrder.DESC
)
@Serializable
data class PaginatedResponse<T>(
val data: List<T>,
val pagination: PaginationInfo
)
@Serializable
data class PaginationInfo(
val currentPage: Int,
val pageSize: Int,
val totalItems: Int,
val totalPages: Int,
val hasNext: Boolean,
val hasPrevious: Boolean
)
@Serializable
enum class SortOrder {
ASC,
DESC
}

View File

@@ -1,61 +0,0 @@
package org.ccoin.models
import kotlinx.serialization.Serializable
@Serializable
data class StartMiningRequest(
val minerAddress: String, // Format: random_word:random_6_digits
val difficulty: Int? = null
)
@Serializable
data class SubmitMiningRequest(
val minerAddress: String,
val nonce: Long,
val hash: String,
val previousHash: String,
val timestamp: Long
)
@Serializable
data class BlockResponse(
val hash: String,
val previousHash: String?,
val merkleRoot: String,
val timestamp: Long,
val difficulty: Int,
val nonce: Long,
val minerAddress: String,
val reward: Double,
val height: Int,
val transactionCount: Int,
val confirmations: Int = 0
)
@Serializable
data class MiningJobResponse(
val jobId: String,
val target: String,
val difficulty: Int,
val previousHash: String,
val height: Int,
val timestamp: Long,
val expiresAt: Long
)
@Serializable
data class MiningStatsResponse(
val minerAddress: String,
val totalBlocksMined: Int,
val totalRewardEarned: Double,
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

@@ -1,42 +0,0 @@
package org.ccoin.models
import kotlinx.serialization.Serializable
@Serializable
data class SendTransactionRequest(
val fromAddress: String, // Format random_word:random_6_digits
val toAddress: String, // Format random_word:random_6_digits
val amount: Double,
val fee: Double = 0.0,
val memo: String? = null
)
@Serializable
data class TransactionResponse(
val hash: String,
val fromAddress: String?,
val toAddress: String,
val amount: Double,
val fee: Double,
val memo: String?,
val blockHash: String?,
val timestamp: Long,
val status: TransactionStatus,
val confirmations: Int = 0
)
@Serializable
data class TransactionHistoryResponse(
val transactions: List<TransactionResponse>,
val totalCount: Int,
val page: Int,
val pageSize: Int
)
@Serializable
enum class TransactionStatus {
PENDING,
CONFIRMED,
FAILED,
CANCELLED
}

View File

@@ -1,28 +0,0 @@
package org.ccoin.models
import kotlinx.serialization.Serializable
@Serializable
data class CreateWalletRequest(
val label: String? = null
)
@Serializable
data class WalletResponse(
val address: String, // Format random_word:random_6_digits (e.g. "phoenix:123456")
val balance: Double,
val label: String?,
val createdAt: Long,
val lastActivity: Long?
)
@Serializable
data class BalanceResponse(
val address: String,
val balance: Double
)
@Serializable
data class UpdateWalletRequest(
val label: String?
)

View File

@@ -1,189 +0,0 @@
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.services.ValidationService
import org.ccoin.services.WalletService
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)
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(mapOf("address" to address, "balance" to 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()
call.respond(mapOf(
"wallets" to wallets,
"pagination" to mapOf(
"currentPage" to page,
"pageSize" to pageSize,
"totalItems" to totalCount,
"totalPages" to ((totalCount + pageSize - 1) / pageSize),
"hasNext" to (offset + pageSize < totalCount),
"hasPrevious" to (page > 1)
)
))
} 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(mapOf("wallets" to wallets, "minBalance" to 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(mapOf("address" to address, "exists" to exists))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check wallet existence")))
}
}
}
}

View File

@@ -1,218 +0,0 @@
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

@@ -1,185 +0,0 @@
package org.ccoin.services
import org.ccoin.config.ServerConfig
import org.ccoin.database.Tables
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.models.BlockResponse
import org.ccoin.models.MiningJobResponse
import org.ccoin.models.MiningStatsResponse
import org.ccoin.utils.CryptoUtils
import org.ccoin.utils.HashUtils
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.time.Instant
object MiningService {
/** Starts a mining job for a miner */
fun startMining(minerAddress: String, difficulty: Int? = null): MiningJobResponse {
val jobId = CryptoUtils.generateJobId()
val currentDifficulty = difficulty ?: getCurrentDifficulty()
val previousHash = getLatestBlockHash()
val height = getNextBlockHeight()
val timestamp = Instant.now().epochSecond
val expiresAt = timestamp + 300 // 5 minutes
val target = "0".repeat(currentDifficulty)
return MiningJobResponse(
jobId, target, currentDifficulty, previousHash, height, timestamp, expiresAt
)
}
/** Submits mining result and creates block if valid */
fun submitMiningResult(
minerAddress: String,
nonce: Long,
hash: String,
previousHash: String,
timestamp: Long = Instant.now().epochSecond
): BlockResponse {
val currentDifficulty = getCurrentDifficulty()
// Validate hash meets difficulty requirements
if (!CryptoUtils.isValidHash(hash, currentDifficulty)) {
throw InvalidTransactionException("Hash does not meet difficulty requirements")
}
// Validate previous hash matches latest block
val latestHash = getLatestBlockHash()
if (previousHash != latestHash) {
throw InvalidTransactionException("Previous hash does not match latest block")
}
val height = getNextBlockHeight()
val reward = ServerConfig.miningReward
val merkleRoot = calculateMerkleRoot(emptyList()) // No transactions for now
return transaction {
// Create block
Tables.Blocks.insert {
it[Tables.Blocks.hash] = hash
it[Tables.Blocks.previousHash] = if (previousHash == "0".repeat(64)) null else previousHash
it[Tables.Blocks.merkleRoot] = merkleRoot
it[Tables.Blocks.timestamp] = timestamp
it[Tables.Blocks.difficulty] = currentDifficulty
it[Tables.Blocks.nonce] = nonce
it[Tables.Blocks.minerAddress] = minerAddress
it[Tables.Blocks.reward] = BigDecimal.valueOf(reward)
it[transactionCount] = 0
}
// Reward miner with genesis transaction
TransactionService.createGenesisTransaction(minerAddress, reward)
BlockResponse(
hash, if (previousHash == "0".repeat(64)) null else previousHash, merkleRoot,
timestamp, currentDifficulty, nonce, minerAddress, reward, height, 0, 0
)
}
}
/** Gets current mining difficulty */
fun getCurrentDifficulty(): Int = ServerConfig.miningDifficulty
/** Gets mining statistics for a miner */
fun getMinerStats(minerAddress: String): MiningStatsResponse = transaction {
val blocks = Tables.Blocks.selectAll()
.where { Tables.Blocks.minerAddress eq minerAddress }
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
val totalBlocks = blocks.count().toInt()
val totalReward = blocks.sumOf { it[Tables.Blocks.reward] }.toDouble()
val lastBlockMined = blocks.firstOrNull()?.get(Tables.Blocks.timestamp)?.toLong()
MiningStatsResponse(
minerAddress, totalBlocks, totalReward, lastBlockMined, getCurrentDifficulty()
)
}
/** Gets latest block hash */
private fun getLatestBlockHash(): String = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.hash] }
.singleOrNull() ?: "0".repeat(64) // Genesis hash
}
/** Gets next block height */
private fun getNextBlockHeight(): Int = transaction {
val latestHeight = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.height] }
.singleOrNull() ?: 0
latestHeight + 1
}
/** Calculates merkle root for transactions */
private fun calculateMerkleRoot(transactionHashes: List<String>): String {
return if (transactionHashes.isEmpty()) {
HashUtils.sha256Hex("empty_block")
} else {
CryptoUtils.calculateMerkleRoot(transactionHashes)
}
}
/** Gets network hash rate estimate */
fun getNetworkHashRate(): Double = transaction {
val recentBlocks = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
.limit(100)
if (recentBlocks.count() < 2) return@transaction 0.0
val blocks = recentBlocks.toList()
val timeSpan = blocks.first()[Tables.Blocks.timestamp] - blocks.last()[Tables.Blocks.timestamp]
val difficulty = getCurrentDifficulty()
if (timeSpan <= 0) return@transaction 0.0
// Rough estimate: (blocks * 2^difficulty) / time_span
(blocks.size * Math.pow(2.0, difficulty.toDouble())) / timeSpan
}
/** Gets average block time */
fun getAverageBlockTime(): Long = transaction {
val recentBlocks = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
.limit(100)
.map { it[Tables.Blocks.timestamp] }
if (recentBlocks.size < 2) return@transaction 0L
val timeDiffs = recentBlocks.zipWithNext { a, b -> a - b }
timeDiffs.average().toLong()
}
/** Gets total number of active miners */
fun getActiveMinersCount(): Int = transaction {
val oneDayAgo = Instant.now().epochSecond - 86400
Tables.Blocks.selectAll()
.where { Tables.Blocks.timestamp greater oneDayAgo }
.groupBy(Tables.Blocks.minerAddress)
.count().toInt()
}
/** Validates mining job */
fun validateMiningJob(jobId: String, hash: String, nonce: Long): Boolean {
// Simple validation - in production you'd store job details
return CryptoUtils.isValidHash(hash, getCurrentDifficulty())
}
/** Gets pending transactions for mining */
fun getPendingTransactionsForMining(limit: Int = 100): List<String> = transaction {
Tables.Transactions.selectAll()
.where { Tables.Transactions.status eq "pending" }
.orderBy(Tables.Transactions.timestamp, SortOrder.ASC)
.limit(limit)
.map { it[Tables.Transactions.hash] }
}
}

View File

@@ -1,233 +0,0 @@
package org.ccoin.services
import org.ccoin.database.Tables
import org.ccoin.exceptions.InsufficientFundsException
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.exceptions.WalletNotFoundException
import org.ccoin.models.TransactionResponse
import org.ccoin.models.TransactionStatus
import org.ccoin.utils.CryptoUtils
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.time.Instant
object TransactionService {
/** Sends a transaction between wallets */
fun sendTransaction(
fromAddress: String,
toAddress: String,
amount: Double,
fee: Double = 0.0,
memo: String? = null
): TransactionResponse {
val hash = CryptoUtils.generateTransactionHash(fromAddress, toAddress, amount, Instant.now().epochSecond)
val timestamp = Instant.now().epochSecond
val totalAmount = BigDecimal.valueOf(amount + fee)
return transaction {
// Check if sender wallet exists and has sufficient balance
val fromBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq fromAddress }
.map { it[Tables.Wallets.balance] }
.singleOrNull() ?: throw WalletNotFoundException(fromAddress)
if (fromBalance < totalAmount) {
throw InsufficientFundsException(fromAddress, amount + fee, fromBalance.toDouble())
}
// Check if recipient wallet exists
val recipientExists = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.count() > 0
if (!recipientExists) {
throw WalletNotFoundException(toAddress)
}
// Create transaction record
Tables.Transactions.insert {
it[Tables.Transactions.hash] = hash
it[Tables.Transactions.fromAddress] = fromAddress
it[Tables.Transactions.toAddress] = toAddress
it[Tables.Transactions.amount] = BigDecimal.valueOf(amount)
it[Tables.Transactions.fee] = BigDecimal.valueOf(fee)
it[Tables.Transactions.memo] = memo
it[Tables.Transactions.timestamp] = timestamp
it[status] = "confirmed"
}
// Update sender balance
val senderCurrentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq fromAddress }
.map { it[Tables.Wallets.balance] }
.single()
Tables.Wallets.update({ Tables.Wallets.address eq fromAddress }) {
it[balance] = senderCurrentBalance.subtract(totalAmount)
it[lastActivity] = timestamp
}
// Update recipient balance
val recipientCurrentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.map { it[Tables.Wallets.balance] }
.single()
Tables.Wallets.update({ Tables.Wallets.address eq toAddress }) {
it[balance] = recipientCurrentBalance.add(BigDecimal.valueOf(amount))
it[lastActivity] = timestamp
}
TransactionResponse(
hash, fromAddress, toAddress, amount, fee, memo, null, timestamp, TransactionStatus.CONFIRMED, 0
)
}
}
/** Gets transaction by hash */
fun getTransaction(hash: String): TransactionResponse? = transaction {
Tables.Transactions.selectAll().where { Tables.Transactions.hash eq hash }
.map {
TransactionResponse(
it[Tables.Transactions.hash],
it[Tables.Transactions.fromAddress],
it[Tables.Transactions.toAddress],
it[Tables.Transactions.amount].toDouble(),
it[Tables.Transactions.fee].toDouble(),
it[Tables.Transactions.memo],
it[Tables.Transactions.blockHash],
it[Tables.Transactions.timestamp],
TransactionStatus.valueOf(it[Tables.Transactions.status].uppercase()),
it[Tables.Transactions.confirmations]
)
}.singleOrNull()
}
/** Gets transaction history for an address */
fun getTransactionHistory(
address: String,
limit: Int = 50,
offset: Int = 0
): List<TransactionResponse> = transaction {
Tables.Transactions.selectAll()
.where {
(Tables.Transactions.fromAddress eq address) or (Tables.Transactions.toAddress eq address)
}
.orderBy(Tables.Transactions.timestamp, SortOrder.DESC)
.limit(limit)
.offset(offset.toLong())
.map {
TransactionResponse(
it[Tables.Transactions.hash],
it[Tables.Transactions.fromAddress],
it[Tables.Transactions.toAddress],
it[Tables.Transactions.amount].toDouble(),
it[Tables.Transactions.fee].toDouble(),
it[Tables.Transactions.memo],
it[Tables.Transactions.blockHash],
it[Tables.Transactions.timestamp],
TransactionStatus.valueOf(it[Tables.Transactions.status].uppercase()),
it[Tables.Transactions.confirmations]
)
}
}
/** Gets pending transactions */
fun getPendingTransactions(): List<TransactionResponse> = transaction {
Tables.Transactions.selectAll()
.where { Tables.Transactions.status eq "pending" }
.orderBy(Tables.Transactions.timestamp, SortOrder.ASC)
.map {
TransactionResponse(
it[Tables.Transactions.hash],
it[Tables.Transactions.fromAddress],
it[Tables.Transactions.toAddress],
it[Tables.Transactions.amount].toDouble(),
it[Tables.Transactions.fee].toDouble(),
it[Tables.Transactions.memo],
it[Tables.Transactions.blockHash],
it[Tables.Transactions.timestamp],
TransactionStatus.valueOf(it[Tables.Transactions.status].uppercase()),
it[Tables.Transactions.confirmations]
)
}
}
/** Updates transaction status */
fun updateTransactionStatus(hash: String, status: TransactionStatus): Boolean = transaction {
val updated = Tables.Transactions.update({ Tables.Transactions.hash eq hash }) {
it[Tables.Transactions.status] = status.name.lowercase()
}
updated > 0
}
/** Adds transaction to block */
fun addTransactionToBlock(transactionHash: String, blockHash: String): Boolean = transaction {
val updated = Tables.Transactions.update({ Tables.Transactions.hash eq transactionHash }) {
it[Tables.Transactions.blockHash] = blockHash
it[status] = "confirmed"
}
updated > 0
}
/** Gets total transaction count */
fun getTotalTransactionCount(): Long = transaction {
Tables.Transactions.selectAll().count()
}
/** Gets transaction count for address */
fun getTransactionCountForAddress(address: String): Long = transaction {
Tables.Transactions.selectAll()
.where {
(Tables.Transactions.fromAddress eq address) or (Tables.Transactions.toAddress eq address)
}
.count()
}
/** Creates a genesis transaction (mining reward) */
fun createGenesisTransaction(toAddress: String, amount: Double): TransactionResponse {
val hash = CryptoUtils.generateTransactionHash(null, toAddress, amount, Instant.now().epochSecond)
val timestamp = Instant.now().epochSecond
return transaction {
// Check if recipient wallet exists
val recipientExists = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.count() > 0
if (!recipientExists) {
throw WalletNotFoundException(toAddress)
}
// Create genesis transaction
Tables.Transactions.insert {
it[Tables.Transactions.hash] = hash
it[Tables.Transactions.fromAddress] = null
it[Tables.Transactions.toAddress] = toAddress
it[Tables.Transactions.amount] = BigDecimal.valueOf(amount)
it[Tables.Transactions.fee] = BigDecimal.ZERO
it[Tables.Transactions.memo] = "Mining reward"
it[Tables.Transactions.timestamp] = timestamp
it[status] = "confirmed"
}
// Update recipient balance
val recipientCurrentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.map { it[Tables.Wallets.balance] }
.single()
Tables.Wallets.update({ Tables.Wallets.address eq toAddress }) {
it[balance] = recipientCurrentBalance.add(BigDecimal.valueOf(amount))
it[lastActivity] = timestamp
}
TransactionResponse(
hash, null, toAddress, amount, 0.0, "Mining reward", null, timestamp, TransactionStatus.CONFIRMED, 0
)
}
}
}

View File

@@ -1,200 +0,0 @@
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,
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

@@ -1,138 +0,0 @@
package org.ccoin.services
import org.ccoin.database.Tables
import org.ccoin.exceptions.WalletNotFoundException
import org.ccoin.models.WalletResponse
import org.ccoin.utils.CryptoUtils
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.time.Instant
object WalletService {
/** Creates a new wallet with optional label */
fun createWallet(label: String? = null): WalletResponse {
val address = CryptoUtils.generateWalletAddress()
val timestamp = Instant.now().epochSecond
return transaction {
Tables.Wallets.insert {
it[Tables.Wallets.address] = address
it[Tables.Wallets.label] = label
it[createdAt] = timestamp
}
WalletResponse(address, 0.0, label, timestamp, null)
}
}
/** Gets wallet by address */
fun getWallet(address: String): WalletResponse? = transaction {
Tables.Wallets.selectAll().where { Tables.Wallets.address eq address }
.map {
WalletResponse(
it[Tables.Wallets.address],
it[Tables.Wallets.balance].toDouble(),
it[Tables.Wallets.label],
it[Tables.Wallets.createdAt],
it[Tables.Wallets.lastActivity]
)
}.singleOrNull()
}
/** Gets wallet balance */
fun getWalletBalance(address: String): Double = transaction {
Tables.Wallets.selectAll().where { Tables.Wallets.address eq address }
.map { it[Tables.Wallets.balance].toDouble() }
.singleOrNull() ?: throw WalletNotFoundException(address)
}
/** Updates wallet balance */
fun updateBalance(address: String, amount: BigDecimal): Boolean = transaction {
val currentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq address }
.map { it[Tables.Wallets.balance] }
.singleOrNull() ?: return@transaction false
val updated = Tables.Wallets.update({ Tables.Wallets.address eq address }) {
it[balance] = currentBalance.add(amount)
it[lastActivity] = Instant.now().epochSecond
}
updated > 0
}
/** Sets wallet balance to specific amount */
fun setBalance(address: String, amount: BigDecimal): Boolean = transaction {
val updated = Tables.Wallets.update({ Tables.Wallets.address eq address }) {
it[balance] = amount
it[lastActivity] = Instant.now().epochSecond
}
updated > 0
}
/** Updates wallet label */
fun updateLabel(address: String, label: String?): Boolean = transaction {
val updated = Tables.Wallets.update({ Tables.Wallets.address eq address }) {
it[Tables.Wallets.label] = label
}
updated > 0
}
/** Checks if wallet exists */
fun walletExists(address: String): Boolean = transaction {
Tables.Wallets.selectAll().where { Tables.Wallets.address eq address }.count() > 0
}
/** Gets all wallets with pagination */
fun getAllWallets(limit: Int = 50, offset: Int = 0): List<WalletResponse> = transaction {
Tables.Wallets.selectAll()
.orderBy(Tables.Wallets.createdAt, SortOrder.DESC)
.limit(limit)
.offset(offset.toLong())
.map {
WalletResponse(
it[Tables.Wallets.address],
it[Tables.Wallets.balance].toDouble(),
it[Tables.Wallets.label],
it[Tables.Wallets.createdAt],
it[Tables.Wallets.lastActivity]
)
}
}
/** Gets total number of wallets */
fun getTotalWalletCount(): Long = transaction {
Tables.Wallets.selectAll().count()
}
/** Gets wallets with balance greater than specified amount */
fun getWalletsWithBalance(minBalance: Double): List<WalletResponse> = transaction {
Tables.Wallets.selectAll().where { Tables.Wallets.balance greater BigDecimal.valueOf(minBalance) }
.orderBy(Tables.Wallets.balance, SortOrder.DESC)
.map {
WalletResponse(
it[Tables.Wallets.address],
it[Tables.Wallets.balance].toDouble(),
it[Tables.Wallets.label],
it[Tables.Wallets.createdAt],
it[Tables.Wallets.lastActivity]
)
}
}
/** Updates last activity timestamp */
fun updateLastActivity(address: String): Boolean = transaction {
val updated = Tables.Wallets.update({ Tables.Wallets.address eq address }) {
it[lastActivity] = Instant.now().epochSecond
}
updated > 0
}
/** Gets total supply across all wallets */
fun getTotalSupply(): Double = transaction {
Tables.Wallets.select(Tables.Wallets.balance.sum())
.single()[Tables.Wallets.balance.sum()]?.toDouble() ?: 0.0
}
}

View File

@@ -1,123 +0,0 @@
package org.ccoin.utils
import java.security.MessageDigest
import java.security.SecureRandom
import kotlin.random.Random
object CryptoUtils {
private val secureRandom = SecureRandom()
// Word list for address generation
private val words = listOf(
"phoenix", "dragon", "tiger", "eagle", "wolf", "lion", "bear", "shark",
"falcon", "raven", "cobra", "viper", "panther", "jaguar", "leopard", "cheetah",
"thunder", "lightning", "storm", "blizzard", "tornado", "hurricane", "cyclone", "tempest",
"crystal", "diamond", "emerald", "ruby", "sapphire", "topaz", "amethyst", "opal",
"shadow", "ghost", "phantom", "spirit", "wraith", "specter", "demon", "angel",
"fire", "ice", "earth", "wind", "water", "metal", "wood", "void",
"star", "moon", "sun", "comet", "meteor", "galaxy", "nebula", "cosmos",
"blade", "sword", "arrow", "spear", "shield", "armor", "crown", "throne",
"mountain", "ocean", "forest", "desert", "valley", "river", "lake", "cave",
"knight", "warrior", "mage", "archer", "rogue", "paladin", "wizard", "sage"
)
/**
* Generates a wallet address in format: random_word:random_6_digits
* Example: "phoenix:123456", "dragon:654321"
*/
fun generateWalletAddress(): String {
val randomWord = words[secureRandom.nextInt(words.size)]
val randomDigits = String.format("%06d", secureRandom.nextInt(1000000))
return "$randomWord:$randomDigits"
}
/** Validates if an address follows the correct format */
fun isValidAddress(address: String): Boolean {
val parts = address.split(":")
if (parts.size != 2) return false
val word = parts[0]
val digits = parts[1]
return word.isNotEmpty() &&
word.all { it.isLetter() } &&
digits.length == 6 &&
digits.all { it.isDigit() }
}
/** Generates SHA-256 hash of input string */
fun sha256(input: String): String {
return MessageDigest.getInstance("SHA-256")
.digest(input.toByteArray())
.joinToString("") { "%02x".format(it) }
}
/** Generates a transaction hash */
fun generateTransactionHash(
fromAddress: String?,
toAddress: String,
amount: Double,
timestamp: Long,
nonce: Long = secureRandom.nextLong()
): String {
val input = "${fromAddress ?: "genesis"}:$toAddress:$amount:$timestamp:$nonce"
return sha256(input)
}
/** Generates a block hash */
fun generateBlockHash(
previousHash: String,
merkleRoot: String,
timestamp: Long,
difficulty: Int,
nonce: Long
): String {
val input = "$previousHash:$merkleRoot:$timestamp:$difficulty:$nonce"
return sha256(input)
}
/** Validates if a hash meets the mining difficulty requirement */
fun isValidHash(hash: String, difficulty: Int): Boolean {
val target = "0".repeat(difficulty)
return hash.startsWith(target)
}
/** Generates a mining job id */
fun generateJobId(): String = sha256("job:${System.currentTimeMillis()}:${secureRandom.nextLong()}").take(16)
/** Calculates a merkle root from transaction hashes */
fun calculateMerkleRoot(transactionHashes: List<String>): String {
if (transactionHashes.isEmpty()) {
return sha256("empty")
}
if (transactionHashes.size == 1) {
return transactionHashes[0]
}
var hashes = transactionHashes.toMutableList()
while (hashes.size > 1) {
val newHashes = mutableListOf<String>()
for (i in hashes.indices step 2) {
val left = hashes[i]
val right = if (i + 1 < hashes.size) hashes[i + 1] else left
newHashes.add(sha256("$left:$right"))
}
hashes = newHashes
}
return hashes[0]
}
/** Generates a random nonce for mining */
fun generateNonce(): Long = secureRandom.nextLong()
/** Validates transaction hash format */
fun isValidTransactionHash(hash: String): Boolean = hash.length == 64 && hash.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
/** Validates block has format */
fun isValidBlockHash(hash: String): Boolean = hash.length == 64 && hash.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
}

View File

@@ -1,80 +0,0 @@
package org.ccoin.utils
import java.security.MessageDigest
import java.security.SecureRandom
object HashUtils {
private val secureRandom = SecureRandom()
/** Computes SHA-256 hash of a byte array */
fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)
/** Computes SHA-256 hash of a string and returns hex string */
fun sha256Hex(input: String): String = sha256(input.toByteArray()).toHexString()
/** Computes double SHA-256 hash (like Bitcoin) */
fun doubleSha256(data: ByteArray): ByteArray = sha256(sha256(data))
/** Computes double SHA-256 hash and returns hex string */
fun doubleSha256Hex(input: String): String = doubleSha256(input.toByteArray()).toHexString()
/** Converts byte array to hex string */
fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
/** Converts hex string to byte array */
fun String.hexToByteArray(): ByteArray {
require(length % 2 == 0) { "Hex string must have even length" }
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
/** Validates if string is valid hex */
fun isValidHex(hex: String): Boolean = hex.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
/** Generates a random hash for testing purposes */
fun generateRandomHash(): String {
val randomBytes = ByteArray(32)
secureRandom.nextBytes(randomBytes)
return randomBytes.toHexString()
}
/** Computes hash with salt for additional security */
fun hashWithSalt(input: String, salt: String): String = sha256Hex("$input:$salt")
/** Generates a random salt */
fun generateSalt(length: Int = 16): String {
val saltBytes = ByteArray(length)
secureRandom.nextBytes(saltBytes)
return saltBytes.toHexString()
}
/** Computes HMAC-SHA256 */
fun hmacSha256(key: String, message: String): String {
val keyBytes = key.toByteArray()
val messageBytes = message.toByteArray()
val blockSize = 64
val adjustedKey = when {
keyBytes.size > blockSize -> sha256(keyBytes)
keyBytes.size < blockSize -> keyBytes + ByteArray(blockSize - keyBytes.size)
else -> keyBytes
}
val outerPad = ByteArray(blockSize) { (adjustedKey[it].toInt() xor 0x5c).toByte() }
val innerPad = ByteArray(blockSize) { (adjustedKey[it].toInt() xor 0x36).toByte() }
val innerHash = sha256(innerPad + messageBytes)
val finalHash = sha256(outerPad + innerHash)
return finalHash.toHexString()
}
/** Validates hash format (64 character hex string for SHA-256) */
fun isValidSha256Hash(hash: String): Boolean = hash.length == 64 && isValidHex(hash)
/** Computes checksum for data integrity */
fun computeChecksum(data: String): String = sha256Hex(data).take(8)
/** Validates data against checksum */
fun validateChecksum(data: String, checksum: String): Boolean = computeChecksum(data) == checksum
}