feat: added server and db config

This commit is contained in:
darwincereska
2025-12-20 21:17:49 -05:00
parent 59adb32e68
commit d7338f717b
7 changed files with 509 additions and 1 deletions

1
server/.gitignore vendored
View File

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

View File

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

View File

@@ -0,0 +1,187 @@
package server
import (
"github.com/charmbracelet/log"
"fmt"
"os"
"time"
"ccoin/utils"
)
type ServerConfig struct {
// App details
Version string
BuildTime 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 = utils.GetEnvOrDefault("VERSION", "dev")
c.BuildTime = utils.GetEnvOrDefault("BUILD_TIME", time.Now().Format(time.RFC3339))
c.BaseURL = utils.GetEnvOrDefault("BASE_URL", "http://localhost:8080")
// 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("Development mode", "value", c.DevelopmentMode)
c.logger.Info("Mining difficulty", "value", c.MiningDifficulty)
c.logger.Info("Mining reward", "value", c.MiningReward)
c.logger.Info("Block time target", "value", c.BlockTimeTarget)
c.logger.Info("Default transaction fee", "value", c.DefaultTransactionFee)
c.logger.Info("Confirmations required", "value", c.ConfirmationsRequired)
c.logger.Info("Max block size", "value", c.MaxBlockSize)
c.logger.Info("Max transactions per block", "value", c.MaxTransactionsPerBlock)
c.logger.Info("Max transaction size", "value", c.MaxTransactionSize)
c.logger.Info("Log level", "value", c.LogLevel)
c.logger.Info("Max page size", "value", c.MaxPageSize)
if c.JWTSecret == "change-this-in-production" && !c.DevelopmentMode {
c.logger.Warn("Using default JWT secret in production mode!")
}
}
// Returns a map of server information
func (c *ServerConfig) GetServerInfo() map[string]interface{} {
return map[string]interface{}{
"host": c.Host,
"port": c.Port,
"development_mode": c.DevelopmentMode,
"version": c.Version,
"mining_difficulty": c.MiningDifficulty,
"mining_reward": c.MiningReward,
"block_time_target": c.BlockTimeTarget,
"confirmations_required": c.ConfirmationsRequired,
}
}
// Validates the configuration values
func (c *ServerConfig) ValidateConfig() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
if c.MiningDifficulty < 1 || c.MiningDifficulty > 32 {
return fmt.Errorf("mining difficulty must be between 1 and 32")
}
if c.MiningReward <= 0 {
return fmt.Errorf("mining reward must be positive")
}
if c.BlockTimeTarget <= 0 {
return fmt.Errorf("block time target must be positive")
}
if c.DefaultTransactionFee < 0 {
return fmt.Errorf("default transaction fee cannot be negative")
}
if c.ConfirmationsRequired <= 0 {
return fmt.Errorf("confirmations required must be positive")
}
if c.MaxPageSize <= 0 {
return fmt.Errorf("max page size must be positive")
}
if c.DefaultPageSize <= 0 {
return fmt.Errorf("default page size must be positive")
}
c.logger.Info("Server configuration validation passed")
return nil
}
// Wrapper that panics on validation error (similar to Kotlin's require)
func (c *ServerConfig) validateConfig() {
if err := c.ValidateConfig(); err != nil {
// panic(err)
c.logger.Fatal("ERROR", "error", err)
}
}

View File

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

View File

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

View File

@@ -1,5 +1,12 @@
package main package main
import (
"ccoin/config/server"
"ccoin/config/database"
)
func main() { func main() {
println("Hello world") server.NewServerConfig()
dc := database.NewDatabaseConfig()
dc.Init()
} }

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

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