/* * 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:isolate'; import 'package:cw_core/wallet_info.dart' as xmr; import 'package:hive/hive.dart'; import 'package:mutex/mutex.dart'; import '../../app_config.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/node_model.dart'; import '../../models/notification_model.dart'; import '../../models/trade_wallet_lookup.dart'; import '../../services/wallets_service.dart'; import '../../utilities/logger.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; class DB { // legacy (required for migrations) @Deprecated("Left over for migration from old versions of Stack Wallet") static const String boxNameAddressBook = "addressBook"; static const String boxNameTrades = "exchangeTransactionsBox"; static const String boxNameAllWalletsData = "wallets"; static const String boxNameFavoriteWallets = "favoriteWallets"; // in use // TODO: migrate static const String boxNameNodeModels = "nodeModels"; static const String boxNamePrimaryNodes = "primaryNodes"; static const String boxNameNotifications = "notificationModels"; static const String boxNameWatchedTransactions = "watchedTxNotificationModels"; static const String boxNameWatchedTrades = "watchedTradesNotificationModels"; static const String boxNameTradesV2 = "exchangeTradesBox"; static const String boxNameTradeNotes = "tradeNotesBox"; static const String boxNameTradeLookup = "tradeToTxidLookUpBox"; static const String boxNameWalletsToDeleteOnStart = "walletsToDeleteOnStart"; static const String boxNamePriceCache = "priceAPIPrice24hCache"; // in use (keep for now) static const String boxNameDBInfo = "dbInfo"; static const String boxNamePrefs = "prefs"; static const String boxNameOneTimeDialogsShown = "oneTimeDialogsShown"; String _boxNameTxCache({required CryptoCurrency currency}) => "${currency.identifier}_txCache"; // firo only String _boxNameSetCache({required CryptoCurrency currency}) => "${currency.identifier}_anonymitySetCache"; String _boxNameSetSparkCache({required CryptoCurrency currency}) => "${currency.identifier}_anonymitySetSparkCache"; String _boxNameUsedSerialsCache({required CryptoCurrency currency}) => "${currency.identifier}_usedSerialsCache"; String _boxNameSparkUsedCoinsTagsCache({required CryptoCurrency currency}) => "${currency.identifier}_sparkUsedCoinsTagsCache"; Box? _boxNodeModels; Box? _boxPrimaryNodes; Box? _boxAllWalletsData; Box? _boxNotifications; Box? _boxWatchedTransactions; Box? _boxWatchedTrades; Box? _boxTradesV2; Box? _boxTradeNotes; Box? _boxFavoriteWallets; Box? _walletInfoSource; Box? _boxPrefs; Box? _boxTradeLookup; Box? _boxDBInfo; // Box? _boxDesktopData; final Map> _walletBoxes = {}; final Map> _txCacheBoxes = {}; final Map> _setCacheBoxes = {}; final Map> _setSparkCacheBoxes = {}; final Map> _usedSerialsCacheBoxes = {}; final Map> _getSparkUsedCoinsTagsCacheBoxes = {}; // exposed for monero Box get moneroWalletInfoBox => _walletInfoSource!; // mutex for stack backup final mutex = Mutex(); DB._(); static final DB _instance = DB._(); static DB get instance { // "This name does not uniquely identify an isolate. Multiple isolates in the same process may have the same debugName." // if (Isolate.current.debugName != "main") { // TODO: make sure this works properly if (Isolate.current.debugName != "main") { throw Exception( "DB.instance should not be accessed outside the main isolate!", ); } return _instance; } // open hive boxes Future init() async { if (Hive.isBoxOpen(boxNameDBInfo)) { _boxDBInfo = Hive.box(boxNameDBInfo); } else { _boxDBInfo = await Hive.openBox(boxNameDBInfo); } await Hive.openBox(boxNameWalletsToDeleteOnStart); if (Hive.isBoxOpen(boxNamePrefs)) { _boxPrefs = Hive.box(boxNamePrefs); } else { _boxPrefs = await Hive.openBox(boxNamePrefs); } if (Hive.isBoxOpen(boxNameNodeModels)) { _boxNodeModels = Hive.box(boxNameNodeModels); } else { _boxNodeModels = await Hive.openBox(boxNameNodeModels); } if (Hive.isBoxOpen(boxNamePrimaryNodes)) { _boxPrimaryNodes = Hive.box(boxNamePrimaryNodes); } else { _boxPrimaryNodes = await Hive.openBox(boxNamePrimaryNodes); } if (Hive.isBoxOpen(boxNameAllWalletsData)) { _boxAllWalletsData = Hive.box(boxNameAllWalletsData); } else { _boxAllWalletsData = await Hive.openBox(boxNameAllWalletsData); } _boxNotifications = await Hive.openBox(boxNameNotifications); _boxWatchedTransactions = await Hive.openBox(boxNameWatchedTransactions); _boxWatchedTrades = await Hive.openBox(boxNameWatchedTrades); _boxTradesV2 = await Hive.openBox(boxNameTradesV2); _boxTradeNotes = await Hive.openBox(boxNameTradeNotes); _boxTradeLookup = await Hive.openBox(boxNameTradeLookup); _walletInfoSource = await Hive.openBox(xmr.WalletInfo.boxName); _boxFavoriteWallets = await Hive.openBox(boxNameFavoriteWallets); await Future.wait([ Hive.openBox(boxNamePriceCache), _loadWalletBoxes(), ]); } Future _loadWalletBoxes() async { final names = _boxAllWalletsData!.get("names") as Map? ?? {}; names.removeWhere((name, dyn) { final jsonObject = Map.from(dyn as Map); try { AppConfig.getCryptoCurrencyFor(jsonObject["coin"] as String); return false; } catch (e, s) { Logging.instance.log( "Error, ${jsonObject["coin"]} does not exist, $name wallet cannot be loaded", level: LogLevel.Error, ); return true; } }); final mapped = Map.from(names).map( (name, dyn) => MapEntry( name, WalletInfo.fromJson(Map.from(dyn as Map)), ), ); for (final entry in mapped.entries) { if (Hive.isBoxOpen(entry.value.walletId)) { _walletBoxes[entry.value.walletId] = Hive.box(entry.value.walletId); } else { _walletBoxes[entry.value.walletId] = await Hive.openBox(entry.value.walletId); } } } Future> getTxCacheBox({required CryptoCurrency currency}) async { if (_txCacheBoxes[currency.identifier]?.isOpen != true) { _txCacheBoxes.remove(currency.identifier); } return _txCacheBoxes[currency.identifier] ??= await Hive.openBox(_boxNameTxCache(currency: currency)); } Future closeTxCacheBox({required CryptoCurrency currency}) async { await _txCacheBoxes[currency.identifier]?.close(); } Future> getAnonymitySetCacheBox({ required CryptoCurrency currency, }) async { if (_setCacheBoxes[currency.identifier]?.isOpen != true) { _setCacheBoxes.remove(currency.identifier); } return _setCacheBoxes[currency.identifier] ??= await Hive.openBox(_boxNameSetCache(currency: currency)); } Future> getSparkAnonymitySetCacheBox({ required CryptoCurrency currency, }) async { if (_setSparkCacheBoxes[currency.identifier]?.isOpen != true) { _setSparkCacheBoxes.remove(currency.identifier); } return _setSparkCacheBoxes[currency.identifier] ??= await Hive.openBox(_boxNameSetSparkCache(currency: currency)); } Future closeAnonymitySetCacheBox({ required CryptoCurrency currency, }) async { await _setCacheBoxes[currency.identifier]?.close(); } Future> getUsedSerialsCacheBox({ required CryptoCurrency currency, }) async { if (_usedSerialsCacheBoxes[currency.identifier]?.isOpen != true) { _usedSerialsCacheBoxes.remove(currency.identifier); } return _usedSerialsCacheBoxes[currency.identifier] ??= await Hive.openBox( _boxNameUsedSerialsCache(currency: currency), ); } Future> getSparkUsedCoinsTagsCacheBox({ required CryptoCurrency currency, }) async { if (_getSparkUsedCoinsTagsCacheBoxes[currency.identifier]?.isOpen != true) { _getSparkUsedCoinsTagsCacheBoxes.remove(currency.identifier); } return _getSparkUsedCoinsTagsCacheBoxes[currency.identifier] ??= await Hive.openBox( _boxNameSparkUsedCoinsTagsCache(currency: currency), ); } Future closeUsedSerialsCacheBox({ required CryptoCurrency currency, }) async { await _usedSerialsCacheBoxes[currency.identifier]?.close(); } /// Clear all cached transactions for the specified coin Future clearSharedTransactionCache({ required CryptoCurrency currency, }) async { await deleteAll(boxName: _boxNameTxCache(currency: currency)); if (currency is Firo) { await deleteAll(boxName: _boxNameSetCache(currency: currency)); await deleteAll( boxName: _boxNameSetSparkCache(currency: currency), ); await deleteAll( boxName: _boxNameUsedSerialsCache(currency: currency), ); await deleteAll( boxName: _boxNameSparkUsedCoinsTagsCache(currency: currency), ); } } ///////////////////////////////////////// Future addWalletBox({required String walletId}) async { if (_walletBoxes[walletId] != null) { throw Exception("Attempted overwrite of existing wallet box!"); } _walletBoxes[walletId] = await Hive.openBox(walletId); } Future removeWalletBox({required String walletId}) async { _walletBoxes.remove(walletId); } /////////////////////////////////////////// // reads List keys({required String boxName}) => Hive.box(boxName).keys.toList(growable: false); List values({required String boxName}) => Hive.box(boxName).values.toList(growable: false); T? get({ required String boxName, required dynamic key, }) => Hive.box(boxName).get(key); bool containsKey({required String boxName, required dynamic key}) => Hive.box(boxName).containsKey(key); // writes Future put({ required String boxName, required dynamic key, required T value, }) async => await mutex .protect(() async => await Hive.box(boxName).put(key, value)); Future add({required String boxName, required T value}) async => await mutex.protect(() async => await Hive.box(boxName).add(value)); Future addAll({ required String boxName, required Iterable values, }) async => await mutex .protect(() async => await Hive.box(boxName).addAll(values)); Future delete({ required dynamic key, required String boxName, }) async => await mutex.protect(() async => await Hive.box(boxName).delete(key)); Future deleteAll({required String boxName}) async { await mutex.protect(() async { final box = await Hive.openBox(boxName); await box.clear(); }); } Future deleteBoxFromDisk({required String boxName}) async => await mutex.protect(() async => await Hive.deleteBoxFromDisk(boxName)); /////////////////////////////////////////////////////////////////////////// Future deleteEverything() async { try { await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameAddressBook); await DB.instance.deleteBoxFromDisk(boxName: "debugInfoBox"); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameNodeModels); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePrimaryNodes); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameAllWalletsData); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameNotifications); await DB.instance .deleteBoxFromDisk(boxName: DB.boxNameWatchedTransactions); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameWatchedTrades); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTrades); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradesV2); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradeNotes); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradeLookup); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameFavoriteWallets); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePrefs); await DB.instance .deleteBoxFromDisk(boxName: DB.boxNameWalletsToDeleteOnStart); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePriceCache); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameDBInfo); await DB.instance.deleteBoxFromDisk(boxName: "theme"); return true; } catch (e, s) { Logging.instance.log("$e $s", level: LogLevel.Error); return false; } } } abstract class DBKeys { static const String cachedBalance = "cachedBalance"; static const String cachedBalanceSecondary = "cachedBalanceSecondary"; static const String isFavorite = "isFavorite"; static const String id = "id"; static const String storedChainHeight = "storedChainHeight"; }