import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import '../domain/models.dart'; import '../services/mining_service.dart'; import 'wallet_controller.dart'; class MiningState { const MiningState({ required this.running, required this.busy, required this.summary, required this.error, }); final bool running; final bool busy; final XmrigSummary? summary; final String? error; MiningState copyWith({ bool? running, bool? busy, XmrigSummary? summary, String? error, }) { return MiningState( running: running ?? this.running, busy: busy ?? this.busy, summary: summary ?? this.summary, error: error, ); } factory MiningState.initial() { return const MiningState( running: false, busy: false, summary: null, error: null, ); } } class MiningController extends StateNotifier { MiningController({ required MiningService service, required Logger logger, required AppConfig Function() readConfig, required WalletState Function() readWalletState, }) : _service = service, _logger = logger, _readConfig = readConfig, _readWalletState = readWalletState, super(MiningState.initial()) { _timer = Timer.periodic(const Duration(seconds: 2), (_) => refresh()); unawaited(refresh()); } final MiningService _service; final Logger _logger; final AppConfig Function() _readConfig; final WalletState Function() _readWalletState; Timer? _timer; Future start() async { return _runAction(() async { final config = _buildLaunchConfig(); final started = await _service.start(config: config); await refresh(); final confirmed = await _confirmRunning(config); if (!started && !confirmed) { throw StateError('Failed to start miner'); } return true; }); } Future stop() async { return _runAction(() async { final config = _tryBuildLaunchConfig(); final stopped = await _service.stop(config: config); if (!stopped) { throw StateError('Failed to stop miner'); } await refresh(); return true; }); } Future restart() async { return _runAction(() async { final config = _buildLaunchConfig(); final restarted = await _service.restart(config: config); await refresh(); final confirmed = await _confirmRunning(config); if (!restarted && !confirmed) { throw StateError('Failed to restart miner'); } return true; }); } Future refresh() async { final config = _tryBuildLaunchConfig(); if (config == null) { state = state.copyWith( running: false, summary: null, error: state.busy ? state.error : null, ); return; } try { final running = await _service.isRunning(config: config); final summary = running ? await _service.fetchSummary(config: config) : null; state = state.copyWith( running: running, summary: summary, error: null, ); } catch (error, stack) { _logger.w('Failed to refresh miner state', error: error, stackTrace: stack); state = state.copyWith(error: error.toString()); } } Future _runAction(Future Function() action) async { if (state.busy) { return false; } state = state.copyWith(busy: true, error: null); try { return await action(); } catch (error, stack) { _logger.w('Mining action failed', error: error, stackTrace: stack); state = state.copyWith(error: error.toString()); return false; } finally { state = state.copyWith(busy: false); } } Future _confirmRunning(MinerLaunchConfig config) async { for (var i = 0; i < 6; i++) { final running = await _service.isRunning(config: config); if (running) { final summary = await _service.fetchSummary(config: config); state = state.copyWith(running: true, summary: summary, error: null); return true; } await Future.delayed(const Duration(milliseconds: 500)); } return false; } MinerLaunchConfig? _tryBuildLaunchConfig() { try { return _buildLaunchConfig(); } catch (_) { return null; } } MinerLaunchConfig _buildLaunchConfig() { final config = _readConfig(); final wallet = _readWalletState().walletInfo; final address = wallet?.address.trim() ?? ''; if (address.isEmpty) { throw StateError('Open a wallet before starting the miner'); } final mining = config.miningConfig; final localNode = _resolveLocalNodePorts(config.localNodeArgs); final mode = mining.mode == MiningMode.solo ? MinerTargetMode.solo : MinerTargetMode.pool; if (mode == MinerTargetMode.solo && config.nodeConfig.mode != NodeMode.local) { throw StateError('Solo mining requires the wallet to use the local node'); } return MinerLaunchConfig( mode: mode, walletAddress: address, cpuThreads: mining.cpuThreads.clamp(1, 64), apiPort: mining.apiPort, poolHost: mining.poolHost, poolPort: mining.poolPort, daemonHost: localNode.host, daemonPort: localNode.rpcPort, daemonZmqPort: localNode.zmqPort, ); } ({String host, int rpcPort, int zmqPort}) _resolveLocalNodePorts( List args, ) { var host = '127.0.0.1'; var rpcPort = 17750; var zmqPort = 17751; for (var i = 0; i < args.length; i++) { final arg = args[i]; if (arg.startsWith('--rpc-bind-ip=')) { host = arg.split('=').last.trim(); } else if (arg == '--rpc-bind-ip' && i + 1 < args.length) { host = args[i + 1].trim(); } else if (arg.startsWith('--rpc-bind-port=')) { rpcPort = int.tryParse(arg.split('=').last.trim()) ?? rpcPort; } else if (arg == '--rpc-bind-port' && i + 1 < args.length) { rpcPort = int.tryParse(args[i + 1].trim()) ?? rpcPort; } else if (arg.startsWith('--zmq-rpc-bind-port=')) { zmqPort = int.tryParse(arg.split('=').last.trim()) ?? zmqPort; } else if (arg == '--zmq-rpc-bind-port' && i + 1 < args.length) { zmqPort = int.tryParse(args[i + 1].trim()) ?? zmqPort; } } if (host == '0.0.0.0' || host == '::' || host == '[::]') { host = '127.0.0.1'; } return (host: host, rpcPort: rpcPort, zmqPort: zmqPort); } @override void dispose() { _timer?.cancel(); super.dispose(); } }