/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; class Wallets { Wallets._private(); static final Wallets _sharedInstance = Wallets._private(); static Wallets get sharedInstance => _sharedInstance; late NodeService nodeService; late MainDB mainDB; bool get hasWallets => _wallets.isNotEmpty; List get wallets => _wallets.values.toList(); static bool hasLoaded = false; final Map _wallets = {}; Wallet getWallet(String walletId) => _wallets[walletId]!; void addWallet(Wallet wallet) { if (_wallets[wallet.walletId] != null) { throw Exception( "Tried to add wallet that already exists, according to it's wallet Id!", ); } _wallets[wallet.walletId] = wallet; } Future deleteWallet( WalletInfo info, SecureStorageInterface secureStorage, ) async { final walletId = info.walletId; Logging.instance.log( "deleteWallet called with walletId=$walletId", level: LogLevel.Warning, ); final wallet = _wallets[walletId]; _wallets.remove(walletId); await wallet?.exit(); await secureStorage.delete(key: Wallet.mnemonicKey(walletId: walletId)); await secureStorage.delete( key: Wallet.mnemonicPassphraseKey(walletId: walletId)); await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId)); if (info.coin == Coin.wownero) { final wowService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); await wowService.remove(walletId); Logging.instance .log("monero wallet: $walletId deleted", level: LogLevel.Info); } else if (info.coin == Coin.monero) { final xmrService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); await xmrService.remove(walletId); Logging.instance .log("monero wallet: $walletId deleted", level: LogLevel.Info); } else if (info.coin == Coin.epicCash) { final deleteResult = await deleteEpicWallet( walletId: walletId, secureStore: secureStorage); Logging.instance.log( "epic wallet: $walletId deleted with result: $deleteResult", level: LogLevel.Info); } // delete wallet data in main db await MainDB.instance.deleteWalletBlockchainData(walletId); await MainDB.instance.deleteAddressLabels(walletId); await MainDB.instance.deleteTransactionNotes(walletId); // box data may currently still be read/written to if wallet was refreshing // when delete was requested so instead of deleting now we mark the wallet // as needs delete by adding it's id to a list which gets checked on app start await DB.instance.add( boxName: DB.boxNameWalletsToDeleteOnStart, value: walletId); final lookupService = TradeSentFromStackService(); for (final lookup in lookupService.all) { if (lookup.walletIds.contains(walletId)) { // update lookup data to reflect deleted wallet await lookupService.save( tradeWalletLookup: lookup.copyWith( walletIds: lookup.walletIds.where((id) => id != walletId).toList(), ), ); } } // delete notifications tied to deleted wallet for (final notification in NotificationsService.instance.notifications) { if (notification.walletId == walletId) { await NotificationsService.instance.delete(notification, false); } } await mainDB.isar.writeTxn(() async { await mainDB.isar.walletInfo.deleteByWalletId(walletId); }); } Future load(Prefs prefs, MainDB mainDB) async { 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 mainDB.isar.writeTxn(() async => await mainDB.isar.walletInfo .where() .walletIdEqualTo(walletId) .deleteAll()); } // clear list await DB.instance .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); if (walletInfoList.isEmpty) { return; } List> walletInitFutures = []; List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = []; 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 walletInfo in walletInfoList) { try { Logging.instance.log( "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " "IS VERIFIED: ${walletInfo.isMnemonicVerified}", level: LogLevel.Info, ); if (walletInfo.isMnemonicVerified) { // TODO: integrate this into the new wallets somehow? // requires some thinking final txTracker = TransactionNotificationTracker(walletId: walletInfo.walletId); final wallet = await Wallet.load( walletId: walletInfo.walletId, mainDB: mainDB, secureStorageInterface: nodeService.secureStorageInterface, nodeService: nodeService, prefs: prefs, ); final shouldSetAutoSync = shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(walletInfo.walletId); if (walletInfo.coin == Coin.monero || walletInfo.coin == Coin.wownero) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(wallet.init().then((_) { if (shouldSetAutoSync) { wallet.shouldAutoSync = true; } })); } _wallets[wallet.walletId] = wallet; } else { // wallet creation was not completed by user so we remove it completely await _deleteWallet(walletInfo.walletId); // await walletsService.deleteWallet(walletInfo.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, ]); } else if (walletInitFutures.isNotEmpty) { await Future.wait(walletInitFutures); } else if (walletsToInitLinearly.isNotEmpty) { await _initLinearly(walletsToInitLinearly); } } Future loadAfterStackRestore( Prefs prefs, List wallets, ) async { List> walletInitFutures = []; List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = []; 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 wallet in wallets) { Logging.instance.log( "LOADING WALLET: ${wallet.info.name}:${wallet.walletId} IS VERIFIED: ${wallet.info.isMnemonicVerified}", level: LogLevel.Info, ); if (wallet.info.isMnemonicVerified) { final shouldSetAutoSync = shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(wallet.walletId); if (wallet.info.coin == Coin.monero || wallet.info.coin == Coin.wownero) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(wallet.init().then((value) { if (shouldSetAutoSync) { wallet.shouldAutoSync = true; } })); } _wallets[wallet.walletId] = wallet; } else { // wallet creation was not completed by user so we remove it completely await _deleteWallet(wallet.walletId); // await walletsService.deleteWallet(walletInfo.name, false); } } if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { await Future.wait([ _initLinearly(walletsToInitLinearly), ...walletInitFutures, ]); } else if (walletInitFutures.isNotEmpty) { await Future.wait(walletInitFutures); } else if (walletsToInitLinearly.isNotEmpty) { await _initLinearly(walletsToInitLinearly); } } Future _initLinearly( List<({Wallet wallet, bool shouldAutoSync})> dataList, ) async { for (final data in dataList) { await data.wallet.init(); if (data.shouldAutoSync && !data.wallet.shouldAutoSync) { data.wallet.shouldAutoSync = true; } } } Future _deleteWallet(String walletId) async { // TODO proper clean up of other wallet data in addition to the following await mainDB.isar.writeTxn( () async => await mainDB.isar.walletInfo.deleteByWalletId(walletId)); } }