Compare commits
5 Commits
v1.0.0
...
go-rewrite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c5dba721 | ||
|
|
23f6dba3f3 | ||
|
|
d7338f717b | ||
|
|
59adb32e68 | ||
|
|
f5f40eb79c |
4
server/.gitignore
vendored
4
server/.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# Go files
|
||||||
|
ccoin
|
||||||
|
go.sum
|
||||||
|
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
|
|||||||
@@ -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
49
server/Makefile
Normal 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
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
207
server/config/database/config.go
Normal file
207
server/config/database/config.go
Normal 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
|
||||||
|
}
|
||||||
194
server/config/server/config.go
Normal file
194
server/config/server/config.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
33
server/go.mod
Normal 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
|
||||||
|
)
|
||||||
@@ -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
|
|
||||||
70
server/models/api/model.go
Normal file
70
server/models/api/model.go
Normal 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"
|
||||||
|
)
|
||||||
46
server/models/block/model.go
Normal file
46
server/models/block/model.go
Normal 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"`
|
||||||
|
}
|
||||||
39
server/models/transaction/model.go
Normal file
39
server/models/transaction/model.go
Normal 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"
|
||||||
|
)
|
||||||
35
server/models/wallet/model.go
Normal file
35
server/models/wallet/model.go
Normal 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"`
|
||||||
|
}
|
||||||
35
server/routes/wallet/routes.go
Normal file
35
server/routes/wallet/routes.go
Normal 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
35
server/server.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
219
server/services/wallet/service.go
Normal file
219
server/services/wallet/service.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.ccoin.exceptions
|
|
||||||
|
|
||||||
/** Base exception class for all CCoin-related exceptions */
|
|
||||||
open class CCoinException(
|
|
||||||
message: String,
|
|
||||||
cause: Throwable? = null,
|
|
||||||
val errorCode: String? = null
|
|
||||||
) : Exception(message, cause)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.ccoin.exceptions
|
|
||||||
|
|
||||||
class InsufficientFundsException(
|
|
||||||
val address: String,
|
|
||||||
val requestedAmount: Double,
|
|
||||||
val availableBalance: Double
|
|
||||||
) : CCoinException(
|
|
||||||
message = "Insufficient funds in wallet $address. Requested: $requestedAmount, Available: $availableBalance",
|
|
||||||
errorCode = "INSUFFICIENT_FUNDS"
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.ccoin.exceptions
|
|
||||||
|
|
||||||
class InvalidTransactionException(
|
|
||||||
message: String,
|
|
||||||
val transactionHash: String? = null
|
|
||||||
) : CCoinException(
|
|
||||||
message = message,
|
|
||||||
errorCode = "INVALID_TRANSACTION"
|
|
||||||
)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.ccoin.exceptions
|
|
||||||
|
|
||||||
class WalletNotFoundException(
|
|
||||||
val address: String
|
|
||||||
) : CCoinException(
|
|
||||||
message = "Wallet with address '$address' not found",
|
|
||||||
errorCode = "WALLET_NOT_FOUND"
|
|
||||||
)
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package org.ccoin.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ApiResponse<T>(
|
|
||||||
val success: Boolean,
|
|
||||||
val data: T? = null,
|
|
||||||
val error: String? = null,
|
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ErrorResponse(
|
|
||||||
val error: String,
|
|
||||||
val code: String? = null,
|
|
||||||
val details: Map<String, String>? = null,
|
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SuccessResponse(
|
|
||||||
val message: String,
|
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HealthResponse(
|
|
||||||
val status: String,
|
|
||||||
val version: String,
|
|
||||||
val uptime: Long,
|
|
||||||
val database: DatabaseHealth,
|
|
||||||
val blockchain: BlockchainHealth
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class DatabaseHealth(
|
|
||||||
val connected: Boolean,
|
|
||||||
val responseTime: Long,
|
|
||||||
val activeConnections: Int,
|
|
||||||
val maxConnections: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BlockchainHealth(
|
|
||||||
val latestBlock: Int,
|
|
||||||
val pendingTransactions: Int,
|
|
||||||
val networkHashRate: Double,
|
|
||||||
val averageBlockTime: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PaginationRequest(
|
|
||||||
val page: Int = 1,
|
|
||||||
val pageSize: Int = 50,
|
|
||||||
val sortBy: String? = null,
|
|
||||||
val sortOrder: SortOrder = SortOrder.DESC
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PaginatedResponse<T>(
|
|
||||||
val data: List<T>,
|
|
||||||
val pagination: PaginationInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PaginationInfo(
|
|
||||||
val currentPage: Int,
|
|
||||||
val pageSize: Int,
|
|
||||||
val totalItems: Int,
|
|
||||||
val totalPages: Int,
|
|
||||||
val hasNext: Boolean,
|
|
||||||
val hasPrevious: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
enum class SortOrder {
|
|
||||||
ASC,
|
|
||||||
DESC
|
|
||||||
}
|
|
||||||
@@ -1,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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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?
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
package org.ccoin.services
|
|
||||||
|
|
||||||
import org.ccoin.config.ServerConfig
|
|
||||||
import org.ccoin.database.Tables
|
|
||||||
import org.ccoin.exceptions.InvalidTransactionException
|
|
||||||
import org.ccoin.models.BlockResponse
|
|
||||||
import org.ccoin.models.MiningJobResponse
|
|
||||||
import org.ccoin.models.MiningStatsResponse
|
|
||||||
import org.ccoin.utils.CryptoUtils
|
|
||||||
import org.ccoin.utils.HashUtils
|
|
||||||
import org.jetbrains.exposed.sql.*
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
object MiningService {
|
|
||||||
|
|
||||||
/** Starts a mining job for a miner */
|
|
||||||
fun startMining(minerAddress: String, difficulty: Int? = null): MiningJobResponse {
|
|
||||||
val jobId = CryptoUtils.generateJobId()
|
|
||||||
val currentDifficulty = difficulty ?: getCurrentDifficulty()
|
|
||||||
val previousHash = getLatestBlockHash()
|
|
||||||
val height = getNextBlockHeight()
|
|
||||||
val timestamp = Instant.now().epochSecond
|
|
||||||
val expiresAt = timestamp + 300 // 5 minutes
|
|
||||||
val target = "0".repeat(currentDifficulty)
|
|
||||||
|
|
||||||
return MiningJobResponse(
|
|
||||||
jobId, target, currentDifficulty, previousHash, height, timestamp, expiresAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Submits mining result and creates block if valid */
|
|
||||||
fun submitMiningResult(
|
|
||||||
minerAddress: String,
|
|
||||||
nonce: Long,
|
|
||||||
hash: String,
|
|
||||||
previousHash: String,
|
|
||||||
timestamp: Long = Instant.now().epochSecond
|
|
||||||
): BlockResponse {
|
|
||||||
val currentDifficulty = getCurrentDifficulty()
|
|
||||||
|
|
||||||
// Validate hash meets difficulty requirements
|
|
||||||
if (!CryptoUtils.isValidHash(hash, currentDifficulty)) {
|
|
||||||
throw InvalidTransactionException("Hash does not meet difficulty requirements")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate previous hash matches latest block
|
|
||||||
val latestHash = getLatestBlockHash()
|
|
||||||
if (previousHash != latestHash) {
|
|
||||||
throw InvalidTransactionException("Previous hash does not match latest block")
|
|
||||||
}
|
|
||||||
|
|
||||||
val height = getNextBlockHeight()
|
|
||||||
val reward = ServerConfig.miningReward
|
|
||||||
val merkleRoot = calculateMerkleRoot(emptyList()) // No transactions for now
|
|
||||||
|
|
||||||
return transaction {
|
|
||||||
// Create block
|
|
||||||
Tables.Blocks.insert {
|
|
||||||
it[Tables.Blocks.hash] = hash
|
|
||||||
it[Tables.Blocks.previousHash] = if (previousHash == "0".repeat(64)) null else previousHash
|
|
||||||
it[Tables.Blocks.merkleRoot] = merkleRoot
|
|
||||||
it[Tables.Blocks.timestamp] = timestamp
|
|
||||||
it[Tables.Blocks.difficulty] = currentDifficulty
|
|
||||||
it[Tables.Blocks.nonce] = nonce
|
|
||||||
it[Tables.Blocks.minerAddress] = minerAddress
|
|
||||||
it[Tables.Blocks.reward] = BigDecimal.valueOf(reward)
|
|
||||||
it[transactionCount] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reward miner with genesis transaction
|
|
||||||
TransactionService.createGenesisTransaction(minerAddress, reward)
|
|
||||||
|
|
||||||
BlockResponse(
|
|
||||||
hash, if (previousHash == "0".repeat(64)) null else previousHash, merkleRoot,
|
|
||||||
timestamp, currentDifficulty, nonce, minerAddress, reward, height, 0, 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets current mining difficulty */
|
|
||||||
fun getCurrentDifficulty(): Int = ServerConfig.miningDifficulty
|
|
||||||
|
|
||||||
/** Gets mining statistics for a miner */
|
|
||||||
fun getMinerStats(minerAddress: String): MiningStatsResponse = transaction {
|
|
||||||
val blocks = Tables.Blocks.selectAll()
|
|
||||||
.where { Tables.Blocks.minerAddress eq minerAddress }
|
|
||||||
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
|
|
||||||
|
|
||||||
val totalBlocks = blocks.count().toInt()
|
|
||||||
val totalReward = blocks.sumOf { it[Tables.Blocks.reward] }.toDouble()
|
|
||||||
val lastBlockMined = blocks.firstOrNull()?.get(Tables.Blocks.timestamp)?.toLong()
|
|
||||||
|
|
||||||
MiningStatsResponse(
|
|
||||||
minerAddress, totalBlocks, totalReward, lastBlockMined, getCurrentDifficulty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets latest block hash */
|
|
||||||
private fun getLatestBlockHash(): String = transaction {
|
|
||||||
Tables.Blocks.selectAll()
|
|
||||||
.orderBy(Tables.Blocks.height, SortOrder.DESC)
|
|
||||||
.limit(1)
|
|
||||||
.map { it[Tables.Blocks.hash] }
|
|
||||||
.singleOrNull() ?: "0".repeat(64) // Genesis hash
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets next block height */
|
|
||||||
private fun getNextBlockHeight(): Int = transaction {
|
|
||||||
val latestHeight = Tables.Blocks.selectAll()
|
|
||||||
.orderBy(Tables.Blocks.height, SortOrder.DESC)
|
|
||||||
.limit(1)
|
|
||||||
.map { it[Tables.Blocks.height] }
|
|
||||||
.singleOrNull() ?: 0
|
|
||||||
|
|
||||||
latestHeight + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Calculates merkle root for transactions */
|
|
||||||
private fun calculateMerkleRoot(transactionHashes: List<String>): String {
|
|
||||||
return if (transactionHashes.isEmpty()) {
|
|
||||||
HashUtils.sha256Hex("empty_block")
|
|
||||||
} else {
|
|
||||||
CryptoUtils.calculateMerkleRoot(transactionHashes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets network hash rate estimate */
|
|
||||||
fun getNetworkHashRate(): Double = transaction {
|
|
||||||
val recentBlocks = Tables.Blocks.selectAll()
|
|
||||||
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
|
|
||||||
.limit(100)
|
|
||||||
|
|
||||||
if (recentBlocks.count() < 2) return@transaction 0.0
|
|
||||||
|
|
||||||
val blocks = recentBlocks.toList()
|
|
||||||
val timeSpan = blocks.first()[Tables.Blocks.timestamp] - blocks.last()[Tables.Blocks.timestamp]
|
|
||||||
val difficulty = getCurrentDifficulty()
|
|
||||||
|
|
||||||
if (timeSpan <= 0) return@transaction 0.0
|
|
||||||
|
|
||||||
// Rough estimate: (blocks * 2^difficulty) / time_span
|
|
||||||
(blocks.size * Math.pow(2.0, difficulty.toDouble())) / timeSpan
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets average block time */
|
|
||||||
fun getAverageBlockTime(): Long = transaction {
|
|
||||||
val recentBlocks = Tables.Blocks.selectAll()
|
|
||||||
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
|
|
||||||
.limit(100)
|
|
||||||
.map { it[Tables.Blocks.timestamp] }
|
|
||||||
|
|
||||||
if (recentBlocks.size < 2) return@transaction 0L
|
|
||||||
|
|
||||||
val timeDiffs = recentBlocks.zipWithNext { a, b -> a - b }
|
|
||||||
timeDiffs.average().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets total number of active miners */
|
|
||||||
fun getActiveMinersCount(): Int = transaction {
|
|
||||||
val oneDayAgo = Instant.now().epochSecond - 86400
|
|
||||||
|
|
||||||
Tables.Blocks.selectAll()
|
|
||||||
.where { Tables.Blocks.timestamp greater oneDayAgo }
|
|
||||||
.groupBy(Tables.Blocks.minerAddress)
|
|
||||||
.count().toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validates mining job */
|
|
||||||
fun validateMiningJob(jobId: String, hash: String, nonce: Long): Boolean {
|
|
||||||
// Simple validation - in production you'd store job details
|
|
||||||
return CryptoUtils.isValidHash(hash, getCurrentDifficulty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets pending transactions for mining */
|
|
||||||
fun getPendingTransactionsForMining(limit: Int = 100): List<String> = transaction {
|
|
||||||
Tables.Transactions.selectAll()
|
|
||||||
.where { Tables.Transactions.status eq "pending" }
|
|
||||||
.orderBy(Tables.Transactions.timestamp, SortOrder.ASC)
|
|
||||||
.limit(limit)
|
|
||||||
.map { it[Tables.Transactions.hash] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
ktor {
|
|
||||||
deployment {
|
|
||||||
port = 8080
|
|
||||||
port = ${?PORT}
|
|
||||||
}
|
|
||||||
application {
|
|
||||||
modules = [ org.ccoin.ServerKt.module]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
131
server/utils/crypto/main.go
Normal 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
33
server/utils/env.go
Normal 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
|
||||||
|
}
|
||||||
2
server/utils/extensions.go
Normal file
2
server/utils/extensions.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
48
server/utils/general.go
Normal file
48
server/utils/general.go
Normal 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
|
||||||
|
}
|
||||||
2
server/utils/hash/main.go
Normal file
2
server/utils/hash/main.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
package hash
|
||||||
|
|
||||||
Reference in New Issue
Block a user