feat: added wallet service

This commit is contained in:
darwincereska
2025-12-23 17:38:27 -05:00
parent 23f6dba3f3
commit 99c5dba721
9 changed files with 527 additions and 5 deletions

49
server/Makefile Normal file
View File

@@ -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

View File

@@ -8,10 +8,15 @@ import (
"ccoin/utils" "ccoin/utils"
) )
var version = "dev"
var buildTime = time.Now().Format(time.RFC3339)
var commit = "none"
type ServerConfig struct { type ServerConfig struct {
// App details // App details
Version string Version string
BuildTime string BuildTime string
Commit string
BaseURL string BaseURL string
// Server settings // Server settings
@@ -66,9 +71,10 @@ func NewServerConfig() *ServerConfig {
// Loads configuration from environment variables with defaults // Loads configuration from environment variables with defaults
func (c *ServerConfig) loadConfig() { func (c *ServerConfig) loadConfig() {
// App details (these would typically come from build-time variables) // App details (these would typically come from build-time variables)
c.Version = utils.GetEnvOrDefault("VERSION", "dev") c.Version = version
c.BuildTime = utils.GetEnvOrDefault("BUILD_TIME", time.Now().Format(time.RFC3339)) c.BuildTime = utils.GetEnvOrDefault("BUILD_TIME", buildTime)
c.BaseURL = utils.GetEnvOrDefault("BASE_URL", "http://localhost:8080") c.BaseURL = utils.GetEnvOrDefault("BASE_URL", "http://localhost:8080")
c.Commit = commit
// Server settings // Server settings
c.Host = utils.GetEnvOrDefault("SERVER_HOST", "0.0.0.0") 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("Server configuration loaded:")
c.logger.Info("Host", "value", c.Host) c.logger.Info("Host", "value", c.Host)
c.logger.Info("Port", "value", c.Port) 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("Development mode", "value", c.DevelopmentMode)
c.logger.Info("Mining difficulty", "value", c.MiningDifficulty) c.logger.Info("Mining difficulty", "value", c.MiningDifficulty)
c.logger.Info("Mining reward", "value", c.MiningReward) c.logger.Info("Mining reward", "value", c.MiningReward)

View File

@@ -4,6 +4,7 @@ go 1.25.5
require ( require (
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.7.6
) )

View File

@@ -1,5 +1,16 @@
package wallet 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 { type CreateWalletRequest struct {
Label *string `json:"label,omitempty"` Label *string `json:"label,omitempty"`
Password string `json:"password"` Password string `json:"password"`

View File

@@ -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")
}

View File

@@ -1,12 +1,35 @@
package main package main
import ( import (
"ccoin/config/server"
"ccoin/config/database" "ccoin/config/database"
"ccoin/config/server"
"ccoin/routes/wallet"
ws "ccoin/services/wallet"
"github.com/gorilla/mux"
"net/http"
"github.com/charmbracelet/log"
) )
func main() { func main() {
// Initialize server
server.NewServerConfig() 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)
}
}

View File

@@ -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
}

View File

@@ -1,2 +1,131 @@
package crypto 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
}

48
server/utils/general.go Normal file
View File

@@ -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
}