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

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)
}
}