diff --git a/server/.gitignore b/server/.gitignore index 7921cd7..e45f391 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,5 +1,6 @@ # Go files ccoin +go.sum # Compiled class file *.class diff --git a/server/config/database/config.go b/server/config/database/config.go new file mode 100644 index 0000000..0614159 --- /dev/null +++ b/server/config/database/config.go @@ -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 +} diff --git a/server/config/server/config.go b/server/config/server/config.go new file mode 100644 index 0000000..384e046 --- /dev/null +++ b/server/config/server/config.go @@ -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) + } +} diff --git a/server/go.mod b/server/go.mod index 6d33007..d94d879 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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 +) diff --git a/server/models/block/model.go b/server/models/block/model.go index e1c2ab6..98edc9f 100644 --- a/server/models/block/model.go +++ b/server/models/block/model.go @@ -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"` +} diff --git a/server/server.go b/server/server.go index 344d51f..fb8e26f 100644 --- a/server/server.go +++ b/server/server.go @@ -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() } diff --git a/server/utils/env.go b/server/utils/env.go new file mode 100644 index 0000000..03f4b0a --- /dev/null +++ b/server/utils/env.go @@ -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 +}