import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/listenable_list.dart'; import 'package:stackwallet/utilities/listenable_map.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:tuple/tuple.dart'; final ListenableList> _nonFavorites = ListenableList(); ListenableList> get nonFavorites => _nonFavorites; final ListenableList> _favorites = ListenableList(); ListenableList> get favorites => _favorites; class Wallets extends ChangeNotifier { Wallets._private(); @override dispose() { //todo: check if print needed // debugPrint("Wallets dispose was called!!"); super.dispose(); } static final Wallets _sharedInstance = Wallets._private(); static Wallets get sharedInstance => _sharedInstance; late WalletsService walletsService; late NodeService nodeService; // mirrored maps for access to reading managers without using riverpod ref static final ListenableMap> _managerProviderMap = ListenableMap(); static final ListenableMap _managerMap = ListenableMap(); bool get hasWallets => _managerProviderMap.isNotEmpty; List> get managerProviders => _managerProviderMap.values.toList(growable: false); List get managers => _managerMap.values.toList(growable: false); List getWalletIdsFor({required Coin coin}) { final List result = []; for (final manager in _managerMap.values) { if (manager.coin == coin) { result.add(manager.walletId); } } return result; } List>>> getManagerProvidersByCoin() { Map>> map = {}; for (final manager in _managerMap.values) { if (map[manager.coin] == null) { map[manager.coin] = []; } map[manager.coin]!.add(_managerProviderMap[manager.walletId] as ChangeNotifierProvider); } final List>>> result = []; for (final coin in map.keys) { result.add(Tuple2(coin, map[coin]!)); } result.sort((a, b) => a.item1.prettyName.compareTo(b.item1.prettyName)); return result; } List> getManagerProvidersForCoin(Coin coin) { List> result = []; for (final manager in _managerMap.values) { if (manager.coin == coin) { result.add(_managerProviderMap[manager.walletId] as ChangeNotifierProvider); } } return result; } ChangeNotifierProvider getManagerProvider(String walletId) { return _managerProviderMap[walletId] as ChangeNotifierProvider; } Manager getManager(String walletId) { return _managerMap[walletId] as Manager; } void addWallet({required String walletId, required Manager manager}) { _managerMap.add(walletId, manager, true); _managerProviderMap.add( walletId, ChangeNotifierProvider((_) => manager), true); if (manager.isFavorite) { _favorites.add( _managerProviderMap[walletId] as ChangeNotifierProvider, false); } else { _nonFavorites.add( _managerProviderMap[walletId] as ChangeNotifierProvider, false); } notifyListeners(); } void removeWallet({required String walletId}) { if (_managerProviderMap[walletId] == null) { Logging.instance.log( "Wallets.removeWallet($walletId) failed. ManagerProvider with $walletId not found!", level: LogLevel.Warning); return; } final provider = _managerProviderMap[walletId]!; // in both non and favorites for removal _favorites.remove(provider, true); _nonFavorites.remove(provider, true); _managerProviderMap.remove(walletId, true); _managerMap.remove(walletId, true)!.exitCurrentWallet(); notifyListeners(); } static bool hasLoaded = false; Future _initLinearly( List> tuples, ) async { for (final tuple in tuples) { await tuple.item1.initializeExisting(); if (tuple.item2 && !tuple.item1.shouldAutoSync) { tuple.item1.shouldAutoSync = true; } } } static int _count = 0; Future load(Prefs prefs) async { //todo: check if print needed // debugPrint("++++++++++++++ Wallets().load() called: ${++_count} times"); if (hasLoaded) { return; } hasLoaded = true; // clear out any wallet hive boxes where the wallet was deleted in previous app run for (final walletId in DB.instance .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { await DB.instance.deleteBoxFromDisk(boxName: walletId); } // clear list await DB.instance .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); final map = await walletsService.walletNames; List> walletInitFutures = []; List> walletsToInitLinearly = []; final favIdList = await walletsService.getFavoriteWalletIds(); List walletIdsToEnableAutoSync = []; bool shouldAutoSyncAll = false; switch (prefs.syncType) { case SyncingType.currentWalletOnly: // do nothing as this will be set when going into a wallet from the main screen break; case SyncingType.selectedWalletsAtStartup: walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup); break; case SyncingType.allWalletsOnStartup: shouldAutoSyncAll = true; break; } for (final entry in map.entries) { try { final walletId = entry.value.walletId; late final bool isVerified; try { isVerified = await walletsService.isMnemonicVerified(walletId: walletId); } catch (e, s) { Logging.instance.log("$e $s", level: LogLevel.Warning); isVerified = false; } Logging.instance.log( "LOADING WALLET: ${entry.value.toString()} IS VERIFIED: $isVerified", level: LogLevel.Info); if (isVerified) { if (_managerMap[walletId] == null && _managerProviderMap[walletId] == null) { final coin = entry.value.coin; NodeModel node = nodeService.getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); // ElectrumXNode? node = await nodeService.getCurrentNode(coin: coin); // folowing shouldn't be needed as the defaults get saved on init // if (node == null) { // node = DefaultNodes.getNodeFor(coin); // // // save default node // nodeService.add(node, false); // } final txTracker = TransactionNotificationTracker(walletId: walletId); final failovers = nodeService.failoverNodesFor(coin: coin); // load wallet final wallet = CoinServiceAPI.from( coin, walletId, entry.value.name, nodeService.secureStorageInterface, node, txTracker, prefs, failovers, ); final manager = Manager(wallet); final shouldSetAutoSync = shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(manager.walletId); if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(manager.initializeExisting().then((value) { if (shouldSetAutoSync) { manager.shouldAutoSync = true; } })); } _managerMap.add(walletId, manager, false); final managerProvider = ChangeNotifierProvider((_) => manager); _managerProviderMap.add(walletId, managerProvider, false); final favIndex = favIdList.indexOf(walletId); if (favIndex == -1) { _nonFavorites.add(managerProvider, true); } else { // it is a favorite if (favIndex >= _favorites.length) { _favorites.add(managerProvider, true); } else { _favorites.insert(favIndex, managerProvider, true); } } } } else { // wallet creation was not completed by user so we remove it completely await walletsService.deleteWallet(entry.value.name, false); } } catch (e, s) { Logging.instance.log("$e $s", level: LogLevel.Fatal); continue; } } if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { await Future.wait([ _initLinearly(walletsToInitLinearly), ...walletInitFutures, ]); notifyListeners(); } else if (walletInitFutures.isNotEmpty) { await Future.wait(walletInitFutures); notifyListeners(); } else if (walletsToInitLinearly.isNotEmpty) { await _initLinearly(walletsToInitLinearly); notifyListeners(); } } Future loadAfterStackRestore( Prefs prefs, List managers) async { List> walletInitFutures = []; List> walletsToInitLinearly = []; final favIdList = await walletsService.getFavoriteWalletIds(); List walletIdsToEnableAutoSync = []; bool shouldAutoSyncAll = false; switch (prefs.syncType) { case SyncingType.currentWalletOnly: // do nothing as this will be set when going into a wallet from the main screen break; case SyncingType.selectedWalletsAtStartup: walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup); break; case SyncingType.allWalletsOnStartup: shouldAutoSyncAll = true; break; } for (final manager in managers) { final walletId = manager.walletId; final isVerified = await walletsService.isMnemonicVerified(walletId: walletId); //todo: check if print needed // debugPrint( // "LOADING RESTORED WALLET: ${manager.walletName} ${manager.walletId} IS VERIFIED: $isVerified"); if (isVerified) { if (_managerMap[walletId] == null && _managerProviderMap[walletId] == null) { final shouldSetAutoSync = shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(manager.walletId); if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(manager.initializeExisting().then((value) { if (shouldSetAutoSync) { manager.shouldAutoSync = true; } })); } _managerMap.add(walletId, manager, false); final managerProvider = ChangeNotifierProvider((_) => manager); _managerProviderMap.add(walletId, managerProvider, false); final favIndex = favIdList.indexOf(walletId); if (favIndex == -1) { _nonFavorites.add(managerProvider, true); } else { // it is a favorite if (favIndex >= _favorites.length) { _favorites.add(managerProvider, true); } else { _favorites.insert(favIndex, managerProvider, true); } } } } else { // wallet creation was not completed by user so we remove it completely await walletsService.deleteWallet(manager.walletName, false); } } if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { await Future.wait([ _initLinearly(walletsToInitLinearly), ...walletInitFutures, ]); notifyListeners(); } else if (walletInitFutures.isNotEmpty) { await Future.wait(walletInitFutures); notifyListeners(); } else if (walletsToInitLinearly.isNotEmpty) { await _initLinearly(walletsToInitLinearly); notifyListeners(); } } }