diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index a6e219ff1..e3b030fa3 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -22,9 +22,11 @@ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:stackwallet/widgets/coin_card.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -112,8 +114,17 @@ class _FavoriteCardState extends ConsumerState { ), child: GestureDetector( onTap: () async { - if (coin == Coin.monero || coin == Coin.wownero) { - await ref.read(pWallets).getWallet(walletId).init(); + final wallet = ref.read(pWallets).getWallet(walletId); + await wallet.init(); + if (wallet is CwBasedInterface) { + if (mounted) { + await showLoading( + whileFuture: wallet.open(), + context: context, + message: 'Opening ${wallet.info.name}', + isDesktop: Util.isDesktop, + ); + } } if (mounted) { if (Util.isDesktop) { diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 449e62d26..247694477 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -22,7 +22,10 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletListItem extends ConsumerWidget { @@ -60,8 +63,16 @@ class WalletListItem extends ConsumerWidget { .read(pWallets) .wallets .firstWhere((e) => e.info.coin == coin); - if (coin == Coin.monero || coin == Coin.wownero) { - await wallet.init(); + await wallet.init(); + if (wallet is CwBasedInterface) { + if (context.mounted) { + await showLoading( + whileFuture: wallet.open(), + context: context, + message: 'Opening ${wallet.info.name}', + isDesktop: Util.isDesktop, + ); + } } if (context.mounted) { unawaited( diff --git a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart index 43e1774b4..89a9db785 100644 --- a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart @@ -8,8 +8,6 @@ * */ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; @@ -18,6 +16,9 @@ import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; @@ -79,16 +80,24 @@ class CoinWalletsTable extends ConsumerWidget { final wallet = ref.read(pWallets).getWallet(walletIds[i]); - if (wallet.info.coin == Coin.monero || - wallet.info.coin == Coin.wownero) { - // TODO: this can cause ui lag if awaited - unawaited(wallet.init()); + await wallet.init(); + if (wallet is CwBasedInterface) { + if (context.mounted) { + await showLoading( + whileFuture: wallet.open(), + context: context, + message: 'Opening ${wallet.info.name}', + isDesktop: Util.isDesktop, + ); + } } - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: walletIds[i], - ); + if (context.mounted) { + await Navigator.of(context).pushNamed( + DesktopWalletView.routeName, + arguments: walletIds[i], + ); + } }, ), ), diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index fa1125068..deba5a4b9 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; @@ -10,174 +8,131 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:decimal/decimal.dart'; -import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/monero/monero.dart' as xmr_dart; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; import 'package:isar/isar.dart'; -import 'package:mutex/mutex.dart'; import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:tuple/tuple.dart'; -class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { +class MoneroWallet extends CryptonoteWallet with CwBasedInterface { MoneroWallet(CryptoCurrencyNetwork network) : super(Monero(network)); @override - FilterOperation? get changeAddressFilterOperation => null; + Address addressFor({required int index, int account = 0}) { + String address = (cwWalletBase as MoneroWalletBase) + .getTransactionAddress(account, index); + + final newReceivingAddress = Address( + walletId: walletId, + derivationIndex: index, + derivationPath: null, + value: address, + publicKey: [], + type: AddressType.cryptonote, + subType: AddressSubType.receiving, + ); + + return newReceivingAddress; + } @override - FilterOperation? get receivingAddressFilterOperation => null; + Future exitCwWallet() async { + (cwWalletBase as MoneroWalletBase?)?.onNewBlock = null; + (cwWalletBase as MoneroWalletBase?)?.onNewTransaction = null; + (cwWalletBase as MoneroWalletBase?)?.syncStatusChanged = null; + await (cwWalletBase as MoneroWalletBase?)?.save(prioritySave: true); + } - final prepareSendMutex = Mutex(); - final estimateFeeMutex = Mutex(); + @override + Future open() async { + String? password; + try { + password = await cwKeysStorage.getWalletPassword(walletName: walletId); + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } - bool _hasCalledExit = false; + cwWalletBase?.close(); + cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) + as MoneroWalletBase; - WalletService? cwWalletService; - KeyService? cwKeysStorage; - MoneroWalletBase? cwWalletBase; - WalletCreationService? cwWalletCreationService; - Timer? _autoSaveTimer; + (cwWalletBase as MoneroWalletBase?)?.onNewBlock = onNewBlock; + (cwWalletBase as MoneroWalletBase?)?.onNewTransaction = onNewTransaction; + (cwWalletBase as MoneroWalletBase?)?.syncStatusChanged = syncStatusChanged; - bool _txRefreshLock = false; - int _lastCheckedHeight = -1; - int _txCount = 0; - int _currentKnownChainHeight = 0; - double _highestPercentCached = 0; + await updateNode(); + + await cwWalletBase?.startSync(); + unawaited(refresh()); + autoSaveTimer?.cancel(); + autoSaveTimer = Timer.periodic( + const Duration(seconds: 193), + (_) async => await cwWalletBase?.save(), + ); + } @override Future estimateFeeFor(Amount amount, int feeRate) async { + if (cwWalletBase == null || cwWalletBase?.syncStatus is! SyncedSyncStatus) { + return Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + MoneroTransactionPriority priority; - FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; - feeRateType = FeeRateType.average; break; case 2: priority = MoneroTransactionPriority.medium; - feeRateType = FeeRateType.average; break; case 3: priority = MoneroTransactionPriority.fast; - feeRateType = FeeRateType.fast; break; case 4: priority = MoneroTransactionPriority.fastest; - feeRateType = FeeRateType.fast; break; case 0: default: priority = MoneroTransactionPriority.slow; - feeRateType = FeeRateType.slow; break; } - dynamic approximateFee; + int approximateFee = 0; await estimateFeeMutex.protect(() async { - { - try { - final data = await prepareSend( - txData: TxData( - recipients: [ - // This address is only used for getting an approximate fee, never for sending - ( - address: - "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", - amount: amount, - isChange: false, - ), - ], - feeRateType: feeRateType, - ), - ); - approximateFee = data.fee!; - - // unsure why this delay? - await Future.delayed(const Duration(milliseconds: 500)); - } catch (e) { - approximateFee = cwWalletBase!.calculateEstimatedFee( - priority, - amount.raw.toInt(), - ); - } - } + approximateFee = cwWalletBase!.calculateEstimatedFee( + priority, + amount.raw.toInt(), + ); }); - if (approximateFee is Amount) { - return approximateFee as Amount; - } else { - return Amount( - rawValue: BigInt.from(approximateFee as int), - fractionDigits: cryptoCurrency.fractionDigits, - ); - } + return Amount( + rawValue: BigInt.from(approximateFee), + fractionDigits: cryptoCurrency.fractionDigits, + ); } - @override - Future get fees async => FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: MoneroTransactionPriority.fast.raw!, - medium: MoneroTransactionPriority.regular.raw!, - slow: MoneroTransactionPriority.slow.raw!, - ); - @override Future pingCheck() async { - return await cwWalletBase?.isConnected() ?? false; - } - - @override - Future updateBalance() async { - final total = await _totalBalance; - final available = await _availableBalance; - - final balance = Balance( - total: total, - spendable: available, - blockedTotal: Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - pendingSpendable: total - available, - ); - - await info.updateBalance(newBalance: balance, isar: mainDB.isar); - } - - @override - Future updateChainHeight() async { - await info.updateCachedChainHeight( - newHeight: _currentKnownChainHeight, - isar: mainDB.isar, - ); + return await (cwWalletBase as MoneroWalletBase?)?.isConnected() ?? false; } @override @@ -192,19 +147,13 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { trusted: node.trusted ?? false, ), ); - - // TODO: is this sync call needed? Do we need to notify ui here? - // await cwWalletBase?.startSync(); - - // if (shouldRefresh) { - // await refresh(); - // } } @override Future updateTransactions() async { - await cwWalletBase!.updateTransactions(); - final transactions = cwWalletBase?.transactionHistory!.transactions; + await (cwWalletBase as MoneroWalletBase?)?.updateTransactions(); + final transactions = + (cwWalletBase as MoneroWalletBase?)?.transactionHistory?.transactions; // final cachedTransactions = // DB.instance.get(boxName: walletId, key: 'latest_tx_model') @@ -241,13 +190,14 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { final List> txnsData = []; if (transactions != null) { - for (var tx in transactions.entries) { + for (final tx in transactions.entries) { Address? address; TransactionType type; if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; - final addressString = cwWalletBase?.getTransactionAddress( + final addressString = + (cwWalletBase as MoneroWalletBase?)?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); @@ -300,27 +250,15 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { Future init() async { cwWalletService = xmr_dart.monero .createMoneroWalletService(DB.instance.moneroWalletInfoBox); - cwKeysStorage = KeyService(secureStorageInterface); - if (await cwWalletService!.isWalletExit(walletId)) { - String? password; - try { - password = await cwKeysStorage!.getWalletPassword(walletName: walletId); - } catch (e, s) { - throw Exception("Password not found $e, $s"); - } - cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) - as MoneroWalletBase; - - unawaited(_start()); - } else { + if (!(await cwWalletService!.isWalletExit(walletId))) { WalletInfo walletInfo; WalletCredentials credentials; try { String name = walletId; final dirPath = - await _pathForWalletDir(name: name, type: WalletType.monero); - final path = await _pathForWallet(name: name, type: WalletType.monero); + await pathForWalletDir(name: name, type: WalletType.monero); + final path = await pathForWallet(name: name, type: WalletType.monero); credentials = xmr_dart.monero.createMoneroNewWalletCredentials( name: name, language: "English", @@ -335,7 +273,6 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { date: DateTime.now(), path: path, dirPath: dirPath, - // TODO: find out what to put for address address: '', ); credentials.walletInfo = walletInfo; @@ -375,175 +312,17 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); - cwWalletBase?.close(); - cwWalletBase = wallet as MoneroWalletBase; - unawaited(_start()); + wallet.close(); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); cwWalletBase?.close(); } await updateNode(); - await cwWalletBase?.startSync(); - - // cwWalletBase?.close(); } return super.init(); } - Future _start() async { - cwWalletBase?.onNewBlock = onNewBlock; - cwWalletBase?.onNewTransaction = onNewTransaction; - cwWalletBase?.syncStatusChanged = syncStatusChanged; - - if (cwWalletBase != null && !(await cwWalletBase!.isConnected())) { - final node = getCurrentNode(); - final host = Uri.parse(node.host).host; - await cwWalletBase?.connectToNode( - node: Node( - uri: "$host:${node.port}", - type: WalletType.monero, - trusted: node.trusted ?? false, - ), - ); - } - await cwWalletBase?.startSync(); - unawaited(refresh()); - _autoSaveTimer?.cancel(); - _autoSaveTimer = Timer.periodic( - const Duration(seconds: 193), - (_) async => await cwWalletBase?.save(), - ); - } - - @override - Future exit() async { - if (!_hasCalledExit) { - _hasCalledExit = true; - cwWalletBase?.onNewBlock = null; - cwWalletBase?.onNewTransaction = null; - cwWalletBase?.syncStatusChanged = null; - _autoSaveTimer?.cancel(); - await cwWalletBase?.save(prioritySave: true); - cwWalletBase?.close(); - } - } - - @override - Future generateNewReceivingAddress() async { - try { - final currentReceiving = await getCurrentReceivingAddress(); - - final newReceivingIndex = - currentReceiving == null ? 0 : currentReceiving.derivationIndex + 1; - - final newReceivingAddress = _addressFor(index: newReceivingIndex); - - // Add that new receiving address - await mainDB.putAddress(newReceivingAddress); - await info.updateReceivingAddress( - newAddress: newReceivingAddress.value, - isar: mainDB.isar, - ); - } catch (e, s) { - Logging.instance.log( - "Exception in generateNewAddress(): $e\n$s", - level: LogLevel.Error, - ); - } - } - - @override - Future checkReceivingAddressForTransactions() async { - try { - int highestIndex = -1; - for (var element - in cwWalletBase!.transactionHistory!.transactions!.entries) { - if (element.value.direction == TransactionDirection.incoming) { - int curAddressIndex = - element.value.additionalInfo!['addressIndex'] as int; - if (curAddressIndex > highestIndex) { - highestIndex = curAddressIndex; - } - } - } - - // Check the new receiving index - final currentReceiving = await getCurrentReceivingAddress(); - final curIndex = currentReceiving?.derivationIndex ?? -1; - - if (highestIndex >= curIndex) { - // First increment the receiving index - final newReceivingIndex = curIndex + 1; - - // Use new index to derive a new receiving address - final newReceivingAddress = _addressFor(index: newReceivingIndex); - - final existing = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(newReceivingAddress.value) - .findFirst(); - if (existing == null) { - // Add that new change address - await mainDB.putAddress(newReceivingAddress); - } else { - // we need to update the address - await mainDB.updateAddress(existing, newReceivingAddress); - } - // keep checking until address with no tx history is set as current - await checkReceivingAddressForTransactions(); - } - } on SocketException catch (se, s) { - Logging.instance.log( - "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", - level: LogLevel.Error); - return; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - @override - Future refresh() async { - // Awaiting this lock could be dangerous. - // Since refresh is periodic (generally) - if (refreshMutex.isLocked) { - return; - } - - // this acquire should be almost instant due to above check. - // Slight possibility of race but should be irrelevant - await refreshMutex.acquire(); - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - info.coin, - ), - ); - - await updateTransactions(); - await updateBalance(); - - await checkReceivingAddressForTransactions(); - - if (cwWalletBase?.syncStatus is SyncedSyncStatus) { - refreshMutex.release(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - info.coin, - ), - ); - } - } - @override Future recover({required bool isRescan}) async { if (isRescan) { @@ -552,10 +331,10 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { await mainDB.deleteWalletBlockchainData(walletId); var restoreHeight = cwWalletBase?.walletInfo.restoreHeight; - _highestPercentCached = 0; + highestPercentCached = 0; await cwWalletBase?.rescan(height: restoreHeight); }); - await refresh(); + unawaited(refresh()); return; } @@ -582,19 +361,19 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { ); } - // TODO: info.updateRestoreHeight - // await DB.instance - // .put(boxName: walletId, key: "restoreHeight", value: height); + await info.updateRestoreHeight( + newRestoreHeight: height, + isar: mainDB.isar, + ); cwWalletService = xmr_dart.monero .createMoneroWalletService(DB.instance.moneroWalletInfoBox); - cwKeysStorage = KeyService(secureStorageInterface); WalletInfo walletInfo; WalletCredentials credentials; String name = walletId; final dirPath = - await _pathForWalletDir(name: name, type: WalletType.monero); - final path = await _pathForWallet(name: name, type: WalletType.monero); + await pathForWalletDir(name: name, type: WalletType.monero); + final path = await pathForWallet(name: name, type: WalletType.monero); credentials = xmr_dart.monero.createMoneroRestoreWalletFromSeedCredentials( name: name, @@ -603,27 +382,27 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { ); try { walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.monero), - name: name, - type: WalletType.monero, - isRecovery: false, - restoreHeight: credentials.height ?? 0, - date: DateTime.now(), - path: path, - dirPath: dirPath, - // TODO: find out what to put for address - address: ''); + id: WalletBase.idFor(name, WalletType.monero), + name: name, + type: WalletType.monero, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + dirPath: dirPath, + address: '', + ); credentials.walletInfo = walletInfo; - cwWalletCreationService = WalletCreationService( + final cwWalletCreationService = WalletCreationService( secureStorage: secureStorageInterface, walletService: cwWalletService, keyService: cwKeysStorage, ); - cwWalletCreationService!.changeWalletType(); + cwWalletCreationService.type = WalletType.monero; // To restore from a seed final wallet = - await cwWalletCreationService!.restoreFromSeed(credentials); + await cwWalletCreationService.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); @@ -669,7 +448,7 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { try { // check for send all bool isSendAll = false; - final balance = await _availableBalance; + final balance = await availableBalance; if (txData.amount! == balance && txData.recipients!.first.amount == balance) { isSendAll = true; @@ -747,133 +526,12 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { } } - // ====== private ============================================================ - - void onNewBlock({required int height, required int blocksLeft}) { - _currentKnownChainHeight = height; - updateChainHeight(); - _refreshTxDataHelper(); - } - - void onNewTransaction() { - // call this here? - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId ${info.name} in background!", - walletId, - ), - ); - } - - void syncStatusChanged() async { - final syncStatus = cwWalletBase?.syncStatus; - if (syncStatus != null) { - if (syncStatus.progress() == 1 && refreshMutex.isLocked) { - refreshMutex.release(); - } - - WalletSyncStatus? status; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); - - if (syncStatus is SyncingSyncStatus) { - final int blocksLeft = syncStatus.blocksLeft; - - // ensure at least 1 to prevent math errors - final int height = max(1, syncStatus.height); - - final nodeHeight = height + blocksLeft; - _currentKnownChainHeight = nodeHeight; - - final percent = height / nodeHeight; - - final highest = max(_highestPercentCached, percent); - - // update cached - if (_highestPercentCached < percent) { - _highestPercentCached = percent; - } - - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highest, - walletId, - ), - ); - GlobalEventBus.instance.fire( - BlocksRemainingEvent( - blocksLeft, - walletId, - ), - ); - } else if (syncStatus is SyncedSyncStatus) { - status = WalletSyncStatus.synced; - } else if (syncStatus is NotConnectedSyncStatus) { - status = WalletSyncStatus.unableToSync; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); - } else if (syncStatus is StartingSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - _highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is FailedSyncStatus) { - status = WalletSyncStatus.unableToSync; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); - } else if (syncStatus is ConnectingSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - _highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is ConnectedSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - _highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is LostConnectionSyncStatus) { - status = WalletSyncStatus.unableToSync; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); - } - - if (status != null) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - status, - walletId, - info.coin, - ), - ); - } - } - } - - Address _addressFor({required int index, int account = 0}) { - String address = cwWalletBase!.getTransactionAddress(account, index); - - final newReceivingAddress = Address( - walletId: walletId, - derivationIndex: index, - derivationPath: null, - value: address, - publicKey: [], - type: AddressType.cryptonote, - subType: AddressSubType.receiving, - ); - - return newReceivingAddress; - } - - Future get _availableBalance async { + @override + Future get availableBalance async { try { int runningBalance = 0; - for (final entry in cwWalletBase!.balance!.entries) { + for (final entry + in (cwWalletBase as MoneroWalletBase?)!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Amount( @@ -885,9 +543,11 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { } } - Future get _totalBalance async { + @override + Future get totalBalance async { try { - final balanceEntries = cwWalletBase?.balance?.entries; + final balanceEntries = + (cwWalletBase as MoneroWalletBase?)?.balance?.entries; if (balanceEntries != null) { int bal = 0; for (var element in balanceEntries) { @@ -898,7 +558,9 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { fractionDigits: cryptoCurrency.fractionDigits, ); } else { - final transactions = cwWalletBase!.transactionHistory!.transactions; + final transactions = (cwWalletBase as MoneroWalletBase?)! + .transactionHistory! + .transactions; int transactionBalance = 0; for (var tx in transactions!.entries) { if (tx.value.direction == TransactionDirection.incoming) { @@ -917,124 +579,4 @@ class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { return info.cachedBalance.total; } } - - Future _refreshTxDataHelper() async { - if (_txRefreshLock) return; - _txRefreshLock = true; - - final syncStatus = cwWalletBase?.syncStatus; - - if (syncStatus != null && syncStatus is SyncingSyncStatus) { - final int blocksLeft = syncStatus.blocksLeft; - final tenKChange = blocksLeft ~/ 10000; - - // only refresh transactions periodically during a sync - if (_lastCheckedHeight == -1 || tenKChange < _lastCheckedHeight) { - _lastCheckedHeight = tenKChange; - await _refreshTxData(); - } - } else { - await _refreshTxData(); - } - - _txRefreshLock = false; - } - - Future _refreshTxData() async { - await updateTransactions(); - final count = await mainDB.getTransactions(walletId).count(); - - if (count > _txCount) { - _txCount = count; - await updateBalance(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New transaction data found in $walletId ${info.name}!", - walletId, - ), - ); - } - } - - Future _pathForWalletDir({ - required String name, - required WalletType type, - }) async { - Directory root = await StackFileSystem.applicationRootDirectory(); - - final prefix = walletTypeToString(type).toLowerCase(); - final walletsDir = Directory('${root.path}/wallets'); - final walletDire = Directory('${walletsDir.path}/$prefix/$name'); - - if (!walletDire.existsSync()) { - walletDire.createSync(recursive: true); - } - - return walletDire.path; - } - - Future _pathForWallet({ - required String name, - required WalletType type, - }) async => - await _pathForWalletDir(name: name, type: type) - .then((path) => '$path/$name'); - - @override - Future checkChangeAddressForTransactions() async { - // do nothing - } - - @override - Future generateNewChangeAddress() async { - // do nothing - } - -// TODO: [prio=med/low] is this required? -// bool _isActive = false; -// @override -// void Function(bool)? get onIsActiveWalletChanged => (isActive) async { -// if (_isActive == isActive) { -// return; -// } -// _isActive = isActive; -// -// if (isActive) { -// _hasCalledExit = false; -// String? password; -// try { -// password = -// await keysStorage?.getWalletPassword(walletName: _walletId); -// } catch (e, s) { -// throw Exception("Password not found $e, $s"); -// } -// walletBase = (await walletService?.openWallet(_walletId, password!)) -// as MoneroWalletBase?; -// -// walletBase!.onNewBlock = onNewBlock; -// walletBase!.onNewTransaction = onNewTransaction; -// walletBase!.syncStatusChanged = syncStatusChanged; -// -// if (!(await walletBase!.isConnected())) { -// final node = await _getCurrentNode(); -// final host = Uri.parse(node.host).host; -// await walletBase?.connectToNode( -// node: Node( -// uri: "$host:${node.port}", -// type: WalletType.Monero, -// trusted: node.trusted ?? false, -// ), -// ); -// } -// await walletBase?.startSync(); -// await refresh(); -// _autoSaveTimer?.cancel(); -// _autoSaveTimer = Timer.periodic( -// const Duration(seconds: 193), -// (_) async => await walletBase?.save(), -// ); -// } else { -// await exit(); -// } -// }; } diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index a1c322c47..d94265e50 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; @@ -10,70 +8,60 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_wallet.dart'; import 'package:decimal/decimal.dart'; -import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as wownero_output; import 'package:flutter_libmonero/wownero/wownero.dart' as wow_dart; import 'package:isar/isar.dart'; -import 'package:mutex/mutex.dart'; import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:tuple/tuple.dart'; -class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { +class WowneroWallet extends CryptonoteWallet with CwBasedInterface { WowneroWallet(CryptoCurrencyNetwork network) : super(Wownero(network)); @override - FilterOperation? get changeAddressFilterOperation => null; + Address addressFor({required int index, int account = 0}) { + String address = (cwWalletBase as WowneroWalletBase) + .getTransactionAddress(account, index); - @override - FilterOperation? get receivingAddressFilterOperation => null; + final newReceivingAddress = Address( + walletId: walletId, + derivationIndex: index, + derivationPath: null, + value: address, + publicKey: [], + type: AddressType.cryptonote, + subType: AddressSubType.receiving, + ); - final prepareSendMutex = Mutex(); - final estimateFeeMutex = Mutex(); - - bool _hasCalledExit = false; - - WalletService? cwWalletService; - KeyService? cwKeysStorage; - WowneroWalletBase? cwWalletBase; - WalletCreationService? cwWalletCreationService; - Timer? _autoSaveTimer; - - bool _txRefreshLock = false; - int _lastCheckedHeight = -1; - int _txCount = 0; - int _currentKnownChainHeight = 0; - double _highestPercentCached = 0; + return newReceivingAddress; + } @override Future estimateFeeFor(Amount amount, int feeRate) async { + if (cwWalletBase == null || cwWalletBase?.syncStatus is! SyncedSyncStatus) { + return Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + MoneroTransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { @@ -141,45 +129,9 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { } } - @override - Future get fees async => FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: MoneroTransactionPriority.fast.raw!, - medium: MoneroTransactionPriority.regular.raw!, - slow: MoneroTransactionPriority.slow.raw!, - ); - @override Future pingCheck() async { - return await cwWalletBase?.isConnected() ?? false; - } - - @override - Future updateBalance() async { - final total = await _totalBalance; - final available = await _availableBalance; - - final balance = Balance( - total: total, - spendable: available, - blockedTotal: Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - pendingSpendable: total - available, - ); - - await info.updateBalance(newBalance: balance, isar: mainDB.isar); - } - - @override - Future updateChainHeight() async { - await info.updateCachedChainHeight( - newHeight: _currentKnownChainHeight, - isar: mainDB.isar, - ); + return await (cwWalletBase as WowneroWalletBase?)?.isConnected() ?? false; } @override @@ -194,19 +146,13 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { trusted: node.trusted ?? false, ), ); - - // TODO: is this sync call needed? Do we need to notify ui here? - // await cwWalletBase?.startSync(); - - // if (shouldRefresh) { - // await refresh(); - // } } @override Future updateTransactions() async { - await cwWalletBase!.updateTransactions(); - final transactions = cwWalletBase?.transactionHistory!.transactions; + await (cwWalletBase as WowneroWalletBase?)?.updateTransactions(); + final transactions = + (cwWalletBase as WowneroWalletBase?)?.transactionHistory?.transactions; // final cachedTransactions = // DB.instance.get(boxName: walletId, key: 'latest_tx_model') @@ -249,7 +195,8 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; - final addressString = cwWalletBase?.getTransactionAddress( + final addressString = + (cwWalletBase as WowneroWalletBase?)?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); @@ -302,26 +249,15 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { Future init() async { cwWalletService = wow_dart.wownero .createWowneroWalletService(DB.instance.moneroWalletInfoBox); - cwKeysStorage = KeyService(secureStorageInterface); - if (await cwWalletService!.isWalletExit(walletId)) { - String? password; - try { - password = await cwKeysStorage!.getWalletPassword(walletName: walletId); - } catch (e, s) { - throw Exception("Password not found $e, $s"); - } - cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) - as WowneroWalletBase; - unawaited(_start()); - } else { + if (!(await cwWalletService!.isWalletExit(walletId))) { WalletInfo walletInfo; WalletCredentials credentials; try { String name = walletId; final dirPath = - await _pathForWalletDir(name: name, type: WalletType.wownero); - final path = await _pathForWallet(name: name, type: WalletType.wownero); + await pathForWalletDir(name: name, type: WalletType.wownero); + final path = await pathForWallet(name: name, type: WalletType.wownero); credentials = wow_dart.wownero.createWowneroNewWalletCredentials( name: name, language: "English", @@ -337,7 +273,6 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { date: DateTime.now(), path: path, dirPath: dirPath, - // TODO: find out what to put for address address: '', ); credentials.walletInfo = walletInfo; @@ -382,148 +317,51 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); - cwWalletBase?.close(); - cwWalletBase = wallet as WowneroWalletBase; - unawaited(_start()); + wallet.close(); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); cwWalletBase?.close(); } await updateNode(); - await cwWalletBase?.startSync(); - - // cwWalletBase?.close(); } return super.init(); } @override - Future exit() async { - if (!_hasCalledExit) { - _hasCalledExit = true; - cwWalletBase?.onNewBlock = null; - cwWalletBase?.onNewTransaction = null; - cwWalletBase?.syncStatusChanged = null; - _autoSaveTimer?.cancel(); - await cwWalletBase?.save(prioritySave: true); - cwWalletBase?.close(); - } - } - - @override - Future generateNewReceivingAddress() async { + Future open() async { + String? password; try { - final currentReceiving = await getCurrentReceivingAddress(); - - final newReceivingIndex = - currentReceiving == null ? 0 : currentReceiving.derivationIndex + 1; - - final newReceivingAddress = _addressFor(index: newReceivingIndex); - - // Add that new receiving address - await mainDB.putAddress(newReceivingAddress); - await info.updateReceivingAddress( - newAddress: newReceivingAddress.value, - isar: mainDB.isar, - ); + password = await cwKeysStorage.getWalletPassword(walletName: walletId); } catch (e, s) { - Logging.instance.log( - "Exception in generateNewAddress(): $e\n$s", - level: LogLevel.Error, - ); - } - } - - @override - Future checkReceivingAddressForTransactions() async { - try { - int highestIndex = -1; - for (var element - in cwWalletBase!.transactionHistory!.transactions!.entries) { - if (element.value.direction == TransactionDirection.incoming) { - int curAddressIndex = - element.value.additionalInfo!['addressIndex'] as int; - if (curAddressIndex > highestIndex) { - highestIndex = curAddressIndex; - } - } - } - - // Check the new receiving index - final currentReceiving = await getCurrentReceivingAddress(); - final curIndex = currentReceiving?.derivationIndex ?? -1; - - if (highestIndex >= curIndex) { - // First increment the receiving index - final newReceivingIndex = curIndex + 1; - - // Use new index to derive a new receiving address - final newReceivingAddress = _addressFor(index: newReceivingIndex); - - final existing = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(newReceivingAddress.value) - .findFirst(); - if (existing == null) { - // Add that new change address - await mainDB.putAddress(newReceivingAddress); - } else { - // we need to update the address - await mainDB.updateAddress(existing, newReceivingAddress); - } - // keep checking until address with no tx history is set as current - await checkReceivingAddressForTransactions(); - } - } on SocketException catch (se, s) { - Logging.instance.log( - "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", - level: LogLevel.Error); - return; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - @override - Future refresh() async { - // Awaiting this lock could be dangerous. - // Since refresh is periodic (generally) - if (refreshMutex.isLocked) { - return; + throw Exception("Password not found $e, $s"); } - // this acquire should be almost instant due to above check. - // Slight possibility of race but should be irrelevant - await refreshMutex.acquire(); + cwWalletBase?.close(); + cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) + as WowneroWalletBase; - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - info.coin, - ), + (cwWalletBase as WowneroWalletBase?)?.onNewBlock = onNewBlock; + (cwWalletBase as WowneroWalletBase?)?.onNewTransaction = onNewTransaction; + (cwWalletBase as WowneroWalletBase?)?.syncStatusChanged = syncStatusChanged; + + await updateNode(); + + await (cwWalletBase as WowneroWalletBase?)?.startSync(); + unawaited(refresh()); + autoSaveTimer?.cancel(); + autoSaveTimer = Timer.periodic( + const Duration(seconds: 193), + (_) async => await cwWalletBase?.save(), ); + } - await updateTransactions(); - await updateBalance(); - - await checkReceivingAddressForTransactions(); - - if (cwWalletBase?.syncStatus is SyncedSyncStatus) { - refreshMutex.release(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - info.coin, - ), - ); - } + @override + Future exitCwWallet() async { + (cwWalletBase as WowneroWalletBase?)?.onNewBlock = null; + (cwWalletBase as WowneroWalletBase?)?.onNewTransaction = null; + (cwWalletBase as WowneroWalletBase?)?.syncStatusChanged = null; + await (cwWalletBase as WowneroWalletBase?)?.save(prioritySave: true); } @override @@ -534,10 +372,10 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { await mainDB.deleteWalletBlockchainData(walletId); var restoreHeight = cwWalletBase?.walletInfo.restoreHeight; - _highestPercentCached = 0; + highestPercentCached = 0; await cwWalletBase?.rescan(height: restoreHeight); }); - await refresh(); + unawaited(refresh()); return; } @@ -575,13 +413,12 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { cwWalletService = wow_dart.wownero .createWowneroWalletService(DB.instance.moneroWalletInfoBox); - cwKeysStorage = KeyService(secureStorageInterface); WalletInfo walletInfo; WalletCredentials credentials; String name = walletId; final dirPath = - await _pathForWalletDir(name: name, type: WalletType.wownero); - final path = await _pathForWallet(name: name, type: WalletType.wownero); + await pathForWalletDir(name: name, type: WalletType.wownero); + final path = await pathForWallet(name: name, type: WalletType.wownero); credentials = wow_dart.wownero.createWowneroRestoreWalletFromSeedCredentials( name: name, @@ -602,20 +439,20 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { address: ''); credentials.walletInfo = walletInfo; - cwWalletCreationService = WalletCreationService( + final cwWalletCreationService = WalletCreationService( secureStorage: secureStorageInterface, walletService: cwWalletService, keyService: cwKeysStorage, ); - cwWalletCreationService!.changeWalletType(); + cwWalletCreationService.type = WalletType.wownero; // To restore from a seed - final wallet = - await cwWalletCreationService!.restoreFromSeed(credentials); + final wallet = await cwWalletCreationService + .restoreFromSeed(credentials) as WowneroWalletBase; walletInfo.address = wallet.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); cwWalletBase?.close(); - cwWalletBase = wallet as WowneroWalletBase; + cwWalletBase = wallet; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); } @@ -656,7 +493,7 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { try { // check for send all bool isSendAll = false; - final balance = await _availableBalance; + final balance = await availableBalance; if (txData.amount! == balance && txData.recipients!.first.amount == balance) { isSendAll = true; @@ -734,158 +571,12 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { } } - // ====== private ============================================================ - - Future _start() async { - cwWalletBase?.onNewBlock = onNewBlock; - cwWalletBase?.onNewTransaction = onNewTransaction; - cwWalletBase?.syncStatusChanged = syncStatusChanged; - - if (cwWalletBase != null && !(await cwWalletBase!.isConnected())) { - final node = getCurrentNode(); - final host = Uri.parse(node.host).host; - await cwWalletBase?.connectToNode( - node: Node( - uri: "$host:${node.port}", - type: WalletType.monero, - trusted: node.trusted ?? false, - ), - ); - } - await cwWalletBase?.startSync(); - unawaited(refresh()); - _autoSaveTimer?.cancel(); - _autoSaveTimer = Timer.periodic( - const Duration(seconds: 193), - (_) async => await cwWalletBase?.save(), - ); - } - - void onNewBlock({required int height, required int blocksLeft}) { - _currentKnownChainHeight = height; - updateChainHeight(); - _refreshTxDataHelper(); - } - - void onNewTransaction() { - // call this here? - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId ${info.name} in background!", - walletId, - ), - ); - } - - void syncStatusChanged() async { - final syncStatus = cwWalletBase?.syncStatus; - if (syncStatus != null) { - if (syncStatus.progress() == 1 && refreshMutex.isLocked) { - refreshMutex.release(); - } - - WalletSyncStatus? status; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); - - if (syncStatus is SyncingSyncStatus) { - final int blocksLeft = syncStatus.blocksLeft; - - // ensure at least 1 to prevent math errors - final int height = max(1, syncStatus.height); - - final nodeHeight = height + blocksLeft; - _currentKnownChainHeight = nodeHeight; - - final percent = height / nodeHeight; - - final highest = max(_highestPercentCached, percent); - - // update cached - if (_highestPercentCached < percent) { - _highestPercentCached = percent; - } - - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highest, - walletId, - ), - ); - GlobalEventBus.instance.fire( - BlocksRemainingEvent( - blocksLeft, - walletId, - ), - ); - } else if (syncStatus is SyncedSyncStatus) { - status = WalletSyncStatus.synced; - } else if (syncStatus is NotConnectedSyncStatus) { - status = WalletSyncStatus.unableToSync; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); - } else if (syncStatus is StartingSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - _highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is FailedSyncStatus) { - status = WalletSyncStatus.unableToSync; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); - } else if (syncStatus is ConnectingSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - _highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is ConnectedSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - _highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is LostConnectionSyncStatus) { - status = WalletSyncStatus.unableToSync; - xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); - } - - if (status != null) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - status, - walletId, - info.coin, - ), - ); - } - } - } - - Address _addressFor({required int index, int account = 0}) { - String address = cwWalletBase!.getTransactionAddress(account, index); - - final newReceivingAddress = Address( - walletId: walletId, - derivationIndex: index, - derivationPath: null, - value: address, - publicKey: [], - type: AddressType.cryptonote, - subType: AddressSubType.receiving, - ); - - return newReceivingAddress; - } - - Future get _availableBalance async { + @override + Future get availableBalance async { try { int runningBalance = 0; - for (final entry in cwWalletBase!.balance!.entries) { + for (final entry + in (cwWalletBase as WowneroWalletBase?)!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Amount( @@ -897,9 +588,11 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { } } - Future get _totalBalance async { + @override + Future get totalBalance async { try { - final balanceEntries = cwWalletBase?.balance?.entries; + final balanceEntries = + (cwWalletBase as WowneroWalletBase?)?.balance?.entries; if (balanceEntries != null) { int bal = 0; for (var element in balanceEntries) { @@ -929,124 +622,4 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { return info.cachedBalance.total; } } - - Future _refreshTxDataHelper() async { - if (_txRefreshLock) return; - _txRefreshLock = true; - - final syncStatus = cwWalletBase?.syncStatus; - - if (syncStatus != null && syncStatus is SyncingSyncStatus) { - final int blocksLeft = syncStatus.blocksLeft; - final tenKChange = blocksLeft ~/ 10000; - - // only refresh transactions periodically during a sync - if (_lastCheckedHeight == -1 || tenKChange < _lastCheckedHeight) { - _lastCheckedHeight = tenKChange; - await _refreshTxData(); - } - } else { - await _refreshTxData(); - } - - _txRefreshLock = false; - } - - Future _refreshTxData() async { - await updateTransactions(); - final count = await mainDB.getTransactions(walletId).count(); - - if (count > _txCount) { - _txCount = count; - await updateBalance(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New transaction data found in $walletId ${info.name}!", - walletId, - ), - ); - } - } - - Future _pathForWalletDir({ - required String name, - required WalletType type, - }) async { - Directory root = await StackFileSystem.applicationRootDirectory(); - - final prefix = walletTypeToString(type).toLowerCase(); - final walletsDir = Directory('${root.path}/wallets'); - final walletDire = Directory('${walletsDir.path}/$prefix/$name'); - - if (!walletDire.existsSync()) { - walletDire.createSync(recursive: true); - } - - return walletDire.path; - } - - Future _pathForWallet({ - required String name, - required WalletType type, - }) async => - await _pathForWalletDir(name: name, type: type) - .then((path) => '$path/$name'); - - @override - Future checkChangeAddressForTransactions() async { - // do nothing - } - - @override - Future generateNewChangeAddress() async { - // do nothing - } - - // TODO: [prio=med/low] is this required? - // bool _isActive = false; - // @override - // void Function(bool)? get onIsActiveWalletChanged => (isActive) async { - // if (_isActive == isActive) { - // return; - // } - // _isActive = isActive; - // - // if (isActive) { - // _hasCalledExit = false; - // String? password; - // try { - // password = - // await keysStorage?.getWalletPassword(walletName: _walletId); - // } catch (e, s) { - // throw Exception("Password not found $e, $s"); - // } - // walletBase = (await walletService?.openWallet(_walletId, password!)) - // as WowneroWalletBase?; - // - // walletBase!.onNewBlock = onNewBlock; - // walletBase!.onNewTransaction = onNewTransaction; - // walletBase!.syncStatusChanged = syncStatusChanged; - // - // if (!(await walletBase!.isConnected())) { - // final node = await _getCurrentNode(); - // final host = Uri.parse(node.host).host; - // await walletBase?.connectToNode( - // node: Node( - // uri: "$host:${node.port}", - // type: WalletType.wownero, - // trusted: node.trusted ?? false, - // ), - // ); - // } - // await walletBase?.startSync(); - // await refresh(); - // _autoSaveTimer?.cancel(); - // _autoSaveTimer = Timer.periodic( - // const Duration(seconds: 193), - // (_) async => await walletBase?.save(), - // ); - // } else { - // await exit(); - // } - // }; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart new file mode 100644 index 000000000..f6bfbdcf2 --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart @@ -0,0 +1,409 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/cryptonote_currency.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; + +mixin CwBasedInterface on CryptonoteWallet + implements MultiAddressInterface { + final prepareSendMutex = Mutex(); + final estimateFeeMutex = Mutex(); + + KeyService? _cwKeysStorageCached; + KeyService get cwKeysStorage => + _cwKeysStorageCached ??= KeyService(secureStorageInterface); + + WalletService? cwWalletService; + WalletBase? cwWalletBase; + + bool _hasCalledExit = false; + bool _txRefreshLock = false; + int _lastCheckedHeight = -1; + int _txCount = 0; + int currentKnownChainHeight = 0; + double highestPercentCached = 0; + + Timer? autoSaveTimer; + + Future pathForWalletDir({ + required String name, + required WalletType type, + }) async { + final Directory root = await StackFileSystem.applicationRootDirectory(); + + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; + } + + Future pathForWallet({ + required String name, + required WalletType type, + }) async => + await pathForWalletDir(name: name, type: type) + .then((path) => '$path/$name'); + + void onNewBlock({required int height, required int blocksLeft}) { + currentKnownChainHeight = height; + updateChainHeight(); + _refreshTxDataHelper(); + } + + void onNewTransaction() { + // TODO: [prio=low] get rid of UpdatedInBackgroundEvent and move to + // adding the v2 tx to the db which would update ui automagically since the + // db is watched by the ui + // call this here? + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "New data found in $walletId ${info.name} in background!", + walletId, + ), + ); + } + + void syncStatusChanged() async { + final syncStatus = cwWalletBase?.syncStatus; + if (syncStatus != null) { + if (syncStatus.progress() == 1 && refreshMutex.isLocked) { + refreshMutex.release(); + } + + WalletSyncStatus? status; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); + + if (syncStatus is SyncingSyncStatus) { + final int blocksLeft = syncStatus.blocksLeft; + + // ensure at least 1 to prevent math errors + final int height = max(1, syncStatus.height); + + final nodeHeight = height + blocksLeft; + currentKnownChainHeight = nodeHeight; + + final percent = height / nodeHeight; + + final highest = max(highestPercentCached, percent); + + // update cached + if (highestPercentCached < percent) { + highestPercentCached = percent; + } + + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highest, + walletId, + ), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent( + blocksLeft, + walletId, + ), + ); + } else if (syncStatus is SyncedSyncStatus) { + status = WalletSyncStatus.synced; + } else if (syncStatus is NotConnectedSyncStatus) { + status = WalletSyncStatus.unableToSync; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); + } else if (syncStatus is StartingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is FailedSyncStatus) { + status = WalletSyncStatus.unableToSync; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); + } else if (syncStatus is ConnectingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is ConnectedSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is LostConnectionSyncStatus) { + status = WalletSyncStatus.unableToSync; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); + } + + if (status != null) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + status, + walletId, + info.coin, + ), + ); + } + } + } + + // ============ Interface ==================================================== + + Future get availableBalance; + Future get totalBalance; + + Future exitCwWallet(); + + Future open(); + + Address addressFor({required int index, int account = 0}); + + // ============ Private ====================================================== + Future _refreshTxDataHelper() async { + if (_txRefreshLock) return; + _txRefreshLock = true; + + final syncStatus = cwWalletBase?.syncStatus; + + if (syncStatus != null && syncStatus is SyncingSyncStatus) { + final int blocksLeft = syncStatus.blocksLeft; + final tenKChange = blocksLeft ~/ 10000; + + // only refresh transactions periodically during a sync + if (_lastCheckedHeight == -1 || tenKChange < _lastCheckedHeight) { + _lastCheckedHeight = tenKChange; + await _refreshTxData(); + } + } else { + await _refreshTxData(); + } + + _txRefreshLock = false; + } + + Future _refreshTxData() async { + await updateTransactions(); + final count = await mainDB.getTransactions(walletId).count(); + + if (count > _txCount) { + _txCount = count; + await updateBalance(); + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "New transaction data found in $walletId ${info.name}!", + walletId, + ), + ); + } + } + + // ============ Overrides ==================================================== + + @override + FilterOperation? get changeAddressFilterOperation => null; + + @override + FilterOperation? get receivingAddressFilterOperation => null; + + @override + Future updateBalance() async { + final total = await totalBalance; + final available = await availableBalance; + + final balance = Balance( + total: total, + spendable: available, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: total - available, + ); + + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } + + @override + Future refresh() async { + // Awaiting this lock could be dangerous. + // Since refresh is periodic (generally) + if (refreshMutex.isLocked) { + return; + } + + // this acquire should be almost instant due to above check. + // Slight possibility of race but should be irrelevant + await refreshMutex.acquire(); + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + info.coin, + ), + ); + + await updateTransactions(); + await updateBalance(); + + await checkReceivingAddressForTransactions(); + + if (cwWalletBase?.syncStatus is SyncedSyncStatus) { + refreshMutex.release(); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + info.coin, + ), + ); + } + } + + @override + Future exit() async { + if (!_hasCalledExit) { + _hasCalledExit = true; + autoSaveTimer?.cancel(); + await exitCwWallet(); + cwWalletBase?.close(); + } + } + + @override + Future generateNewReceivingAddress() async { + try { + final currentReceiving = await getCurrentReceivingAddress(); + + final newReceivingIndex = + currentReceiving == null ? 0 : currentReceiving.derivationIndex + 1; + + final newReceivingAddress = addressFor(index: newReceivingIndex); + + // Add that new receiving address + await mainDB.putAddress(newReceivingAddress); + await info.updateReceivingAddress( + newAddress: newReceivingAddress.value, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.log( + "Exception in generateNewAddress(): $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future checkReceivingAddressForTransactions() async { + try { + int highestIndex = -1; + for (var element + in cwWalletBase!.transactionHistory!.transactions!.entries) { + if (element.value.direction == TransactionDirection.incoming) { + int curAddressIndex = + element.value.additionalInfo!['addressIndex'] as int; + if (curAddressIndex > highestIndex) { + highestIndex = curAddressIndex; + } + } + } + + // Check the new receiving index + final currentReceiving = await getCurrentReceivingAddress(); + final curIndex = currentReceiving?.derivationIndex ?? -1; + + if (highestIndex >= curIndex) { + // First increment the receiving index + final newReceivingIndex = curIndex + 1; + + // Use new index to derive a new receiving address + final newReceivingAddress = addressFor(index: newReceivingIndex); + + final existing = await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(newReceivingAddress.value) + .findFirst(); + if (existing == null) { + // Add that new change address + await mainDB.putAddress(newReceivingAddress); + } else { + // we need to update the address + await mainDB.updateAddress(existing, newReceivingAddress); + } + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future get fees async => FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); + @override + Future updateChainHeight() async { + await info.updateCachedChainHeight( + newHeight: currentKnownChainHeight, + isar: mainDB.isar, + ); + } + + @override + Future checkChangeAddressForTransactions() async { + // do nothing + } + + @override + Future generateNewChangeAddress() async { + // do nothing + } +} diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 7bcf26597..4225d7c39 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -20,7 +20,6 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/des import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -28,6 +27,7 @@ import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_prov import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; @@ -93,11 +93,17 @@ class SimpleWalletCard extends ConsumerWidget { final nav = Navigator.of(context); final wallet = ref.read(pWallets).getWallet(walletId); - if (wallet.info.coin == Coin.monero || wallet.info.coin == Coin.wownero) { - // TODO: this can cause ui lag if awaited - unawaited(wallet.init()); - } + await wallet.init(); + if (context.mounted) { + if (wallet is CwBasedInterface) { + await showLoading( + whileFuture: wallet.open(), + context: context, + message: 'Opening ${wallet.info.name}', + isDesktop: Util.isDesktop, + ); + } if (popPrevious) nav.pop(); if (desktopNavigatorState != null) {