diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart deleted file mode 100644 index 7b3691846..000000000 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ /dev/null @@ -1,1355 +0,0 @@ -/* - * 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_wownero/api/exceptions/creation_transaction_exception.dart'; -import 'package:cw_wownero/api/wallet.dart'; -import 'package:cw_wownero/pending_wownero_transaction.dart'; -import 'package:cw_wownero/wownero_wallet.dart'; -import 'package:decimal/decimal.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_libmonero/core/key_service.dart'; -import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart' - as wownero_output; -import 'package:flutter_libmonero/wownero/wownero.dart'; -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 = 15; - -class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { - WowneroWallet({ - 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; - WowneroWalletBase? 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; - // wow 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 pendingWowneroTransaction = - txData['pendingWowneroTransaction'] as PendingWowneroTransaction; - try { - await pendingWowneroTransaction.commit(); - Logging.instance.log( - "transaction ${pendingWowneroTransaction.id} has been sent", - level: LogLevel.Info); - return pendingWowneroTransaction.id; - } catch (e, s) { - Logging.instance.log("$walletName wownero 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; - 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; - } - var aprox; - await estimateFeeMutex.protect(() async { - { - try { - aprox = (await prepareSend( - // This address is only used for getting an approximate fee, never for sending - address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", - amount: amount, - args: {"feeRate": feeRateType}))['fee']; - await Future.delayed(const Duration(milliseconds: 500)); - } catch (e, s) { - aprox = walletBase!.calculateEstimatedFee( - priority, - amount.raw.toInt(), - ); - } - } - }); - - print("this is the aprox fee $aprox for $amount"); - - if (aprox is Amount) { - return aprox as Amount; - } else { - return Amount( - rawValue: BigInt.from(aprox as int), - 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) { - //todo: check if print needed - // debugPrint("Exception was thrown"); - throw Exception( - "Attempted to initialize an existing wallet using an unknown wallet ID!"); - } - - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStorage); - - await _prefs.init(); - - String? password; - try { - password = await keysStorage?.getWalletPassword(walletName: _walletId); - } catch (e, s) { - throw Exception("Password not found $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as WowneroWalletBase; - - // await _checkCurrentReceivingAddressesForTransactions(); - - Logging.instance.log( - "Opened existing ${coin.prettyName} wallet $walletName", - level: LogLevel.Info, - ); - } - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, { - int seedWordsLength = 14, - }) 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!"); - } - - // TODO: Wallet Service may need to be switched to Wownero - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStorage); - WalletInfo walletInfo; - WalletCredentials credentials; - try { - String name = _walletId; - final dirPath = - await _pathForWalletDir(name: name, type: WalletType.wownero); - final path = await _pathForWallet(name: name, type: WalletType.wownero); - credentials = wownero.createWowneroNewWalletCredentials( - name: name, - language: "English", - seedWordsLength: seedWordsLength, - ); - - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.wownero), - name: name, - type: WalletType.wownero, - 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?.create(credentials); - - final bufferedCreateHeight = (seedWordsLength == 14) - ? getSeedHeightSync(wallet?.seed.trim() as String) - : wownero.getHeightByDate( - date: DateTime.now().subtract(const Duration( - days: - 2))); // subtract a couple days to ensure we have a buffer for SWB - - await DB.instance.put( - boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); - walletInfo.restoreHeight = bufferedCreateHeight; - - 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 WowneroWalletBase; - } catch (e, s) { - 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.wownero, - 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 wow - 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 { - 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("$address $amount $args", level: LogLevel.Info); - String amountToSend = amount.decimal.toString(); - Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); - - wownero_output.Output output = wownero_output.Output(walletBase!); - output.address = address; - output.sendAll = isSendAll; - output.setCryptoAmount(amountToSend); - - List outputs = [output]; - Object tmp = wownero.createWowneroTransactionCreationCredentials( - 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); - } - - PendingWowneroTransaction pendingWowneroTransaction = - await (awaitPendingTransaction!) as PendingWowneroTransaction; - final int realFee = Amount.fromDecimal( - Decimal.parse(pendingWowneroTransaction.feeFormatted), - fractionDigits: coin.decimals, - ).raw.toInt(); - - Map txData = { - "pendingWowneroTransaction": pendingWowneroTransaction, - "fee": realFee, - "addresss": address, - "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 { - final int seedLength = mnemonic.trim().split(" ").length; - if (!(seedLength == 14 || seedLength == 25)) { - throw Exception("Invalid wownero mnemonic length found: $seedLength"); - } - - await _prefs.init(); - longMutex = true; - final start = DateTime.now(); - try { - // 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 ?? "", - ); - - // extract seed height from 14 word seed - if (seedLength == 14) { - height = getSeedHeightSync(mnemonic.trim()); - } else { - // 25 word seed. TODO validate - if (height == 0) { - height = wownero.getHeightByDate( - date: DateTime.now().subtract(const Duration( - days: - 2))); // subtract a couple days to ensure we have a buffer for SWB\ - } - } - - await DB.instance - .put(boxName: walletId, key: "restoreHeight", value: height); - - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStorage); - WalletInfo walletInfo; - WalletCredentials credentials; - String name = _walletId; - final dirPath = - await _pathForWalletDir(name: name, type: WalletType.wownero); - final path = await _pathForWallet(name: name, type: WalletType.wownero); - credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, - height: height, - mnemonic: mnemonic.trim(), - ); - try { - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.wownero), - name: name, - type: WalletType.wownero, - 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 WowneroWalletBase; - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } catch (e, s) { - //todo: come back to this - 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.wownero, - 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 WowneroWalletBase?; - - walletBase!.onNewBlock = onNewBlock; - walletBase!.onNewTransaction = onNewTransaction; - walletBase!.syncStatusChanged = syncStatusChanged; - - if (!(await walletBase!.isConnected())) { - final node = await _getCurrentNode(); - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node( - uri: "$host:${node.port}", - type: WalletType.wownero, - trusted: node.trusted ?? false, - ), - ); - } - await walletBase?.startSync(); - await refresh(); - _autoSaveTimer?.cancel(); - _autoSaveTimer = Timer.periodic( - const Duration(seconds: 193), - (_) async => await walletBase?.save(), - ); - } else { - await exit(); - } - }; - - Future _updateCachedBalance(int sats) async { - await DB.instance.put( - boxName: walletId, - key: "cachedWowneroBalanceSats", - value: sats, - ); - } - - int _getCachedBalance() => - DB.instance.get( - boxName: walletId, - key: "cachedWowneroBalanceSats", - ) 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.wownero, - 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); - - 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); - - // 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) { - // 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); - // String am = wowneroAmountToString(amount: tx.value.amount!); - // final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2); - // Map midSortedTx = {}; - // // // create final tx map - // midSortedTx["txid"] = tx.value.id; - // midSortedTx["confirmed_status"] = !tx.value.isPending && - // tx.value.confirmations != null && - // tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; - // midSortedTx["confirmations"] = tx.value.confirmations ?? 0; - // midSortedTx["timestamp"] = - // (tx.value.date.millisecondsSinceEpoch ~/ 1000); - // midSortedTx["txType"] = - // tx.value.direction == TransactionDirection.incoming - // ? "Received" - // : "Sent"; - // midSortedTx["amount"] = tx.value.amount; - // midSortedTx["worthNow"] = worthNow; - // midSortedTx["worthAtBlockTimestamp"] = worthNow; - // midSortedTx["fees"] = tx.value.fee; - // if (tx.value.direction == TransactionDirection.incoming) { - // final addressInfo = tx.value.additionalInfo; - // - // midSortedTx["address"] = walletBase?.getTransactionAddress( - // addressInfo!['accountIndex'] as int, - // addressInfo['addressIndex'] as int, - // ); - // } else { - // midSortedTx["address"] = ""; - // } - // - // final int txHeight = tx.value.height ?? 0; - // midSortedTx["height"] = txHeight; - // // if (txHeight >= latestTxnBlockHeight) { - // // latestTxnBlockHeight = txHeight; - // // } - // - // midSortedTx["aliens"] = []; - // midSortedTx["inputSize"] = 0; - // midSortedTx["outputSize"] = 0; - // midSortedTx["inputs"] = []; - // midSortedTx["outputs"] = []; - // midSortedArray.add(midSortedTx); - - 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 Wownero Block! :: $walletName"); - print("============================="); - updateCachedChainHeight(height); - _refreshTxDataHelper(); - } - - 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 onNewTransaction() { - // - print("============================="); - print("New Wownero Transaction! :: $walletName"); - print("============================="); - - // call this here? - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - 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); - } - // 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; - } - } - - 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/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 77af4b4d1..981ae0786 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -8,7 +8,6 @@ * */ -import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as btc; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart' as bch; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' @@ -29,7 +28,6 @@ import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart' as xlm; import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart' as tezos; -import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/utilities/constants.dart'; enum Coin { @@ -379,7 +377,7 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: case Coin.bitcoinTestNet: - return btc.MINIMUM_CONFIRMATIONS; + throw UnimplementedError("moved"); case Coin.litecoin: case Coin.litecoinTestNet: @@ -420,7 +418,7 @@ extension CoinExt on Coin { return tezos.MINIMUM_CONFIRMATIONS; case Coin.wownero: - return wow.MINIMUM_CONFIRMATIONS; + throw UnimplementedError("moved"); case Coin.namecoin: return nmc.MINIMUM_CONFIRMATIONS; diff --git a/lib/wallets/crypto_currency/coins/wownero.dart b/lib/wallets/crypto_currency/coins/wownero.dart index 48959ad96..4eb76419b 100644 --- a/lib/wallets/crypto_currency/coins/wownero.dart +++ b/lib/wallets/crypto_currency/coins/wownero.dart @@ -1,19 +1,14 @@ +import 'package:cw_wownero/api/wallet.dart' as wownero_wallet; import 'package:stackwallet/wallets/crypto_currency/intermediate/cryptonote_currency.dart'; class Wownero extends CryptonoteCurrency { Wownero(super.network); @override - // TODO: implement genesisHash - String get genesisHash => throw UnimplementedError(); - - @override - // TODO: implement minConfirms - int get minConfirms => throw UnimplementedError(); + int get minConfirms => 15; @override bool validateAddress(String address) { - // TODO: implement validateAddress - throw UnimplementedError(); + return wownero_wallet.validateAddress(address); } } diff --git a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart index d00b14c03..79d1f4de4 100644 --- a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart @@ -2,4 +2,9 @@ import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; abstract class CryptonoteCurrency extends CryptoCurrency { CryptonoteCurrency(super.network); + + @override + String get genesisHash { + return "not used in stack's cryptonote coins"; + } } diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index eb25b2561..06977823f 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -1,3 +1,4 @@ +import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -38,6 +39,9 @@ class TxData { final BigInt? chainId; final BigInt? feeInWei; + // wownero specific + final PendingWowneroTransaction? pendingWowneroTransaction; + TxData({ this.feeRateType, this.feeRateAmount, @@ -59,6 +63,7 @@ class TxData { this.nonce, this.chainId, this.feeInWei, + this.pendingWowneroTransaction, }); Amount? get amount => recipients != null && recipients!.isNotEmpty @@ -92,6 +97,7 @@ class TxData { int? nonce, BigInt? chainId, BigInt? feeInWei, + PendingWowneroTransaction? pendingWowneroTransaction, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -114,6 +120,8 @@ class TxData { nonce: nonce ?? this.nonce, chainId: chainId ?? this.chainId, feeInWei: feeInWei ?? this.feeInWei, + pendingWowneroTransaction: + pendingWowneroTransaction ?? this.pendingWowneroTransaction, ); } @@ -139,5 +147,6 @@ class TxData { 'nonce: $nonce, ' 'chainId: $chainId, ' 'feeInWei: $feeInWei, ' + 'pendingWowneroTransaction: $pendingWowneroTransaction, ' '}'; } diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index 4a38e759c..11140b866 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -1,54 +1,996 @@ -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; -import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; -class WowneroWallet extends CryptonoteWallet { +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_wownero/api/wallet.dart'; +import 'package:cw_wownero/pending_wownero_transaction.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:flutter_libmonero/core/wallet_creation_service.dart'; +import 'package:flutter_libmonero/view_model/send/output.dart' + as wownero_output; +import 'package:flutter_libmonero/wownero/wownero.dart' as wow_dart; +import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; +import 'package:stackwallet/wallets/wallet/mixins/multi_address.dart'; +import 'package:tuple/tuple.dart'; + +class WowneroWallet extends CryptonoteWallet with MultiAddress { WowneroWallet(Wownero wownero) : super(wownero); + final prepareSendMutex = Mutex(); + final estimateFeeMutex = Mutex(); + + bool _hasCalledExit = false; + + WalletService? cwWalletService; + KeyService? cwKeysStorage; + WowneroWalletBase? cwWalletBase; + WalletCreationService? cwWalletCreationService; + Timer? _autoSaveTimer; + + bool _txRefreshLock = false; + int _lastCheckedHeight = -1; + int _txCount = 0; + int _currentKnownChainHeight = 0; + double _highestPercentCached = 0; + @override - Future estimateFeeFor(Amount amount, int feeRate) { - // TODO: implement estimateFeeFor - throw UnimplementedError(); + 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, + ), + ], + 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 - // TODO: implement fees - Future get fees => throw UnimplementedError(); + 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() { - // TODO: implement pingCheck - throw UnimplementedError(); + Future pingCheck() async { + return await cwWalletBase?.isConnected() ?? false; } @override - Future updateBalance() { - // TODO: implement updateBalance - throw UnimplementedError(); + 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() { - // TODO: implement updateChainHeight - throw UnimplementedError(); + Future updateChainHeight() async { + await info.updateCachedChainHeight( + newHeight: _currentKnownChainHeight, + isar: mainDB.isar, + ); } @override - Future updateNode() { - // TODO: implement updateNode - throw UnimplementedError(); + Future updateNode() async { + final node = getCurrentNode(); + + final host = Uri.parse(node.host).host; + await cwWalletBase?.connectToNode( + node: Node( + uri: "$host:${node.port}", + type: WalletType.wownero, + 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() { - // TODO: implement updateTransactions - throw UnimplementedError(); + 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 updateUTXOs() { - // TODO: implement updateUTXOs - throw UnimplementedError(); + Future init() async { + cwWalletService = wow_dart.wownero + .createWowneroWalletService(DB.instance.moneroWalletInfoBox); + cwKeysStorage = KeyService(secureStorageInterface); + + if (await cwWalletService!.isWalletExit(walletId)) { + String? password; + try { + password = await cwKeysStorage!.getWalletPassword(walletName: walletId); + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } + cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) + as WowneroWalletBase; + } else { + WalletInfo walletInfo; + WalletCredentials credentials; + try { + String name = walletId; + final dirPath = + await _pathForWalletDir(name: name, type: WalletType.wownero); + final path = await _pathForWallet(name: name, type: WalletType.wownero); + credentials = wow_dart.wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + seedWordsLength: 14, + ); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.wownero), + name: name, + type: WalletType.wownero, + 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.changeWalletType(); + _walletCreationService.type = WalletType.wownero; + // To restore from a seed + final wallet = await _walletCreationService.create(credentials); + // + // final bufferedCreateHeight = (seedWordsLength == 14) + // ? getSeedHeightSync(wallet?.seed.trim() as String) + // : wownero.getHeightByDate( + // date: DateTime.now().subtract(const Duration( + // days: + // 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); + + walletInfo.restoreHeight = bufferedCreateHeight; + + walletInfo.address = wallet.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + + cwWalletBase?.close(); + cwWalletBase = wallet as WowneroWalletBase; + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Fatal); + cwWalletBase?.close(); + } + await updateNode(); + await cwWalletBase?.startSync(); + + cwWalletBase?.close(); + } + + return super.init(); } + + @override + Future exit() async { + if (!_hasCalledExit) { + _hasCalledExit = true; + cwWalletBase?.onNewBlock = null; + cwWalletBase?.onNewTransaction = null; + cwWalletBase?.syncStatusChanged = null; + _autoSaveTimer?.cancel(); + await cwWalletBase?.save(prioritySave: true); + cwWalletBase?.close(); + } + } + + @override + Future generateNewReceivingAddress() async { + 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 == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + + try { + int height = info.restoreHeight; + + // extract seed height from 14 word seed + if (seedLength == 14) { + height = getSeedHeightSync(mnemonic.trim()); + } else { + // 25 word seed. TODO validate + if (height == 0) { + height = wow_dart.wownero.getHeightByDate( + 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 = wow_dart.wownero + .createWowneroWalletService(DB.instance.moneroWalletInfoBox); + cwKeysStorage = KeyService(secureStorageInterface); + WalletInfo walletInfo; + WalletCredentials credentials; + String name = walletId; + final dirPath = + await _pathForWalletDir(name: name, type: WalletType.wownero); + final path = await _pathForWallet(name: name, type: WalletType.wownero); + credentials = + wow_dart.wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, + height: height, + mnemonic: mnemonic.trim(), + ); + try { + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.wownero), + name: name, + type: WalletType.wownero, + 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 WowneroWalletBase; + } 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 = wownero_output.Output(cwWalletBase!); + output.address = recipient.address; + output.sendAll = isSendAll; + String amountToSend = recipient.amount.decimal.toString(); + output.setCryptoAmount(amountToSend); + } + + final tmp = + wow_dart.wownero.createWowneroTransactionCreationCredentials( + 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); + } + + PendingWowneroTransaction pendingWowneroTransaction = + await (awaitPendingTransaction!) as PendingWowneroTransaction; + final realFee = Amount.fromDecimal( + Decimal.parse(pendingWowneroTransaction.feeFormatted), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + return txData.copyWith( + fee: realFee, + pendingWowneroTransaction: pendingWowneroTransaction, + ); + } 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.pendingWowneroTransaction!.commit(); + Logging.instance.log( + "transaction ${txData.pendingWowneroTransaction!.id} has been sent", + level: LogLevel.Info); + return txData.copyWith(txid: txData.pendingWowneroTransaction!.id); + } catch (e, s) { + Logging.instance.log("${info.name} wownero 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.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'); + + // TODO: [prio=med/low] is this required? + // bool _isActive = false; + // @override + // void Function(bool)? get onIsActiveWalletChanged => (isActive) async { + // if (_isActive == isActive) { + // return; + // } + // _isActive = isActive; + // + // if (isActive) { + // _hasCalledExit = false; + // String? password; + // try { + // password = + // await keysStorage?.getWalletPassword(walletName: _walletId); + // } catch (e, s) { + // throw Exception("Password not found $e, $s"); + // } + // walletBase = (await walletService?.openWallet(_walletId, password!)) + // as WowneroWalletBase?; + // + // walletBase!.onNewBlock = onNewBlock; + // walletBase!.onNewTransaction = onNewTransaction; + // walletBase!.syncStatusChanged = syncStatusChanged; + // + // if (!(await walletBase!.isConnected())) { + // final node = await _getCurrentNode(); + // final host = Uri.parse(node.host).host; + // await walletBase?.connectToNode( + // node: Node( + // uri: "$host:${node.port}", + // type: WalletType.wownero, + // trusted: node.trusted ?? false, + // ), + // ); + // } + // await walletBase?.startSync(); + // await refresh(); + // _autoSaveTimer?.cancel(); + // _autoSaveTimer = Timer.periodic( + // const Duration(seconds: 193), + // (_) async => await walletBase?.save(), + // ); + // } else { + // await exit(); + // } + // }; } diff --git a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart index fe50f71c3..0b2f5224f 100644 --- a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart +++ b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart @@ -26,4 +26,9 @@ abstract class CryptonoteWallet extends Wallet // TODO: implement recover throw UnimplementedError(); } + + @override + Future updateUTXOs() async { + // do nothing for now + } } diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index e2d1858b7..0bf2951c7 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:mutex/mutex.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -13,6 +14,7 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -20,13 +22,16 @@ import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart'; +import 'package:stackwallet/wallets/wallet/mixins/multi_address.dart'; abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 @@ -41,6 +46,7 @@ abstract class Wallet { late final MainDB mainDB; late final SecureStorageInterface secureStorageInterface; + late final NodeService nodeService; late final Prefs prefs; final refreshMutex = Mutex(); @@ -75,6 +81,11 @@ abstract class Wallet { bool _isConnected = false; + void xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture( + bool flag) { + _isConnected = flag; + } + //============================================================================ // ========== Wallet Info Convenience Getters ================================ @@ -207,10 +218,10 @@ abstract class Wallet { }) async { final Wallet wallet = _loadWallet( walletInfo: walletInfo, - nodeService: nodeService, ); wallet.prefs = prefs; + wallet.nodeService = nodeService; if (wallet is ElectrumXMixin) { // initialize electrumx instance @@ -226,37 +237,36 @@ abstract class Wallet { static Wallet _loadWallet({ required WalletInfo walletInfo, - required NodeService nodeService, }) { switch (walletInfo.coin) { case Coin.bitcoin: return BitcoinWallet( Bitcoin(CryptoCurrencyNetwork.main), - nodeService: nodeService, ); case Coin.bitcoinTestNet: return BitcoinWallet( Bitcoin(CryptoCurrencyNetwork.test), - nodeService: nodeService, ); case Coin.bitcoincash: return BitcoincashWallet( Bitcoincash(CryptoCurrencyNetwork.main), - nodeService: nodeService, ); case Coin.bitcoincashTestnet: return BitcoincashWallet( Bitcoincash(CryptoCurrencyNetwork.test), - nodeService: nodeService, ); case Coin.epicCash: return EpiccashWallet( Epiccash(CryptoCurrencyNetwork.main), - nodeService: nodeService, + ); + + case Coin.wownero: + return WowneroWallet( + Wownero(CryptoCurrencyNetwork.main), ); default: @@ -348,6 +358,13 @@ abstract class Wallet { //=========================================== + NodeModel getCurrentNode() { + final node = nodeService.getPrimaryNodeFor(coin: cryptoCurrency.coin) ?? + DefaultNodes.getNodeFor(cryptoCurrency.coin); + + return node; + } + // Should fire events Future refresh() async { // Awaiting this lock could be dangerous. @@ -388,13 +405,16 @@ abstract class Wallet { // final feeObj = _getFees(); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.60, walletId)); + await utxosRefreshFuture; GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.70, walletId)); // _feeObject = Future(() => feeObj); - await utxosRefreshFuture; + await fetchFuture; GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.80, walletId)); - await fetchFuture; + if (this is MultiAddress) { + await (this as MultiAddress).checkReceivingAddressForTransactions(); + } // await getAllTxsToWatch(); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId)); @@ -458,6 +478,7 @@ abstract class Wallet { newAddress: address!.value, isar: mainDB.isar, ); + // TODO: make sure subclasses override this if they require some set up // especially xmr/wow/epiccash }