Files
peyawallet/lib/state/mining_controller.dart
Codex Bot b1091a05e4
build / Build Linux (lite) (push) Successful in 2m25s
build / Build Linux (mining) (push) Successful in 2m31s
build / Build Windows (lite) (push) Successful in 19m1s
build / Build Windows (mining) (push) Successful in 19m11s
Confirm xmrig startup before reporting failure
2026-04-20 23:58:46 +02:00

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();
}
}