stack_wallet/lib/services/wallets.dart

636 lines
20 KiB
Dart

/*
* 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 'dart:async';
import 'package:compat/compat.dart' as lib_monero_compat;
import 'package:isar/isar.dart';
import '../app_config.dart';
import '../db/hive/db.dart';
import '../db/isar/main_db.dart';
import '../utilities/enums/sync_type_enum.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../utilities/stack_file_system.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/isar/models/wallet_info.dart';
import '../wallets/wallet/impl/epiccash_wallet.dart';
import '../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../wallets/wallet/wallet.dart';
import 'event_bus/events/wallet_added_event.dart';
import 'event_bus/global_event_bus.dart';
import 'node_service.dart';
import 'notifications_service.dart';
import 'trade_sent_from_stack_service.dart';
class Wallets {
Wallets._private();
static final Wallets _sharedInstance = Wallets._private();
static Wallets get sharedInstance => _sharedInstance;
late NodeService nodeService;
late MainDB mainDB;
List<Wallet> get wallets => _wallets.values.toList();
static bool hasLoaded = false;
final Map<String, Wallet> _wallets = {};
Wallet getWallet(String walletId) {
if (_wallets[walletId] != null) {
return _wallets[walletId]!;
} else {
throw Exception("Wallet with id $walletId not found");
}
}
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;
GlobalEventBus.instance.fire(WalletAddedEvent());
}
Future<void> 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 is Wownero) {
await lib_monero_compat.deleteWalletFiles(
name: walletId,
type: lib_monero_compat.WalletType.wownero,
appRoot: await StackFileSystem.applicationRootDirectory(),
);
Logging.instance
.log("monero wallet: $walletId deleted", level: LogLevel.Info);
} else if (info.coin is Monero) {
await lib_monero_compat.deleteWalletFiles(
name: walletId,
type: lib_monero_compat.WalletType.monero,
appRoot: await StackFileSystem.applicationRootDirectory(),
);
Logging.instance
.log("monero wallet: $walletId deleted", level: LogLevel.Info);
} else if (info.coin is 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<String>(
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<void> load(Prefs prefs, MainDB mainDB) async {
// return await _loadV1(prefs, mainDB);
// return await _loadV2(prefs, mainDB);
return await _loadV3(prefs, mainDB);
}
Future<void> _loadV1(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<String>(boxName: DB.boxNameWalletsToDeleteOnStart)) {
await mainDB.isar.writeTxn(
() async => await mainDB.isar.walletInfo
.where()
.walletIdEqualTo(walletId)
.deleteAll(),
);
}
// clear list
await DB.instance
.deleteAll<String>(boxName: DB.boxNameWalletsToDeleteOnStart);
final walletInfoList = await mainDB.isar.walletInfo.where().findAll();
if (walletInfoList.isEmpty) {
return;
}
final List<Future<void>> walletInitFutures = [];
final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly =
[];
final List<String> 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 {
final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar);
Logging.instance.log(
"LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} "
"IS VERIFIED: $isVerified",
level: LogLevel.Info,
);
if (isVerified) {
// 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 (wallet is LibMoneroWallet) {
// 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);
}
}
/// should be fastest but big ui performance hit
Future<void> _loadV2(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<String>(boxName: DB.boxNameWalletsToDeleteOnStart)) {
await mainDB.isar.writeTxn(
() async => await mainDB.isar.walletInfo
.where()
.walletIdEqualTo(walletId)
.deleteAll(),
);
}
// clear list
await DB.instance
.deleteAll<String>(boxName: DB.boxNameWalletsToDeleteOnStart);
final walletInfoList = await mainDB.isar.walletInfo.where().findAll();
if (walletInfoList.isEmpty) {
return;
}
final List<Future<String>> walletIDInitFutures = [];
final List<Future<void>> deleteFutures = [];
final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly =
[];
final List<String> 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 {
final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar);
Logging.instance.log(
"LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} "
"IS VERIFIED: $isVerified",
level: LogLevel.Info,
);
if (isVerified) {
// TODO: integrate this into the new wallets somehow?
// requires some thinking
// final txTracker =
// TransactionNotificationTracker(walletId: walletInfo.walletId);
final walletIdCompleter = Completer<String>();
walletIDInitFutures.add(walletIdCompleter.future);
await Wallet.load(
walletId: walletInfo.walletId,
mainDB: mainDB,
secureStorageInterface: nodeService.secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
).then((wallet) {
if (wallet is LibMoneroWallet) {
// walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync));
walletIdCompleter.complete("dummy_ignore");
} else {
walletIdCompleter.complete(wallet.walletId);
}
_wallets[wallet.walletId] = wallet;
});
} else {
// wallet creation was not completed by user so we remove it completely
deleteFutures.add(_deleteWallet(walletInfo.walletId));
}
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Fatal);
continue;
}
}
final asyncWalletIds = await Future.wait(walletIDInitFutures);
asyncWalletIds.removeWhere((e) => e == "dummy_ignore");
final List<Future<void>> walletInitFutures = asyncWalletIds
.map(
(id) => _wallets[id]!.init().then(
(_) {
if (shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(id)) {
_wallets[id]!.shouldAutoSync = true;
}
},
),
)
.toList();
if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) {
unawaited(
Future.wait([
_initLinearly(walletsToInitLinearly),
...walletInitFutures,
]),
);
} else if (walletInitFutures.isNotEmpty) {
unawaited(Future.wait(walletInitFutures));
} else if (walletsToInitLinearly.isNotEmpty) {
unawaited(_initLinearly(walletsToInitLinearly));
}
// finally await any deletions that haven't completed yet
await Future.wait(deleteFutures);
}
/// should be best performance
Future<void> _loadV3(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<String>(boxName: DB.boxNameWalletsToDeleteOnStart)) {
await mainDB.isar.writeTxn(
() async => await mainDB.isar.walletInfo
.where()
.walletIdEqualTo(walletId)
.deleteAll(),
);
}
// clear list
await DB.instance
.deleteAll<String>(boxName: DB.boxNameWalletsToDeleteOnStart);
final walletInfoList = await mainDB.isar.walletInfo
.where()
.filter()
.anyOf<String, CryptoCurrency>(
AppConfig.coins.map((e) => e.identifier),
(q, element) => q.coinNameMatches(element),
)
.findAll();
if (walletInfoList.isEmpty) {
return;
}
final List<Future<String>> walletIDInitFutures = [];
final List<Future<void>> deleteFutures = [];
final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly =
[];
final List<String> walletIdsToSyncOnceOnStartup = [];
bool shouldSyncAllOnceOnStartup = 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:
walletIdsToSyncOnceOnStartup.addAll(prefs.walletIdsSyncOnStartup);
break;
case SyncingType.allWalletsOnStartup:
shouldSyncAllOnceOnStartup = true;
break;
}
for (final walletInfo in walletInfoList) {
try {
final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar);
Logging.instance.log(
"LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} "
"IS VERIFIED: $isVerified",
level: LogLevel.Info,
);
if (isVerified) {
// TODO: integrate this into the new wallets somehow?
// requires some thinking
// final txTracker =
// TransactionNotificationTracker(walletId: walletInfo.walletId);
final walletIdCompleter = Completer<String>();
walletIDInitFutures.add(walletIdCompleter.future);
await Wallet.load(
walletId: walletInfo.walletId,
mainDB: mainDB,
secureStorageInterface: nodeService.secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
).then((wallet) {
if (wallet is LibMoneroWallet) {
// walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync));
walletIdCompleter.complete("dummy_ignore");
} else {
walletIdCompleter.complete(wallet.walletId);
}
_wallets[wallet.walletId] = wallet;
});
} else {
// wallet creation was not completed by user so we remove it completely
deleteFutures.add(_deleteWallet(walletInfo.walletId));
}
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Fatal);
continue;
}
}
final asyncWalletIds = await Future.wait(walletIDInitFutures);
asyncWalletIds.removeWhere((e) => e == "dummy_ignore");
final List<String> idsToRefresh = [];
final List<Future<void>> walletInitFutures = asyncWalletIds
.map(
(id) => _wallets[id]!.init().then(
(_) {
if (shouldSyncAllOnceOnStartup ||
walletIdsToSyncOnceOnStartup.contains(id)) {
idsToRefresh.add(id);
}
},
),
)
.toList();
Future<void> _refreshFutures(List<String> idsToRefresh) async {
final start = DateTime.now();
Logging.instance.log(
"Initial refresh start: ${start.toUtc()}",
level: LogLevel.Warning,
);
const groupCount = 3;
for (int i = 0; i < idsToRefresh.length; i += groupCount) {
final List<Future<void>> futures = [];
for (int j = 0; j < groupCount; j++) {
if (i + j >= idsToRefresh.length) {
break;
}
futures.add(
_wallets[idsToRefresh[i + j]]!.refresh(),
);
}
await Future.wait(futures);
}
Logging.instance.log(
"Initial refresh duration: ${DateTime.now().difference(start)}",
level: LogLevel.Warning,
);
}
if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) {
unawaited(
Future.wait([
_initLinearly(walletsToInitLinearly),
...walletInitFutures,
]).then(
(value) => _refreshFutures(idsToRefresh),
),
);
} else if (walletInitFutures.isNotEmpty) {
unawaited(
Future.wait(walletInitFutures).then(
(value) => _refreshFutures(idsToRefresh),
),
);
} else if (walletsToInitLinearly.isNotEmpty) {
unawaited(_initLinearly(walletsToInitLinearly));
}
// finally await any deletions that haven't completed yet
await Future.wait(deleteFutures);
}
Future<void> loadAfterStackRestore(
Prefs prefs,
List<Wallet> wallets,
bool isDesktop,
) async {
final List<Future<void>> walletInitFutures = [];
final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly =
[];
final List<String> 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) {
final isVerified = await wallet.info.isMnemonicVerified(mainDB.isar);
Logging.instance.log(
"LOADING WALLET: ${wallet.info.name}:${wallet.walletId} IS VERIFIED: $isVerified",
level: LogLevel.Info,
);
if (isVerified) {
final shouldSetAutoSync = shouldAutoSyncAll ||
walletIdsToEnableAutoSync.contains(wallet.walletId);
if (isDesktop) {
if (wallet is LibMoneroWallet) {
// 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(wallet.walletId);
// await walletsService.deleteWallet(walletInfo.name, false);
}
}
if (isDesktop) {
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<void> _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<void> _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),
);
}
}