import 'dart:io'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:path/path.dart' as p; import '../data/wallet_repository.dart'; import '../domain/models.dart'; import '../domain/send.dart'; import '../domain/staking.dart'; import '../domain/transactions.dart'; import '../services/wallet_cache_recovery.dart'; class WalletState { const WalletState({ required this.walletInfo, required this.balanceAtomic, required this.unlockedAtomic, required this.transactions, required this.subaddresses, required this.syncStatus, required this.isLoading, required this.isSyncing, required this.error, }); final WalletInfo? walletInfo; final int balanceAtomic; final int unlockedAtomic; final List transactions; final List subaddresses; final SyncStatus syncStatus; final bool isLoading; final bool isSyncing; final String? error; WalletState copyWith({ WalletInfo? walletInfo, int? balanceAtomic, int? unlockedAtomic, List? transactions, List? subaddresses, SyncStatus? syncStatus, bool? isLoading, bool? isSyncing, String? error, }) { return WalletState( walletInfo: walletInfo ?? this.walletInfo, balanceAtomic: balanceAtomic ?? this.balanceAtomic, unlockedAtomic: unlockedAtomic ?? this.unlockedAtomic, transactions: transactions ?? this.transactions, subaddresses: subaddresses ?? this.subaddresses, syncStatus: syncStatus ?? this.syncStatus, isLoading: isLoading ?? this.isLoading, isSyncing: isSyncing ?? this.isSyncing, error: error, ); } factory WalletState.initial() { return WalletState( walletInfo: null, balanceAtomic: 0, unlockedAtomic: 0, transactions: const [], subaddresses: const [], syncStatus: SyncStatus.initial(), isLoading: false, isSyncing: false, error: null, ); } } class WalletController extends StateNotifier { WalletController({ required WalletRepository repository, required Logger logger, required AppConfig Function() readConfig, }) : _repository = repository, _logger = logger, _readConfig = readConfig, _cacheRecovery = WalletCacheRecovery(logger: logger), super(WalletState.initial()); final WalletRepository _repository; final Logger _logger; final AppConfig Function() _readConfig; final WalletCacheRecovery _cacheRecovery; Future tryAutoOpen(AppConfig config) async { final wallet = config.lastWallet; if (wallet == null) { return false; } final resolvedPath = _resolveOpenPath(wallet.path); if (_isDirectoryPath(resolvedPath)) { state = state.copyWith(error: 'Wallet file not found in ${wallet.path}'); return false; } if (!File(resolvedPath).existsSync() && !File('$resolvedPath.keys').existsSync()) { state = state.copyWith(error: 'Wallet not found at ${wallet.path}'); return false; } if (!await verifyWalletPassword(path: resolvedPath, password: '')) { return true; } await _cacheRecovery.recoverIfNeeded(resolvedPath); await openWallet(path: resolvedPath, password: ''); return false; } Future createWallet({ required String name, required String path, required String password, }) async { await _runWithLoading(() async { await Directory(path).create(recursive: true); final resolvedPath = _resolveWalletFilePath(path, name: name); await _cacheRecovery.markOpenAttempt(resolvedPath); await _repository.createWallet( name: name, path: path, password: password); try { await _postOpen(name: name, path: resolvedPath); } finally { await _cacheRecovery.clearOpenAttempt(); } }); } Future prepareWalletSeedDraft({ required String name, required String password, }) async { return _runWithLoading(() async { final tempDir = await Directory.systemTemp.createTemp('peyawallet-draft-'); try { await _repository.createWallet( name: name, path: tempDir.path, password: password, ); final seed = await _repository.getSeed(); await _repository.closeWallet(); return seed; } finally { try { await _repository.closeWallet(); } catch (_) {} if (tempDir.existsSync()) { await tempDir.delete(recursive: true); } } }); } Future restoreWalletFromSeed({ required String name, required String seed, required String path, required String password, }) async { await _runWithLoading(() async { await Directory(path).create(recursive: true); final resolvedPath = _resolveWalletFilePath(path, name: name); await _cacheRecovery.markOpenAttempt(resolvedPath); await _repository.restoreWalletFromSeed( name: name, seed: seed, path: path, password: password, ); try { await _postOpen(name: name, path: resolvedPath); } finally { await _cacheRecovery.clearOpenAttempt(); } }); } Future openWallet({ required String path, required String password, }) async { await _runWithLoading(() async { final resolvedPath = _resolveOpenPath(path); await _cacheRecovery.recoverIfNeeded(resolvedPath); await _cacheRecovery.markOpenAttempt(resolvedPath); try { await _repository.openWallet(path: resolvedPath, password: password); await _postOpen(name: p.basename(resolvedPath), path: resolvedPath); } finally { await _cacheRecovery.clearOpenAttempt(); } }); } Future verifyWalletPassword({ required String path, required String password, }) async { final resolvedPath = _resolveOpenPath(path); return _repository.verifyWalletPassword( path: resolvedPath, password: password, ); } Future walletRequiresPassword(String path) async { return !await verifyWalletPassword(path: path, password: ''); } Future connectNode(NodeConfig nodeConfig) async { await _runWithLoading(() async { await _repository.connectNode(nodeConfig); }); } Future switchWallet() async { await _repository.closeWallet(); await _cacheRecovery.clearOpenAttempt(); state = WalletState.initial(); } Future getSeed() async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } return _repository.getSeed(); } Future createSubaddress({String? label}) async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } return _runWithLoading(() => _repository.createSubaddress(label: label)); } Future setSubaddressLabel({ required int accountIndex, required int addressIndex, required String label, }) async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } await _runWithLoading(() { return _repository.setSubaddressLabel( accountIndex: accountIndex, addressIndex: addressIndex, label: label, ); }); await refreshSubaddresses(); } Future refreshSubaddresses() async { if (state.walletInfo == null) { return; } final subaddresses = await _repository.getSubaddresses(); state = state.copyWith(subaddresses: subaddresses); } Future prepareSend(SendRequest request) async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } return _runWithLoading(() => _repository.prepareSend(request)); } Future commitPreparedSend() async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } return _runWithLoading(() async { final result = await _repository.commitPreparedSend(); await _refreshSnapshot(); return result; }); } Future discardPreparedSend() async { await _repository.discardPreparedSend(); } Future prepareStake({ required int amountAtomic, int accountIndex = 0, }) async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } return _runWithLoading(() { return _repository.prepareStake( amountAtomic: amountAtomic, accountIndex: accountIndex, ); }); } Future> getActiveStakeYields({ required List activeStakes, required int currentHeight, }) async { if (state.walletInfo == null || activeStakes.isEmpty) { return const {}; } return _repository.getActiveStakeYields( activeStakes: activeStakes, currentHeight: currentHeight, ); } Future commitPreparedStake() async { if (state.walletInfo == null) { throw StateError('No wallet loaded'); } return _runWithLoading(() async { final result = await _repository.commitPreparedStake(); await _refreshSnapshot(); return result; }); } Future discardPreparedStake() async { await _repository.discardPreparedStake(); } Future syncNow() async { if (state.isSyncing || state.walletInfo == null) { return; } state = state.copyWith(isSyncing: true); try { await _ensureConnected(); await _refreshSnapshot(); } catch (error, stack) { _logger.e('Sync failed', error: error, stackTrace: stack); state = state.copyWith(error: error.toString()); } finally { state = state.copyWith(isSyncing: false); } } Future rescanBlockchain({int? fromHeight}) async { if (state.walletInfo == null) { return; } state = state.copyWith(isSyncing: true, error: null); try { await _ensureConnected(); await _repository.rescanBlockchain(fromHeight: fromHeight); await _refreshSnapshot(); } catch (error, stack) { _logger.e('Rescan failed', error: error, stackTrace: stack); state = state.copyWith(error: error.toString()); } finally { state = state.copyWith(isSyncing: false); } } Future _ensureConnected() async { if (await _repository.isConnected()) { return; } final config = _readConfig(); await _repository.connectNode(config.nodeConfig); if (!await _repository.isConnected()) { throw StateError('Failed to connect to node'); } } Future _postOpen({required String name, required String path}) async { final address = await _repository.getAddress(); final subaddresses = await _repository.getSubaddresses(); state = state.copyWith( walletInfo: WalletInfo(name: name, path: path, address: address), balanceAtomic: 0, syncStatus: SyncStatus.initial(), transactions: const [], subaddresses: subaddresses, error: null, ); } Future _refreshSnapshot() async { final snapshot = await _repository.refresh(); final transactions = await _repository.getTransactions(); final syncStatus = SyncStatus( synced: snapshot.synced, progress: snapshot.progress, nodeHeight: snapshot.nodeHeight, walletHeight: snapshot.walletHeight, lastSync: DateTime.now(), ); final updatedWalletInfo = state.walletInfo?.copyWith(address: snapshot.address); state = state.copyWith( walletInfo: updatedWalletInfo ?? state.walletInfo, balanceAtomic: snapshot.balanceAtomic, unlockedAtomic: snapshot.unlockedAtomic, transactions: transactions, subaddresses: snapshot.subaddresses.isEmpty ? state.subaddresses : snapshot.subaddresses, syncStatus: syncStatus, error: null, ); return snapshot; } Future _runWithLoading(Future Function() action) async { state = state.copyWith(isLoading: true, error: null); try { await SchedulerBinding.instance.endOfFrame; return await action(); } catch (error, stack) { _logger.e('Wallet action failed', error: error, stackTrace: stack); state = state.copyWith(error: error.toString()); rethrow; } finally { state = state.copyWith(isLoading: false); } } String _resolveWalletFilePath(String basePath, {required String name}) { if (basePath.isEmpty) { return basePath; } if (_isDirectoryPath(basePath)) { return p.join(basePath, name); } return basePath; } String _resolveOpenPath(String basePath) { if (!_isDirectoryPath(basePath)) { return basePath; } final dirName = p.basename(basePath); final candidate = p.join(basePath, dirName); if (File(candidate).existsSync() || File('$candidate.keys').existsSync()) { return candidate; } final dir = Directory(basePath); if (!dir.existsSync()) { return basePath; } final entries = dir .listSync() .whereType() .map((entry) => entry.path) .where((entryPath) => !entryPath.endsWith('.keys')) .toList() ..sort(); for (final entryPath in entries) { if (File('$entryPath.keys').existsSync()) { return entryPath; } } return basePath; } bool _isDirectoryPath(String path) { if (path.endsWith(p.separator)) { return true; } return Directory(path).existsSync(); } }