2026-01-21 17:17:14 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
|
2026-01-28 15:29:42 +00:00
|
|
|
"github.com/host-uk/core/pkg/module"
|
2026-01-21 17:17:14 +00:00
|
|
|
"github.com/Snider/Mining/pkg/mining"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// MiningBridge wraps the Mining service for integration with lthn-desktop.
|
|
|
|
|
// It implements module.GinModule to register routes with the module registry.
|
|
|
|
|
type MiningBridge struct {
|
|
|
|
|
manager *mining.Manager
|
|
|
|
|
service *mining.Service
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewMiningBridge creates a new mining bridge with its own manager.
|
|
|
|
|
func NewMiningBridge() *MiningBridge {
|
|
|
|
|
return &MiningBridge{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ServiceName returns the canonical service name for Wails.
|
|
|
|
|
func (m *MiningBridge) ServiceName() string {
|
|
|
|
|
return "lthn-desktop/mining"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ServiceStartup initializes the mining manager and service.
|
|
|
|
|
func (m *MiningBridge) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
|
|
|
|
// Create the mining manager
|
|
|
|
|
m.manager = mining.NewManager()
|
|
|
|
|
|
2026-01-27 21:12:48 +00:00
|
|
|
// Create the service - we use a dummy address since we're not starting the server
|
|
|
|
|
// The Mining service will register routes on its Router which we can proxy to
|
|
|
|
|
service, err := mining.NewService(m.manager, "127.0.0.1:0", "127.0.0.1:0", "/api/mining")
|
2026-01-21 17:17:14 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
m.service = service
|
|
|
|
|
|
2026-01-27 21:12:48 +00:00
|
|
|
// Note: We don't call ServiceStartup() as that starts a standalone HTTP server
|
|
|
|
|
// The mining routes are already registered on m.service.Router by NewService
|
|
|
|
|
// Frontend can use the Wails-bound methods directly
|
2026-01-21 17:17:14 +00:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ServiceShutdown cleans up the mining manager.
|
|
|
|
|
func (m *MiningBridge) ServiceShutdown() error {
|
|
|
|
|
if m.manager != nil {
|
|
|
|
|
m.manager.Stop()
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegisterRoutes implements module.GinModule to register mining API routes.
|
|
|
|
|
func (m *MiningBridge) RegisterRoutes(group *gin.RouterGroup) {
|
|
|
|
|
// The mining service router handles all /api/mining/* routes
|
|
|
|
|
// Forward requests from the module registry's group to the mining router
|
|
|
|
|
if m.service != nil && m.service.Router != nil {
|
|
|
|
|
group.Any("/*path", func(c *gin.Context) {
|
|
|
|
|
// Rewrite the path to match what the mining router expects
|
|
|
|
|
originalPath := c.Request.URL.Path
|
|
|
|
|
c.Request.URL.Path = "/api/mining" + c.Param("path")
|
|
|
|
|
m.service.Router.ServeHTTP(c.Writer, c.Request)
|
|
|
|
|
c.Request.URL.Path = originalPath
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ModuleConfig returns the module configuration for registration.
|
|
|
|
|
func (m *MiningBridge) ModuleConfig() module.Config {
|
|
|
|
|
return module.Config{
|
|
|
|
|
Code: "mining",
|
|
|
|
|
Type: module.TypeCore,
|
|
|
|
|
Name: "Mining Module",
|
|
|
|
|
Version: "0.1.0",
|
|
|
|
|
Namespace: "mining",
|
|
|
|
|
Description: "Cryptocurrency mining management powered by Snider/Mining",
|
|
|
|
|
Author: "Lethean",
|
|
|
|
|
Contexts: []module.Context{module.ContextMiner, module.ContextDefault},
|
|
|
|
|
Menu: []module.MenuItem{
|
|
|
|
|
{
|
|
|
|
|
ID: "mining",
|
|
|
|
|
Label: "Mining",
|
|
|
|
|
Order: 200,
|
|
|
|
|
Contexts: []module.Context{module.ContextMiner},
|
|
|
|
|
Children: []module.MenuItem{
|
|
|
|
|
{ID: "mining-dashboard", Label: "Dashboard", Route: "/mining/dashboard", Order: 1},
|
|
|
|
|
{ID: "mining-pools", Label: "Pools", Route: "/mining/pools", Order: 2},
|
|
|
|
|
{ID: "mining-miners", Label: "Miners", Route: "/mining/miners", Order: 3},
|
|
|
|
|
{ID: "mining-sep1", Separator: true, Order: 4},
|
|
|
|
|
{ID: "mining-start", Label: "Start Mining", Action: "mining:start", Order: 5},
|
|
|
|
|
{ID: "mining-stop", Label: "Stop Mining", Action: "mining:stop", Order: 6},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: []module.Route{
|
|
|
|
|
{Path: "/mining/dashboard", Component: "mining-dashboard", Title: "Mining Dashboard", Contexts: []module.Context{module.ContextMiner}},
|
|
|
|
|
{Path: "/mining/pools", Component: "mining-pools", Title: "Mining Pools", Contexts: []module.Context{module.ContextMiner}},
|
|
|
|
|
{Path: "/mining/miners", Component: "mining-miners", Title: "Miners", Contexts: []module.Context{module.ContextMiner}},
|
|
|
|
|
},
|
|
|
|
|
API: []module.APIEndpoint{
|
|
|
|
|
{Method: "GET", Path: "/health", Description: "Health check"},
|
|
|
|
|
{Method: "GET", Path: "/ready", Description: "Readiness check"},
|
|
|
|
|
{Method: "GET", Path: "/info", Description: "Get mining service info"},
|
|
|
|
|
{Method: "GET", Path: "/metrics", Description: "Get mining metrics"},
|
|
|
|
|
{Method: "GET", Path: "/miners", Description: "List running miners"},
|
|
|
|
|
{Method: "GET", Path: "/miners/available", Description: "List available miners"},
|
|
|
|
|
{Method: "POST", Path: "/miners/:miner_name/install", Description: "Install a miner"},
|
|
|
|
|
{Method: "DELETE", Path: "/miners/:miner_name/uninstall", Description: "Uninstall a miner"},
|
|
|
|
|
{Method: "DELETE", Path: "/miners/:miner_name", Description: "Stop a miner"},
|
|
|
|
|
{Method: "GET", Path: "/miners/:miner_name/stats", Description: "Get miner stats"},
|
|
|
|
|
{Method: "GET", Path: "/miners/:miner_name/logs", Description: "Get miner logs"},
|
|
|
|
|
{Method: "POST", Path: "/profiles", Description: "Create mining profile"},
|
|
|
|
|
{Method: "GET", Path: "/profiles", Description: "List mining profiles"},
|
|
|
|
|
{Method: "POST", Path: "/profiles/:id/start", Description: "Start mining with profile"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Wails-bound methods for frontend access ---
|
|
|
|
|
|
|
|
|
|
// MinerInfo represents basic miner information for the frontend.
|
|
|
|
|
type MinerInfo struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
IsRunning bool `json:"isRunning"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MinerStats represents miner statistics for the frontend.
|
|
|
|
|
type MinerStats struct {
|
|
|
|
|
Hashrate int `json:"hashrate"`
|
|
|
|
|
Shares int `json:"shares"`
|
|
|
|
|
Rejected int `json:"rejected"`
|
|
|
|
|
Uptime int `json:"uptime"`
|
|
|
|
|
LastShare int64 `json:"lastShare"`
|
|
|
|
|
Algorithm string `json:"algorithm"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AvailableMiner represents an available miner for installation.
|
|
|
|
|
type AvailableMiner struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetManager returns the mining manager for direct access.
|
|
|
|
|
func (m *MiningBridge) GetManager() *mining.Manager {
|
|
|
|
|
return m.manager
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListMiners returns a list of running miners.
|
|
|
|
|
func (m *MiningBridge) ListMiners() []MinerInfo {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
miners := m.manager.ListMiners()
|
|
|
|
|
result := make([]MinerInfo, len(miners))
|
|
|
|
|
for i, miner := range miners {
|
|
|
|
|
result[i] = MinerInfo{
|
|
|
|
|
Name: miner.GetName(),
|
|
|
|
|
Type: miner.GetName(), // Type is typically the same as name for now
|
|
|
|
|
IsRunning: true, // If it's in ListMiners, it's running
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetMinerStats returns stats for a specific miner.
|
|
|
|
|
func (m *MiningBridge) GetMinerStats(name string) (*MinerStats, error) {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
miner, err := m.manager.GetMiner(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
stats, err := miner.GetStats()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &MinerStats{
|
|
|
|
|
Hashrate: stats.Hashrate,
|
|
|
|
|
Shares: stats.Shares,
|
|
|
|
|
Rejected: stats.Rejected,
|
|
|
|
|
Uptime: stats.Uptime,
|
|
|
|
|
LastShare: stats.LastShare,
|
|
|
|
|
Algorithm: stats.Algorithm,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StartMiner starts a miner with the given configuration.
|
|
|
|
|
func (m *MiningBridge) StartMiner(minerType string, pool string, wallet string) error {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
config := &mining.Config{
|
|
|
|
|
Pool: pool,
|
|
|
|
|
Wallet: wallet,
|
|
|
|
|
LogOutput: true,
|
|
|
|
|
}
|
|
|
|
|
_, err := m.manager.StartMiner(minerType, config)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StopMiner stops a running miner.
|
|
|
|
|
func (m *MiningBridge) StopMiner(name string) error {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return m.manager.StopMiner(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetAvailableMiners returns available miner types with their installation status.
|
|
|
|
|
func (m *MiningBridge) GetAvailableMiners() []AvailableMiner {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
available := m.manager.ListAvailableMiners()
|
|
|
|
|
result := make([]AvailableMiner, len(available))
|
|
|
|
|
for i, am := range available {
|
|
|
|
|
result[i] = AvailableMiner{
|
|
|
|
|
Name: am.Name,
|
|
|
|
|
Description: am.Description,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InstallMiner installs a miner by type.
|
|
|
|
|
func (m *MiningBridge) InstallMiner(minerType string) error {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Start the miner with no config to trigger installation
|
|
|
|
|
// The Mining package handles installation when starting an uninstalled miner
|
|
|
|
|
miner, err := m.manager.GetMiner(minerType)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Miner not found, create a temporary config to start (which will install)
|
|
|
|
|
config := &mining.Config{
|
|
|
|
|
Pool: "stratum+tcp://placeholder:3333",
|
|
|
|
|
Wallet: "placeholder",
|
|
|
|
|
}
|
|
|
|
|
_, err = m.manager.StartMiner(minerType, config)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
// Stop it after installation
|
|
|
|
|
return m.manager.StopMiner(minerType)
|
|
|
|
|
}
|
|
|
|
|
// Miner exists, install it directly
|
|
|
|
|
return miner.Install()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UninstallMiner uninstalls a miner by type.
|
|
|
|
|
func (m *MiningBridge) UninstallMiner(minerType string) error {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return m.manager.UninstallMiner(minerType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetHashrateHistory returns hashrate history for a miner.
|
|
|
|
|
func (m *MiningBridge) GetHashrateHistory(name string) ([]mining.HashratePoint, error) {
|
|
|
|
|
if m.manager == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return m.manager.GetMinerHashrateHistory(name)
|
|
|
|
|
}
|