Files
ccoin/server/config/database/config.go
2025-12-20 21:17:49 -05:00

208 lines
5.9 KiB
Go

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
}