Compare commits

15 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
darwincereska
3f4349c9d2 feat: first release 2025-12-19 10:14:05 -05:00
darwincereska
194fd7357c feat: fixed some route serialization 2025-12-18 19:48:50 -05:00
darwincereska
9a644b689a feat: added health and api routes 2025-12-18 09:48:54 -05:00
darwincereska
9bc861f1d1 feat: added block routes 2025-12-18 09:42:28 -05:00
darwincereska
89e45128b6 feat: added mining routes 2025-12-18 09:40:05 -05:00
darwincereska
3c097af03d feat: added transaction routes 2025-12-18 09:37:43 -05:00
darwincereska
35a73c340c feat: added wallet route 2025-12-18 09:34:36 -05:00
darwincereska
1c8fe77a43 feat: added rest of services 2025-12-18 09:22:54 -05:00
darwincereska
6d360df21d feat: added mining service 2025-12-17 10:32:53 -05:00
darwincereska
a6021f9523 feat: added transaction service 2025-12-17 10:26:56 -05:00
60 changed files with 1182 additions and 1106 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,121 +0,0 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val exposed_version: String by project
val postgresql_version: String by project
val hikari_version: String by project
val flyway_version: String by project
val bouncycastle_version: String by project
plugins {
kotlin("jvm") version "2.2.21"
kotlin("plugin.serialization") version "2.2.21"
id("io.ktor.plugin") version "3.3.3"
id("com.gradleup.shadow") version "9.3.0"
id("org.flywaydb.flyway") version "11.19.0"
application
}
group = "org.ccoin"
version = "1.0.0"
application {
mainClass.set("org.ccoin.ServerKt")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}
repositories {
mavenCentral()
}
dependencies {
// Ktor server
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cors-jvm:$ktor_version")
implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-status-pages-jvm:$ktor_version")
implementation("io.ktor:ktor-server-compression-jvm:$ktor_version")
implementation("io.ktor:ktor-server-default-headers-jvm:$ktor_version")
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
implementation("io.ktor:ktor-server-config-yaml:$ktor_version")
// Database
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version")
implementation("org.postgresql:postgresql:$postgresql_version")
implementation("com.zaxxer:HikariCP:$hikari_version")
implementation("org.flywaydb:flyway-core:$flyway_version")
implementation("org.flywaydb:flyway-database-postgresql:$flyway_version")
// Logging
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.0")
// Crypto utilities
implementation("org.bouncycastle:bcprov-jdk18on:$bouncycastle_version")
implementation("org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version")
// JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// Testing
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
testImplementation("io.mockk:mockk:1.13.13")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
testImplementation("org.testcontainers:testcontainers:1.20.3")
testImplementation("org.testcontainers:postgresql:1.20.3")
testImplementation("org.testcontainers:junit-jupiter:1.20.3")
}
tasks.withType<Test> {
useJUnitPlatform()
}
// Fat JAR configuration for Docker
tasks.jar {
enabled = false
}
tasks.shadowJar {
archiveClassifier.set("")
manifest {
attributes["Main-Class"] = "org.ccoin.ServerKt"
}
mergeServiceFiles()
}
// Flyway configuration
flyway {
url = "jdbc:postgresql://localhost:5432/ccoin"
user = "ccoin"
password = "ccoin"
locations = arrayOf("classpath:db/migration")
baselineOnMigrate = true
validateOnMigrate = true
}
// Docker build task
tasks.register<Exec>("buildDocker") {
dependsOn("shadowJar")
commandLine("docker", "build", "-t", "ccoin-server:latest", ".")
}
// Development task
tasks.register("dev") {
dependsOn("run")
doFirst {
project.ext.set("development", true)
}
}

View File

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

View File

@@ -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,5 +0,0 @@
package org.ccoin
fun main() {
println("CCoin Server Started")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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