Compare commits

9 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
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
43 changed files with 0 additions and 1524 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,24 +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
# 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,53 +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
)

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