From f43ae8788da22ea6e97c83db94481d51fc2884e0 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 8 Jan 2024 13:40:07 -0600 Subject: [PATCH] monero refactor with some fixes applied to wownero as well --- ...w_wallet_recovery_phrase_warning_view.dart | 8 +- .../my_stack_view/coin_wallets_table.dart | 6 +- lib/services/coins/coin_service.dart | 9 +- lib/services/coins/monero/monero_wallet.dart | 2548 ++++++++--------- lib/wallets/crypto_currency/coins/monero.dart | 47 + lib/wallets/isar/models/wallet_info.dart | 24 +- lib/wallets/models/tx_data.dart | 9 + lib/wallets/wallet/impl/monero_wallet.dart | 1040 +++++++ lib/wallets/wallet/impl/wownero_wallet.dart | 52 +- lib/wallets/wallet/wallet.dart | 26 +- lib/widgets/wallet_card.dart | 3 +- 11 files changed, 2469 insertions(+), 1303 deletions(-) create mode 100644 lib/wallets/crypto_currency/coins/monero.dart create mode 100644 lib/wallets/wallet/impl/monero_wallet.dart diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 8c019962d..23ce0b1a5 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -500,7 +500,13 @@ class _NewWalletRecoveryPhraseWarningViewState Constants.defaultSeedPhraseLengthFor( coin: info.coin, ); - if (wordCount > 0) { + + if (coin == Coin.monero || + coin == Coin.wownero) { + // currently a special case due to the + // xmr/wow libraries handling their + // own mnemonic generation + } else if (wordCount > 0) { if (ref .read(pNewWalletOptions.state) .state != 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 859e2dbd6..43e1774b4 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,6 +8,8 @@ * */ +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'; @@ -79,8 +81,8 @@ class CoinWalletsTable extends ConsumerWidget { ref.read(pWallets).getWallet(walletIds[i]); if (wallet.info.coin == Coin.monero || wallet.info.coin == Coin.wownero) { - // TODO: this can cause ui lag - await wallet.init(); + // TODO: this can cause ui lag if awaited + unawaited(wallet.init()); } await Navigator.of(context).pushNamed( diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 85f1763dd..17322de2d 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -16,7 +16,6 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; -import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart'; @@ -120,13 +119,7 @@ abstract class CoinServiceAPI { ); case Coin.monero: - return MoneroWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStorage: secureStorageInterface, - // tracker: tracker, - ); + throw UnimplementedError("moved"); case Coin.particl: return ParticlWallet( diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index a5284cc60..8633e3828 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1,1274 +1,1274 @@ -/* - * 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 'dart:io'; -import 'dart:math'; - -import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.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_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/cupertino.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'; -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/db/isar/main_db.dart'; -import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/services/coins/coin_service.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/services/mixins/wallet_cache.dart'; -import 'package:stackwallet/services/mixins/wallet_db.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:stackwallet/utilities/stack_file_system.dart'; -import 'package:tuple/tuple.dart'; - -const int MINIMUM_CONFIRMATIONS = 10; - -class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { - MoneroWallet({ - required String walletId, - required String walletName, - required Coin coin, - required SecureStorageInterface secureStorage, - Prefs? prefs, - MainDB? mockableOverride, - }) { - _walletId = walletId; - _walletName = walletName; - _coin = coin; - _secureStorage = secureStorage; - _prefs = prefs ?? Prefs.instance; - initCache(walletId, coin); - initWalletDB(mockableOverride: mockableOverride); - } - - late final String _walletId; - late final Coin _coin; - late final SecureStorageInterface _secureStorage; - late final Prefs _prefs; - - late String _walletName; - - bool _shouldAutoSync = false; - bool _isConnected = false; - bool _hasCalledExit = false; - bool refreshMutex = false; - bool longMutex = false; - - WalletService? walletService; - KeyService? keysStorage; - MoneroWalletBase? walletBase; - WalletCreationService? _walletCreationService; - Timer? _autoSaveTimer; - - Future get _currentReceivingAddress => - db.getAddresses(walletId).sortByDerivationIndexDesc().findFirst(); - Future? _feeObject; - - Mutex prepareSendMutex = Mutex(); - Mutex estimateFeeMutex = Mutex(); - - @override - set isFavorite(bool markFavorite) { - _isFavorite = markFavorite; - updateCachedIsFavorite(markFavorite); - } - - @override - bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); - - bool? _isFavorite; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - // xmr wallets cannot be open at the same time - // leave following commented out for now - - // if (!shouldAutoSync) { - // timer?.cancel(); - // moneroAutosaveTimer?.cancel(); - // timer = null; - // moneroAutosaveTimer = null; - // stopNetworkAlivePinging(); - // } else { - // startNetworkAlivePinging(); - // // Walletbase needs to be open for this to work - // refresh(); - // } - } - } - - @override - String get walletName => _walletName; - - // setter for updating on rename - @override - set walletName(String newName) => _walletName = newName; - - @override - Coin get coin => _coin; - - @override - Future confirmSend({required Map txData}) async { - try { - Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); - final pendingMoneroTransaction = - txData['pendingMoneroTransaction'] as PendingMoneroTransaction; - try { - await pendingMoneroTransaction.commit(); - Logging.instance.log( - "transaction ${pendingMoneroTransaction.id} has been sent", - level: LogLevel.Info); - return pendingMoneroTransaction.id; - } catch (e, s) { - Logging.instance.log("$walletName monero confirmSend: $e\n$s", - level: LogLevel.Error); - rethrow; - } - } catch (e, s) { - Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", - level: LogLevel.Info); - rethrow; - } - } - - @override - Future get currentReceivingAddress async => - (await _currentReceivingAddress)?.value ?? - (await _generateAddressForChain(0, 0)).value; - - @override - Future estimateFeeFor(Amount amount, int feeRate) async { - MoneroTransactionPriority priority; - - switch (feeRate) { - case 1: - priority = MoneroTransactionPriority.regular; - break; - case 2: - priority = MoneroTransactionPriority.medium; - break; - case 3: - priority = MoneroTransactionPriority.fast; - break; - case 4: - priority = MoneroTransactionPriority.fastest; - break; - case 0: - default: - priority = MoneroTransactionPriority.slow; - break; - } - - final fee = walletBase!.calculateEstimatedFee(priority, amount.raw.toInt()); - - return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); - } - - @override - Future exit() async { - if (!_hasCalledExit) { - walletBase?.onNewBlock = null; - walletBase?.onNewTransaction = null; - walletBase?.syncStatusChanged = null; - _hasCalledExit = true; - _autoSaveTimer?.cancel(); - await walletBase?.save(prioritySave: true); - walletBase?.close(); - } - } - - @override - Future get fees => _feeObject ??= _getFees(); - - @override - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - // clear blockchain info - await db.deleteWalletBlockchainData(walletId); - - var restoreHeight = walletBase?.walletInfo.restoreHeight; - highestPercentCached = 0; - await walletBase?.rescan(height: restoreHeight); - await refresh(); - } - - @override - Future generateNewAddress() async { - try { - final currentReceiving = await _currentReceivingAddress; - - final newReceivingIndex = currentReceiving!.derivationIndex + 1; - - // Use new index to derive a new receiving address - final newReceivingAddress = await _generateAddressForChain( - 0, - newReceivingIndex, - ); - - // Add that new receiving address - await db.putAddress(newReceivingAddress); - - return true; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from generateNewAddress(): $e\n$s", - level: LogLevel.Error); - return false; - } - } - - @override - bool get hasCalledExit => _hasCalledExit; - - @override - Future initializeExisting() async { - Logging.instance.log( - "initializeExisting() ${coin.prettyName} wallet $walletName...", - level: LogLevel.Info, - ); - - if (getCachedId() == null) { - throw Exception( - "Attempted to initialize an existing wallet using an unknown wallet ID!"); - } - - walletService = - monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStorage); - - await _prefs.init(); - - // final data = - // DB.instance.get(boxName: walletId, key: "latest_tx_model") - // as TransactionData?; - // if (data != null) { - // _transactionData = Future(() => data); - // } - - String password; - try { - password = await keysStorage!.getWalletPassword(walletName: _walletId); - } catch (_) { - throw Exception("Monero password not found for $walletName"); - } - walletBase = (await walletService!.openWallet(_walletId, password)) - as MoneroWalletBase; - - // await _checkCurrentReceivingAddressesForTransactions(); - - Logging.instance.log( - "Opened existing ${coin.prettyName} wallet $walletName", - level: LogLevel.Info, - ); - } - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - await _prefs.init(); - - // this should never fail - if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - - walletService = - monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStorage); - 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); - credentials = monero.createMoneroNewWalletCredentials( - name: name, - language: "English", - ); - - // subtract a couple days to ensure we have a buffer for SWB - final bufferedCreateHeight = monero.getHeigthByDate( - date: DateTime.now().subtract(const Duration(days: 2))); - - await DB.instance.put( - boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); - - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.monero), - name: name, - type: WalletType.monero, - isRecovery: false, - restoreHeight: bufferedCreateHeight, - date: DateTime.now(), - path: path, - dirPath: dirPath, - // TODO: find out what to put for address - address: ''); - credentials.walletInfo = walletInfo; - - _walletCreationService = WalletCreationService( - secureStorage: _secureStorage, - walletService: walletService, - keyService: keysStorage, - ); - _walletCreationService?.changeWalletType(); - // To restore from a seed - final wallet = await _walletCreationService?.create(credentials); - - await _secureStorage.write( - key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); - await _secureStorage.write( - key: '${_walletId}_mnemonicPassphrase', - value: "", - ); - walletInfo.address = wallet?.walletAddresses.address; - await DB.instance - .add(boxName: WalletInfo.boxName, value: walletInfo); - walletBase?.close(); - walletBase = wallet as MoneroWalletBase; - // walletBase!.onNewBlock = onNewBlock; - // walletBase!.onNewTransaction = onNewTransaction; - // walletBase!.syncStatusChanged = syncStatusChanged; - } catch (e, s) { - //todo: come back to this - debugPrint("some nice searchable string thing"); - debugPrint(e.toString()); - debugPrint(s.toString()); - walletBase?.close(); - } - 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 Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - - // Generate and add addresses to relevant arrays - final initialReceivingAddress = await _generateAddressForChain(0, 0); - // final initialChangeAddress = await _generateAddressForChain(1, 0); - - await db.putAddress(initialReceivingAddress); - - walletBase?.close(); - Logging.instance - .log("initializeNew for $walletName $walletId", level: LogLevel.Info); - } - - @override - bool get isConnected => _isConnected; - - @override - bool get isRefreshing => refreshMutex; - - @override - // not used in xmr - Future get maxFee => throw UnimplementedError(); - - @override - Future> get mnemonic async { - final _mnemonicString = await mnemonicString; - if (_mnemonicString == null) { - return []; - } - final List data = _mnemonicString.split(' '); - return data; - } - - @override - Future get mnemonicString => - _secureStorage.read(key: '${_walletId}_mnemonic'); - - @override - Future get mnemonicPassphrase => _secureStorage.read( - key: '${_walletId}_mnemonicPassphrase', - ); - - @override - Future> prepareSend({ - required String address, - required Amount amount, - Map? args, - }) async { - String toAddress = address; - try { - final feeRate = args?["feeRate"]; - if (feeRate is FeeRateType) { - MoneroTransactionPriority feePriority; - switch (feeRate) { - case FeeRateType.fast: - feePriority = MoneroTransactionPriority.fast; - break; - case FeeRateType.average: - feePriority = MoneroTransactionPriority.regular; - break; - case FeeRateType.slow: - feePriority = MoneroTransactionPriority.slow; - break; - default: - throw ArgumentError("Invalid use of custom fee"); - } - - Future? awaitPendingTransaction; - try { - // check for send all - bool isSendAll = false; - final balance = await _availableBalance; - if (amount == balance) { - isSendAll = true; - } - Logging.instance - .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = amount.decimal.toString(); - Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); - - monero_output.Output output = monero_output.Output(walletBase!); - output.address = toAddress; - output.sendAll = isSendAll; - output.setCryptoAmount(amountToSend); - - List outputs = [output]; - Object tmp = monero.createMoneroTransactionCreationCredentials( - outputs: outputs, priority: feePriority); - - await prepareSendMutex.protect(() async { - awaitPendingTransaction = walletBase!.createTransaction(tmp); - }); - } catch (e, s) { - Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", - level: LogLevel.Warning); - } - - PendingMoneroTransaction pendingMoneroTransaction = - await (awaitPendingTransaction!) as PendingMoneroTransaction; - - final int realFee = Amount.fromDecimal( - Decimal.parse(pendingMoneroTransaction.feeFormatted), - fractionDigits: coin.decimals, - ).raw.toInt(); - - Map txData = { - "pendingMoneroTransaction": pendingMoneroTransaction, - "fee": realFee, - "addresss": toAddress, - "recipientAmt": amount, - }; - - Logging.instance.log("prepare send: $txData", level: LogLevel.Info); - return txData; - } else { - throw ArgumentError("Invalid fee rate argument provided!"); - } - } catch (e, s) { - Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", - level: LogLevel.Info); - - if (e.toString().contains("Incorrect unlocked balance")) { - throw Exception("Insufficient balance!"); - } else if (e is CreationTransactionException) { - throw Exception("Insufficient funds to pay for transaction fee!"); - } else { - throw Exception("Transaction failed with error code $e"); - } - } - } - - @override - Future recoverFromMnemonic({ - required String mnemonic, - String? mnemonicPassphrase, // not used at the moment - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height, - }) async { - await _prefs.init(); - longMutex = true; - final start = DateTime.now(); - try { - // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); - // if (!integrationTestFlag) { - // final features = await electrumXClient.getServerFeatures(); - // Logging.instance.log("features: $features"); - // if (_networkType == BasicNetworkType.main) { - // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // throw Exception("genesis hash does not match main net!"); - // } - // } else if (_networkType == BasicNetworkType.test) { - // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - // throw Exception("genesis hash does not match test net!"); - // } - // } - // } - // check to make sure we aren't overwriting a mnemonic - // this should never fail - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - await _secureStorage.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - await _secureStorage.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - - await DB.instance - .put(boxName: walletId, key: "restoreHeight", value: height); - - walletService = - monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStorage); - 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); - credentials = monero.createMoneroRestoreWalletFromSeedCredentials( - name: name, - height: height, - mnemonic: mnemonic.trim(), - ); - 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: ''); - credentials.walletInfo = walletInfo; - - _walletCreationService = WalletCreationService( - secureStorage: _secureStorage, - walletService: walletService, - keyService: keysStorage, - ); - _walletCreationService!.changeWalletType(); - // To restore from a seed - final wallet = - await _walletCreationService!.restoreFromSeed(credentials); - walletInfo.address = wallet.walletAddresses.address; - await DB.instance - .add(boxName: WalletInfo.boxName, value: walletInfo); - walletBase?.close(); - walletBase = wallet as MoneroWalletBase; - // walletBase!.onNewBlock = onNewBlock; - // walletBase!.onNewTransaction = onNewTransaction; - // walletBase!.syncStatusChanged = syncStatusChanged; - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } catch (e, s) { - debugPrint(e.toString()); - debugPrint(s.toString()); - } - 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!.rescan(height: credentials.height); - walletBase!.close(); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - longMutex = false; - rethrow; - } - longMutex = false; - - final end = DateTime.now(); - Logging.instance.log( - "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", - level: LogLevel.Info); - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - await _refreshTransactions(); - await _updateBalance(); - - await _checkCurrentReceivingAddressesForTransactions(); - - if (walletBase?.syncStatus is SyncedSyncStatus) { - refreshMutex = false; - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - } - } - - @override - Future testNetworkConnection() async { - return await walletBase?.isConnected() ?? false; - } - - 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(); - // _autoSaveTimer?.cancel(); - // await walletBase?.save(prioritySave: true); - // walletBase?.close(); - } - }; - - Future _updateCachedBalance(int sats) async { - await DB.instance.put( - boxName: walletId, - key: "cachedMoneroBalanceSats", - value: sats, - ); - } - - int _getCachedBalance() => - DB.instance.get( - boxName: walletId, - key: "cachedMoneroBalanceSats", - ) as int? ?? - 0; - - Future _updateBalance() async { - final total = await _totalBalance; - final available = await _availableBalance; - _balance = Balance( - total: total, - spendable: available, - blockedTotal: Amount( - rawValue: BigInt.zero, - fractionDigits: coin.decimals, - ), - pendingSpendable: total - available, - ); - await updateCachedBalance(_balance!); - } - - Future get _availableBalance async { - try { - int runningBalance = 0; - for (final entry in walletBase!.balance!.entries) { - runningBalance += entry.value.unlockedBalance; - } - return Amount( - rawValue: BigInt.from(runningBalance), - fractionDigits: coin.decimals, - ); - } catch (_) { - return Amount( - rawValue: BigInt.zero, - fractionDigits: coin.decimals, - ); - } - } - - Future get _totalBalance async { - try { - final balanceEntries = walletBase?.balance?.entries; - if (balanceEntries != null) { - int bal = 0; - for (var element in balanceEntries) { - bal = bal + element.value.fullBalance; - } - await _updateCachedBalance(bal); - return Amount( - rawValue: BigInt.from(bal), - fractionDigits: coin.decimals, - ); - } else { - final transactions = walletBase!.transactionHistory!.transactions; - int transactionBalance = 0; - for (var tx in transactions!.entries) { - if (tx.value.direction == TransactionDirection.incoming) { - transactionBalance += tx.value.amount!; - } else { - transactionBalance += -tx.value.amount! - tx.value.fee!; - } - } - - await _updateCachedBalance(transactionBalance); - return Amount( - rawValue: BigInt.from(transactionBalance), - fractionDigits: coin.decimals, - ); - } - } catch (_) { - return Amount( - rawValue: BigInt.from(_getCachedBalance()), - fractionDigits: coin.decimals, - ); - } - } - - @override - Future updateNode(bool shouldRefresh) async { - 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, - ), - ); - - // TODO: is this sync call needed? Do we need to notify ui here? - await walletBase?.startSync(); - - if (shouldRefresh) { - await refresh(); - } - } - - @override - Future updateSentCachedTxData(Map txData) async { - // not used for xmr - return; - } - - @override - bool validateAddress(String address) => walletBase!.validateAddress(address); - - @override - String get walletId => _walletId; - - Future _generateAddressForChain( - int chain, - int index, - ) async { - // - String address = walletBase!.getTransactionAddress(chain, index); - - if (address.contains("111")) { - return await _generateAddressForChain(chain, index + 1); - } - - return isar_models.Address( - walletId: walletId, - derivationIndex: index, - derivationPath: null, - value: address, - publicKey: [], - type: isar_models.AddressType.cryptonote, - subType: chain == 0 - ? isar_models.AddressSubType.receiving - : isar_models.AddressSubType.change, - ); - } - - Future _getFees() async { - // TODO: not use random hard coded values here - return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: MoneroTransactionPriority.fast.raw!, - medium: MoneroTransactionPriority.regular.raw!, - slow: MoneroTransactionPriority.slow.raw!, - ); - } - - Future _refreshTransactions() async { - await walletBase!.updateTransactions(); - final transactions = walletBase?.transactionHistory!.transactions; - - // final cachedTransactions = - // DB.instance.get(boxName: walletId, key: 'latest_tx_model') - // as TransactionData?; - // int latestTxnBlockHeight = - // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") - // as int? ?? - // 0; - - // final txidsList = DB.instance - // .get(boxName: walletId, key: "cachedTxids") as List? ?? - // []; - // - // final Set cachedTxids = Set.from(txidsList); - - final List> txnsData = - []; - - if (transactions != null) { - for (var tx in transactions.entries) { - // cachedTxids.add(tx.value.id); - // Logging.instance.log( - // "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " - // "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " - // "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" - // " ${tx.value.keyIndex}", - // level: LogLevel.Info); - - isar_models.Address? address; - isar_models.TransactionType type; - if (tx.value.direction == TransactionDirection.incoming) { - final addressInfo = tx.value.additionalInfo; - - final addressString = walletBase?.getTransactionAddress( - addressInfo!['accountIndex'] as int, - addressInfo['addressIndex'] as int, - ); - - if (addressString != null) { - address = await db - .getAddresses(walletId) - .filter() - .valueEqualTo(addressString) - .findFirst(); - } - - type = isar_models.TransactionType.incoming; - } else { - // txn.address = ""; - type = isar_models.TransactionType.outgoing; - } - - final txn = isar_models.Transaction( - walletId: walletId, - txid: tx.value.id, - timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000), - type: type, - subType: isar_models.TransactionSubType.none, - amount: tx.value.amount ?? 0, - amountString: Amount( - rawValue: BigInt.from(tx.value.amount ?? 0), - fractionDigits: coin.decimals, - ).toJsonString(), - fee: tx.value.fee ?? 0, - height: tx.value.height, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - txnsData.add(Tuple2(txn, address)); - } - } - - await db.addNewTransactionData(txnsData, walletId); - - // quick hack to notify manager to call notifyListeners if - // transactions changed - if (txnsData.isNotEmpty) { - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "Transactions updated/added for: $walletId $walletName ", - 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'); - - Future _getCurrentNode() async { - return NodeService(secureStorageInterface: _secureStorage) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - void onNewBlock({required int height, required int blocksLeft}) { - // - print("============================="); - print("New Block! :: $walletName"); - print("============================="); - updateCachedChainHeight(height); - _refreshTxDataHelper(); - } - - void onNewTransaction() { - // - print("============================="); - print("New Transaction! :: $walletName"); - print("============================="); - - // call this here? - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId, - ), - ); - } - - bool _txRefreshLock = false; - int _lastCheckedHeight = -1; - int _txCount = 0; - - Future _refreshTxDataHelper() async { - if (_txRefreshLock) return; - _txRefreshLock = true; - - final syncStatus = walletBase?.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 _refreshTransactions(); - final count = await db.getTransactions(walletId).count(); - - if (count > _txCount) { - _txCount = count; - await _updateBalance(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New transaction data found in $walletId $walletName!", - walletId, - ), - ); - } - } - - void syncStatusChanged() async { - final syncStatus = walletBase?.syncStatus; - if (syncStatus != null) { - if (syncStatus.progress() == 1) { - refreshMutex = false; - } - - WalletSyncStatus? status; - _isConnected = 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; - - final percent = height / nodeHeight; - - final highest = max(highestPercentCached, percent); - - // update cached - if (highestPercentCached < percent) { - highestPercentCached = percent; - } - await updateCachedChainHeight(height); - - 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; - _isConnected = false; - } else if (syncStatus is StartingSyncStatus) { - status = WalletSyncStatus.syncing; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), - ); - } else if (syncStatus is FailedSyncStatus) { - status = WalletSyncStatus.unableToSync; - _isConnected = 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; - _isConnected = false; - } - - if (status != null) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - status, - walletId, - coin, - ), - ); - } - } - } - - Future _checkCurrentReceivingAddressesForTransactions() async { - try { - await _checkReceivingAddressForTransactions(); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _checkReceivingAddressForTransactions() async { - try { - int highestIndex = -1; - for (var element - in walletBase!.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 _currentReceivingAddress; - 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 = - await _generateAddressForChain(0, newReceivingIndex); - - final existing = await db - .getAddresses(walletId) - .filter() - .valueEqualTo(newReceivingAddress.value) - .findFirst(); - if (existing == null) { - // Add that new change address - await db.putAddress(newReceivingAddress); - } else { - // we need to update the address - await db.updateAddress(existing, newReceivingAddress); - - // since we updated an existing address there is a chance it has - // some tx history. To prevent address reuse we will call check again - // recursively - 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; - } - } - - double get highestPercentCached => - DB.instance.get(boxName: walletId, key: "highestPercentCached") - as double? ?? - 0; - - set highestPercentCached(double value) => DB.instance.put( - boxName: walletId, - key: "highestPercentCached", - value: value, - ); - - @override - int get storedChainHeight => getCachedChainHeight(); - - @override - Balance get balance => _balance ??= getCachedBalance(); - Balance? _balance; - - @override - Future> get transactions => - db.getTransactions(walletId).sortByTimestampDesc().findAll(); - - @override - // TODO: implement utxos - Future> get utxos => throw UnimplementedError(); -} +// /* +// * 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 'dart:io'; +// import 'dart:math'; +// +// import 'package:cw_core/monero_transaction_priority.dart'; +// import 'package:cw_core/node.dart'; +// import 'package:cw_core/pending_transaction.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_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/cupertino.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'; +// 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/db/isar/main_db.dart'; +// import 'package:stackwallet/models/balance.dart'; +// import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; +// import 'package:stackwallet/models/node_model.dart'; +// import 'package:stackwallet/models/paymint/fee_object_model.dart'; +// import 'package:stackwallet/services/coins/coin_service.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/services/mixins/wallet_cache.dart'; +// import 'package:stackwallet/services/mixins/wallet_db.dart'; +// import 'package:stackwallet/services/node_service.dart'; +// import 'package:stackwallet/utilities/amount/amount.dart'; +// import 'package:stackwallet/utilities/default_nodes.dart'; +// import 'package:stackwallet/utilities/enums/coin_enum.dart'; +// import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +// import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +// import 'package:stackwallet/utilities/logger.dart'; +// import 'package:stackwallet/utilities/prefs.dart'; +// import 'package:stackwallet/utilities/stack_file_system.dart'; +// import 'package:tuple/tuple.dart'; +// +// const int MINIMUM_CONFIRMATIONS = 10; +// +// class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { +// MoneroWallet({ +// required String walletId, +// required String walletName, +// required Coin coin, +// required SecureStorageInterface secureStorage, +// Prefs? prefs, +// MainDB? mockableOverride, +// }) { +// _walletId = walletId; +// _walletName = walletName; +// _coin = coin; +// _secureStorage = secureStorage; +// _prefs = prefs ?? Prefs.instance; +// initCache(walletId, coin); +// initWalletDB(mockableOverride: mockableOverride); +// } +// +// late final String _walletId; +// late final Coin _coin; +// late final SecureStorageInterface _secureStorage; +// late final Prefs _prefs; +// +// late String _walletName; +// +// bool _shouldAutoSync = false; +// bool _isConnected = false; +// bool _hasCalledExit = false; +// bool refreshMutex = false; +// bool longMutex = false; +// +// WalletService? walletService; +// KeyService? keysStorage; +// MoneroWalletBase? walletBase; +// WalletCreationService? _walletCreationService; +// Timer? _autoSaveTimer; +// +// Future get _currentReceivingAddress => +// db.getAddresses(walletId).sortByDerivationIndexDesc().findFirst(); +// Future? _feeObject; +// +// Mutex prepareSendMutex = Mutex(); +// Mutex estimateFeeMutex = Mutex(); +// +// @override +// set isFavorite(bool markFavorite) { +// _isFavorite = markFavorite; +// updateCachedIsFavorite(markFavorite); +// } +// +// @override +// bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); +// +// bool? _isFavorite; +// +// @override +// bool get shouldAutoSync => _shouldAutoSync; +// +// @override +// set shouldAutoSync(bool shouldAutoSync) { +// if (_shouldAutoSync != shouldAutoSync) { +// _shouldAutoSync = shouldAutoSync; +// // xmr wallets cannot be open at the same time +// // leave following commented out for now +// +// // if (!shouldAutoSync) { +// // timer?.cancel(); +// // moneroAutosaveTimer?.cancel(); +// // timer = null; +// // moneroAutosaveTimer = null; +// // stopNetworkAlivePinging(); +// // } else { +// // startNetworkAlivePinging(); +// // // Walletbase needs to be open for this to work +// // refresh(); +// // } +// } +// } +// +// // @override +// // String get walletName => _walletName; +// // +// // // setter for updating on rename +// // @override +// // set walletName(String newName) => _walletName = newName; +// // +// // @override +// // Coin get coin => _coin; +// // +// // @override +// // Future confirmSend({required Map txData}) async { +// // try { +// // Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); +// // final pendingMoneroTransaction = +// // txData['pendingMoneroTransaction'] as PendingMoneroTransaction; +// // try { +// // await pendingMoneroTransaction.commit(); +// // Logging.instance.log( +// // "transaction ${pendingMoneroTransaction.id} has been sent", +// // level: LogLevel.Info); +// // return pendingMoneroTransaction.id; +// // } catch (e, s) { +// // Logging.instance.log("$walletName monero confirmSend: $e\n$s", +// // level: LogLevel.Error); +// // rethrow; +// // } +// // } catch (e, s) { +// // Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", +// // level: LogLevel.Info); +// // rethrow; +// // } +// // } +// +// // @override +// // Future get currentReceivingAddress async => +// // (await _currentReceivingAddress)?.value ?? +// // (await _generateAddressForChain(0, 0)).value; +// // +// // @override +// // Future estimateFeeFor(Amount amount, int feeRate) async { +// // MoneroTransactionPriority priority; +// // +// // switch (feeRate) { +// // case 1: +// // priority = MoneroTransactionPriority.regular; +// // break; +// // case 2: +// // priority = MoneroTransactionPriority.medium; +// // break; +// // case 3: +// // priority = MoneroTransactionPriority.fast; +// // break; +// // case 4: +// // priority = MoneroTransactionPriority.fastest; +// // break; +// // case 0: +// // default: +// // priority = MoneroTransactionPriority.slow; +// // break; +// // } +// // +// // final fee = walletBase!.calculateEstimatedFee(priority, amount.raw.toInt()); +// // +// // return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); +// // } +// +// @override +// Future exit() async { +// if (!_hasCalledExit) { +// walletBase?.onNewBlock = null; +// walletBase?.onNewTransaction = null; +// walletBase?.syncStatusChanged = null; +// _hasCalledExit = true; +// _autoSaveTimer?.cancel(); +// await walletBase?.save(prioritySave: true); +// walletBase?.close(); +// } +// } +// +// @override +// Future get fees => _feeObject ??= _getFees(); +// +// @override +// Future fullRescan( +// int maxUnusedAddressGap, +// int maxNumberOfIndexesToCheck, +// ) async { +// // clear blockchain info +// await db.deleteWalletBlockchainData(walletId); +// +// var restoreHeight = walletBase?.walletInfo.restoreHeight; +// highestPercentCached = 0; +// await walletBase?.rescan(height: restoreHeight); +// await refresh(); +// } +// +// @override +// Future generateNewAddress() async { +// try { +// final currentReceiving = await _currentReceivingAddress; +// +// final newReceivingIndex = currentReceiving!.derivationIndex + 1; +// +// // Use new index to derive a new receiving address +// final newReceivingAddress = await _generateAddressForChain( +// 0, +// newReceivingIndex, +// ); +// +// // Add that new receiving address +// await db.putAddress(newReceivingAddress); +// +// return true; +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from generateNewAddress(): $e\n$s", +// level: LogLevel.Error); +// return false; +// } +// } +// +// @override +// bool get hasCalledExit => _hasCalledExit; +// +// @override +// Future initializeExisting() async { +// Logging.instance.log( +// "initializeExisting() ${coin.prettyName} wallet $walletName...", +// level: LogLevel.Info, +// ); +// +// if (getCachedId() == null) { +// throw Exception( +// "Attempted to initialize an existing wallet using an unknown wallet ID!"); +// } +// +// walletService = +// monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); +// keysStorage = KeyService(_secureStorage); +// +// await _prefs.init(); +// +// // final data = +// // DB.instance.get(boxName: walletId, key: "latest_tx_model") +// // as TransactionData?; +// // if (data != null) { +// // _transactionData = Future(() => data); +// // } +// +// String password; +// try { +// password = await keysStorage!.getWalletPassword(walletName: _walletId); +// } catch (_) { +// throw Exception("Monero password not found for $walletName"); +// } +// walletBase = (await walletService!.openWallet(_walletId, password)) +// as MoneroWalletBase; +// +// // await _checkCurrentReceivingAddressesForTransactions(); +// +// Logging.instance.log( +// "Opened existing ${coin.prettyName} wallet $walletName", +// level: LogLevel.Info, +// ); +// } +// +// @override +// Future initializeNew( +// ({String mnemonicPassphrase, int wordCount})? data, +// ) async { +// await _prefs.init(); +// +// // this should never fail +// if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { +// throw Exception( +// "Attempted to overwrite mnemonic on generate new wallet!"); +// } +// +// walletService = +// monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); +// keysStorage = KeyService(_secureStorage); +// 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); +// credentials = monero.createMoneroNewWalletCredentials( +// name: name, +// language: "English", +// ); +// +// // subtract a couple days to ensure we have a buffer for SWB +// final bufferedCreateHeight = monero.getHeigthByDate( +// date: DateTime.now().subtract(const Duration(days: 2))); +// +// await DB.instance.put( +// boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); +// +// walletInfo = WalletInfo.external( +// id: WalletBase.idFor(name, WalletType.monero), +// name: name, +// type: WalletType.monero, +// isRecovery: false, +// restoreHeight: bufferedCreateHeight, +// date: DateTime.now(), +// path: path, +// dirPath: dirPath, +// // TODO: find out what to put for address +// address: ''); +// credentials.walletInfo = walletInfo; +// +// _walletCreationService = WalletCreationService( +// secureStorage: _secureStorage, +// walletService: walletService, +// keyService: keysStorage, +// ); +// _walletCreationService?.changeWalletType(); +// // To restore from a seed +// final wallet = await _walletCreationService?.create(credentials); +// +// await _secureStorage.write( +// key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); +// await _secureStorage.write( +// key: '${_walletId}_mnemonicPassphrase', +// value: "", +// ); +// walletInfo.address = wallet?.walletAddresses.address; +// await DB.instance +// .add(boxName: WalletInfo.boxName, value: walletInfo); +// walletBase?.close(); +// walletBase = wallet as MoneroWalletBase; +// // walletBase!.onNewBlock = onNewBlock; +// // walletBase!.onNewTransaction = onNewTransaction; +// // walletBase!.syncStatusChanged = syncStatusChanged; +// } catch (e, s) { +// //todo: come back to this +// debugPrint("some nice searchable string thing"); +// debugPrint(e.toString()); +// debugPrint(s.toString()); +// walletBase?.close(); +// } +// 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 Future.wait([ +// updateCachedId(walletId), +// updateCachedIsFavorite(false), +// ]); +// +// // Generate and add addresses to relevant arrays +// final initialReceivingAddress = await _generateAddressForChain(0, 0); +// // final initialChangeAddress = await _generateAddressForChain(1, 0); +// +// await db.putAddress(initialReceivingAddress); +// +// walletBase?.close(); +// Logging.instance +// .log("initializeNew for $walletName $walletId", level: LogLevel.Info); +// } +// +// @override +// bool get isConnected => _isConnected; +// +// @override +// bool get isRefreshing => refreshMutex; +// +// @override +// // not used in xmr +// Future get maxFee => throw UnimplementedError(); +// +// @override +// Future> get mnemonic async { +// final _mnemonicString = await mnemonicString; +// if (_mnemonicString == null) { +// return []; +// } +// final List data = _mnemonicString.split(' '); +// return data; +// } +// +// @override +// Future get mnemonicString => +// _secureStorage.read(key: '${_walletId}_mnemonic'); +// +// @override +// Future get mnemonicPassphrase => _secureStorage.read( +// key: '${_walletId}_mnemonicPassphrase', +// ); +// +// @override +// Future> prepareSend({ +// required String address, +// required Amount amount, +// Map? args, +// }) async { +// String toAddress = address; +// try { +// final feeRate = args?["feeRate"]; +// if (feeRate is FeeRateType) { +// MoneroTransactionPriority feePriority; +// switch (feeRate) { +// case FeeRateType.fast: +// feePriority = MoneroTransactionPriority.fast; +// break; +// case FeeRateType.average: +// feePriority = MoneroTransactionPriority.regular; +// break; +// case FeeRateType.slow: +// feePriority = MoneroTransactionPriority.slow; +// break; +// default: +// throw ArgumentError("Invalid use of custom fee"); +// } +// +// Future? awaitPendingTransaction; +// try { +// // check for send all +// bool isSendAll = false; +// final balance = await _availableBalance; +// if (amount == balance) { +// isSendAll = true; +// } +// Logging.instance +// .log("$toAddress $amount $args", level: LogLevel.Info); +// String amountToSend = amount.decimal.toString(); +// Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); +// +// monero_output.Output output = monero_output.Output(walletBase!); +// output.address = toAddress; +// output.sendAll = isSendAll; +// output.setCryptoAmount(amountToSend); +// +// List outputs = [output]; +// Object tmp = monero.createMoneroTransactionCreationCredentials( +// outputs: outputs, priority: feePriority); +// +// await prepareSendMutex.protect(() async { +// awaitPendingTransaction = walletBase!.createTransaction(tmp); +// }); +// } catch (e, s) { +// Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", +// level: LogLevel.Warning); +// } +// +// PendingMoneroTransaction pendingMoneroTransaction = +// await (awaitPendingTransaction!) as PendingMoneroTransaction; +// +// final int realFee = Amount.fromDecimal( +// Decimal.parse(pendingMoneroTransaction.feeFormatted), +// fractionDigits: coin.decimals, +// ).raw.toInt(); +// +// Map txData = { +// "pendingMoneroTransaction": pendingMoneroTransaction, +// "fee": realFee, +// "addresss": toAddress, +// "recipientAmt": amount, +// }; +// +// Logging.instance.log("prepare send: $txData", level: LogLevel.Info); +// return txData; +// } else { +// throw ArgumentError("Invalid fee rate argument provided!"); +// } +// } catch (e, s) { +// Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", +// level: LogLevel.Info); +// +// if (e.toString().contains("Incorrect unlocked balance")) { +// throw Exception("Insufficient balance!"); +// } else if (e is CreationTransactionException) { +// throw Exception("Insufficient funds to pay for transaction fee!"); +// } else { +// throw Exception("Transaction failed with error code $e"); +// } +// } +// } +// +// @override +// Future recoverFromMnemonic({ +// required String mnemonic, +// String? mnemonicPassphrase, // not used at the moment +// required int maxUnusedAddressGap, +// required int maxNumberOfIndexesToCheck, +// required int height, +// }) async { +// await _prefs.init(); +// longMutex = true; +// final start = DateTime.now(); +// try { +// // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); +// // if (!integrationTestFlag) { +// // final features = await electrumXClient.getServerFeatures(); +// // Logging.instance.log("features: $features"); +// // if (_networkType == BasicNetworkType.main) { +// // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { +// // throw Exception("genesis hash does not match main net!"); +// // } +// // } else if (_networkType == BasicNetworkType.test) { +// // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { +// // throw Exception("genesis hash does not match test net!"); +// // } +// // } +// // } +// // check to make sure we aren't overwriting a mnemonic +// // this should never fail +// if ((await mnemonicString) != null || +// (await this.mnemonicPassphrase) != null) { +// longMutex = false; +// throw Exception("Attempted to overwrite mnemonic on restore!"); +// } +// await _secureStorage.write( +// key: '${_walletId}_mnemonic', value: mnemonic.trim()); +// await _secureStorage.write( +// key: '${_walletId}_mnemonicPassphrase', +// value: mnemonicPassphrase ?? "", +// ); +// +// await DB.instance +// .put(boxName: walletId, key: "restoreHeight", value: height); +// +// walletService = +// monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); +// keysStorage = KeyService(_secureStorage); +// 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); +// credentials = monero.createMoneroRestoreWalletFromSeedCredentials( +// name: name, +// height: height, +// mnemonic: mnemonic.trim(), +// ); +// 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: ''); +// credentials.walletInfo = walletInfo; +// +// _walletCreationService = WalletCreationService( +// secureStorage: _secureStorage, +// walletService: walletService, +// keyService: keysStorage, +// ); +// _walletCreationService!.changeWalletType(); +// // To restore from a seed +// final wallet = +// await _walletCreationService!.restoreFromSeed(credentials); +// walletInfo.address = wallet.walletAddresses.address; +// await DB.instance +// .add(boxName: WalletInfo.boxName, value: walletInfo); +// walletBase?.close(); +// walletBase = wallet as MoneroWalletBase; +// // walletBase!.onNewBlock = onNewBlock; +// // walletBase!.onNewTransaction = onNewTransaction; +// // walletBase!.syncStatusChanged = syncStatusChanged; +// +// await Future.wait([ +// updateCachedId(walletId), +// updateCachedIsFavorite(false), +// ]); +// } catch (e, s) { +// debugPrint(e.toString()); +// debugPrint(s.toString()); +// } +// 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!.rescan(height: credentials.height); +// walletBase!.close(); +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from recoverFromMnemonic(): $e\n$s", +// level: LogLevel.Error); +// longMutex = false; +// rethrow; +// } +// longMutex = false; +// +// final end = DateTime.now(); +// Logging.instance.log( +// "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", +// level: LogLevel.Info); +// } +// +// @override +// Future refresh() async { +// if (refreshMutex) { +// Logging.instance.log("$walletId $walletName refreshMutex denied", +// level: LogLevel.Info); +// return; +// } else { +// refreshMutex = true; +// } +// +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.syncing, +// walletId, +// coin, +// ), +// ); +// +// await _refreshTransactions(); +// await _updateBalance(); +// +// await _checkCurrentReceivingAddressesForTransactions(); +// +// if (walletBase?.syncStatus is SyncedSyncStatus) { +// refreshMutex = false; +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.synced, +// walletId, +// coin, +// ), +// ); +// } +// } +// +// @override +// Future testNetworkConnection() async { +// return await walletBase?.isConnected() ?? false; +// } +// +// 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(); +// // _autoSaveTimer?.cancel(); +// // await walletBase?.save(prioritySave: true); +// // walletBase?.close(); +// } +// }; +// +// Future _updateCachedBalance(int sats) async { +// await DB.instance.put( +// boxName: walletId, +// key: "cachedMoneroBalanceSats", +// value: sats, +// ); +// } +// +// int _getCachedBalance() => +// DB.instance.get( +// boxName: walletId, +// key: "cachedMoneroBalanceSats", +// ) as int? ?? +// 0; +// +// Future _updateBalance() async { +// final total = await _totalBalance; +// final available = await _availableBalance; +// _balance = Balance( +// total: total, +// spendable: available, +// blockedTotal: Amount( +// rawValue: BigInt.zero, +// fractionDigits: coin.decimals, +// ), +// pendingSpendable: total - available, +// ); +// await updateCachedBalance(_balance!); +// } +// +// Future get _availableBalance async { +// try { +// int runningBalance = 0; +// for (final entry in walletBase!.balance!.entries) { +// runningBalance += entry.value.unlockedBalance; +// } +// return Amount( +// rawValue: BigInt.from(runningBalance), +// fractionDigits: coin.decimals, +// ); +// } catch (_) { +// return Amount( +// rawValue: BigInt.zero, +// fractionDigits: coin.decimals, +// ); +// } +// } +// +// Future get _totalBalance async { +// try { +// final balanceEntries = walletBase?.balance?.entries; +// if (balanceEntries != null) { +// int bal = 0; +// for (var element in balanceEntries) { +// bal = bal + element.value.fullBalance; +// } +// await _updateCachedBalance(bal); +// return Amount( +// rawValue: BigInt.from(bal), +// fractionDigits: coin.decimals, +// ); +// } else { +// final transactions = walletBase!.transactionHistory!.transactions; +// int transactionBalance = 0; +// for (var tx in transactions!.entries) { +// if (tx.value.direction == TransactionDirection.incoming) { +// transactionBalance += tx.value.amount!; +// } else { +// transactionBalance += -tx.value.amount! - tx.value.fee!; +// } +// } +// +// await _updateCachedBalance(transactionBalance); +// return Amount( +// rawValue: BigInt.from(transactionBalance), +// fractionDigits: coin.decimals, +// ); +// } +// } catch (_) { +// return Amount( +// rawValue: BigInt.from(_getCachedBalance()), +// fractionDigits: coin.decimals, +// ); +// } +// } +// +// @override +// Future updateNode(bool shouldRefresh) async { +// 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, +// ), +// ); +// +// // TODO: is this sync call needed? Do we need to notify ui here? +// await walletBase?.startSync(); +// +// if (shouldRefresh) { +// await refresh(); +// } +// } +// +// @override +// Future updateSentCachedTxData(Map txData) async { +// // not used for xmr +// return; +// } +// +// @override +// bool validateAddress(String address) => walletBase!.validateAddress(address); +// +// @override +// String get walletId => _walletId; +// +// Future _generateAddressForChain( +// int chain, +// int index, +// ) async { +// // +// String address = walletBase!.getTransactionAddress(chain, index); +// +// if (address.contains("111")) { +// return await _generateAddressForChain(chain, index + 1); +// } +// +// return isar_models.Address( +// walletId: walletId, +// derivationIndex: index, +// derivationPath: null, +// value: address, +// publicKey: [], +// type: isar_models.AddressType.cryptonote, +// subType: chain == 0 +// ? isar_models.AddressSubType.receiving +// : isar_models.AddressSubType.change, +// ); +// } +// +// Future _getFees() async { +// // TODO: not use random hard coded values here +// return FeeObject( +// numberOfBlocksFast: 10, +// numberOfBlocksAverage: 15, +// numberOfBlocksSlow: 20, +// fast: MoneroTransactionPriority.fast.raw!, +// medium: MoneroTransactionPriority.regular.raw!, +// slow: MoneroTransactionPriority.slow.raw!, +// ); +// } +// +// Future _refreshTransactions() async { +// await walletBase!.updateTransactions(); +// final transactions = walletBase?.transactionHistory!.transactions; +// +// // final cachedTransactions = +// // DB.instance.get(boxName: walletId, key: 'latest_tx_model') +// // as TransactionData?; +// // int latestTxnBlockHeight = +// // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") +// // as int? ?? +// // 0; +// +// // final txidsList = DB.instance +// // .get(boxName: walletId, key: "cachedTxids") as List? ?? +// // []; +// // +// // final Set cachedTxids = Set.from(txidsList); +// +// final List> txnsData = +// []; +// +// if (transactions != null) { +// for (var tx in transactions.entries) { +// // cachedTxids.add(tx.value.id); +// // Logging.instance.log( +// // "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " +// // "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " +// // "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" +// // " ${tx.value.keyIndex}", +// // level: LogLevel.Info); +// +// isar_models.Address? address; +// isar_models.TransactionType type; +// if (tx.value.direction == TransactionDirection.incoming) { +// final addressInfo = tx.value.additionalInfo; +// +// final addressString = walletBase?.getTransactionAddress( +// addressInfo!['accountIndex'] as int, +// addressInfo['addressIndex'] as int, +// ); +// +// if (addressString != null) { +// address = await db +// .getAddresses(walletId) +// .filter() +// .valueEqualTo(addressString) +// .findFirst(); +// } +// +// type = isar_models.TransactionType.incoming; +// } else { +// // txn.address = ""; +// type = isar_models.TransactionType.outgoing; +// } +// +// final txn = isar_models.Transaction( +// walletId: walletId, +// txid: tx.value.id, +// timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000), +// type: type, +// subType: isar_models.TransactionSubType.none, +// amount: tx.value.amount ?? 0, +// amountString: Amount( +// rawValue: BigInt.from(tx.value.amount ?? 0), +// fractionDigits: coin.decimals, +// ).toJsonString(), +// fee: tx.value.fee ?? 0, +// height: tx.value.height, +// isCancelled: false, +// isLelantus: false, +// slateId: null, +// otherData: null, +// nonce: null, +// inputs: [], +// outputs: [], +// numberOfMessages: null, +// ); +// +// txnsData.add(Tuple2(txn, address)); +// } +// } +// +// await db.addNewTransactionData(txnsData, walletId); +// +// // quick hack to notify manager to call notifyListeners if +// // transactions changed +// if (txnsData.isNotEmpty) { +// GlobalEventBus.instance.fire( +// UpdatedInBackgroundEvent( +// "Transactions updated/added for: $walletId $walletName ", +// 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'); +// +// Future _getCurrentNode() async { +// return NodeService(secureStorageInterface: _secureStorage) +// .getPrimaryNodeFor(coin: coin) ?? +// DefaultNodes.getNodeFor(coin); +// } +// +// void onNewBlock({required int height, required int blocksLeft}) { +// // +// print("============================="); +// print("New Block! :: $walletName"); +// print("============================="); +// updateCachedChainHeight(height); +// _refreshTxDataHelper(); +// } +// +// void onNewTransaction() { +// // +// print("============================="); +// print("New Transaction! :: $walletName"); +// print("============================="); +// +// // call this here? +// GlobalEventBus.instance.fire( +// UpdatedInBackgroundEvent( +// "New data found in $walletId $walletName in background!", +// walletId, +// ), +// ); +// } +// +// bool _txRefreshLock = false; +// int _lastCheckedHeight = -1; +// int _txCount = 0; +// +// Future _refreshTxDataHelper() async { +// if (_txRefreshLock) return; +// _txRefreshLock = true; +// +// final syncStatus = walletBase?.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 _refreshTransactions(); +// final count = await db.getTransactions(walletId).count(); +// +// if (count > _txCount) { +// _txCount = count; +// await _updateBalance(); +// GlobalEventBus.instance.fire( +// UpdatedInBackgroundEvent( +// "New transaction data found in $walletId $walletName!", +// walletId, +// ), +// ); +// } +// } +// +// void syncStatusChanged() async { +// final syncStatus = walletBase?.syncStatus; +// if (syncStatus != null) { +// if (syncStatus.progress() == 1) { +// refreshMutex = false; +// } +// +// WalletSyncStatus? status; +// _isConnected = 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; +// +// final percent = height / nodeHeight; +// +// final highest = max(highestPercentCached, percent); +// +// // update cached +// if (highestPercentCached < percent) { +// highestPercentCached = percent; +// } +// await updateCachedChainHeight(height); +// +// 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; +// _isConnected = false; +// } else if (syncStatus is StartingSyncStatus) { +// status = WalletSyncStatus.syncing; +// GlobalEventBus.instance.fire( +// RefreshPercentChangedEvent( +// highestPercentCached, +// walletId, +// ), +// ); +// } else if (syncStatus is FailedSyncStatus) { +// status = WalletSyncStatus.unableToSync; +// _isConnected = 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; +// _isConnected = false; +// } +// +// if (status != null) { +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// status, +// walletId, +// coin, +// ), +// ); +// } +// } +// } +// +// Future _checkCurrentReceivingAddressesForTransactions() async { +// try { +// await _checkReceivingAddressForTransactions(); +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future _checkReceivingAddressForTransactions() async { +// try { +// int highestIndex = -1; +// for (var element +// in walletBase!.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 _currentReceivingAddress; +// 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 = +// await _generateAddressForChain(0, newReceivingIndex); +// +// final existing = await db +// .getAddresses(walletId) +// .filter() +// .valueEqualTo(newReceivingAddress.value) +// .findFirst(); +// if (existing == null) { +// // Add that new change address +// await db.putAddress(newReceivingAddress); +// } else { +// // we need to update the address +// await db.updateAddress(existing, newReceivingAddress); +// +// // since we updated an existing address there is a chance it has +// // some tx history. To prevent address reuse we will call check again +// // recursively +// 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; +// } +// } +// +// double get highestPercentCached => +// DB.instance.get(boxName: walletId, key: "highestPercentCached") +// as double? ?? +// 0; +// +// set highestPercentCached(double value) => DB.instance.put( +// boxName: walletId, +// key: "highestPercentCached", +// value: value, +// ); +// +// @override +// int get storedChainHeight => getCachedChainHeight(); +// +// @override +// Balance get balance => _balance ??= getCachedBalance(); +// Balance? _balance; +// +// @override +// Future> get transactions => +// db.getTransactions(walletId).sortByTimestampDesc().findAll(); +// +// @override +// // TODO: implement utxos +// Future> get utxos => throw UnimplementedError(); +// } diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart new file mode 100644 index 000000000..748d1b7eb --- /dev/null +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -0,0 +1,47 @@ +import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/cryptonote_currency.dart'; + +class Monero extends CryptonoteCurrency { + Monero(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.monero; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 10; + + @override + bool validateAddress(String address) { + return monero_wallet.validateAddress(address); + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "https://monero.stackwallet.com", + port: 18081, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.monero), + useSSL: true, + enabled: true, + coinName: Coin.monero.name, + isFailover: true, + isDown: false, + trusted: true, + ); + + default: + throw UnimplementedError(); + } + } +} diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index defca17d5..405728d4c 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -286,7 +286,7 @@ class WalletInfo implements IsarId { } } - /// copies this with a new name and updates the db + /// Can be dangerous. Don't use unless you know the consequences Future setMnemonicVerified({ required Isar isar, }) async { @@ -294,7 +294,6 @@ class WalletInfo implements IsarId { await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst(); if (meta == null) { await isar.writeTxn(() async { - await isar.walletInfoMeta.deleteByWalletId(walletId); await isar.walletInfoMeta.put( WalletInfoMeta( walletId: walletId, @@ -304,6 +303,7 @@ class WalletInfo implements IsarId { }); } else if (meta.isMnemonicVerified == false) { await isar.writeTxn(() async { + await isar.walletInfoMeta.deleteByWalletId(walletId); await isar.walletInfoMeta.put( WalletInfoMeta( walletId: walletId, @@ -319,6 +319,26 @@ class WalletInfo implements IsarId { } } + /// copies this with a new name and updates the db + Future updateRestoreHeight({ + required int newRestoreHeight, + required Isar isar, + }) async { + // don't allow empty names + if (newRestoreHeight < 0) { + throw Exception("Negative restore height not allowed!"); + } + + // only update if there were changes to the name + if (restoreHeight != newRestoreHeight) { + _restoreHeight = newRestoreHeight; + await isar.writeTxn(() async { + await isar.walletInfo.deleteByWalletId(walletId); + await isar.walletInfo.put(this); + }); + } + } + //============================================================================ WalletInfo({ diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index c8621d623..f8b3f6803 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -1,3 +1,4 @@ +import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; @@ -45,6 +46,9 @@ class TxData { // wownero specific final PendingWowneroTransaction? pendingWowneroTransaction; + // monero specific + final PendingMoneroTransaction? pendingMoneroTransaction; + // firo lelantus specific final int? jMintValue; final List? spendCoinIndexes; @@ -91,6 +95,7 @@ class TxData { this.chainId, this.feeInWei, this.pendingWowneroTransaction, + this.pendingMoneroTransaction, this.jMintValue, this.spendCoinIndexes, this.height, @@ -165,6 +170,7 @@ class TxData { BigInt? chainId, BigInt? feeInWei, PendingWowneroTransaction? pendingWowneroTransaction, + PendingMoneroTransaction? pendingMoneroTransaction, int? jMintValue, List? spendCoinIndexes, int? height, @@ -207,6 +213,8 @@ class TxData { feeInWei: feeInWei ?? this.feeInWei, pendingWowneroTransaction: pendingWowneroTransaction ?? this.pendingWowneroTransaction, + pendingMoneroTransaction: + pendingMoneroTransaction ?? this.pendingMoneroTransaction, jMintValue: jMintValue ?? this.jMintValue, spendCoinIndexes: spendCoinIndexes ?? this.spendCoinIndexes, height: height ?? this.height, @@ -244,6 +252,7 @@ class TxData { 'chainId: $chainId, ' 'feeInWei: $feeInWei, ' 'pendingWowneroTransaction: $pendingWowneroTransaction, ' + 'pendingMoneroTransaction: $pendingMoneroTransaction, ' 'jMintValue: $jMintValue, ' 'spendCoinIndexes: $spendCoinIndexes, ' 'height: $height, ' diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart new file mode 100644 index 000000000..fa1125068 --- /dev/null +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -0,0 +1,1040 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pending_transaction.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_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:tuple/tuple.dart'; + +class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { + MoneroWallet(CryptoCurrencyNetwork network) : super(Monero(network)); + + @override + FilterOperation? get changeAddressFilterOperation => null; + + @override + FilterOperation? get receivingAddressFilterOperation => null; + + final prepareSendMutex = Mutex(); + final estimateFeeMutex = Mutex(); + + bool _hasCalledExit = false; + + WalletService? cwWalletService; + KeyService? cwKeysStorage; + MoneroWalletBase? cwWalletBase; + WalletCreationService? cwWalletCreationService; + Timer? _autoSaveTimer; + + bool _txRefreshLock = false; + int _lastCheckedHeight = -1; + int _txCount = 0; + int _currentKnownChainHeight = 0; + double _highestPercentCached = 0; + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + 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; + 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(), + ); + } + } + }); + + if (approximateFee is Amount) { + return approximateFee as Amount; + } else { + return Amount( + rawValue: BigInt.from(approximateFee as int), + 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, + ); + } + + @override + Future updateNode() async { + 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, + ), + ); + + // 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; + + // final cachedTransactions = + // DB.instance.get(boxName: walletId, key: 'latest_tx_model') + // as TransactionData?; + // int latestTxnBlockHeight = + // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + // as int? ?? + // 0; + // + // final txidsList = DB.instance + // .get(boxName: walletId, key: "cachedTxids") as List? ?? + // []; + // + // final Set cachedTxids = Set.from(txidsList); + + // TODO: filter to skip cached + confirmed txn processing in next step + // final unconfirmedCachedTransactions = + // cachedTransactions?.getAllTransactions() ?? {}; + // unconfirmedCachedTransactions + // .removeWhere((key, value) => value.confirmedStatus); + // + // if (cachedTransactions != null) { + // for (final tx in allTxHashes.toList(growable: false)) { + // final txHeight = tx["height"] as int; + // if (txHeight > 0 && + // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + // allTxHashes.remove(tx); + // } + // } + // } + // } + + final List> txnsData = []; + + if (transactions != null) { + for (var tx in transactions.entries) { + Address? address; + TransactionType type; + if (tx.value.direction == TransactionDirection.incoming) { + final addressInfo = tx.value.additionalInfo; + + final addressString = cwWalletBase?.getTransactionAddress( + addressInfo!['accountIndex'] as int, + addressInfo['addressIndex'] as int, + ); + + if (addressString != null) { + address = await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(addressString) + .findFirst(); + } + + type = TransactionType.incoming; + } else { + // txn.address = ""; + type = TransactionType.outgoing; + } + + final txn = Transaction( + walletId: walletId, + txid: tx.value.id, + timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000), + type: type, + subType: TransactionSubType.none, + amount: tx.value.amount ?? 0, + amountString: Amount( + rawValue: BigInt.from(tx.value.amount ?? 0), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + fee: tx.value.fee ?? 0, + height: tx.value.height, + isCancelled: false, + isLelantus: false, + slateId: null, + otherData: null, + nonce: null, + inputs: [], + outputs: [], + numberOfMessages: null, + ); + + txnsData.add(Tuple2(txn, address)); + } + } + + await mainDB.addNewTransactionData(txnsData, walletId); + } + + @override + 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 { + 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); + credentials = xmr_dart.monero.createMoneroNewWalletCredentials( + name: name, + language: "English", + ); + + 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: '', + ); + credentials.walletInfo = walletInfo; + + final _walletCreationService = WalletCreationService( + secureStorage: secureStorageInterface, + walletService: cwWalletService, + keyService: cwKeysStorage, + ); + _walletCreationService.type = WalletType.monero; + // To restore from a seed + final wallet = await _walletCreationService.create(credentials); + + // subtract a couple days to ensure we have a buffer for SWB + final bufferedCreateHeight = xmr_dart.monero.getHeigthByDate( + date: DateTime.now().subtract(const Duration(days: 2))); + + await info.updateRestoreHeight( + newRestoreHeight: bufferedCreateHeight, + isar: mainDB.isar, + ); + + // special case for xmr/wow. Normally mnemonic + passphrase is saved + // before wallet.init() is called + await secureStorageInterface.write( + key: Wallet.mnemonicKey(walletId: walletId), + value: wallet.seed.trim(), + ); + await secureStorageInterface.write( + key: Wallet.mnemonicPassphraseKey(walletId: walletId), + value: "", + ); + + walletInfo.restoreHeight = bufferedCreateHeight; + + walletInfo.address = wallet.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + + cwWalletBase?.close(); + cwWalletBase = wallet as MoneroWalletBase; + unawaited(_start()); + } 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) { + await refreshMutex.protect(() async { + // clear blockchain info + await mainDB.deleteWalletBlockchainData(walletId); + + var restoreHeight = cwWalletBase?.walletInfo.restoreHeight; + _highestPercentCached = 0; + await cwWalletBase?.rescan(height: restoreHeight); + }); + await refresh(); + return; + } + + await refreshMutex.protect(() async { + final mnemonic = await getMnemonic(); + final seedLength = mnemonic.trim().split(" ").length; + + if (seedLength != 25) { + throw Exception("Invalid monero mnemonic length found: $seedLength"); + } + + try { + int height = info.restoreHeight; + + // 25 word seed. TODO validate + if (height == 0) { + height = xmr_dart.monero.getHeigthByDate( + date: DateTime.now().subtract( + const Duration( + // subtract a couple days to ensure we have a buffer for SWB + days: 2, + ), + ), + ); + } + + // TODO: info.updateRestoreHeight + // await DB.instance + // .put(boxName: walletId, key: "restoreHeight", value: height); + + 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); + credentials = + xmr_dart.monero.createMoneroRestoreWalletFromSeedCredentials( + name: name, + height: height, + mnemonic: mnemonic.trim(), + ); + 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: ''); + credentials.walletInfo = walletInfo; + + cwWalletCreationService = WalletCreationService( + secureStorage: secureStorageInterface, + walletService: cwWalletService, + keyService: cwKeysStorage, + ); + cwWalletCreationService!.changeWalletType(); + // To restore from a seed + final wallet = + await cwWalletCreationService!.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + cwWalletBase?.close(); + cwWalletBase = wallet as MoneroWalletBase; + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Fatal); + } + await updateNode(); + + await cwWalletBase?.rescan(height: credentials.height); + cwWalletBase?.close(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + }); + } + + @override + Future prepareSend({required TxData txData}) async { + try { + final feeRate = txData.feeRateType; + if (feeRate is FeeRateType) { + MoneroTransactionPriority feePriority; + switch (feeRate) { + case FeeRateType.fast: + feePriority = MoneroTransactionPriority.fast; + break; + case FeeRateType.average: + feePriority = MoneroTransactionPriority.regular; + break; + case FeeRateType.slow: + feePriority = MoneroTransactionPriority.slow; + break; + default: + throw ArgumentError("Invalid use of custom fee"); + } + + Future? awaitPendingTransaction; + try { + // check for send all + bool isSendAll = false; + final balance = await _availableBalance; + if (txData.amount! == balance && + txData.recipients!.first.amount == balance) { + isSendAll = true; + } + + List outputs = []; + for (final recipient in txData.recipients!) { + final output = monero_output.Output(cwWalletBase!); + output.address = recipient.address; + output.sendAll = isSendAll; + String amountToSend = recipient.amount.decimal.toString(); + output.setCryptoAmount(amountToSend); + } + + final tmp = + xmr_dart.monero.createMoneroTransactionCreationCredentials( + outputs: outputs, + priority: feePriority, + ); + + await prepareSendMutex.protect(() async { + awaitPendingTransaction = cwWalletBase!.createTransaction(tmp); + }); + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Warning); + } + + PendingMoneroTransaction pendingMoneroTransaction = + await (awaitPendingTransaction!) as PendingMoneroTransaction; + final realFee = Amount.fromDecimal( + Decimal.parse(pendingMoneroTransaction.feeFormatted), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + return txData.copyWith( + fee: realFee, + pendingMoneroTransaction: pendingMoneroTransaction, + ); + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", + level: LogLevel.Info); + + if (e.toString().contains("Incorrect unlocked balance")) { + throw Exception("Insufficient balance!"); + } else if (e is CreationTransactionException) { + throw Exception("Insufficient funds to pay for transaction fee!"); + } else { + throw Exception("Transaction failed with error code $e"); + } + } + } + + @override + Future confirmSend({required TxData txData}) async { + try { + try { + await txData.pendingMoneroTransaction!.commit(); + Logging.instance.log( + "transaction ${txData.pendingMoneroTransaction!.id} has been sent", + level: LogLevel.Info); + return txData.copyWith(txid: txData.pendingMoneroTransaction!.id); + } catch (e, s) { + Logging.instance.log("${info.name} monero confirmSend: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Info); + rethrow; + } + } + + // ====== 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 { + try { + int runningBalance = 0; + for (final entry in cwWalletBase!.balance!.entries) { + runningBalance += entry.value.unlockedBalance; + } + return Amount( + rawValue: BigInt.from(runningBalance), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } catch (_) { + return info.cachedBalance.spendable; + } + } + + Future get _totalBalance async { + try { + final balanceEntries = cwWalletBase?.balance?.entries; + if (balanceEntries != null) { + int bal = 0; + for (var element in balanceEntries) { + bal = bal + element.value.fullBalance; + } + return Amount( + rawValue: BigInt.from(bal), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } else { + final transactions = cwWalletBase!.transactionHistory!.transactions; + int transactionBalance = 0; + for (var tx in transactions!.entries) { + if (tx.value.direction == TransactionDirection.incoming) { + transactionBalance += tx.value.amount!; + } else { + transactionBalance += -tx.value.amount! - tx.value.fee!; + } + } + + return Amount( + rawValue: BigInt.from(transactionBalance), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + } catch (_) { + 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 8f5747d68..a1c322c47 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -42,6 +42,7 @@ 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:tuple/tuple.dart'; @@ -312,6 +313,7 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { } cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) as WowneroWalletBase; + unawaited(_start()); } else { WalletInfo walletInfo; WalletCredentials credentials; @@ -358,11 +360,21 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { // 2))); // subtract a couple days to ensure we have a buffer for SWB final bufferedCreateHeight = getSeedHeightSync(wallet!.seed.trim()); - // TODO: info.updateRestoreHeight - await DB.instance.put( - boxName: walletId, - key: "restoreHeight", - value: bufferedCreateHeight); + await info.updateRestoreHeight( + newRestoreHeight: bufferedCreateHeight, + isar: mainDB.isar, + ); + + // special case for xmr/wow. Normally mnemonic + passphrase is saved + // before wallet.init() is called + await secureStorageInterface.write( + key: Wallet.mnemonicKey(walletId: walletId), + value: wallet.seed.trim(), + ); + await secureStorageInterface.write( + key: Wallet.mnemonicPassphraseKey(walletId: walletId), + value: "", + ); walletInfo.restoreHeight = bufferedCreateHeight; @@ -372,6 +384,7 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { cwWalletBase?.close(); cwWalletBase = wallet as WowneroWalletBase; + unawaited(_start()); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); cwWalletBase?.close(); @@ -379,7 +392,7 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { await updateNode(); await cwWalletBase?.startSync(); - cwWalletBase?.close(); + // cwWalletBase?.close(); } return super.init(); @@ -723,6 +736,31 @@ 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(); @@ -742,7 +780,7 @@ class WowneroWallet extends CryptonoteWallet with MultiAddressInterface { void syncStatusChanged() async { final syncStatus = cwWalletBase?.syncStatus; if (syncStatus != null) { - if (syncStatus.progress() == 1) { + if (syncStatus.progress() == 1 && refreshMutex.isLocked) { refreshMutex.release(); } diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 1ad13553c..8a8846a4a 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -30,11 +30,13 @@ import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/litecoin_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/namecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/particl_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/tezos_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; import 'package:stackwallet/wallets/wallet/private_key_based_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; @@ -139,14 +141,19 @@ abstract class Wallet { ); if (wallet is MnemonicInterface) { - await secureStorageInterface.write( - key: mnemonicKey(walletId: walletInfo.walletId), - value: mnemonic!, - ); - await secureStorageInterface.write( - key: mnemonicPassphraseKey(walletId: walletInfo.walletId), - value: mnemonicPassphrase!, - ); + if (wallet is CryptonoteWallet) { + // currently a special case due to the xmr/wow libraries handling their + // own mnemonic generation + } else { + await secureStorageInterface.write( + key: mnemonicKey(walletId: walletInfo.walletId), + value: mnemonic!, + ); + await secureStorageInterface.write( + key: mnemonicPassphraseKey(walletId: walletInfo.walletId), + value: mnemonicPassphrase!, + ); + } } if (wallet is PrivateKeyBasedWallet) { @@ -281,6 +288,9 @@ abstract class Wallet { case Coin.litecoinTestNet: return LitecoinWallet(CryptoCurrencyNetwork.test); + case Coin.monero: + return MoneroWallet(CryptoCurrencyNetwork.main); + case Coin.namecoin: return NamecoinWallet(CryptoCurrencyNetwork.main); diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index fcddb9803..04dd7c917 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -97,7 +97,8 @@ class SimpleWalletCard extends ConsumerWidget { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet.info.coin == Coin.monero || wallet.info.coin == Coin.wownero) { - await wallet.init(); + // TODO: this can cause ui lag if awaited + unawaited(wallet.init()); } if (context.mounted) { if (popPrevious) nav.pop();