package mining import ( "encoding/json" "net/http" "strconv" "strings" "github.com/Snider/Mining/pkg/node" "github.com/gin-gonic/gin" ) // NodeService handles P2P node-related API endpoints. type NodeService struct { nodeManager *node.NodeManager peerRegistry *node.PeerRegistry transport *node.Transport controller *node.Controller worker *node.Worker } // NewNodeService creates a new NodeService instance. func NewNodeService() (*NodeService, error) { nm, err := node.NewNodeManager() if err != nil { return nil, err } pr, err := node.NewPeerRegistry() if err != nil { return nil, err } config := node.DefaultTransportConfig() transport := node.NewTransport(nm, pr, config) ns := &NodeService{ nodeManager: nm, peerRegistry: pr, transport: transport, } // Initialize controller and worker ns.controller = node.NewController(nm, pr, transport) ns.worker = node.NewWorker(nm, transport) return ns, nil } // SetupRoutes configures all node-related API routes. func (ns *NodeService) SetupRoutes(router *gin.RouterGroup) { // Node identity endpoints nodeGroup := router.Group("/node") { nodeGroup.GET("/info", ns.handleNodeInfo) nodeGroup.POST("/init", ns.handleNodeInit) } // Peer management endpoints peerGroup := router.Group("/peers") { peerGroup.GET("", ns.handleListPeers) peerGroup.POST("", ns.handleAddPeer) peerGroup.GET("/:id", ns.handleGetPeer) peerGroup.DELETE("/:id", ns.handleRemovePeer) peerGroup.POST("/:id/ping", ns.handlePingPeer) peerGroup.POST("/:id/connect", ns.handleConnectPeer) peerGroup.POST("/:id/disconnect", ns.handleDisconnectPeer) // Allowlist management peerGroup.GET("/auth/mode", ns.handleGetAuthMode) peerGroup.PUT("/auth/mode", ns.handleSetAuthMode) peerGroup.GET("/auth/allowlist", ns.handleListAllowlist) peerGroup.POST("/auth/allowlist", ns.handleAddToAllowlist) peerGroup.DELETE("/auth/allowlist/:key", ns.handleRemoveFromAllowlist) } // Remote operations endpoints remoteGroup := router.Group("/remote") { remoteGroup.GET("/stats", ns.handleRemoteStats) remoteGroup.GET("/:peerId/stats", ns.handlePeerStats) remoteGroup.POST("/:peerId/start", ns.handleRemoteStart) remoteGroup.POST("/:peerId/stop", ns.handleRemoteStop) remoteGroup.GET("/:peerId/logs/:miner", ns.handleRemoteLogs) } } // StartTransport starts the P2P transport server. func (ns *NodeService) StartTransport() error { return ns.transport.Start() } // StopTransport stops the P2P transport server. func (ns *NodeService) StopTransport() error { return ns.transport.Stop() } // Node Info Response type NodeInfoResponse struct { HasIdentity bool `json:"hasIdentity"` Identity *node.NodeIdentity `json:"identity,omitempty"` RegisteredPeers int `json:"registeredPeers"` ConnectedPeers int `json:"connectedPeers"` } // handleNodeInfo godoc // @Summary Get node identity information // @Description Get the current node's identity and connection status // @Tags node // @Produce json // @Success 200 {object} NodeInfoResponse // @Router /node/info [get] func (ns *NodeService) handleNodeInfo(c *gin.Context) { response := NodeInfoResponse{ HasIdentity: ns.nodeManager.HasIdentity(), RegisteredPeers: ns.peerRegistry.Count(), ConnectedPeers: len(ns.peerRegistry.GetConnectedPeers()), } if ns.nodeManager.HasIdentity() { response.Identity = ns.nodeManager.GetIdentity() } c.JSON(http.StatusOK, response) } // NodeInitRequest is the request body for node initialization. type NodeInitRequest struct { Name string `json:"name" binding:"required"` Role string `json:"role"` // "controller", "worker", or "dual" } // handleNodeInit godoc // @Summary Initialize node identity // @Description Create a new node identity with X25519 keypair // @Tags node // @Accept json // @Produce json // @Param request body NodeInitRequest true "Node initialization parameters" // @Success 200 {object} node.NodeIdentity // @Router /node/init [post] func (ns *NodeService) handleNodeInit(c *gin.Context) { var req NodeInitRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if ns.nodeManager.HasIdentity() { c.JSON(http.StatusConflict, gin.H{"error": "node identity already exists"}) return } role := node.RoleDual switch req.Role { case "controller": role = node.RoleController case "worker": role = node.RoleWorker case "dual", "": role = node.RoleDual default: c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"}) return } if err := ns.nodeManager.GenerateIdentity(req.Name, role); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, ns.nodeManager.GetIdentity()) } // handleListPeers godoc // @Summary List registered peers // @Description Get a list of all registered peers with their status // @Tags peers // @Produce json // @Success 200 {array} node.Peer // @Router /peers [get] func (ns *NodeService) handleListPeers(c *gin.Context) { peers := ns.peerRegistry.ListPeers() c.JSON(http.StatusOK, peers) } // AddPeerRequest is the request body for adding a peer. type AddPeerRequest struct { Address string `json:"address" binding:"required"` Name string `json:"name"` } // handleAddPeer godoc // @Summary Add a new peer // @Description Register a new peer node by address // @Tags peers // @Accept json // @Produce json // @Param request body AddPeerRequest true "Peer information" // @Success 201 {object} node.Peer // @Router /peers [post] func (ns *NodeService) handleAddPeer(c *gin.Context) { var req AddPeerRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } peer := &node.Peer{ ID: "pending-" + req.Address, // Will be updated on handshake Name: req.Name, Address: req.Address, Role: node.RoleDual, Score: 50, } if err := ns.peerRegistry.AddPeer(peer); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, peer) } // handleGetPeer godoc // @Summary Get peer information // @Description Get information about a specific peer // @Tags peers // @Produce json // @Param id path string true "Peer ID" // @Success 200 {object} node.Peer // @Router /peers/{id} [get] func (ns *NodeService) handleGetPeer(c *gin.Context) { peerID := c.Param("id") peer := ns.peerRegistry.GetPeer(peerID) if peer == nil { c.JSON(http.StatusNotFound, gin.H{"error": "peer not found"}) return } c.JSON(http.StatusOK, peer) } // handleRemovePeer godoc // @Summary Remove a peer // @Description Remove a peer from the registry // @Tags peers // @Produce json // @Param id path string true "Peer ID" // @Success 200 {object} map[string]string // @Router /peers/{id} [delete] func (ns *NodeService) handleRemovePeer(c *gin.Context) { peerID := c.Param("id") if err := ns.peerRegistry.RemovePeer(peerID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "peer removed"}) } // handlePingPeer godoc // @Summary Ping a peer // @Description Send a ping to a peer and measure latency // @Tags peers // @Produce json // @Param id path string true "Peer ID" // @Success 200 {object} map[string]float64 // @Failure 404 {object} APIError "Peer not found" // @Router /peers/{id}/ping [post] func (ns *NodeService) handlePingPeer(c *gin.Context) { peerID := c.Param("id") rtt, err := ns.controller.PingPeer(peerID) if err != nil { if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "not connected") { respondWithError(c, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found or not connected", err.Error()) return } respondWithError(c, http.StatusInternalServerError, ErrCodeInternal, "ping failed", err.Error()) return } c.JSON(http.StatusOK, gin.H{"rtt_ms": rtt}) } // handleConnectPeer godoc // @Summary Connect to a peer // @Description Establish a WebSocket connection to a peer // @Tags peers // @Produce json // @Param id path string true "Peer ID" // @Success 200 {object} map[string]string // @Failure 404 {object} APIError "Peer not found" // @Router /peers/{id}/connect [post] func (ns *NodeService) handleConnectPeer(c *gin.Context) { peerID := c.Param("id") if err := ns.controller.ConnectToPeer(peerID); err != nil { if strings.Contains(err.Error(), "not found") { respondWithError(c, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found", err.Error()) return } respondWithError(c, http.StatusInternalServerError, ErrCodeConnectionFailed, "connection failed", err.Error()) return } c.JSON(http.StatusOK, gin.H{"status": "connected"}) } // handleDisconnectPeer godoc // @Summary Disconnect from a peer // @Description Close the connection to a peer. Idempotent - returns success if peer not connected. // @Tags peers // @Produce json // @Param id path string true "Peer ID" // @Success 200 {object} map[string]string // @Router /peers/{id}/disconnect [post] func (ns *NodeService) handleDisconnectPeer(c *gin.Context) { peerID := c.Param("id") if err := ns.controller.DisconnectFromPeer(peerID); err != nil { // Make disconnect idempotent - if peer not connected, still return success if strings.Contains(err.Error(), "not connected") { c.JSON(http.StatusOK, gin.H{"status": "disconnected"}) return } respondWithError(c, http.StatusInternalServerError, ErrCodeInternal, "disconnect failed", err.Error()) return } c.JSON(http.StatusOK, gin.H{"status": "disconnected"}) } // handleRemoteStats godoc // @Summary Get stats from all remote peers // @Description Fetch mining statistics from all connected peers // @Tags remote // @Produce json // @Success 200 {object} map[string]node.StatsPayload // @Router /remote/stats [get] func (ns *NodeService) handleRemoteStats(c *gin.Context) { stats := ns.controller.GetAllStats() c.JSON(http.StatusOK, stats) } // handlePeerStats godoc // @Summary Get stats from a specific peer // @Description Fetch mining statistics from a specific peer // @Tags remote // @Produce json // @Param peerId path string true "Peer ID" // @Success 200 {object} node.StatsPayload // @Router /remote/{peerId}/stats [get] func (ns *NodeService) handlePeerStats(c *gin.Context) { peerID := c.Param("peerId") stats, err := ns.controller.GetRemoteStats(peerID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // RemoteStartRequest is the request body for starting a remote miner. type RemoteStartRequest struct { MinerType string `json:"minerType" binding:"required"` ProfileID string `json:"profileId,omitempty"` Config json.RawMessage `json:"config,omitempty"` } // handleRemoteStart godoc // @Summary Start miner on remote peer // @Description Start a miner on a remote peer using a profile // @Tags remote // @Accept json // @Produce json // @Param peerId path string true "Peer ID" // @Param request body RemoteStartRequest true "Start parameters" // @Success 200 {object} map[string]string // @Router /remote/{peerId}/start [post] func (ns *NodeService) handleRemoteStart(c *gin.Context) { peerID := c.Param("peerId") var req RemoteStartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := ns.controller.StartRemoteMiner(peerID, req.MinerType, req.ProfileID, req.Config); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "miner started"}) } // RemoteStopRequest is the request body for stopping a remote miner. type RemoteStopRequest struct { MinerName string `json:"minerName" binding:"required"` } // handleRemoteStop godoc // @Summary Stop miner on remote peer // @Description Stop a running miner on a remote peer // @Tags remote // @Accept json // @Produce json // @Param peerId path string true "Peer ID" // @Param request body RemoteStopRequest true "Stop parameters" // @Success 200 {object} map[string]string // @Router /remote/{peerId}/stop [post] func (ns *NodeService) handleRemoteStop(c *gin.Context) { peerID := c.Param("peerId") var req RemoteStopRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := ns.controller.StopRemoteMiner(peerID, req.MinerName); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "miner stopped"}) } // handleRemoteLogs godoc // @Summary Get logs from remote miner // @Description Retrieve console logs from a miner on a remote peer // @Tags remote // @Produce json // @Param peerId path string true "Peer ID" // @Param miner path string true "Miner Name" // @Param lines query int false "Number of lines (max 10000)" default(100) // @Success 200 {array} string // @Router /remote/{peerId}/logs/{miner} [get] func (ns *NodeService) handleRemoteLogs(c *gin.Context) { peerID := c.Param("peerId") minerName := c.Param("miner") lines := 100 const maxLines = 10000 // Prevent resource exhaustion if l := c.Query("lines"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { lines = parsed if lines > maxLines { lines = maxLines } } } logs, err := ns.controller.GetRemoteLogs(peerID, minerName, lines) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, logs) } // AuthModeResponse is the response for auth mode endpoints. type AuthModeResponse struct { Mode string `json:"mode"` } // handleGetAuthMode godoc // @Summary Get peer authentication mode // @Description Get the current peer authentication mode (open or allowlist) // @Tags peers // @Produce json // @Success 200 {object} AuthModeResponse // @Router /peers/auth/mode [get] func (ns *NodeService) handleGetAuthMode(c *gin.Context) { mode := ns.peerRegistry.GetAuthMode() modeStr := "open" if mode == node.PeerAuthAllowlist { modeStr = "allowlist" } c.JSON(http.StatusOK, AuthModeResponse{Mode: modeStr}) } // SetAuthModeRequest is the request for setting auth mode. type SetAuthModeRequest struct { Mode string `json:"mode" binding:"required"` } // handleSetAuthMode godoc // @Summary Set peer authentication mode // @Description Set the peer authentication mode (open or allowlist) // @Tags peers // @Accept json // @Produce json // @Param request body SetAuthModeRequest true "Auth mode (open or allowlist)" // @Success 200 {object} AuthModeResponse // @Failure 400 {object} APIError "Invalid mode" // @Router /peers/auth/mode [put] func (ns *NodeService) handleSetAuthMode(c *gin.Context) { var req SetAuthModeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var mode node.PeerAuthMode switch req.Mode { case "open": mode = node.PeerAuthOpen case "allowlist": mode = node.PeerAuthAllowlist default: respondWithError(c, http.StatusBadRequest, "INVALID_MODE", "mode must be 'open' or 'allowlist'", "") return } ns.peerRegistry.SetAuthMode(mode) c.JSON(http.StatusOK, AuthModeResponse{Mode: req.Mode}) } // AllowlistResponse is the response for listing allowlisted keys. type AllowlistResponse struct { PublicKeys []string `json:"publicKeys"` } // handleListAllowlist godoc // @Summary List allowlisted public keys // @Description Get all public keys in the peer allowlist // @Tags peers // @Produce json // @Success 200 {object} AllowlistResponse // @Router /peers/auth/allowlist [get] func (ns *NodeService) handleListAllowlist(c *gin.Context) { keys := ns.peerRegistry.ListAllowedPublicKeys() c.JSON(http.StatusOK, AllowlistResponse{PublicKeys: keys}) } // AddAllowlistRequest is the request for adding a key to the allowlist. type AddAllowlistRequest struct { PublicKey string `json:"publicKey" binding:"required"` } // handleAddToAllowlist godoc // @Summary Add public key to allowlist // @Description Add a public key to the peer allowlist // @Tags peers // @Accept json // @Produce json // @Param request body AddAllowlistRequest true "Public key to allow" // @Success 201 {object} map[string]string // @Failure 400 {object} APIError "Invalid request" // @Router /peers/auth/allowlist [post] func (ns *NodeService) handleAddToAllowlist(c *gin.Context) { var req AddAllowlistRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if len(req.PublicKey) < 16 { respondWithError(c, http.StatusBadRequest, "INVALID_KEY", "public key too short", "") return } ns.peerRegistry.AllowPublicKey(req.PublicKey) c.JSON(http.StatusCreated, gin.H{"status": "added"}) } // handleRemoveFromAllowlist godoc // @Summary Remove public key from allowlist // @Description Remove a public key from the peer allowlist // @Tags peers // @Produce json // @Param key path string true "Public key to remove (URL-encoded)" // @Success 200 {object} map[string]string // @Router /peers/auth/allowlist/{key} [delete] func (ns *NodeService) handleRemoveFromAllowlist(c *gin.Context) { key := c.Param("key") if key == "" { respondWithError(c, http.StatusBadRequest, "MISSING_KEY", "public key required", "") return } ns.peerRegistry.RevokePublicKey(key) c.JSON(http.StatusOK, gin.H{"status": "removed"}) }