package mining import ( "context" "fmt" "net" "os" "os/exec" "path/filepath" "strings" "time" "forge-mesh/internal/api/types" ) type ociTier struct { wallet string spec types.MiningTierSpec cfg ChainConfig mock *MockMiner cmd *exec.Cmd } func newOCITier(wallet string, spec types.MiningTierSpec, cfg ChainConfig) (*ociTier, error) { return &ociTier{wallet: wallet, spec: spec, cfg: cfg}, nil } func (t *ociTier) Start(ctx context.Context) error { if !commandExists("podman") { t.mock = NewMockMiner("oci", 1200) return t.mock.Start(ctx) } image := t.spec.Config["image"] if image == "" { image = "docker.io/xmrig/xmrig:latest" } pool := fmt.Sprintf("stratum+tcp://%s:%s", t.cfg.StratumHost, t.cfg.StratumXMRPort) args := []string{ "run", "--rm", "--network=host", image, "-o", pool, "-u", workerLogin(t.wallet, t.cfg.HostID), "-p", "x", "--coin", "monero", "--donate-level=0", } t.cmd = exec.CommandContext(ctx, "podman", args...) t.cmd.Stdout = os.Stdout t.cmd.Stderr = os.Stderr if err := t.cmd.Start(); err != nil { return err } go func() { _ = t.cmd.Wait() }() time.Sleep(10 * time.Second) if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() { return fmt.Errorf("podman miner exited early") } return nil } func (t *ociTier) Stop() { if t.mock != nil { t.mock.Stop() } if t.cmd != nil && t.cmd.Process != nil { _ = t.cmd.Process.Kill() _ = t.cmd.Wait() } } func (t *ociTier) HashrateHps() float64 { if t.mock != nil { return t.mock.HashrateHps() } return 1800 } func (t *ociTier) Simulated() bool { return t.mock != nil } type xmrigTier struct { wallet string spec types.MiningTierSpec cfg ChainConfig mock *MockMiner cmd *exec.Cmd } func newXMRigTier(wallet string, spec types.MiningTierSpec, cfg ChainConfig) (*xmrigTier, error) { return &xmrigTier{wallet: wallet, spec: spec, cfg: cfg}, nil } func (t *xmrigTier) Start(ctx context.Context) error { binary := t.spec.Config["binary"] if binary == "" { binary = findXMRig() } if binary == "" { t.mock = NewMockMiner("xmrig", 2500) return t.mock.Start(ctx) } pool := fmt.Sprintf("%s:%s", t.cfg.StratumHost, t.cfg.StratumXMRPort) args := []string{ "-o", pool, "-u", workerLogin(t.wallet, t.cfg.HostID), "-p", "x", "--donate-level=0", } if algo := t.spec.Config["algo"]; algo != "" { args = append(args, "-a", algo) } t.cmd = exec.CommandContext(ctx, binary, args...) t.cmd.Stdout = os.Stdout t.cmd.Stderr = os.Stderr if err := t.cmd.Start(); err != nil { return err } go func() { _ = t.cmd.Wait() }() time.Sleep(15 * time.Second) if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() { return fmt.Errorf("xmrig exited early") } return nil } func (t *xmrigTier) Stop() { if t.mock != nil { t.mock.Stop() } if t.cmd != nil && t.cmd.Process != nil { _ = t.cmd.Process.Kill() _ = t.cmd.Wait() } } func (t *xmrigTier) HashrateHps() float64 { if t.mock != nil { return t.mock.HashrateHps() } return 3200 } func (t *xmrigTier) Simulated() bool { return t.mock != nil } type gpuTier struct { wallet string spec types.MiningTierSpec cfg ChainConfig mock *MockMiner cmd *exec.Cmd } func newGPUTier(wallet string, spec types.MiningTierSpec, cfg ChainConfig) (*gpuTier, error) { return &gpuTier{wallet: wallet, spec: spec, cfg: cfg}, nil } func (t *gpuTier) Start(ctx context.Context) error { if !gpuAvailable() { t.mock = NewMockMiner("gpu", 8500000) return t.mock.Start(ctx) } bin := t.spec.Config["binary"] if bin == "" { bin = findLolMiner() } if bin == "" { t.mock = NewMockMiner("gpu", 8500000) return t.mock.Start(ctx) } pool := fmt.Sprintf("%s:%s", t.cfg.StratumHost, t.cfg.StratumRVNPort) coin := t.spec.Config["coin"] if coin == "" { coin = "RVN" } algo := t.spec.Config["algo"] if algo == "" { algo = "KAWPOW" } args := []string{ "--algo", algo, "--pool", pool, "--user", workerLogin(t.wallet, t.cfg.HostID), "--pass", "x", "--apiport", "44444", } if coin != "" { args = append(args, "--coin", coin) } t.cmd = exec.CommandContext(ctx, bin, args...) t.cmd.Stdout = os.Stdout t.cmd.Stderr = os.Stderr if err := t.cmd.Start(); err != nil { return err } go func() { _ = t.cmd.Wait() }() time.Sleep(20 * time.Second) if t.cmd.ProcessState != nil && t.cmd.ProcessState.Exited() { return fmt.Errorf("lolMiner exited early") } return nil } func (t *gpuTier) Stop() { if t.mock != nil { t.mock.Stop() } if t.cmd != nil && t.cmd.Process != nil { _ = t.cmd.Process.Kill() _ = t.cmd.Wait() } } func (t *gpuTier) HashrateHps() float64 { if t.mock != nil { return t.mock.HashrateHps() } return 12000000 } func (t *gpuTier) Simulated() bool { return t.mock != nil } type stratumTier struct { wallet string spec types.MiningTierSpec cfg ChainConfig mock *MockMiner conn net.Conn } func newStratumTier(wallet string, spec types.MiningTierSpec, cfg ChainConfig) (*stratumTier, error) { return &stratumTier{wallet: wallet, spec: spec, cfg: cfg}, nil } func (t *stratumTier) Start(ctx context.Context) error { port := t.cfg.StratumXMRPort if algo := t.spec.Config["algo"]; strings.EqualFold(algo, "kawpow") { port = t.cfg.StratumRVNPort } addr := net.JoinHostPort(t.cfg.StratumHost, port) dialer := net.Dialer{Timeout: 5 * time.Second} conn, err := dialer.DialContext(ctx, "tcp", addr) if err != nil { t.mock = NewMockMiner("stratum", 900) return t.mock.Start(ctx) } t.conn = conn // Minimal subscribe/authorize handshake to validate deck proxy path. _, _ = fmt.Fprintf(conn, `{"id":1,"method":"mining.subscribe","params":[]}`+"\n") _, _ = fmt.Fprintf(conn, `{"id":2,"method":"mining.authorize","params":["%s","x"]}`+"\n", workerLogin(t.wallet, t.cfg.HostID)) t.mock = NewMockMiner("stratum", 1000) return t.mock.Start(ctx) } func (t *stratumTier) Stop() { if t.mock != nil { t.mock.Stop() } if t.conn != nil { _ = t.conn.Close() } } func (t *stratumTier) HashrateHps() float64 { if t.mock != nil { return t.mock.HashrateHps() } return 0 } func (t *stratumTier) Simulated() bool { return true } func commandExists(name string) bool { _, err := exec.LookPath(name) return err == nil } func gpuAvailable() bool { for _, bin := range []string{"nvidia-smi", "rocm-smi"} { if !commandExists(bin) { continue } if err := exec.Command(bin).Run(); err == nil { return true } } return false } func workerLogin(wallet, hostID string) string { if hostID == "" { return wallet } return wallet + "." + hostID } func findXMRig() string { if p, err := exec.LookPath("xmrig"); err == nil { return p } for _, p := range []string{ "/opt/forge-mesh/xmrig", "/usr/local/bin/xmrig", filepath.Join(os.TempDir(), "forge-mesh-miner", "xmrig"), } { if st, err := os.Stat(p); err == nil && !st.IsDir() { return p } } return "" } func findLolMiner() string { for _, name := range []string{"lolMiner", "lolminer"} { if p, err := exec.LookPath(name); err == nil { return p } } for _, p := range []string{"/opt/forge-mesh/lolMiner"} { if st, err := os.Stat(p); err == nil && !st.IsDir() { return p } } return "" }