Compare commits
9 Commits
6d360df21d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5f40eb79c | ||
|
|
3f4349c9d2 | ||
|
|
194fd7357c | ||
|
|
9a644b689a | ||
|
|
9bc861f1d1 | ||
|
|
89e45128b6 | ||
|
|
3c097af03d | ||
|
|
35a73c340c | ||
|
|
1c8fe77a43 |
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.ccoin
|
||||
|
||||
fun main() {
|
||||
println("CCoin Server Started")
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.ccoin.exceptions
|
||||
|
||||
class InvalidTransactionException(
|
||||
message: String,
|
||||
val transactionHash: String? = null
|
||||
) : CCoinException(
|
||||
message = message,
|
||||
errorCode = "INVALID_TRANSACTION"
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user