commit inicial do projeto

This commit is contained in:
Júnior
2025-05-23 10:44:32 -03:00
commit 8f04473c0b
106 changed files with 5673 additions and 0 deletions

View File

@ -0,0 +1,82 @@
package api
import (
"dejo_node/internal/state"
"dejo_node/internal/storage"
"encoding/json"
"net/http"
"strings"
)
var GlobalState *state.State
var GlobalBlockStore *storage.BlockStore
func HandleGetBalance(w http.ResponseWriter, r *http.Request) {
addr := strings.TrimPrefix(r.URL.Path, "/accounts/")
if strings.Contains(addr, "/") {
addr = strings.Split(addr, "/")[0]
}
if addr == "" {
http.Error(w, "endereço inválido", http.StatusBadRequest)
return
}
balance := GlobalState.GetBalance(addr)
resp := map[string]interface{}{
"address": addr,
"balance": balance,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func HandleGetTransactionsByAddress(w http.ResponseWriter, r *http.Request) {
addr := strings.TrimPrefix(r.URL.Path, "/accounts/")
addr = strings.TrimSuffix(addr, "/txs")
if addr == "" {
http.Error(w, "endereço inválido", http.StatusBadRequest)
return
}
txs := []map[string]interface{}{}
blocks, err := GlobalBlockStore.LoadAll()
if err != nil {
http.Error(w, "erro ao carregar blocos", http.StatusInternalServerError)
return
}
for _, blk := range blocks {
for _, tx := range blk.Txns {
if tx.From == addr || tx.To == addr {
txs = append(txs, map[string]interface{}{
"from": tx.From,
"to": tx.To,
"value": tx.Value,
"hash": tx.Hash(),
"block": blk.Index,
})
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(txs)
}
func (h *Handler) GetTransactionByHash(w http.ResponseWriter, r *http.Request) {
hash := strings.TrimPrefix(r.URL.Path, "/transaction/")
blocks, err := h.Store.LoadAll()
if err != nil {
http.Error(w, "erro ao carregar blocos", http.StatusInternalServerError)
return
}
for _, blk := range blocks {
for _, tx := range blk.Txns {
if tx.Hash() == hash {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tx)
return
}
}
}
http.Error(w, "transação não encontrada", http.StatusNotFound)
}

View File

@ -0,0 +1,31 @@
package api
import (
"net/http"
"strconv"
"github.com/gorilla/mux"
)
func (h *Handler) ListBlocks(w http.ResponseWriter, r *http.Request) {
blocks, err := h.Store.ListBlocks()
if err != nil {
WriteError(w, http.StatusInternalServerError, "Erro ao listar blocos")
return
}
WriteJSON(w, blocks)
}
func (h *Handler) GetBlockByHeight(w http.ResponseWriter, r *http.Request) {
heightStr := mux.Vars(r)["height"]
height, err := strconv.Atoi(heightStr)
if err != nil {
WriteError(w, http.StatusBadRequest, "Altura inválida")
return
}
block, err := h.Store.LoadBlock(height)
if err != nil {
WriteError(w, http.StatusNotFound, "Bloco não encontrado")
return
}
WriteJSON(w, block)
}

15
internal/api/dao_api.go Normal file
View File

@ -0,0 +1,15 @@
package api
import (
"dejo_node/internal/dao"
"sync/atomic"
)
// DaoStore é acessado globalmente pelos handlers DAO.
var DaoStore *dao.ProposalStore
// MinStakePtr é o ponteiro global para o valor atual de minStake.
var MinStakePtr *atomic.Uint64
// SetMinStakeFunc permite alterar dinamicamente via DAO.
var SetMinStakeFunc func(uint64)

View File

@ -0,0 +1,84 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"dejo_node/internal/dao"
)
type CreateProposalRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Type string `json:"type"`
Creator string `json:"creator"`
Duration int64 `json:"duration"`
}
type VoteRequest struct {
Address string `json:"address"`
Approve bool `json:"approve"`
}
func (h *Handler) CreateProposal(w http.ResponseWriter, r *http.Request) {
var req CreateProposalRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "JSON inválido")
return
}
p := DaoStore.Create(req.Title, req.Content, req.Creator, dao.ProposalType(req.Type), req.Duration)
_ = DaoStore.SaveToDisk("data/proposals.db")
WriteJSON(w, p)
}
func (h *Handler) VoteProposal(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
WriteError(w, http.StatusBadRequest, "ID inválido")
return
}
var req VoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "JSON inválido")
return
}
p, err := DaoStore.Get(id)
if err != nil {
WriteError(w, http.StatusNotFound, "Proposta não encontrada")
return
}
_ = p.Vote(req.Address, req.Approve)
p.CheckAndClose(h.snapshotStakes())
_ = DaoStore.SaveToDisk("data/proposals.db")
WriteJSON(w, p)
}
func (h *Handler) GetProposal(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
WriteError(w, http.StatusBadRequest, "ID inválido")
return
}
p, err := DaoStore.Get(id)
if err != nil {
WriteError(w, http.StatusNotFound, "Proposta não encontrada")
return
}
WriteJSON(w, p)
}
func (h *Handler) ListProposals(w http.ResponseWriter, r *http.Request) {
WriteJSON(w, DaoStore.List())
}
func (h *Handler) snapshotStakes() map[string]uint64 {
snapshot := make(map[string]uint64)
for addr, entry := range h.StakingStore.Snapshot() {
snapshot[addr] = entry.Amount
}
return snapshot
}

20
internal/api/handler.go Normal file
View File

@ -0,0 +1,20 @@
package api
import (
"dejo_node/internal/mempool"
"dejo_node/internal/staking"
"dejo_node/internal/storage"
)
type Handler struct {
Store *storage.BlockStore
StakingStore *staking.StakingStore
Mempool *mempool.Mempool
}
func NewHandler() *Handler {
return &Handler{
Store: storage.NewBlockStore("data/blocks.json"),
StakingStore: staking.NewStakingStore(),
}
}

10
internal/api/health.go Normal file
View File

@ -0,0 +1,10 @@
package api
import (
"net/http"
)
func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}

View File

@ -0,0 +1,11 @@
package api
import (
"net/http"
)
// HandlePing responde a heartbeat dos peers
func HandlePing(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("pong"))
}

15
internal/api/liveness.go Normal file
View File

@ -0,0 +1,15 @@
package api
import (
"net/http"
)
func (h *Handler) Startup(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("started"))
}
func (h *Handler) Ready(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
}

View File

@ -0,0 +1,91 @@
package api
import (
"context"
"net"
"net/http"
"strings"
"sync"
"time"
limiter "github.com/ulule/limiter/v3"
memory "github.com/ulule/limiter/v3/drivers/store/memory"
stdmiddleware "github.com/ulule/limiter/v3/drivers/middleware/stdlib"
)
// SybilProtector mantém IPs bloqueados temporariamente
var sybilBlocklist = struct {
mu sync.RWMutex
block map[string]time.Time
}{block: make(map[string]time.Time)}
// isBlocked verifica se um IP está na lista de bloqueio
func isBlocked(ip string) bool {
sybilBlocklist.mu.RLock()
defer sybilBlocklist.mu.RUnlock()
expire, exists := sybilBlocklist.block[ip]
return exists && time.Now().Before(expire)
}
// blockIP adiciona um IP à blocklist por X segundos
func blockIP(ip string, duration time.Duration) {
sybilBlocklist.mu.Lock()
defer sybilBlocklist.mu.Unlock()
sybilBlocklist.block[ip] = time.Now().Add(duration)
}
// extractIP extrai IP puro do RemoteAddr
func extractIP(addr string) string {
ip, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return ip
}
// NewRateLimiterMiddleware cria middleware de rate limiting e proteção anti-Sybil
func NewRateLimiterMiddleware() func(http.Handler) http.Handler {
rate := limiter.Rate{
Period: 1 * time.Second,
Limit: 5,
}
store := memory.NewStore()
limiterInstance := limiter.New(store, rate)
middleware := stdmiddleware.NewMiddleware(limiterInstance)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Ignorar WebSocket
if strings.HasPrefix(r.URL.Path, "/ws") {
next.ServeHTTP(w, r)
return
}
ip := extractIP(r.RemoteAddr)
if isBlocked(ip) {
w.WriteHeader(http.StatusTooManyRequests)
WriteError(w, http.StatusTooManyRequests, "IP bloqueado temporariamente por abuso")
return
}
ctx := context.WithValue(r.Context(), "real-ip", ip)
rec := &responseRecorder{ResponseWriter: w, status: 200}
middleware.Handler(next).ServeHTTP(rec, r.WithContext(ctx))
if rec.status == http.StatusTooManyRequests {
blockIP(ip, 30*time.Second)
}
})
}
}
// responseRecorder intercepta status code para detectar excesso de requisições
type responseRecorder struct {
http.ResponseWriter
status int
}
func (rr *responseRecorder) WriteHeader(code int) {
rr.status = code
rr.ResponseWriter.WriteHeader(code)
}

42
internal/api/router.go Normal file
View File

@ -0,0 +1,42 @@
package api
import (
"github.com/gorilla/mux"
"net/http"
)
func NewRouter(h *Handler) http.Handler {
r := mux.NewRouter()
r.HandleFunc("/health", h.Health).Methods("GET")
r.HandleFunc("/startup", h.Startup).Methods("GET")
r.HandleFunc("/ready", h.Ready).Methods("GET")
// ⚡ Transações
r.HandleFunc("/transaction", h.SendTransaction).Methods("POST")
r.HandleFunc("/transaction/{hash}", h.GetTransactionByHash).Methods("GET")
// 🔐 Staking
r.HandleFunc("/stake", h.StakeHandler).Methods("POST")
r.HandleFunc("/unstake", h.UnstakeHandler).Methods("POST")
r.HandleFunc("/stake/{address}", h.GetStakeInfo).Methods("GET")
// 🗳️ DAO
r.HandleFunc("/dao/proposals", h.CreateProposal).Methods("POST")
r.HandleFunc("/dao/proposals/{id}/vote", h.VoteProposal).Methods("POST")
r.HandleFunc("/dao/proposals", h.ListProposals).Methods("GET")
r.HandleFunc("/dao/proposals/{id}", h.GetProposal).Methods("GET")
// 💰 Accounts
r.HandleFunc("/accounts/{address}", HandleGetBalance).Methods("GET")
r.HandleFunc("/accounts/{address}/txs", HandleGetTransactionsByAddress).Methods("GET")
// 📦 Blocos
r.HandleFunc("/blocks", h.ListBlocks).Methods("GET")
r.HandleFunc("/blocks/{height}", h.GetBlockByHeight).Methods("GET")
// ❤️ Heartbeat
r.HandleFunc("/ping", HandlePing).Methods("GET")
return r
}

21
internal/api/server.go Normal file
View File

@ -0,0 +1,21 @@
package api
import (
"github.com/gorilla/mux"
"net/http"
"dejo_node/internal/ws"
)
func NewServer(handler *Handler) http.Handler {
r := mux.NewRouter()
// ⚠️ Endpoints removidos temporariamente pois ainda não foram implementados
// r.HandleFunc("/block/latest", handler.GetLatestBlock).Methods("GET")
// r.HandleFunc("/block/{index}", handler.GetBlockByIndex).Methods("GET")
// r.HandleFunc("/tx/{hash}", handler.GetTransactionByHash).Methods("GET")
// r.HandleFunc("/tx", handler.PostTransaction).Methods("POST")
// r.HandleFunc("/oracle/{key}", handler.GetOracleValue).Methods("GET")
r.HandleFunc("/ws", ws.HandleWS).Methods("GET")
return r
}

View File

@ -0,0 +1,53 @@
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
type StakeRequest struct {
Address string `json:"address"`
Amount uint64 `json:"amount"`
Duration int64 `json:"duration"`
}
type UnstakeRequest struct {
Address string `json:"address"`
}
func (h *Handler) StakeHandler(w http.ResponseWriter, r *http.Request) {
var req StakeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "Invalid request")
return
}
if err := h.StakingStore.Stake(req.Address, req.Amount, uint64(req.Duration)); err != nil {
WriteError(w, http.StatusInternalServerError, "Failed to stake")
return
}
WriteJSON(w, map[string]string{"message": "Stake registered successfully"})
}
func (h *Handler) UnstakeHandler(w http.ResponseWriter, r *http.Request) {
var req UnstakeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "Invalid request")
return
}
if err := h.StakingStore.Unstake(req.Address); err != nil {
WriteError(w, http.StatusInternalServerError, "Failed to unstake")
return
}
WriteJSON(w, map[string]string{"message": "Unstake successful"})
}
func (h *Handler) GetStakeInfo(w http.ResponseWriter, r *http.Request) {
address := mux.Vars(r)["address"]
info, ok := h.StakingStore.GetStakeInfo(address)
if !ok {
WriteError(w, http.StatusNotFound, "Stake info not found")
return
}
WriteJSON(w, info)
}

View File

@ -0,0 +1,30 @@
package api
import (
"encoding/json"
"log"
"net/http"
"dejo_node/internal/transactions"
)
func (h *Handler) SendTransaction(w http.ResponseWriter, r *http.Request) {
log.Println("📥 Nova transação recebida")
var tx transactions.Transaction
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&tx); err != nil {
http.Error(w, "formato inválido", http.StatusBadRequest)
return
}
if tx.From == "" || tx.To == "" || tx.Value <= 0 {
http.Error(w, "transação inválida", http.StatusBadRequest)
return
}
h.Mempool.Add(&tx)
log.Printf("✅ Transação adicionada ao mempool: %+v\n", tx)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

22
internal/api/utils.go Normal file
View File

@ -0,0 +1,22 @@
package api
import (
"encoding/json"
"net/http"
)
func WriteError(w http.ResponseWriter, code int, msg string) {
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": msg,
})
}
func WriteJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"data": data,
})
}