import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:uuid/uuid.dart'; class WalletInfo { final Coin coin; final String walletId; final String name; const WalletInfo( {required this.coin, required this.walletId, required this.name}); factory WalletInfo.fromJson(Map jsonObject) { return WalletInfo( coin: Coin.values.byName(jsonObject["coin"] as String), walletId: jsonObject["id"] as String, name: jsonObject["name"] as String, ); } Map toMap() { return { "name": name, "id": walletId, "coin": coin.name, }; } String toJsonString() { return jsonEncode(toMap()); } @override String toString() { return "WalletInfo: ${toJsonString()}"; } } class WalletsService extends ChangeNotifier { late final FlutterSecureStorageInterface _secureStore; Future>? _walletNames; Future> get walletNames => _walletNames ??= _fetchWalletNames(); WalletsService({ FlutterSecureStorageInterface secureStorageInterface = const SecureStorageWrapper( FlutterSecureStorage(), ), }) { _secureStore = secureStorageInterface; } // Future getWalletCryptoCurrency({required String walletName}) async { // final id = await getWalletId(walletName); // final currency = DB.instance.get( // boxName: DB.boxNameAllWalletsData, key: "${id}_cryptoCurrency"); // return Coin.values.byName(currency as String); // } Future renameWallet({ required String from, required String to, required bool shouldNotifyListeners, }) async { if (from == to) { return true; } final walletInfo = DB.instance .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; final info = walletInfo.values.firstWhere( (element) => element['name'] == from, orElse: () => {}) as Map; if (info.isEmpty) { // tried to rename a non existing wallet Logging.instance .log("Tried to rename a non existing wallet!", level: LogLevel.Error); return false; } if (from != to && (walletInfo.values.firstWhere((element) => element['name'] == to, orElse: () => {}) as Map) .isNotEmpty) { // name already exists Logging.instance.log("wallet with name \"$to\" already exists!", level: LogLevel.Error); return false; } info["name"] = to; walletInfo[info['id']] = info; await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: 'names', value: walletInfo); await refreshWallets(shouldNotifyListeners); return true; } Future> _fetchWalletNames() async { final names = DB.instance .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; if (names == null) { Logging.instance.log( "Fetched wallet 'names' returned null. Setting initializing 'names'", level: LogLevel.Info); await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: 'names', value: {}); return {}; } Logging.instance.log("Fetched wallet names: $names", level: LogLevel.Info); final mapped = Map.from(names); mapped.removeWhere((name, dyn) { final jsonObject = Map.from(dyn as Map); try { Coin.values.byName(jsonObject["coin"] as String); return false; } catch (e, s) { Logging.instance.log("Error, ${jsonObject["coin"]} does not exist", level: LogLevel.Error); return true; } }); return mapped.map((name, dyn) => MapEntry( name, WalletInfo.fromJson(Map.from(dyn as Map)))); } Future addExistingStackWallet({ required String name, required String walletId, required Coin coin, required bool shouldNotifyListeners, }) async { final _names = DB.instance .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; Map names; if (_names == null) { names = {}; } else { names = Map.from(_names); } if (names.keys.contains(walletId)) { throw Exception("Wallet with walletId \"$walletId\" already exists!"); } if (names.values.where((element) => element['name'] == name).isNotEmpty) { throw Exception("Wallet with name \"$name\" already exists!"); } names[walletId] = { "id": walletId, "coin": coin.name, "name": name, }; await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: 'names', value: names); await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: "${walletId}_cryptoCurrency", value: coin.name); await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: "${walletId}_mnemonicHasBeenVerified", value: false); await DB.instance.addWalletBox(walletId: walletId); await refreshWallets(shouldNotifyListeners); } /// returns the new walletId if successful, otherwise null Future addNewWallet({ required String name, required Coin coin, required bool shouldNotifyListeners, }) async { final _names = DB.instance .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; Map names; if (_names == null) { names = {}; } else { names = Map.from(_names); } // Prevent overwriting or storing empty names if (name.isEmpty || names.values.where((element) => element['name'] == name).isNotEmpty) { return null; } final id = const Uuid().v1(); names[id] = { "id": id, "coin": coin.name, "name": name, }; await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: 'names', value: names); await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: "${id}_cryptoCurrency", value: coin.name); await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: "${id}_mnemonicHasBeenVerified", value: false); await DB.instance.addWalletBox(walletId: id); await refreshWallets(shouldNotifyListeners); return id; } Future> getFavoriteWalletIds() async { return DB.instance .values(boxName: DB.boxNameFavoriteWallets) .toList(); } Future saveFavoriteWalletIds(List walletIds) async { await DB.instance.deleteAll(boxName: DB.boxNameFavoriteWallets); await DB.instance .addAll(boxName: DB.boxNameFavoriteWallets, values: walletIds); debugPrint("saveFavoriteWalletIds list: $walletIds"); } Future addFavorite(String walletId) async { final list = await getFavoriteWalletIds(); if (!list.contains(walletId)) { list.add(walletId); } await saveFavoriteWalletIds(list); } Future removeFavorite(String walletId) async { final list = await getFavoriteWalletIds(); list.remove(walletId); await saveFavoriteWalletIds(list); } Future moveFavorite({ required int fromIndex, required int toIndex, }) async { final list = await getFavoriteWalletIds(); if (fromIndex < toIndex) { toIndex -= 1; } final walletId = list.removeAt(fromIndex); list.insert(toIndex, walletId); await saveFavoriteWalletIds(list); } Future checkForDuplicate(String name) async { final names = DB.instance .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; if (names == null) return false; return names.values.where((element) => element['name'] == name).isNotEmpty; } Future getWalletId(String walletName) async { final names = DB.instance .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; final shells = names.values.where((element) => element['name'] == walletName); if (shells.isEmpty) { return null; } return shells.first["id"] as String; } Future isMnemonicVerified({required String walletId}) async { final isVerified = DB.instance.get( boxName: DB.boxNameAllWalletsData, key: "${walletId}_mnemonicHasBeenVerified") as bool?; if (isVerified == null) { Logging.instance.log( "isMnemonicVerified(walletId: $walletId) returned null which should never happen!", level: LogLevel.Error, ); throw Exception( "isMnemonicVerified(walletId: $walletId) returned null which should never happen!"); } else { return isVerified; } } Future setMnemonicVerified({required String walletId}) async { final isVerified = DB.instance.get( boxName: DB.boxNameAllWalletsData, key: "${walletId}_mnemonicHasBeenVerified") as bool?; if (isVerified == null) { Logging.instance.log( "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!", level: LogLevel.Error, ); throw Exception( "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!"); } else if (isVerified) { Logging.instance.log( "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!", level: LogLevel.Error, ); throw Exception( "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!"); } else { await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: "${walletId}_mnemonicHasBeenVerified", value: true); Logging.instance.log( "setMnemonicVerified(walletId: $walletId) successful", level: LogLevel.Error, ); } } // pin + mnemonic as well as anything else in secureStore Future deleteWallet(String name, bool shouldNotifyListeners) async { final names = DB.instance.get( boxName: DB.boxNameAllWalletsData, key: 'names') as Map? ?? {}; final walletId = await getWalletId(name); if (walletId == null) { return 3; } Logging.instance.log( "deleteWallet called with name=$name and id=$walletId", level: LogLevel.Warning, ); final shell = names.remove(walletId); if (shell == null) { return 0; } // TODO delete derivations!!! await _secureStore.delete(key: "${walletId}_pin"); await _secureStore.delete(key: "${walletId}_mnemonic"); await DB.instance.delete( boxName: DB.boxNameAllWalletsData, key: "${walletId}_cryptoCurrency"); await DB.instance.delete( boxName: DB.boxNameAllWalletsData, key: "${walletId}_mnemonicHasBeenVerified"); if (coinFromPrettyName(shell['coin'] as String) == 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 (coinFromPrettyName(shell['coin'] as String) == Coin.epicCash) { final deleteResult = await deleteEpicWallet(walletId: walletId, secureStore: _secureStore); Logging.instance.log( "epic wallet: $walletId deleted with result: $deleteResult", level: LogLevel.Info); } // 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); } } if (names.isEmpty) { await DB.instance.deleteAll(boxName: DB.boxNameAllWalletsData); _walletNames = Future(() => {}); notifyListeners(); return 2; // error code no wallets on device } await DB.instance.put( boxName: DB.boxNameAllWalletsData, key: 'names', value: names); await refreshWallets(shouldNotifyListeners); return 0; } Future refreshWallets(bool shouldNotifyListeners) async { final newNames = await _fetchWalletNames(); _walletNames = Future(() => newNames); if (shouldNotifyListeners) notifyListeners(); } }