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 }