From 99c5dba7217a454cc692b1336cabc26b4d7c46a3 Mon Sep 17 00:00:00 2001 From: darwincereska Date: Tue, 23 Dec 2025 17:38:27 -0500 Subject: [PATCH] feat: added wallet service --- server/Makefile | 49 +++++++ server/config/server/config.go | 11 +- server/go.mod | 1 + server/models/wallet/model.go | 11 ++ server/routes/wallet/routes.go | 35 +++++ server/server.go | 29 +++- server/services/wallet/service.go | 219 ++++++++++++++++++++++++++++++ server/utils/crypto/main.go | 129 ++++++++++++++++++ server/utils/general.go | 48 +++++++ 9 files changed, 527 insertions(+), 5 deletions(-) create mode 100644 server/Makefile create mode 100644 server/routes/wallet/routes.go create mode 100644 server/services/wallet/service.go create mode 100644 server/utils/general.go diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..71d337e --- /dev/null +++ b/server/Makefile @@ -0,0 +1,49 @@ +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +GOMOD=$(GOCMD) mod +GOFMT=gofmt +GOLINT=golangci-lint + +# Binary names +BINARY_NAME=ccoin +BINARY_LINUX=$(BINARY_NAME)-linux-x64 +BINARY_MAC=$(BINARY_NAME)-macos-arm64 +BINARY_WINDOWS=$(BINARY_NAME)-windows-x64.exe + +# Build directory +BUILD_DIR=build + +# Version info (can be overridden) +VERSION?=1.0.0 +BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S') +COMMIT?=$(shell git rev-parse HEAD) + +# Linker flags to embed version info +LDFLAGS=-ldflags "-X ccoin/config/server.version=$(VERSION) -X ccoin/config/server.buildTime=$(BUILD_TIME) -X ccoin/config/server.commit=$(COMMIT) -s -w" -trimpath + +.PHONY: all build clean test coverage deps fmt lint vet help +.PHONY: build-linux build-windows build-darwin run dev +.PHONY: docker-build install uninstall + +# Default target +all: clean deps fmt vet test build + +# Build the binary +build: + $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) -v + +# Build for Linux +build-linux: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_LINUX) -v + +# Build for MacOS +build-macos: + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_MAC) -v + +# Build for windows +build-windows: + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_WINDOWS) -v diff --git a/server/config/server/config.go b/server/config/server/config.go index 384e046..5fd5f2b 100644 --- a/server/config/server/config.go +++ b/server/config/server/config.go @@ -8,10 +8,15 @@ import ( "ccoin/utils" ) +var version = "dev" +var buildTime = time.Now().Format(time.RFC3339) +var commit = "none" + type ServerConfig struct { // App details Version string BuildTime string + Commit string BaseURL string // Server settings @@ -66,9 +71,10 @@ func NewServerConfig() *ServerConfig { // 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.Version = version + c.BuildTime = utils.GetEnvOrDefault("BUILD_TIME", buildTime) c.BaseURL = utils.GetEnvOrDefault("BASE_URL", "http://localhost:8080") + c.Commit = commit // Server settings c.Host = utils.GetEnvOrDefault("SERVER_HOST", "0.0.0.0") @@ -109,6 +115,7 @@ 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("Version", "value", c.Version) c.logger.Info("Development mode", "value", c.DevelopmentMode) c.logger.Info("Mining difficulty", "value", c.MiningDifficulty) c.logger.Info("Mining reward", "value", c.MiningReward) diff --git a/server/go.mod b/server/go.mod index d94d879..ea8da55 100644 --- a/server/go.mod +++ b/server/go.mod @@ -4,6 +4,7 @@ go 1.25.5 require ( github.com/charmbracelet/log v0.4.2 + github.com/gorilla/mux v1.8.1 github.com/jackc/pgx/v5 v5.7.6 ) diff --git a/server/models/wallet/model.go b/server/models/wallet/model.go index 79b3568..90bd03e 100644 --- a/server/models/wallet/model.go +++ b/server/models/wallet/model.go @@ -1,5 +1,16 @@ package wallet +import "time" + +type Wallet struct { + Address string `db:"address" json:"address"` + Label *string `db:"label" json:"label,omitempty"` + PasswordHash string `db:"password_hash" json:"password_hash"` + Balance float64 `db:"balance" json:"balance"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastActivity *time.Time `db:"last_activity" json:"last_activity,omitempty"` +} + type CreateWalletRequest struct { Label *string `json:"label,omitempty"` Password string `json:"password"` diff --git a/server/routes/wallet/routes.go b/server/routes/wallet/routes.go new file mode 100644 index 0000000..3a98589 --- /dev/null +++ b/server/routes/wallet/routes.go @@ -0,0 +1,35 @@ +package routes + +import ( + "ccoin/models/wallet" + ws "ccoin/services/wallet" + "encoding/json" + "net/http" + "github.com/gorilla/mux" +) + +// Sets up routes for wallet operations +func WalletRoutes(router *mux.Router, walletService *ws.WalletService) { + walletRouter := router.PathPrefix("/wallet").Subrouter() + + // Create a new wallet + walletRouter.HandleFunc("/create", func(w http.ResponseWriter, r *http.Request) { + var req wallet.CreateWalletRequest + + // Parse the request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + // Create the wallet using the service + walletResponse, err := walletService.CreateWallet(req) + if err != nil { + http.Error(w, "Failed to create wallet: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(walletResponse) + }).Methods("POST") +} diff --git a/server/server.go b/server/server.go index fb8e26f..0c97d54 100644 --- a/server/server.go +++ b/server/server.go @@ -1,12 +1,35 @@ package main import ( + "ccoin/config/database" "ccoin/config/server" - "ccoin/config/database" + "ccoin/routes/wallet" + ws "ccoin/services/wallet" + "github.com/gorilla/mux" + "net/http" + "github.com/charmbracelet/log" ) func main() { + // Initialize server server.NewServerConfig() - dc := database.NewDatabaseConfig() - dc.Init() + + // Initialize database + dbConfig := database.NewDatabaseConfig() + if err := dbConfig.Init(); err != nil { + log.Fatal("Database initialization failed:", err) + } + + walletService := ws.NewWalletService(dbConfig.GetDB()) + + // Set up router + r := mux.NewRouter() + routes.WalletRoutes(r, walletService) + + // Start server + log.Info("Starting server on :8080") + if err := http.ListenAndServe(":8080", r); err != nil { + log.Fatal("Failed to start server:", err) + } } + diff --git a/server/services/wallet/service.go b/server/services/wallet/service.go new file mode 100644 index 0000000..e4eb4df --- /dev/null +++ b/server/services/wallet/service.go @@ -0,0 +1,219 @@ +package wallet + +import ( + "ccoin/utils/crypto" + "database/sql" + "ccoin/models/wallet" + "errors" + "fmt" + "time" +) + +type WalletService struct { + db *sql.DB +} + +// Creates a new instance of WalletService +func NewWalletService(db *sql.DB) *WalletService { + return &WalletService{db: db} +} + +// Creates a new wallet with optional label +func (ws *WalletService) CreateWallet(req wallet.CreateWalletRequest) (wallet.WalletResponse, error) { + address := crypto.GenerateWalletAddress() + timestamp := time.Now() + var passwordHash string + if req.Password != "" { + passwordHash = crypto.HashPassword(req.Password) + } else { + return wallet.WalletResponse{}, fmt.Errorf("password field is required") + } + + _, err := ws.db.Exec(` + INSERT INTO wallets (address, balance, label, password_hash, created_at) + VALUES ($1, $2, $3, $4, $5)`, + address, 0.0, req.Label, passwordHash, timestamp.Unix(), + ) + if err != nil { + return wallet.WalletResponse{}, err + } + + return wallet.WalletResponse{ + Address: address, + Balance: 0.0, + Label: req.Label, + PasswordHash: passwordHash, + CreatedAt: timestamp.Unix(), + LastActivity: nil, + }, nil +} + +// Gets wallet by address +func (ws *WalletService) GetWallet(address string) (*wallet.WalletResponse, error) { + var w wallet.Wallet + row := ws.db.QueryRow(` + SELECT address, balance, label, password_hash, created_at, last_activity + FROM wallets WHERE address = $1`, address) + + err := row.Scan(&w.Address, &w.Balance, &w.Label, &w.PasswordHash, &w.CreatedAt, &w.LastActivity) + if err != nil { + return nil, err + } + + return &wallet.WalletResponse{ + Address: w.Address, + Balance: w.Balance, + Label: w.Label, + PasswordHash: w.PasswordHash, + CreatedAt: w.CreatedAt.Unix(), + LastActivity: ptrTime(w.LastActivity), + }, nil +} + +// Gets wallet balance +func (ws *WalletService) GetWalletBalance(address string) (wallet.BalanceResponse, error) { + var balance float64 + err := ws.db.QueryRow(`SELECT balance FROM wallets WHERE address = $1`, address).Scan(&balance) + if err != nil { + return wallet.BalanceResponse{}, errors.New("wallet not found") + } + return wallet.BalanceResponse{ + Address: address, + Balance: balance, + }, nil +} + +// Updates wallet balance +func (ws *WalletService) UpdateBalance(address string, amount float64) (bool, error) { + _, err := ws.db.Exec(`UPDATE wallets SET balance = balance + $1, last_activity = $2 WHERE address = $3`, amount, time.Now().Unix(), address) + if err != nil { + return false, err + } + return true, nil +} + +// Sets wallet balance to specific amount +func (ws *WalletService) SetBalance(address string, amount float64) (bool, error) { + _, err := ws.db.Exec(`UPDATE wallets SET balance = $1, last_activity = $2 WHERE address = $3`, amount, time.Now().Unix(), address) + if err != nil { + return false, err + } + return true, nil +} + +// Updates wallet label +func (ws *WalletService) UpdateLabel(address string, req wallet.UpdateWalletRequest) (bool, error) { + result, err := ws.db.Exec(`UPDATE wallets SET label = $1 WHERE address = $2`, address, req.Label) + if err != nil { + return false, err + } + rowsAffected, _ := result.RowsAffected() + return rowsAffected > 0, nil +} + +// Checks if wallet exists +func (ws *WalletService) WalletExists(address string) (bool, error) { + var count int64 + err := ws.db.QueryRow(`SELECT COUNT(*) FROM wallets WHERE address = $1`, address).Scan(&count) + if err != nil { + return false, err + } + return count > 0, nil +} + +// Gets all wallets with pagination +func (ws *WalletService) GetAllWallets(limit, offset int) ([]wallet.WalletResponse, error) { + query := fmt.Sprintf(` + SELECT address, balance, label, password_hash, created_at, last_activity + FROM wallets ORDER BY created_at DESC LIMIT %d OFFSET %d`, limit, offset) + + rows, err := ws.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var wallets []wallet.WalletResponse + for rows.Next() { + var w wallet.Wallet + if err := rows.Scan(&w.Address, &w.Balance, &w.Label, &w.PasswordHash, + &w.CreatedAt, &w.LastActivity); err != nil { + return nil, err + } + + wallets = append(wallets, wallet.WalletResponse{ + Address: w.Address, + Balance: w.Balance, + Label: w.Label, + PasswordHash: w.PasswordHash, + CreatedAt: w.CreatedAt.Unix(), + LastActivity: ptrTime(w.LastActivity), + }) + } + return wallets, nil +} + +// Gets total number of wallets +func (ws *WalletService) GetTotalWalletCount() (int64, error) { + var count int64 + err := ws.db.QueryRow(`SELECT COUNT(*) FROM wallets`).Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} + +// Gets wallet with balance greater than specified amount +func (ws *WalletService) GetWalletsWithBalance(minBalance float64) ([]wallet.WalletResponse, error) { + rows, err := ws.db.Query(`SELECT address, balance, label, password_hash, created_at, last_activity FROM wallets WHERE balance > $1 ORDER BY balance DESC`, minBalance) + if err != nil { + return nil, err + } + defer rows.Close() + + var wallets []wallet.WalletResponse + for rows.Next() { + var w wallet.Wallet + if err := rows.Scan(&w.Address, &w.Balance, &w.Label, &w.PasswordHash, &w.CreatedAt, &w.LastActivity); err != nil { + return nil, err + } + + wallets = append(wallets, wallet.WalletResponse{ + Address: w.Address, + Balance: w.Balance, + Label: w.Label, + PasswordHash: w.PasswordHash, + CreatedAt: w.CreatedAt.Unix(), + LastActivity: ptrTime(w.LastActivity), + }) + } + return wallets, nil +} + +// Updates last activity timestamp +func (ws *WalletService) UpdateLastActivity(address string) (bool, error) { + _, err := ws.db.Exec(`UPDATE wallets SET last_activity = $1 WHERE address = $2`, time.Now().Unix(), address) + if err != nil { + return false, err + } + return true, nil +} + +// Gets total supply across all wallets +func (ws *WalletService) GetTotalSupply() (float64, error) { + var totalSupply float64 + err := ws.db.QueryRow(`SELECT SUM(balance) FROM wallets`).Scan(&totalSupply) + if err != nil { + return 0, err + } + return totalSupply, nil +} + +// Helper function to convert *time.Time to *int64 for JSON response +func ptrTime(t *time.Time) *int64 { + if t == nil { + return nil + } + unixTime := t.Unix() + return &unixTime +} diff --git a/server/utils/crypto/main.go b/server/utils/crypto/main.go index e4f8dd1..a848150 100644 --- a/server/utils/crypto/main.go +++ b/server/utils/crypto/main.go @@ -1,2 +1,131 @@ package crypto +import ( + "crypto/sha256" + "fmt" + "ccoin/utils" + "strings" +) + +var words = []string{ + "phoenix", "dragon", "tiger", "eagle", "wolf", "lion", "bear", "shark", + "falcon", "raven", "cobra", "viper", "panther", "jaguar", "leopard", "cheetah", + "thunder", "lightning", "storm", "blizzard", "tornado", "hurricane", "cyclone", "tempest", + "crystal", "diamond", "emerald", "ruby", "sapphire", "topaz", "amethyst", "opal", + "shadow", "ghost", "phantom", "spirit", "wraith", "specter", "demon", "angel", + "fire", "ice", "earth", "wind", "water", "metal", "wood", "void", + "star", "moon", "sun", "comet", "meteor", "galaxy", "nebula", "cosmos", + "blade", "sword", "arrow", "spear", "shield", "armor", "crown", "throne", + "mountain", "ocean", "forest", "desert", "valley", "river", "lake", "cave", + "knight", "warrior", "mage", "archer", "rogue", "paladin", "wizard", "sage", +} + +// Generates a wallet address in format: random_word:random_6_digits +func GenerateWalletAddress() string { + randomWord := words[utils.RandInt(0, len(words))] + randomDigits := fmt.Sprintf("%06d", utils.RandInt(0, 1000000)) + return fmt.Sprintf("%s:%s", randomWord, randomDigits) +} + +// Validates if an address follows the correct format +func IsValidAddress(address string) bool { + parts := strings.Split(address, ":") + if len(parts) != 2 { + return false + } + + word := parts[0] + digits := parts[1] + + return len(word) > 0 && + utils.IsAlpha(word) && + len(digits) == 6 && + utils.IsDigit(digits) +} + +// Generates SHA-256 hash of input string +func SHA256(input string) string { + hash := sha256.New() + hash.Write([]byte(input)) + return fmt.Sprintf("%x", hash.Sum(nil)) +} + +// Hashes password +func HashPassword(password string) string { + return SHA256(fmt.Sprintf("ccoin_password_%s", password)) +} + +// Generates a transaction hash +func GenerateTransactionHash(fromAddress *string, toAddress string, amount float64, timestamp int64, nonce int64) string { + from := "genesis" + if fromAddress != nil { + from = *fromAddress + } + input := fmt.Sprintf("%s:%s:%f:%d:%d", from, toAddress, amount, timestamp, nonce) + return SHA256(input) +} + +// Generates a block hash +func GenerateBlockHash(previousHash string, merkleRoot string, timestamp int64, difficulty int, nonce int64) string { + input := fmt.Sprintf("%s:%s:%d:%d:%d", previousHash, merkleRoot, timestamp, difficulty, nonce) + return SHA256(input) +} + +// Validates if a hash meets the mining difficulty requirement +func IsValidHash(hash string, difficulty int) bool { + target := strings.Repeat("0", difficulty) + return strings.HasPrefix(hash, target) +} + +// Generates mining job id +func GenerateJobId() string { + return SHA256(fmt.Sprintf("job:%d:%d", utils.GetCurrentTimeMillis(), utils.RandInt(0, 1<<63-1)))[:16] +} + +// Calculate merkle root from transaction hashes +func CalculateMerkleRoot(transactionHashes []string) string { + if len(transactionHashes) == 0 { + return SHA256("empty") + } + + if len(transactionHashes) == 1 { + return transactionHashes[0] + } + + hashes := transactionHashes + + for len(hashes) > 1 { + var newHashes []string + for i := 0; i < len(hashes); i += 2 { + left := hashes[i] + right := left // Default to left if there's no right + if i+1< len(hashes) { + right = hashes[i+1] + } + newHashes = append(newHashes, SHA256(fmt.Sprintf("%s:%s", left, right))) + } + hashes = newHashes + } + + return hashes[0] +} + +// Generates a random nonce for mining +func GenerateNonce() int64 { + return int64(utils.RandInt(0, 1<<63-1)) +} + +// Validates transaction hash format +func IsValidTransactionHash(hash string) bool { + return len(hash) == 64 && utils.IsHex(hash) +} + +// Validates block hash format +func IsValidBlockHash(hash string) bool { + return len(hash) == 64 && utils.IsHex(hash) +} + +// Verifies password hash +func VerifyPassword(password string, storedHash string) bool { + return HashPassword(password) == storedHash +} diff --git a/server/utils/general.go b/server/utils/general.go new file mode 100644 index 0000000..b9c2f0d --- /dev/null +++ b/server/utils/general.go @@ -0,0 +1,48 @@ +package utils + +import ( + "crypto/rand" + "math/big" + "time" +) + +// Generates a random integer in the range (min, max) +func RandInt(min, max int) int { + n, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min))) + return int(n.Int64()) + min +} + +// Get the current time in milliseconds +func GetCurrentTimeMillis() int64 { + return time.Now().UnixNano() / int64(time.Millisecond) // Convert nanoseconds to milliseconds +} + +// IsAlpha checks if the string contains only alphabetic characters +func IsAlpha(s string) bool { + for _, r := range s { + if !('a' <= r && r <= 'z') && !('A' <= r && r <= 'Z') { + return false + } + } + return true +} + +// IsDigit checks if the string contains only digits +func IsDigit(s string) bool { + for _, r := range s { + if !(r >= '0' && r <= '9') { + return false + } + } + return true +} + +// IsHex checks if the string is a valid hexadecimal representation +func IsHex(s string) bool { + for _, r := range s { + if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { + return false + } + } + return true +}