commit inicial do projeto
This commit is contained in:
82
internal/api/account_handlers.go
Normal file
82
internal/api/account_handlers.go
Normal 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)
|
||||
}
|
||||
31
internal/api/block_handlers.go
Normal file
31
internal/api/block_handlers.go
Normal 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
15
internal/api/dao_api.go
Normal 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)
|
||||
84
internal/api/dao_handlers.go
Normal file
84
internal/api/dao_handlers.go
Normal 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
20
internal/api/handler.go
Normal 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
10
internal/api/health.go
Normal 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"))
|
||||
}
|
||||
11
internal/api/heartbeat_handler.go
Normal file
11
internal/api/heartbeat_handler.go
Normal 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
15
internal/api/liveness.go
Normal 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"))
|
||||
}
|
||||
91
internal/api/middleware.go
Normal file
91
internal/api/middleware.go
Normal 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
42
internal/api/router.go
Normal 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
21
internal/api/server.go
Normal 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
|
||||
}
|
||||
53
internal/api/staking_handlers.go
Normal file
53
internal/api/staking_handlers.go
Normal 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)
|
||||
}
|
||||
30
internal/api/tx_handlers.go
Normal file
30
internal/api/tx_handlers.go
Normal 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
22
internal/api/utils.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user