package mining import ( "encoding/json" "net/http" "strconv" "forge.lthn.ai/Snider/Mining/pkg/node" "github.com/gin-gonic/gin" ) // nodeService, err := NewNodeService() // router.Group("/api/v1/mining").Group("/node").GET("/info", nodeService.handleNodeInfo) type NodeService struct { nodeManager *node.NodeManager peerRegistry *node.PeerRegistry transport *node.Transport controller *node.Controller worker *node.Worker } // nodeService, err := NewNodeService() // initialises node manager, peer registry, transport, controller, worker func NewNodeService() (*NodeService, error) { nodeManager, err := node.NewNodeManager() if err != nil { return nil, err } peerRegistry, err := node.NewPeerRegistry() if err != nil { return nil, err } transportConfig := node.DefaultTransportConfig() transport := node.NewTransport(nodeManager, peerRegistry, transportConfig) nodeService := &NodeService{ nodeManager: nodeManager, peerRegistry: peerRegistry, transport: transport, } nodeService.controller = node.NewController(nodeManager, peerRegistry, transport) nodeService.worker = node.NewWorker(nodeManager, transport) return nodeService, nil } // router.Group("/api/v1/mining") exposes /node, /peers, and /remote route groups. func (nodeService *NodeService) SetupRoutes(router *gin.RouterGroup) { // router.Group("/node").GET("/info", nodeService.handleNodeInfo) exposes node identity and peer counts. nodeGroup := router.Group("/node") { nodeGroup.GET("/info", nodeService.handleNodeInfo) nodeGroup.POST("/init", nodeService.handleNodeInit) } // router.Group("/peers").POST("", nodeService.handleAddPeer) registers a peer like 10.0.0.2:9090. peerGroup := router.Group("/peers") { peerGroup.GET("", nodeService.handleListPeers) peerGroup.POST("", nodeService.handleAddPeer) peerGroup.GET("/:id", nodeService.handleGetPeer) peerGroup.DELETE("/:id", nodeService.handleRemovePeer) peerGroup.POST("/:id/ping", nodeService.handlePingPeer) peerGroup.POST("/:id/connect", nodeService.handleConnectPeer) peerGroup.POST("/:id/disconnect", nodeService.handleDisconnectPeer) // router.Group("/peers/auth/allowlist").POST("", nodeService.handleAddToAllowlist) accepts keys like ed25519:abc... peerGroup.GET("/auth/mode", nodeService.handleGetAuthMode) peerGroup.PUT("/auth/mode", nodeService.handleSetAuthMode) peerGroup.GET("/auth/allowlist", nodeService.handleListAllowlist) peerGroup.POST("/auth/allowlist", nodeService.handleAddToAllowlist) peerGroup.DELETE("/auth/allowlist/:key", nodeService.handleRemoveFromAllowlist) } // router.Group("/remote").POST("/:peerId/start", nodeService.handleRemoteStart) starts a miner on a peer like peer-123. remoteGroup := router.Group("/remote") { remoteGroup.GET("/stats", nodeService.handleRemoteStats) remoteGroup.GET("/:peerId/stats", nodeService.handlePeerStats) remoteGroup.POST("/:peerId/start", nodeService.handleRemoteStart) remoteGroup.POST("/:peerId/stop", nodeService.handleRemoteStop) remoteGroup.GET("/:peerId/logs/:miner", nodeService.handleRemoteLogs) } } // if err := nodeService.StartTransport(); err != nil { log.Fatal(err) } // starts WebSocket listener on configured port func (nodeService *NodeService) StartTransport() error { return nodeService.transport.Start() } // defer nodeService.StopTransport() // gracefully shuts down WebSocket listener and closes peer connections func (nodeService *NodeService) StopTransport() error { return nodeService.transport.Stop() } // response := NodeInfoResponse{HasIdentity: true, Identity: identity, RegisteredPeers: 3, ConnectedPeers: 1} 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 (nodeService *NodeService) handleNodeInfo(requestContext *gin.Context) { response := NodeInfoResponse{ HasIdentity: nodeService.nodeManager.HasIdentity(), RegisteredPeers: nodeService.peerRegistry.Count(), ConnectedPeers: len(nodeService.peerRegistry.GetConnectedPeers()), } if nodeService.nodeManager.HasIdentity() { response.Identity = nodeService.nodeManager.GetIdentity() } requestContext.JSON(http.StatusOK, response) } // POST /node/init {"name": "my-node", "role": "worker"} 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 (nodeService *NodeService) handleNodeInit(requestContext *gin.Context) { var request NodeInitRequest if err := requestContext.ShouldBindJSON(&request); err != nil { requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if nodeService.nodeManager.HasIdentity() { requestContext.JSON(http.StatusConflict, gin.H{"error": "node identity already exists"}) return } role := node.RoleDual switch request.Role { case "controller": role = node.RoleController case "worker": role = node.RoleWorker case "dual", "": role = node.RoleDual default: requestContext.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"}) return } if err := nodeService.nodeManager.GenerateIdentity(request.Name, role); err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.JSON(http.StatusOK, nodeService.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 (nodeService *NodeService) handleListPeers(requestContext *gin.Context) { peers := nodeService.peerRegistry.ListPeers() requestContext.JSON(http.StatusOK, peers) } // POST /peers {"address": "10.0.0.2:9090", "name": "worker-1"} 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 (nodeService *NodeService) handleAddPeer(requestContext *gin.Context) { var request AddPeerRequest if err := requestContext.ShouldBindJSON(&request); err != nil { requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } peer := &node.Peer{ ID: "pending-" + request.Address, // Will be updated on handshake Name: request.Name, Address: request.Address, Role: node.RoleDual, Score: 50, } if err := nodeService.peerRegistry.AddPeer(peer); err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.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 (nodeService *NodeService) handleGetPeer(requestContext *gin.Context) { peerID := requestContext.Param("id") peer := nodeService.peerRegistry.GetPeer(peerID) if peer == nil { requestContext.JSON(http.StatusNotFound, gin.H{"error": "peer not found"}) return } requestContext.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 (nodeService *NodeService) handleRemovePeer(requestContext *gin.Context) { peerID := requestContext.Param("id") if err := nodeService.peerRegistry.RemovePeer(peerID); err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.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 (nodeService *NodeService) handlePingPeer(requestContext *gin.Context) { peerID := requestContext.Param("id") rtt, err := nodeService.controller.PingPeer(peerID) if err != nil { if containsStr(err.Error(), "not found") || containsStr(err.Error(), "not connected") { respondWithError(requestContext, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found or not connected", err.Error()) return } respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "ping failed", err.Error()) return } requestContext.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 (nodeService *NodeService) handleConnectPeer(requestContext *gin.Context) { peerID := requestContext.Param("id") if err := nodeService.controller.ConnectToPeer(peerID); err != nil { if containsStr(err.Error(), "not found") { respondWithError(requestContext, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found", err.Error()) return } respondWithError(requestContext, http.StatusInternalServerError, ErrCodeConnectionFailed, "connection failed", err.Error()) return } requestContext.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 (nodeService *NodeService) handleDisconnectPeer(requestContext *gin.Context) { peerID := requestContext.Param("id") if err := nodeService.controller.DisconnectFromPeer(peerID); err != nil { // Make disconnect idempotent - if peer not connected, still return success if containsStr(err.Error(), "not connected") { requestContext.JSON(http.StatusOK, gin.H{"status": "disconnected"}) return } respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "disconnect failed", err.Error()) return } requestContext.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 (nodeService *NodeService) handleRemoteStats(requestContext *gin.Context) { stats := nodeService.controller.GetAllStats() requestContext.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 (nodeService *NodeService) handlePeerStats(requestContext *gin.Context) { peerID := requestContext.Param("peerId") stats, err := nodeService.controller.GetRemoteStats(peerID) if err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.JSON(http.StatusOK, stats) } // POST /remote/{peerId}/start {"minerType": "xmrig", "profileId": "abc123"} 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 (nodeService *NodeService) handleRemoteStart(requestContext *gin.Context) { peerID := requestContext.Param("peerId") var request RemoteStartRequest if err := requestContext.ShouldBindJSON(&request); err != nil { requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := nodeService.controller.StartRemoteMiner(peerID, request.MinerType, request.ProfileID, request.Config); err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.JSON(http.StatusOK, gin.H{"status": "miner started"}) } // POST /remote/{peerId}/stop {"minerName": "xmrig-main"} 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 (nodeService *NodeService) handleRemoteStop(requestContext *gin.Context) { peerID := requestContext.Param("peerId") var request RemoteStopRequest if err := requestContext.ShouldBindJSON(&request); err != nil { requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := nodeService.controller.StopRemoteMiner(peerID, request.MinerName); err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.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 (nodeService *NodeService) handleRemoteLogs(requestContext *gin.Context) { peerID := requestContext.Param("peerId") minerName := requestContext.Param("miner") lines := 100 const maxLines = 10000 // Prevent resource exhaustion if linesParam := requestContext.Query("lines"); linesParam != "" { if parsed, err := strconv.Atoi(linesParam); err == nil && parsed > 0 { lines = parsed if lines > maxLines { lines = maxLines } } } logs, err := nodeService.controller.GetRemoteLogs(peerID, minerName, lines) if err != nil { requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } requestContext.JSON(http.StatusOK, logs) } // GET /peers/auth/mode → {"mode": "open"} or {"mode": "allowlist"} 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 (nodeService *NodeService) handleGetAuthMode(requestContext *gin.Context) { mode := nodeService.peerRegistry.GetAuthMode() modeStr := "open" if mode == node.PeerAuthAllowlist { modeStr = "allowlist" } requestContext.JSON(http.StatusOK, AuthModeResponse{Mode: modeStr}) } // PUT /peers/auth/mode {"mode": "allowlist"} // or "open" 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 (nodeService *NodeService) handleSetAuthMode(requestContext *gin.Context) { var request SetAuthModeRequest if err := requestContext.ShouldBindJSON(&request); err != nil { requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var mode node.PeerAuthMode switch request.Mode { case "open": mode = node.PeerAuthOpen case "allowlist": mode = node.PeerAuthAllowlist default: respondWithError(requestContext, http.StatusBadRequest, "INVALID_MODE", "mode must be 'open' or 'allowlist'", "") return } nodeService.peerRegistry.SetAuthMode(mode) requestContext.JSON(http.StatusOK, AuthModeResponse{Mode: request.Mode}) } // GET /peers/auth/allowlist → {"publicKeys": ["ed25519:abc...", "ed25519:def..."]} 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 (nodeService *NodeService) handleListAllowlist(requestContext *gin.Context) { keys := nodeService.peerRegistry.ListAllowedPublicKeys() requestContext.JSON(http.StatusOK, AllowlistResponse{PublicKeys: keys}) } // POST /peers/auth/allowlist {"publicKey": "ed25519:abc123..."} 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 (nodeService *NodeService) handleAddToAllowlist(requestContext *gin.Context) { var request AddAllowlistRequest if err := requestContext.ShouldBindJSON(&request); err != nil { requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if len(request.PublicKey) < 16 { respondWithError(requestContext, http.StatusBadRequest, "INVALID_KEY", "public key too short", "") return } nodeService.peerRegistry.AllowPublicKey(request.PublicKey) requestContext.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 (nodeService *NodeService) handleRemoveFromAllowlist(requestContext *gin.Context) { key := requestContext.Param("key") if key == "" { respondWithError(requestContext, http.StatusBadRequest, "MISSING_KEY", "public key required", "") return } nodeService.peerRegistry.RevokePublicKey(key) requestContext.JSON(http.StatusOK, gin.H{"status": "removed"}) }