232 lines
6.5 KiB
Dart
232 lines
6.5 KiB
Dart
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<MiningState> {
|
|
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<bool> 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<bool> 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<bool> 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<void> 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<bool> _runAction(Future<bool> 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<bool> _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<String> 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();
|
|
}
|
|
}
|