5 Commits

Author SHA1 Message Date
darwincereska
99c5dba721 feat: added wallet service 2025-12-23 17:38:27 -05:00
darwincereska
23f6dba3f3 fix: removed unused folder 2025-12-20 21:19:19 -05:00
darwincereska
d7338f717b feat: added server and db config 2025-12-20 21:17:49 -05:00
darwincereska
59adb32e68 feat: added models 2025-12-20 04:21:13 -05:00
darwincereska
f5f40eb79c reset: wiped and rewriting in go 2025-12-20 03:03:41 -05:00
61 changed files with 1182 additions and 3869 deletions

4
server/.gitignore vendored
View File

@@ -1,3 +1,7 @@
# Go files
ccoin
go.sum
# Compiled class file
*.class

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"]

49
server/Makefile Normal file
View File

@@ -0,0 +1,49 @@
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOFMT=gofmt
GOLINT=golangci-lint
# Binary names
BINARY_NAME=ccoin
BINARY_LINUX=$(BINARY_NAME)-linux-x64
BINARY_MAC=$(BINARY_NAME)-macos-arm64
BINARY_WINDOWS=$(BINARY_NAME)-windows-x64.exe
# Build directory
BUILD_DIR=build
# Version info (can be overridden)
VERSION?=1.0.0
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
COMMIT?=$(shell git rev-parse HEAD)
# Linker flags to embed version info
LDFLAGS=-ldflags "-X ccoin/config/server.version=$(VERSION) -X ccoin/config/server.buildTime=$(BUILD_TIME) -X ccoin/config/server.commit=$(COMMIT) -s -w" -trimpath
.PHONY: all build clean test coverage deps fmt lint vet help
.PHONY: build-linux build-windows build-darwin run dev
.PHONY: docker-build install uninstall
# Default target
all: clean deps fmt vet test build
# Build the binary
build:
$(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) -v
# Build for Linux
build-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_LINUX) -v
# Build for MacOS
build-macos:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_MAC) -v
# Build for windows
build-windows:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_WINDOWS) -v

View File

View File

@@ -1,130 +0,0 @@
import java.time.Instant
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"
id("com.github.gmazzo.buildconfig") version "6.0.6"
application
}
group = "org.ccoin"
version = "1.0.0"
buildConfig {
buildConfigField("String", "VERSION", "\"${project.version}\"")
buildConfigField("BASE_URL", "https://ccoin.darwincereska.dev")
buildConfigField("String", "BUILD_TIME", "\"${Instant.now()}\"")
packageName("org.ccoin")
}
application {
mainClass.set("org.ccoin.ServerKt")
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

@@ -0,0 +1,207 @@
package database
import (
"context"
"database/sql"
"fmt"
"time"
"ccoin/utils"
"github.com/charmbracelet/log"
_ "github.com/jackc/pgx/v5/stdlib"
"os"
)
type DatabaseConfig struct {
DB *sql.DB
logger *log.Logger
}
// Holds database configuration
type DatabaseSettings struct {
URL string
User string
Password string
MaxPoolSize int
MinIdle int
ConnTimeout time.Duration
IdleTimeout time.Duration
MaxLifetime time.Duration
ValidationQuery string
}
// Creates and initializes a new database configuration
func NewDatabaseConfig() *DatabaseConfig {
return &DatabaseConfig{
logger: log.New(os.Stdout),
}
}
// Initializes the database connection
func (dc *DatabaseConfig) Init() error {
dc.logger.Info("Initializing database connection...")
settings := dc.loadSettings()
// Create connection string
connStr := fmt.Sprintf("%s?user=%s&password=%s", settings.URL, settings.User, settings.Password)
// Open database connection
db, err := sql.Open("pgx", connStr)
if err != nil {
dc.logger.Error("Failed to open database connection", "error", err)
return fmt.Errorf("failed to open database: %w", err)
}
// Configure database connection pool
db.SetMaxOpenConns(settings.MaxPoolSize)
db.SetMaxIdleConns(settings.MinIdle)
db.SetConnMaxLifetime(settings.MaxLifetime)
db.SetConnMaxIdleTime(settings.IdleTimeout)
// Test the connection
ctx, cancel := context.WithTimeout(context.Background(), settings.ConnTimeout)
defer cancel()
if err := db.PingContext(ctx); err != nil {
dc.logger.Error("Failed to ping database", "error", err)
return fmt.Errorf("failed to ping database: %w", err)
}
dc.DB = db
dc.logger.Info("Database connection established successfully")
// Create tables if they don't exist
if err := dc.createTables(); err != nil {
return fmt.Errorf("failed to create tables: %w", err)
}
return nil
}
// Loads database settings from environment variables
func (dc *DatabaseConfig) loadSettings() DatabaseSettings {
return DatabaseSettings{
URL: utils.GetEnvOrDefault("DATABASE_URL", "postgres://localhost:5432/ccoin"),
User: utils.GetEnvOrDefault("DATABASE_USER", "ccoin"),
Password: utils.GetEnvOrDefault("DATABASE_PASSWORD", "ccoin"),
MaxPoolSize: utils.GetEnvOrDefault("DATABASE_POOL_SIZE", 20),
ConnTimeout: 30 * time.Second,
IdleTimeout: 10 * time.Minute,
MaxLifetime: 30 * time.Minute,
ValidationQuery: "SELECT 1",
}
}
// Creates database tables if they don't exist
func (dc *DatabaseConfig) createTables() error {
dc.logger.Info("Creating database tables if they don't exist...")
// Define tables
tables := []string{
// Wallets table
`CREATE TABLE IF NOT EXISTS wallets (
address VARCHAR(64) PRIMARY KEY,
balance DECIMAL(20,8) DEFAULT 0,
label VARCHAR(255),
password_hash VARCHAR(64),
created_at BIGINT NOT NULL,
last_activity BIGINT
)`,
// Transactions table
`CREATE TABLE IF NOT EXISTS transactions (
hash VARCHAR(64) PRIMARY KEY,
from_address VARCHAR(64),
to_address VARCHAR(64) NOT NULL,
amount DECIMAL(20,8) NOT NULL,
fee DECIMAL(20,8) DEFAULT 0,
memo TEXT,
block_hash VARCHAR(64),
timestamp BIGINT NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
confirmations INTEGER DEFAULT 0
)`,
// Blocks table
`CREATE TABLE IF NOT EXISTS blocks (
hash VARCHAR(64) PRIMARY KEY,
previous_hash VARCHAR(64),
merkle_root VARCHAR(64) NOT NULL,
timestamp BIGINT NOT NULL,
difficulty INTEGER NOT NULL,
nonce BIGINT NOT NULL,
miner_address VARCHAR(64) NOT NULL,
reward DECIMAL(20,8) NOT NULL,
height SERIAL,
transaction_count INTEGER DEFAULT 0,
confirmations INTEGER DEFAULT 0
)`,
}
// Create tables
for _, tableSQL := range tables {
if _, err := dc.DB.Exec(tableSQL); err != nil {
dc.logger.Error("Failed to create table", "error", err, "sql", tableSQL)
return err
}
}
// Create indexes
indexes := []string{
// Wallets indexes
"CREATE INDEX IF NOT EXISTS idx_wallets_created_at ON wallets(created_at)",
"CREATE INDEX IF NOT EXISTS idx_wallets_last_activity ON wallets(last_activity)",
// Transactions indexes
"CREATE INDEX IF NOT EXISTS idx_transactions_from_address ON transactions(from_address)",
"CREATE INDEX IF NOT EXISTS idx_transactions_to_address ON transactions(to_address)",
"CREATE INDEX IF NOT EXISTS idx_transactions_block_hash ON transactions(block_hash)",
"CREATE INDEX IF NOT EXISTS idx_transactions_timestamp ON transactions(timestamp)",
"CREATE INDEX IF NOT EXISTS idx_transactions_status ON transactions(status)",
// Blocks indexes
"CREATE INDEX IF NOT EXISTS idx_blocks_height ON blocks(height)",
"CREATE INDEX IF NOT EXISTS idx_blocks_miner_address ON blocks(miner_address)",
"CREATE INDEX IF NOT EXISTS idx_blocks_timestamp ON blocks(timestamp)",
"CREATE INDEX IF NOT EXISTS idx_blocks_previous_hash ON blocks(previous_hash)",
// Foreign key-like indexes for referential integrity
"CREATE INDEX IF NOT EXISTS idx_transactions_from_wallet ON transactions(from_address)",
"CREATE INDEX IF NOT EXISTS idx_transactions_to_wallet ON transactions(to_address)",
"CREATE INDEX IF NOT EXISTS idx_blocks_miner_wallet ON blocks(miner_address)",
}
// Create indexes
for _, indexSQL := range indexes {
if _, err := dc.DB.Exec(indexSQL); err != nil {
dc.logger.Error("Failed to create index", "error", err, "sql", indexSQL)
return err
}
}
dc.logger.Info("Database tables and indexes created/verified successfully")
return nil
}
// Returns database connection information
func (dc *DatabaseConfig) GetConnectionInfo() map[string]interface{} {
settings := dc.loadSettings()
return map[string]interface{}{
"url": settings.URL,
"user": settings.User,
"poolSize": settings.MaxPoolSize,
}
}
// Closes the database connection
func (dc *DatabaseConfig) Close() error {
if dc.DB != nil {
return dc.DB.Close()
}
return nil
}
// Returns the database connection
func (dc *DatabaseConfig) GetDB() *sql.DB {
return dc.DB
}

View File

@@ -0,0 +1,194 @@
package server
import (
"github.com/charmbracelet/log"
"fmt"
"os"
"time"
"ccoin/utils"
)
var version = "dev"
var buildTime = time.Now().Format(time.RFC3339)
var commit = "none"
type ServerConfig struct {
// App details
Version string
BuildTime string
Commit string
BaseURL string
// Server settings
Host string
Port int
DevelopmentMode bool
// Mining settings
MiningDifficulty int
MiningReward float64
BlockTimeTarget time.Duration
// Transaction settings
DefaultTransactionFee float64
MaxTransactionSize int
MaxMemoLength int
// Security settings
JWTSecret string
RateLimitRequests int
RateLimitWindow time.Duration
// Blockchain settings
MaxBlockSize int
MaxTransactionsPerBlock int
ConfirmationsRequired int
// API settings
MaxPageSize int
DefaultPageSize int
APITimeout time.Duration
// Logging settings
LogLevel string
logger *log.Logger
}
// Creates and initializes a new ServerConfig
func NewServerConfig() *ServerConfig {
config := &ServerConfig{
logger: log.New(os.Stdout),
}
// Load configuration from environment variables with defaults
config.loadConfig()
config.logConfig()
config.validateConfig()
return config
}
// Loads configuration from environment variables with defaults
func (c *ServerConfig) loadConfig() {
// App details (these would typically come from build-time variables)
c.Version = version
c.BuildTime = utils.GetEnvOrDefault("BUILD_TIME", buildTime)
c.BaseURL = utils.GetEnvOrDefault("BASE_URL", "http://localhost:8080")
c.Commit = commit
// Server settings
c.Host = utils.GetEnvOrDefault("SERVER_HOST", "0.0.0.0")
c.Port = utils.GetEnvOrDefault("SERVER_PORT", 8080)
c.DevelopmentMode = utils.GetEnvOrDefault("DEVELOPMENT_MODE", true)
// Mining settings
c.MiningDifficulty = utils.GetEnvOrDefault("MINING_DIFFICULTY", 4)
c.MiningReward = utils.GetEnvOrDefault("MINING_REWARD", 50.0)
c.BlockTimeTarget = time.Duration(utils.GetEnvOrDefault("BLOCK_TIME_TARGET", 600000)) * time.Millisecond // 10 minutes
// Transaction settings
c.DefaultTransactionFee = utils.GetEnvOrDefault("DEFAULT_TRANSACTION_FEE", 5.0)
c.MaxMemoLength = utils.GetEnvOrDefault("MAX_MEMO_LENGTH", 256)
c.MaxTransactionSize = utils.GetEnvOrDefault("MAX_TRANSACTION_SIZE", 1024*1024) // 1MB
// Security settings
c.JWTSecret = utils.GetEnvOrDefault("JWT_SECRET", "change-this-in-production")
c.RateLimitRequests = utils.GetEnvOrDefault("RATE_LIMIT_REQUESTS", 100)
c.RateLimitWindow = time.Duration(utils.GetEnvOrDefault("RATE_LIMIT_WINDOW", 60000)) * time.Millisecond // 1 minute
// Blockchain settings
c.MaxBlockSize = utils.GetEnvOrDefault("MAX_BLOCK_SIZE", 1024*1024) // 1MB
c.MaxTransactionsPerBlock = utils.GetEnvOrDefault("MAX_TRANSACTIONS_PER_BLOCK", 1000)
c.ConfirmationsRequired = utils.GetEnvOrDefault("CONFIRMATIONS_REQUIRED", 6)
// API Settings
c.MaxPageSize = utils.GetEnvOrDefault("MAX_PAGE_SIZE", 100)
c.DefaultPageSize = utils.GetEnvOrDefault("DEFAULT_PAGE_SIZE", 50)
c.APITimeout = time.Duration(utils.GetEnvOrDefault("API_TIMEOUT", 30000)) * time.Millisecond // 30 seconds
// Logging settings
c.LogLevel = utils.GetEnvOrDefault("LOG_LEVEL", "INFO")
}
// Logs the current configuration
func (c *ServerConfig) logConfig() {
c.logger.Info("Server configuration loaded:")
c.logger.Info("Host", "value", c.Host)
c.logger.Info("Port", "value", c.Port)
c.logger.Info("Version", "value", c.Version)
c.logger.Info("Development mode", "value", c.DevelopmentMode)
c.logger.Info("Mining difficulty", "value", c.MiningDifficulty)
c.logger.Info("Mining reward", "value", c.MiningReward)
c.logger.Info("Block time target", "value", c.BlockTimeTarget)
c.logger.Info("Default transaction fee", "value", c.DefaultTransactionFee)
c.logger.Info("Confirmations required", "value", c.ConfirmationsRequired)
c.logger.Info("Max block size", "value", c.MaxBlockSize)
c.logger.Info("Max transactions per block", "value", c.MaxTransactionsPerBlock)
c.logger.Info("Max transaction size", "value", c.MaxTransactionSize)
c.logger.Info("Log level", "value", c.LogLevel)
c.logger.Info("Max page size", "value", c.MaxPageSize)
if c.JWTSecret == "change-this-in-production" && !c.DevelopmentMode {
c.logger.Warn("Using default JWT secret in production mode!")
}
}
// Returns a map of server information
func (c *ServerConfig) GetServerInfo() map[string]interface{} {
return map[string]interface{}{
"host": c.Host,
"port": c.Port,
"development_mode": c.DevelopmentMode,
"version": c.Version,
"mining_difficulty": c.MiningDifficulty,
"mining_reward": c.MiningReward,
"block_time_target": c.BlockTimeTarget,
"confirmations_required": c.ConfirmationsRequired,
}
}
// Validates the configuration values
func (c *ServerConfig) ValidateConfig() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
if c.MiningDifficulty < 1 || c.MiningDifficulty > 32 {
return fmt.Errorf("mining difficulty must be between 1 and 32")
}
if c.MiningReward <= 0 {
return fmt.Errorf("mining reward must be positive")
}
if c.BlockTimeTarget <= 0 {
return fmt.Errorf("block time target must be positive")
}
if c.DefaultTransactionFee < 0 {
return fmt.Errorf("default transaction fee cannot be negative")
}
if c.ConfirmationsRequired <= 0 {
return fmt.Errorf("confirmations required must be positive")
}
if c.MaxPageSize <= 0 {
return fmt.Errorf("max page size must be positive")
}
if c.DefaultPageSize <= 0 {
return fmt.Errorf("default page size must be positive")
}
c.logger.Info("Server configuration validation passed")
return nil
}
// Wrapper that panics on validation error (similar to Kotlin's require)
func (c *ServerConfig) validateConfig() {
if err := c.ValidateConfig(); err != nil {
// panic(err)
c.logger.Fatal("ERROR", "error", err)
}
}

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

33
server/go.mod Normal file
View File

@@ -0,0 +1,33 @@
module ccoin
go 1.25.5
require (
github.com/charmbracelet/log v0.4.2
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.6
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
)

View File

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

View File

@@ -0,0 +1,70 @@
package api
type ApiResponse[T any] struct {
Success bool `json:"success"`
Data *T `json:"data,omitempty"` // Nullable data
Error *string `json:"error,omitempty"` // Nullable error message
Timestamp int64 `json:"timestamp"`
}
type ErrorResponse struct {
Error string `json:"error"`
Code *string `json:"code,omitempty"`
Details map[string]string `json:"details,omitempty"`
Timestamp int64 `json:"timestamp"`
}
type SuccessResponse struct {
Message string `json:"message"`
Timestamp int64 `json:"timestamp"`
}
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime int64 `json:"uptime"`
Database DatabaseHealth `json:"database"`
BlockChain BlockChainHealth `json:"blockchain"`
}
type DatabaseHealth struct {
Connected bool `json:"connected"`
ResponseTime int64 `json:"response_time"`
ActiveConnections int `json:"active_connections"`
MaxConnections int `json:"max_connections"`
}
type BlockChainHealth struct {
LatestBlock int `json:"latest_block"`
PendingTransactions int `json:"pending_transactions"`
NetworkHashRate float64 `json:"network_hash_rate"`
AverageBlockTime int64 `json:"average_block_time"`
}
type PaginationRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
SortBy *string `json:"sort_by,omitempty"`
SortOrder SortOrder `json:"sort_order"`
}
type PaginatedResponse[T any] struct {
Data []T `json:"data"`
Pagination PaginationInfo `json:"pagination"`
}
type PaginationInfo struct {
CurrentPage int `json:"current_page"`
PageSize int `json:"page_size"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrevious bool `json:"has_previous"`
}
type SortOrder string
const (
ASC SortOrder = "ASC"
DESC SortOrder = "DESC"
)

View File

@@ -0,0 +1,46 @@
package block
type StartMiningRequest struct {
MinerAddress string `json:"miner_address"`
Difficulty int `json:"difficulty"`
}
type SubmitMiningRequest struct {
MinerAddress string `json:"miner_address"`
Hash string `json:"hash"`
Nonce int64 `json:"nonce"`
PreviousHash *string `json:"previous_hash,omitempty"`
Timestamp int64 `json:"timestamp"`
}
type BlockResponse struct {
Hash string `json:"hash"`
PreviousHash *string `json:"previous_hash,omitempty"`
MerkleRoot string `json:"merkle_root"`
Timestamp int64 `json:"timestamp"`
Difficulty int `json:"difficulty"`
Nonce int64 `json:"nonce"`
MinerAddress string `json:"miner_address"`
Reward float64 `json:"reward"`
Height int `json:"height"`
TransactionCount int `json:"transaction_count"`
Confirmations int `json:"confirmations"`
}
type MiningStatsResponse struct {
MinerAddress string `json:"miner_address"`
TotalBlocksMined int `json:"total_blocks_mined"`
TotalRewardsEarned float64 `json:"total_rewards_earned"`
LastBlockMined *int64 `json:"last_block_mined,omitempty"`
CurrentDifficulty int `json:"current_difficulty"`
}
type MiningJobResponse struct {
JobId string `json:"job_id"`
Target string `json:"target"`
Difficulty int `json:"difficulty"`
PreviousHash string `json:"previous_hash"`
Height int `json:"height"`
Timestamp int64 `json:"timestamp"`
ExpiresAt int64 `json:"expires_at"`
}

View File

@@ -0,0 +1,39 @@
package transaction
type SendTransactionRequest struct {
FromAddress string `json:"from_address"`
ToAddress string `json:"to_address"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee"`
Memo *string `json:"memo,omitempty"`
Password string `json:"password"`
}
type TransactionResponse struct {
Hash string `json:"hash"`
FromAddress *string `json:"from_address,omitempty"`
ToAddress string `json:"to_address"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee"`
Memo *string `json:"memo,omitempty"`
BlockHash string `json:"block_hash"`
Timestamp int64 `json:"timestamp"`
Status string `json:"status"`
Confirmations int `json:"confirmations"`
}
type TransactionHistoryResponse struct {
Transactions []TransactionResponse `json:"transactions"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type TransactionStatus string
const (
PENDING TransactionStatus = "PENDING"
CONFIRMED TransactionStatus = "CONFIRMED"
FAILED TransactionStatus = "FAILED"
CANCELLED TransactionStatus = "CANCELLED"
)

View File

@@ -0,0 +1,35 @@
package wallet
import "time"
type Wallet struct {
Address string `db:"address" json:"address"`
Label *string `db:"label" json:"label,omitempty"`
PasswordHash string `db:"password_hash" json:"password_hash"`
Balance float64 `db:"balance" json:"balance"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
LastActivity *time.Time `db:"last_activity" json:"last_activity,omitempty"`
}
type CreateWalletRequest struct {
Label *string `json:"label,omitempty"`
Password string `json:"password"`
}
type WalletResponse struct {
Address string `json:"address"` // Format: "random_word:123456" (e.g. "cave:595462")
Balance float64 `json:"balance"`
Label *string `json:"label,omitempty"`
PasswordHash string `json:"password_hash"`
CreatedAt int64 `json:"created_at"`
LastActivity *int64 `json:"last_activity,omitempty"`
}
type BalanceResponse struct {
Address string `json:"address"`
Balance float64 `json:"balance"`
}
type UpdateWalletRequest struct {
Label *string `json:"label,omitempty"`
}

View File

@@ -0,0 +1,35 @@
package routes
import (
"ccoin/models/wallet"
ws "ccoin/services/wallet"
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
// Sets up routes for wallet operations
func WalletRoutes(router *mux.Router, walletService *ws.WalletService) {
walletRouter := router.PathPrefix("/wallet").Subrouter()
// Create a new wallet
walletRouter.HandleFunc("/create", func(w http.ResponseWriter, r *http.Request) {
var req wallet.CreateWalletRequest
// Parse the request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request format", http.StatusBadRequest)
return
}
// Create the wallet using the service
walletResponse, err := walletService.CreateWallet(req)
if err != nil {
http.Error(w, "Failed to create wallet: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(walletResponse)
}).Methods("POST")
}

35
server/server.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import (
"ccoin/config/database"
"ccoin/config/server"
"ccoin/routes/wallet"
ws "ccoin/services/wallet"
"github.com/gorilla/mux"
"net/http"
"github.com/charmbracelet/log"
)
func main() {
// Initialize server
server.NewServerConfig()
// Initialize database
dbConfig := database.NewDatabaseConfig()
if err := dbConfig.Init(); err != nil {
log.Fatal("Database initialization failed:", err)
}
walletService := ws.NewWalletService(dbConfig.GetDB())
// Set up router
r := mux.NewRouter()
routes.WalletRoutes(r, walletService)
// Start server
log.Info("Starting server on :8080")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal("Failed to start server:", err)
}
}

View File

@@ -0,0 +1,219 @@
package wallet
import (
"ccoin/utils/crypto"
"database/sql"
"ccoin/models/wallet"
"errors"
"fmt"
"time"
)
type WalletService struct {
db *sql.DB
}
// Creates a new instance of WalletService
func NewWalletService(db *sql.DB) *WalletService {
return &WalletService{db: db}
}
// Creates a new wallet with optional label
func (ws *WalletService) CreateWallet(req wallet.CreateWalletRequest) (wallet.WalletResponse, error) {
address := crypto.GenerateWalletAddress()
timestamp := time.Now()
var passwordHash string
if req.Password != "" {
passwordHash = crypto.HashPassword(req.Password)
} else {
return wallet.WalletResponse{}, fmt.Errorf("password field is required")
}
_, err := ws.db.Exec(`
INSERT INTO wallets (address, balance, label, password_hash, created_at)
VALUES ($1, $2, $3, $4, $5)`,
address, 0.0, req.Label, passwordHash, timestamp.Unix(),
)
if err != nil {
return wallet.WalletResponse{}, err
}
return wallet.WalletResponse{
Address: address,
Balance: 0.0,
Label: req.Label,
PasswordHash: passwordHash,
CreatedAt: timestamp.Unix(),
LastActivity: nil,
}, nil
}
// Gets wallet by address
func (ws *WalletService) GetWallet(address string) (*wallet.WalletResponse, error) {
var w wallet.Wallet
row := ws.db.QueryRow(`
SELECT address, balance, label, password_hash, created_at, last_activity
FROM wallets WHERE address = $1`, address)
err := row.Scan(&w.Address, &w.Balance, &w.Label, &w.PasswordHash, &w.CreatedAt, &w.LastActivity)
if err != nil {
return nil, err
}
return &wallet.WalletResponse{
Address: w.Address,
Balance: w.Balance,
Label: w.Label,
PasswordHash: w.PasswordHash,
CreatedAt: w.CreatedAt.Unix(),
LastActivity: ptrTime(w.LastActivity),
}, nil
}
// Gets wallet balance
func (ws *WalletService) GetWalletBalance(address string) (wallet.BalanceResponse, error) {
var balance float64
err := ws.db.QueryRow(`SELECT balance FROM wallets WHERE address = $1`, address).Scan(&balance)
if err != nil {
return wallet.BalanceResponse{}, errors.New("wallet not found")
}
return wallet.BalanceResponse{
Address: address,
Balance: balance,
}, nil
}
// Updates wallet balance
func (ws *WalletService) UpdateBalance(address string, amount float64) (bool, error) {
_, err := ws.db.Exec(`UPDATE wallets SET balance = balance + $1, last_activity = $2 WHERE address = $3`, amount, time.Now().Unix(), address)
if err != nil {
return false, err
}
return true, nil
}
// Sets wallet balance to specific amount
func (ws *WalletService) SetBalance(address string, amount float64) (bool, error) {
_, err := ws.db.Exec(`UPDATE wallets SET balance = $1, last_activity = $2 WHERE address = $3`, amount, time.Now().Unix(), address)
if err != nil {
return false, err
}
return true, nil
}
// Updates wallet label
func (ws *WalletService) UpdateLabel(address string, req wallet.UpdateWalletRequest) (bool, error) {
result, err := ws.db.Exec(`UPDATE wallets SET label = $1 WHERE address = $2`, address, req.Label)
if err != nil {
return false, err
}
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0, nil
}
// Checks if wallet exists
func (ws *WalletService) WalletExists(address string) (bool, error) {
var count int64
err := ws.db.QueryRow(`SELECT COUNT(*) FROM wallets WHERE address = $1`, address).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
// Gets all wallets with pagination
func (ws *WalletService) GetAllWallets(limit, offset int) ([]wallet.WalletResponse, error) {
query := fmt.Sprintf(`
SELECT address, balance, label, password_hash, created_at, last_activity
FROM wallets ORDER BY created_at DESC LIMIT %d OFFSET %d`, limit, offset)
rows, err := ws.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var wallets []wallet.WalletResponse
for rows.Next() {
var w wallet.Wallet
if err := rows.Scan(&w.Address, &w.Balance, &w.Label, &w.PasswordHash,
&w.CreatedAt, &w.LastActivity); err != nil {
return nil, err
}
wallets = append(wallets, wallet.WalletResponse{
Address: w.Address,
Balance: w.Balance,
Label: w.Label,
PasswordHash: w.PasswordHash,
CreatedAt: w.CreatedAt.Unix(),
LastActivity: ptrTime(w.LastActivity),
})
}
return wallets, nil
}
// Gets total number of wallets
func (ws *WalletService) GetTotalWalletCount() (int64, error) {
var count int64
err := ws.db.QueryRow(`SELECT COUNT(*) FROM wallets`).Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
// Gets wallet with balance greater than specified amount
func (ws *WalletService) GetWalletsWithBalance(minBalance float64) ([]wallet.WalletResponse, error) {
rows, err := ws.db.Query(`SELECT address, balance, label, password_hash, created_at, last_activity FROM wallets WHERE balance > $1 ORDER BY balance DESC`, minBalance)
if err != nil {
return nil, err
}
defer rows.Close()
var wallets []wallet.WalletResponse
for rows.Next() {
var w wallet.Wallet
if err := rows.Scan(&w.Address, &w.Balance, &w.Label, &w.PasswordHash, &w.CreatedAt, &w.LastActivity); err != nil {
return nil, err
}
wallets = append(wallets, wallet.WalletResponse{
Address: w.Address,
Balance: w.Balance,
Label: w.Label,
PasswordHash: w.PasswordHash,
CreatedAt: w.CreatedAt.Unix(),
LastActivity: ptrTime(w.LastActivity),
})
}
return wallets, nil
}
// Updates last activity timestamp
func (ws *WalletService) UpdateLastActivity(address string) (bool, error) {
_, err := ws.db.Exec(`UPDATE wallets SET last_activity = $1 WHERE address = $2`, time.Now().Unix(), address)
if err != nil {
return false, err
}
return true, nil
}
// Gets total supply across all wallets
func (ws *WalletService) GetTotalSupply() (float64, error) {
var totalSupply float64
err := ws.db.QueryRow(`SELECT SUM(balance) FROM wallets`).Scan(&totalSupply)
if err != nil {
return 0, err
}
return totalSupply, nil
}
// Helper function to convert *time.Time to *int64 for JSON response
func ptrTime(t *time.Time) *int64 {
if t == nil {
return nil
}
unixTime := t.Unix()
return &unixTime
}

View File

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

View File

@@ -1,76 +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") ?: "jdbc: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 = true
// 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,88 +0,0 @@
package org.ccoin.config
import org.slf4j.LoggerFactory
import org.ccoin.BuildConfig
object ServerConfig {
private val logger = LoggerFactory.getLogger(ServerConfig::class.java)
// App Details
val version: String = BuildConfig.VERSION
val buildTime: String = BuildConfig.BUILD_TIME
val baseUrl: String = BuildConfig.BASE_URL
// Server settings
val host: String = System.getenv("SERVER_HOST") ?: "0.0.0.0"
val port: Int = System.getenv("SERVER_PORT")?.toIntOrNull() ?: 8080
val developmentMode: Boolean = System.getenv("DEVELOPMENT_MODE")?.toBoolean() ?: true
// 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 version,
"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,70 +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 passwordHash = varchar("password_hash", 64).nullable()
val createdAt = long("created_at")
val lastActivity = long("last_activity").nullable()
override val primaryKey = PrimaryKey(address)
init {
index(false, createdAt)
index(false, lastActivity)
}
}
object Transactions : Table("transactions") {
val hash = varchar("hash", 64)
val fromAddress = varchar("from_address", 64).nullable()
val toAddress = varchar("to_address", 64)
val amount = decimal("amount", 20, 8)
val fee = decimal("fee", 20, 8).default(BigDecimal.ZERO)
val memo = text("memo").nullable()
val blockHash = varchar("block_hash", 64).nullable()
val timestamp = long("timestamp")
val status = varchar("status", 20).default("pending")
val confirmations = integer("confirmations").default(0)
override val primaryKey = PrimaryKey(hash)
init {
index(false, fromAddress)
index(false, toAddress)
index(false, blockHash)
index(false, timestamp)
index(false, status)
}
}
object Blocks : Table("blocks") {
val hash = varchar("hash", 64)
val previousHash = varchar("previous_hash", 64).nullable()
val merkleRoot = varchar("merkle_root", 64)
val timestamp = long("timestamp")
val difficulty = integer("difficulty")
val nonce = long("nonce")
val minerAddress = varchar("miner_address", 64)
val reward = decimal("reward", 20, 8)
val height = integer("height").autoIncrement()
val transactionCount = integer("transaction_count").default(0)
val confirmations = integer("confirmations").default(0)
override val primaryKey = PrimaryKey(hash)
init {
index(false, height)
index(false, minerAddress)
index(false, timestamp)
index(false, previousHash)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +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,
val password: String
)
@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,30 +0,0 @@
package org.ccoin.models
import kotlinx.serialization.Serializable
@Serializable
data class CreateWalletRequest(
val label: String? = null,
val password: String
)
@Serializable
data class WalletResponse(
val address: String, // Format random_word:random_6_digits (e.g. "phoenix:123456")
val balance: Double,
val label: String?,
val passwordHash: String,
val createdAt: Long,
val lastActivity: Long?
)
@Serializable
data class BalanceResponse(
val address: String,
val balance: Double
)
@Serializable
data class UpdateWalletRequest(
val label: String?
)

View File

@@ -1,208 +0,0 @@
package org.ccoin.routes
import io.ktor.http.*
import io.ktor.server.application.*
import org.ccoin.config.ServerConfig
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
fun Route.apiRoutes() {
route("/api") {
/** Get all available API routes */
get("/routes") {
try {
val routes = mapOf(
"wallet" to mapOf(
"POST /wallet/create" to "Create a new wallet",
"GET /wallet/{address}" to "Get wallet by address",
"GET /wallet/{address}/balance" to "Get wallet balance",
"PUT /wallet/{address}/label" to "Update wallet label",
"GET /wallet/list" to "Get all wallets with pagination",
"GET /wallet/rich" to "Get wallets with minimum balance",
"GET /wallet/{address}/exists" to "Check if wallet exists"
),
"transaction" to mapOf(
"POST /transaction/send" to "Send a transaction",
"GET /transaction/{hash}" to "Get transaction by hash",
"GET /transaction/history/{address}" to "Get transaction history for address",
"GET /transaction/pending" to "Get pending transactions",
"GET /transaction/count/{address}" to "Get transaction count for address",
"GET /transaction/list" to "Get all transactions with pagination",
"GET /transaction/stats" to "Get network transaction statistics"
),
"mining" to mapOf(
"POST /mining/start" to "Start a mining job",
"POST /mining/submit" to "Submit mining result",
"GET /mining/difficulty" to "Get current mining difficulty",
"GET /mining/stats/{address}" to "Get mining statistics for miner",
"GET /mining/network" to "Get network mining statistics",
"GET /mining/pending-transactions" to "Get pending transactions for mining",
"POST /mining/validate" to "Validate mining job",
"GET /mining/leaderboard" to "Get mining leaderboard"
),
"block" to mapOf(
"GET /block/{hash}" to "Get block by hash",
"GET /block/height/{height}" to "Get block by height",
"GET /block/{hash}/exists" to "Check if block exists",
"GET /blocks/latest" to "Get latest blocks",
"GET /blocks/range" to "Get blocks in height range",
"GET /blocks/miner/{address}" to "Get blocks by miner address",
"GET /blocks/time-range" to "Get blocks by timestamp range",
"GET /blocks/difficulty/{difficulty}" to "Get blocks by difficulty",
"GET /blocks/stats" to "Get blockchain statistics"
),
"health" to mapOf(
"GET /health" to "Basic health check",
"GET /health/detailed" to "Detailed health check with system metrics",
"GET /health/database" to "Database health check",
"GET /health/blockchain" to "Blockchain health check",
"GET /ready" to "Readiness probe (Kubernetes)",
"GET /live" to "Liveness probe (Kubernetes)",
"GET /version" to "Service version and build info"
),
"api" to mapOf(
"GET /api/routes" to "Get all available API routes",
"GET /api/docs" to "Get API documentation",
"GET /api/examples" to "Get API usage examples"
)
)
val summary = ApiSummary(
totalEndpoints = routes.values.sumOf { it.size },
categories = routes.keys.toList(),
baseUrl = ServerConfig.baseUrl,
documentation = "https://github.com/your-repo/ccoin-server/docs",
version = ServerConfig.version
)
val response = ApiRoutesResponse(summary, routes)
call.respond(response)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf(
"error" to (e.message ?: "Failed to get routes information")
))
}
}
/** Get API documentation */
get("/docs") {
try {
call.respond(mapOf(
"title" to "CCoin API Documentation",
"version" to ServerConfig.version,
"description" to "REST API for CCoin cryptocurrency server",
"baseUrl" to ServerConfig.baseUrl,
"authentication" to "None required",
"contentType" to "application/json",
"rateLimit" to "100 requests per minute",
"addressFormat" to "random_word:random_6_digits (e.g., phoenix:123456)",
"hashFormat" to "64-character hex string",
"defaultDifficulty" to "4",
"defaultReward" to "50.0",
"memoMaxLength" to "256"
))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf(
"error" to (e.message ?: "Failed to get API documentation")
))
}
}
/** Get API usage examples */
get("/examples") {
try {
val examples = mapOf(
"createWallet" to ApiExample(
method = "POST",
url = "/wallet/create",
body = """{"label": "My Wallet"}""",
response = """{"address": "phoenix:123456", "balance": 0.0, "label": "My Wallet", "createdAt": 1703097600, "lastActivity": null}"""
),
"sendTransaction" to ApiExample(
method = "POST",
url = "/transaction/send",
body = """{"fromAddress": "phoenix:123456", "toAddress": "dragon:789012", "amount": 10.5, "fee": 0.01, "memo": "Payment for services"}""",
response = """{"hash": "abc123...", "fromAddress": "phoenix:123456", "toAddress": "dragon:789012", "amount": 10.5, "fee": 0.01, "memo": "Payment for services", "timestamp": 1703097600, "status": "CONFIRMED"}"""
),
"startMining" to ApiExample(
method = "POST",
url = "/mining/start",
body = """{"minerAddress": "tiger:456789", "difficulty": 4}""",
response = """{"jobId": "job123", "target": "0000", "difficulty": 4, "previousHash": "def456...", "height": 100, "timestamp": 1703097600, "expiresAt": 1703097900}"""
),
"getBlock" to ApiExample(
method = "GET",
url = "/block/abc123...",
response = """{"hash": "abc123...", "previousHash": "def456...", "merkleRoot": "ghi789...", "timestamp": 1703097600, "difficulty": 4, "nonce": 12345, "minerAddress": "tiger:456789", "reward": 50.0, "height": 100, "transactionCount": 0, "confirmations": 6}"""
)
)
val curlExamples = mapOf(
"createWallet" to """curl -X POST http://localhost:8080/wallet/create -H 'Content-Type: application/json' -d '{"label":"My Wallet"}'""",
"getBalance" to "curl http://localhost:8080/wallet/phoenix:123456/balance",
"sendTransaction" to """curl -X POST http://localhost:8080/transaction/send -H 'Content-Type: application/json' -d '{"fromAddress":"phoenix:123456","toAddress":"dragon:789012","amount":10.5}'""",
"getLatestBlocks" to "curl http://localhost:8080/blocks/latest?limit=5"
)
val response = ApiExamplesResponse(
title = "CCoin API Examples",
description = "Common usage examples for the CCoin API",
examples = examples,
curlExamples = curlExamples
)
call.respond(response)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf(
"error" to (e.message ?: "Failed to get API examples")
))
}
}
}
}
@Serializable
data class ApiSummary(
val totalEndpoints: Int,
val categories: List<String>,
val baseUrl: String,
val documentation: String,
val version: String
)
@Serializable
data class ApiRoutesResponse(
val summary: ApiSummary,
val routes: Map<String, Map<String, String>>
)
@Serializable
data class ApiExample(
val method: String,
val url: String,
val body: String? = null,
val response: String
)
@Serializable
data class ApiExamplesResponse(
val title: String,
val description: String,
val examples: Map<String, ApiExample>,
val curlExamples: Map<String, String>
)
@Serializable
data class PaginationInfo(
val currentPage: Int,
val pageSize: Int,
val totalItems: Long,
val totalPages: Long,
val hasNext: Boolean,
val hasPrevious: Boolean
)

View File

@@ -1,349 +0,0 @@
package org.ccoin.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.ccoin.services.BlockService
import org.ccoin.services.ValidationService
import org.ccoin.models.BlockResponse
import kotlinx.serialization.Serializable
fun Route.blockRoutes() {
route("/block") {
/** Get block by hash */
get("/{hash}") {
try {
val hash = call.parameters["hash"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Block hash parameter required"))
return@get
}
// Validate hash format
if (!ValidationService.validateBlockHash(hash)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid block hash format"))
return@get
}
val block = BlockService.getBlock(hash)
if (block != null) {
call.respond(block)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Block not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get block")))
}
}
/** Get block by height */
get("/height/{height}") {
try {
val height = call.parameters["height"]?.toIntOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid height parameter required"))
return@get
}
if (height < 0) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Height must be non-negative"))
return@get
}
val block = BlockService.getBlockByHeight(height)
if (block != null) {
call.respond(block)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Block not found at height $height"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get block by height")))
}
}
/** Check if block exists */
get("/{hash}/exists") {
try {
val hash = call.parameters["hash"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Block hash parameter required"))
return@get
}
// Validate hash format
if (!ValidationService.validateBlockHash(hash)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid block hash format"))
return@get
}
val exists = BlockService.blockExists(hash)
call.respond(
BlockExistsResponse(
hash = hash,
exists = exists
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to check block existence")))
}
}
}
route("/blocks") {
/** Get latest blocks */
get("/latest") {
try {
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10
if (limit !in 1..100) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Limit must be between 1 and 100"))
return@get
}
val blocks = BlockService.getLatestBlocks(limit)
call.respond(
BlocksLatestResponse(
blocks = blocks,
count = blocks.size
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get latest blocks")))
}
}
/** Get blocks in height range */
get("/range") {
try {
val fromHeight = call.request.queryParameters["from"]?.toIntOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'from' height parameter required"))
return@get
}
val toHeight = call.request.queryParameters["to"]?.toIntOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'to' height parameter required"))
return@get
}
if (fromHeight < 0 || toHeight < 0) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Heights must be non-negative"))
return@get
}
if (fromHeight > toHeight) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "From height must be less than or equal to to height"))
return@get
}
if (toHeight - fromHeight > 1000) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Range too large (max 1000 blocks)"))
return@get
}
val blockRange = BlockService.getBlocksInRange(fromHeight, toHeight)
call.respond(blockRange)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks in range")))
}
}
/** Get blocks by miner address */
get("/miner/{address}") {
try {
val address = call.parameters["address"] ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Address parameter required"))
return@get
}
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 50
// Validate address format
if (!ValidationService.validateWalletAddress(address)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid address format"))
return@get
}
// Validate pagination
val validation = ValidationService.validatePagination(page, pageSize)
if (!validation.isValid) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to validation.getErrorMessage()))
return@get
}
val offset = (page - 1) * pageSize
val blocks = BlockService.getBlocksByMiner(address, pageSize, offset)
// call.respond(mapOf(
// "blocks" to blocks,
// "minerAddress" to address,
// "pagination" to mapOf(
// "currentPage" to page,
// "pageSize" to pageSize,
// "hasNext" to (blocks.size == pageSize),
// "hasPrevious" to (page > 1)
// )
// ))
call.respond(
BlocksMinerResponse(
blocks = blocks,
minerAddress = address,
pagination = PaginationInfo(
currentPage = page,
pageSize = pageSize,
hasNext = (blocks.size == pageSize),
hasPrevious = (page > 1),
totalPages = blocks.size.toLong(),
totalItems = blocks.size.toLong()
)
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by miner")))
}
}
/** Get blocks by timestamp range */
get("/time-range") {
try {
val fromTime = call.request.queryParameters["from"]?.toLongOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'from' timestamp parameter required"))
return@get
}
val toTime = call.request.queryParameters["to"]?.toLongOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid 'to' timestamp parameter required"))
return@get
}
if (fromTime < 0 || toTime < 0) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Timestamps must be non-negative"))
return@get
}
if (fromTime > toTime) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "From time must be less than or equal to to time"))
return@get
}
// Limit to 30 days max
if (toTime - fromTime > 30 * 24 * 60 * 60) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Time range too large (max 30 days)"))
return@get
}
val blocks = BlockService.getBlocksByTimeRange(fromTime, toTime)
call.respond(
BlocksTimeRangeResponse(
blocks = blocks,
fromTime = fromTime,
toTime = toTime,
count = blocks.size
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by time range")))
}
}
/** Get blocks by difficulty */
get("/difficulty/{difficulty}") {
try {
val difficulty = call.parameters["difficulty"]?.toIntOrNull() ?: run {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Valid difficulty parameter required"))
return@get
}
if (!ValidationService.validateMiningDifficulty(difficulty)) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid mining difficulty"))
return@get
}
val blocks = BlockService.getBlocksByDifficulty(difficulty)
call.respond(
BlocksDifficultyResponse(
blocks = blocks,
difficulty = difficulty,
count = blocks.size
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blocks by difficulty")))
}
}
/** Get blockchain statistics */
get("/stats") {
try {
val totalBlocks = BlockService.getTotalBlockCount()
val latestHeight = BlockService.getLatestBlockHeight()
val latestHash = BlockService.getLatestBlockHash()
val averageBlockTime = BlockService.getAverageBlockTime()
val totalRewards = BlockService.getTotalRewardsDistributed()
call.respond(
BlocksStatsResponse(
totalBlocks = totalBlocks,
latestHeight = latestHeight,
latestHash = latestHash,
averageBlockTime = averageBlockTime,
totalRewardsDistributed = totalRewards
)
)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get blockchain statistics")))
}
}
}
}
@Serializable
data class BlockExistsResponse(
val hash: String,
val exists: Boolean
)
@Serializable
data class BlocksLatestResponse(
val blocks: List<BlockResponse>,
val count: Int
)
@Serializable
data class BlocksMinerResponse(
val blocks: List<BlockResponse>,
val minerAddress: String,
val pagination: PaginationInfo
)
@Serializable
data class BlocksTimeRangeResponse(
val blocks: List<BlockResponse>,
val fromTime: Long,
val toTime: Long,
val count: Int
)
@Serializable
data class BlocksDifficultyResponse(
val blocks: List<BlockResponse>,
val difficulty: Int,
val count: Int
)
@Serializable
data class BlocksStatsResponse(
val totalBlocks: Long,
val latestHeight: Int,
val latestHash: String,
val averageBlockTime: Long,
val totalRewardsDistributed: Double
)

View File

@@ -1,315 +0,0 @@
package org.ccoin.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.ccoin.config.DatabaseConfig
import org.ccoin.config.ServerConfig
import org.ccoin.models.DatabaseHealth
import org.ccoin.models.BlockchainHealth
import org.ccoin.models.HealthResponse
import org.ccoin.services.BlockService
import org.ccoin.services.TransactionService
import org.ccoin.services.WalletService
import org.ccoin.services.MiningService
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transaction
import java.lang.management.ManagementFactory
import kotlinx.serialization.Serializable
fun Route.healthRoutes() {
/** Basic health check */
get("/health") {
try {
val startTime = System.currentTimeMillis()
// Check database connectivity
val dbHealth = checkDatabaseHealth()
// Check blockchain health
val blockchainHealth = checkBlockchainHealth()
val uptime = ManagementFactory.getRuntimeMXBean().uptime
val health = HealthResponse(
status = if (dbHealth.connected) "healthy" else "unhealthy",
version = ServerConfig.version,
uptime = uptime,
database = dbHealth,
blockchain = blockchainHealth
)
val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable
call.respond(statusCode, health)
} catch (e: Exception) {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"status" to "unhealthy",
"error" to (e.message ?: "Health check failed")
))
}
}
/** Detailed health check */
get("/health/detailed") {
try {
val dbHealth = checkDatabaseHealth()
val blockchainHealth = checkBlockchainHealth()
// Additional checks
val memoryUsage = getMemoryUsage()
val diskSpace = getDiskSpace()
val networkStats = getNetworkStats()
call.respond(HealthDetailedResponse(
status = if (dbHealth.connected) "healthy" else "unhealthy",
version = ServerConfig.version,
uptime = ManagementFactory.getRuntimeMXBean().uptime,
timestamp = System.currentTimeMillis(),
database = dbHealth,
blockchain = blockchainHealth,
system = SystemInfo(memoryUsage, diskSpace),
network = networkStats,
))
} catch (e: Exception) {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"status" to "unhealthy",
"error" to (e.message ?: "Detailed health check failed")
))
}
}
/** Database health check */
get("/health/database") {
try {
val dbHealth = checkDatabaseHealth()
val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable
call.respond(statusCode, dbHealth)
} catch (e: Exception) {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"connected" to false,
"error" to (e.message ?: "Database health check failed")
))
}
}
/** Blockchain health check */
get("/health/blockchain") {
try {
val blockchainHealth = checkBlockchainHealth()
call.respond(blockchainHealth)
} catch (e: Exception) {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"error" to (e.message ?: "Blockchain health check failed")
))
}
}
/** Readiness probe (for Kubernetes) */
get("/ready") {
try {
val dbHealth = checkDatabaseHealth()
if (dbHealth.connected) {
call.respond(HttpStatusCode.OK, mapOf("status" to "ready"))
} else {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf("status" to "not ready"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"status" to "not ready",
"error" to (e.message ?: "Readiness check failed")
))
}
}
/** Liveness probe (for Kubernetes) */
get("/live") {
try {
// Simple liveness check - just return OK if the service is running
call.respond(HttpStatusCode.OK, mapOf("status" to "alive"))
} catch (e: Exception) {
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
"status" to "dead",
"error" to (e.message ?: "Liveness check failed")
))
}
}
/** Service version and build info */
get("/version") {
try {
call.respond(mapOf(
"version" to ServerConfig.version,
"buildTime" to ServerConfig.buildTime,
"gitCommit" to System.getProperty("git.commit", "unknown"),
"javaVersion" to System.getProperty("java.version"),
// "kotlinVersion" to System.getProperty("kotlin_version").toString()
))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf(
"error" to (e.message ?: "Version check failed")
))
}
}
}
/** Check database health */
private fun checkDatabaseHealth(): DatabaseHealth {
return try {
val startTime = System.currentTimeMillis()
transaction {
// Simple query to test connectivity
WalletService.getTotalWalletCount()
}
val responseTime = System.currentTimeMillis() - startTime
DatabaseHealth(
connected = true,
responseTime = responseTime,
activeConnections = 0, // Would need HikariCP integration to get real numbers
maxConnections = 20
)
} catch (e: Exception) {
DatabaseHealth(
connected = false,
responseTime = -1,
activeConnections = 0,
maxConnections = 20
)
}
}
/** Check blockchain health */
private fun checkBlockchainHealth(): BlockchainHealth {
return try {
val latestBlock = BlockService.getLatestBlockHeight()
val pendingTransactions = TransactionService.getPendingTransactions().size
val networkHashRate = MiningService.getNetworkHashRate()
val averageBlockTime = BlockService.getAverageBlockTime()
BlockchainHealth(
latestBlock = latestBlock,
pendingTransactions = pendingTransactions,
networkHashRate = networkHashRate,
averageBlockTime = averageBlockTime
)
} catch (e: Exception) {
BlockchainHealth(
latestBlock = 0,
pendingTransactions = 0,
networkHashRate = 0.0,
averageBlockTime = 0L
)
}
}
/** Get memory usage information */
private fun getMemoryUsage(): MemoryInfo {
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory()
val totalMemory = runtime.totalMemory()
val freeMemory = runtime.freeMemory()
val usedMemory = totalMemory - freeMemory
return MemoryInfo(
maxMemory = maxMemory,
totalMemory = totalMemory,
usedMemory = usedMemory,
freeMemory = freeMemory,
usagePercentage = ((usedMemory.toDouble() / maxMemory) * 100).toInt()
)
}
/** Get disk space information */
private fun getDiskSpace(): DiskInfo {
return try {
val file = java.io.File(".")
val totalSpace = file.totalSpace
val freeSpace = file.freeSpace
val usedSpace = totalSpace - freeSpace
DiskInfo(
totalSpace = totalSpace,
freeSpace = freeSpace,
usedSpace = usedSpace,
usagePercentage = ((usedSpace.toDouble() / totalSpace) * 100).toInt()
)
} catch (e: Exception) {
DiskInfo(0,0,0,0)
}
}
/** Get network statistics */
private fun getNetworkStats(): NetworkStats {
return try {
val totalBlocks = BlockService.getTotalBlockCount()
val totalTransactions = TransactionService.getTotalTransactionCount()
val totalWallets = WalletService.getTotalWalletCount()
val totalSupply = WalletService.getTotalSupply()
NetworkStats(
totalBlocks = totalBlocks,
totalTransactions = totalTransactions,
totalWallets = totalWallets,
totalSupply = totalSupply
)
} catch (e: Exception) {
NetworkStats(0,0,0,0.0)
}
}
@Serializable
data class HealthDetailedResponse(
val status: String,
val version: String,
val uptime: Long,
val timestamp: Long,
val database: DatabaseHealth,
val blockchain: BlockchainHealth,
val system: SystemInfo,
val network: NetworkStats,
// val config: Map<String, Any>
)
@Serializable
data class MemoryInfo(
val maxMemory: Long,
val totalMemory: Long,
val usedMemory: Long,
val freeMemory: Long,
val usagePercentage: Int
)
@Serializable
data class DiskInfo(
val totalSpace: Long,
val freeSpace: Long,
val usedSpace: Long,
val usagePercentage: Int
)
@Serializable
data class NetworkStats(
val totalBlocks: Long,
val totalTransactions: Long,
val totalWallets: Long,
val totalSupply: Double
)
@Serializable
data class SystemInfo(
val memory: MemoryInfo,
val disk: DiskInfo
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,150 +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, password: String): WalletResponse {
val address = CryptoUtils.generateWalletAddress()
val timestamp = Instant.now().epochSecond
val passwordHash = CryptoUtils.hashPassword(password)
return transaction {
Tables.Wallets.insert {
it[Tables.Wallets.address] = address
it[Tables.Wallets.label] = label
it[Tables.Wallets.passwordHash] = passwordHash
it[createdAt] = timestamp
}
WalletResponse(
address = address,
balance = 0.0,
label = label,
passwordHash = passwordHash,
createdAt = timestamp,
lastActivity = 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.passwordHash].toString(),
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.passwordHash].toString(),
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.passwordHash].toString(),
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,129 +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) }
}
/** Hashes password */
fun hashPassword(password: String): String = sha256("ccoin_password_$password")
/** 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' }
/** Verifies password hash */
fun verifyPassword(password: String, storedHash: String): Boolean = hashPassword(password) == storedHash
}

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
}

View File

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

View File

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

131
server/utils/crypto/main.go Normal file
View File

@@ -0,0 +1,131 @@
package crypto
import (
"crypto/sha256"
"fmt"
"ccoin/utils"
"strings"
)
var words = []string{
"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
func GenerateWalletAddress() string {
randomWord := words[utils.RandInt(0, len(words))]
randomDigits := fmt.Sprintf("%06d", utils.RandInt(0, 1000000))
return fmt.Sprintf("%s:%s", randomWord, randomDigits)
}
// Validates if an address follows the correct format
func IsValidAddress(address string) bool {
parts := strings.Split(address, ":")
if len(parts) != 2 {
return false
}
word := parts[0]
digits := parts[1]
return len(word) > 0 &&
utils.IsAlpha(word) &&
len(digits) == 6 &&
utils.IsDigit(digits)
}
// Generates SHA-256 hash of input string
func SHA256(input string) string {
hash := sha256.New()
hash.Write([]byte(input))
return fmt.Sprintf("%x", hash.Sum(nil))
}
// Hashes password
func HashPassword(password string) string {
return SHA256(fmt.Sprintf("ccoin_password_%s", password))
}
// Generates a transaction hash
func GenerateTransactionHash(fromAddress *string, toAddress string, amount float64, timestamp int64, nonce int64) string {
from := "genesis"
if fromAddress != nil {
from = *fromAddress
}
input := fmt.Sprintf("%s:%s:%f:%d:%d", from, toAddress, amount, timestamp, nonce)
return SHA256(input)
}
// Generates a block hash
func GenerateBlockHash(previousHash string, merkleRoot string, timestamp int64, difficulty int, nonce int64) string {
input := fmt.Sprintf("%s:%s:%d:%d:%d", previousHash, merkleRoot, timestamp, difficulty, nonce)
return SHA256(input)
}
// Validates if a hash meets the mining difficulty requirement
func IsValidHash(hash string, difficulty int) bool {
target := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, target)
}
// Generates mining job id
func GenerateJobId() string {
return SHA256(fmt.Sprintf("job:%d:%d", utils.GetCurrentTimeMillis(), utils.RandInt(0, 1<<63-1)))[:16]
}
// Calculate merkle root from transaction hashes
func CalculateMerkleRoot(transactionHashes []string) string {
if len(transactionHashes) == 0 {
return SHA256("empty")
}
if len(transactionHashes) == 1 {
return transactionHashes[0]
}
hashes := transactionHashes
for len(hashes) > 1 {
var newHashes []string
for i := 0; i < len(hashes); i += 2 {
left := hashes[i]
right := left // Default to left if there's no right
if i+1< len(hashes) {
right = hashes[i+1]
}
newHashes = append(newHashes, SHA256(fmt.Sprintf("%s:%s", left, right)))
}
hashes = newHashes
}
return hashes[0]
}
// Generates a random nonce for mining
func GenerateNonce() int64 {
return int64(utils.RandInt(0, 1<<63-1))
}
// Validates transaction hash format
func IsValidTransactionHash(hash string) bool {
return len(hash) == 64 && utils.IsHex(hash)
}
// Validates block hash format
func IsValidBlockHash(hash string) bool {
return len(hash) == 64 && utils.IsHex(hash)
}
// Verifies password hash
func VerifyPassword(password string, storedHash string) bool {
return HashPassword(password) == storedHash
}

33
server/utils/env.go Normal file
View File

@@ -0,0 +1,33 @@
package utils
import (
"os"
"strconv"
)
// Gets value from environment or uses default value
func GetEnvOrDefault[T string | int | bool | float64](key string, defaultValue T) T {
value, exists := os.LookupEnv(key)
if !exists {
return defaultValue
}
// Handle conversion based on the type of defaultValue
switch any(defaultValue).(type) {
case string:
return any(value).(T)
case int:
if intValue, err := strconv.Atoi(value); err == nil {
return any(intValue).(T)
}
case bool:
if boolValue, err := strconv.ParseBool(value); err == nil {
return any(boolValue).(T)
}
case float64:
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
return any(floatValue).(T)
}
}
return defaultValue // return default value if conversion fails
}

View File

@@ -0,0 +1,2 @@
package utils

48
server/utils/general.go Normal file
View File

@@ -0,0 +1,48 @@
package utils
import (
"crypto/rand"
"math/big"
"time"
)
// Generates a random integer in the range (min, max)
func RandInt(min, max int) int {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min)))
return int(n.Int64()) + min
}
// Get the current time in milliseconds
func GetCurrentTimeMillis() int64 {
return time.Now().UnixNano() / int64(time.Millisecond) // Convert nanoseconds to milliseconds
}
// IsAlpha checks if the string contains only alphabetic characters
func IsAlpha(s string) bool {
for _, r := range s {
if !('a' <= r && r <= 'z') && !('A' <= r && r <= 'Z') {
return false
}
}
return true
}
// IsDigit checks if the string contains only digits
func IsDigit(s string) bool {
for _, r := range s {
if !(r >= '0' && r <= '9') {
return false
}
}
return true
}
// IsHex checks if the string is a valid hexadecimal representation
func IsHex(s string) bool {
for _, r := range s {
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
return false
}
}
return true
}

View File

@@ -0,0 +1,2 @@
package hash