feat: added server and db config
This commit is contained in:
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Go files
|
||||
ccoin
|
||||
go.sum
|
||||
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
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
|
||||
}
|
||||
187
server/config/server/config.go
Normal file
187
server/config/server/config.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
module ccoin
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,2 +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"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ccoin/config/server"
|
||||
"ccoin/config/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
println("Hello world")
|
||||
server.NewServerConfig()
|
||||
dc := database.NewDatabaseConfig()
|
||||
dc.Init()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user