diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 7186b0bb9..66369ef5b 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 7186b0bb936428cba441b69f77b508d007c50fb2 +Subproject commit 66369ef5b432e4d58e76b6cc4f91a5e24eb6b1ea diff --git a/lib/main.dart b/lib/main.dart index c7b3fccac..3dc8f53bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,8 +20,6 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_libmonero/monero/monero.dart'; -import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -237,9 +235,6 @@ void main(List args) async { } } - monero.onStartup(); - wownero.onStartup(); - // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, // overlays: [SystemUiOverlay.bottom]); unawaited(NotificationApi.init()); diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart index 7bbf50896..dd492d9b8 100644 --- a/lib/models/isar/models/blockchain_data/utxo.dart +++ b/lib/models/isar/models/blockchain_data/utxo.dart @@ -8,6 +8,7 @@ * */ +import 'dart:convert'; import 'dart:math'; import 'package:isar/isar.dart'; @@ -84,6 +85,20 @@ class UTXO { return confirmations >= minimumConfirms; } + @ignore + String? get keyImage { + if (otherData == null) { + return null; + } + + try { + final map = jsonDecode(otherData!) as Map; + return map["keyImage"] as String; + } catch (_) { + return null; + } + } + UTXO copyWith({ Id? id, String? walletId, diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index deb38a648..2920bbc48 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -69,6 +69,7 @@ class _AddWalletViewState extends ConsumerState { ...AppConfig.coins.where((e) => e.network == CryptoCurrencyNetwork.main), ]; final List coinEntities = []; + final List coinTestnetEntities = []; final List tokenEntities = []; final bool isDesktop = Util.isDesktop; @@ -139,7 +140,7 @@ class _AddWalletViewState extends ConsumerState { coinEntities.addAll(_coins.map((e) => CoinEntity(e))); if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { - coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); + coinTestnetEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); } if (AppConfig.coins.whereType().isNotEmpty) { @@ -286,6 +287,14 @@ class _AddWalletViewState extends ConsumerState { initialState: ExpandableState.expanded, animationDurationMultiplier: 0.5, ), + if (coinTestnetEntities.isNotEmpty) + ExpandingSubListItem( + title: "Testnet", + entities: + filter(_searchTerm, coinTestnetEntities), + initialState: ExpandableState.expanded, + animationDurationMultiplier: 0.5, + ), if (tokenEntities.isNotEmpty) ExpandingSubListItem( title: "Tokens", @@ -419,6 +428,13 @@ class _AddWalletViewState extends ConsumerState { entities: filter(_searchTerm, coinEntities), initialState: ExpandableState.expanded, ), + if (coinTestnetEntities.isNotEmpty) + ExpandingSubListItem( + title: "Testnet", + entities: + filter(_searchTerm, coinTestnetEntities), + initialState: ExpandableState.expanded, + ), if (tokenEntities.isNotEmpty) ExpandingSubListItem( title: "Tokens", diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index 54a6f6e5a..d2b378f79 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -13,9 +13,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; + import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; -import '../wallet_view/transaction_views/transaction_details_view.dart'; import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; @@ -33,6 +33,7 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; +import '../wallet_view/transaction_views/transaction_details_view.dart'; class UtxoDetailsView extends ConsumerStatefulWidget { const UtxoDetailsView({ diff --git a/lib/services/event_bus/events/global/updated_in_background_event.dart b/lib/services/event_bus/events/global/updated_in_background_event.dart index 582978357..36a23360f 100644 --- a/lib/services/event_bus/events/global/updated_in_background_event.dart +++ b/lib/services/event_bus/events/global/updated_in_background_event.dart @@ -8,6 +8,8 @@ * */ +import 'package:flutter/foundation.dart'; + import '../../../../utilities/logger.dart'; class UpdatedInBackgroundEvent { @@ -15,9 +17,11 @@ class UpdatedInBackgroundEvent { String walletId; UpdatedInBackgroundEvent(this.message, this.walletId) { - Logging.instance.log( - "UpdatedInBackgroundEvent fired with message: $message", - level: LogLevel.Info, - ); + if (kDebugMode) { + Logging.instance.log( + "UpdatedInBackgroundEvent fired with message: $message", + level: LogLevel.Info, + ); + } } } diff --git a/lib/wallets/crypto_currency/coins/wownero.dart b/lib/wallets/crypto_currency/coins/wownero.dart index 8624ecb75..c894bde2e 100644 --- a/lib/wallets/crypto_currency/coins/wownero.dart +++ b/lib/wallets/crypto_currency/coins/wownero.dart @@ -1,4 +1,4 @@ -import 'package:cw_wownero/api/wallet.dart' as wownero_wallet; +import 'package:monero/wownero.dart' as wownero; import '../../../models/node_model.dart'; import '../../../utilities/default_nodes.dart'; @@ -48,7 +48,7 @@ class Wownero extends CryptonoteCurrency { @override bool validateAddress(String address) { - return wownero_wallet.addressValid(address); + return wownero.Wallet_addressValid(address, 0); } @override diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index b84b557f2..c3c3ea02a 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -7,12 +7,14 @@ 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/utxo.dart' as cw; 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_type.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/monero_wallet.dart'; +import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; @@ -25,6 +27,7 @@ import 'package:tuple/tuple.dart'; import '../../../db/hive/db.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../models/keys/cw_key_data.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../../services/event_bus/events/global/tor_status_changed_event.dart'; @@ -71,11 +74,37 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { await updateNode(); }, ); + + // Potentially dangerous hack. See comments in _startInit() + _startInit(); + } + + // cw based wallet listener to handle synchronization of utxo frozen states + late final StreamSubscription> _streamSub; + Future _startInit() async { + // Delay required as `mainDB` is not initialized in constructor. + // This is a hack and could lead to a race condition. + Future.delayed(const Duration(seconds: 2), () { + _streamSub = mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .watch(fireImmediately: true) + .listen((utxos) async { + await onUTXOsCHanged(utxos); + await updateBalance(shouldUpdateUtxos: false); + }); + }); } + @override + MoneroWalletBase? cwWalletBase; + + @override + MoneroWalletService? cwWalletService; + @override Address addressFor({required int index, int account = 0}) { - final String address = (CwBasedInterface.cwWalletBase as MoneroWalletBase) + final String address = (cwWalletBase as MoneroWalletBase) .getTransactionAddress(account, index); final newReceivingAddress = Address( @@ -91,22 +120,8 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { return newReceivingAddress; } - @override - Future exitCwWallet() async { - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)?.onNewBlock = null; - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)?.onNewTransaction = - null; - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)?.syncStatusChanged = - null; - await (CwBasedInterface.cwWalletBase as MoneroWalletBase?) - ?.save(prioritySave: true); - } - @override Future open() async { - // await any previous exit - await CwBasedInterface.exitMutex.protect(() async {}); - String? password; try { password = await cwKeysStorage.getWalletPassword(walletName: walletId); @@ -114,18 +129,20 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { throw Exception("Password not found $e, $s"); } - CwBasedInterface.cwWalletBase?.close(); - CwBasedInterface.cwWalletBase = (await CwBasedInterface.cwWalletService! - .openWallet(walletId, password)) as MoneroWalletBase; + bool wasNull = false; - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)?.onNewBlock = - onNewBlock; - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)?.onNewTransaction = - onNewTransaction; - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)?.syncStatusChanged = - syncStatusChanged; + if (cwWalletBase == null) { + wasNull = true; + // cwWalletBaseT?.close(); + cwWalletBase ??= (await cwWalletService!.openWallet(walletId, password)) + as MoneroWalletBase; - await updateNode(); + cwWalletBase?.onNewBlock ??= onNewBlock; + cwWalletBase?.onNewTransaction ??= onNewTransaction; + cwWalletBase?.syncStatusChanged ??= syncStatusChanged; + + await updateNode(); + } Address? currentAddress = await getCurrentReceivingAddress(); if (currentAddress == null) { @@ -139,19 +156,23 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { ); } - await CwBasedInterface.cwWalletBase?.startSync(); + if (wasNull) { + await cwWalletBase?.startSync(); + } else { + cwWalletBase?.wallet.startListeners(); + } unawaited(refresh()); + autoSaveTimer?.cancel(); autoSaveTimer = Timer.periodic( const Duration(seconds: 193), - (_) async => await CwBasedInterface.cwWalletBase?.save(), + (_) async => await cwWalletBase?.save(), ); } @override Future estimateFeeFor(Amount amount, int feeRate) async { - if (CwBasedInterface.cwWalletBase == null || - CwBasedInterface.cwWalletBase?.syncStatus is! SyncedSyncStatus) { + if (cwWalletBase == null || cwWalletBase?.syncStatus is! SyncedSyncStatus) { return Amount.zeroWith( fractionDigits: cryptoCurrency.fractionDigits, ); @@ -179,7 +200,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { int approximateFee = 0; await estimateFeeMutex.protect(() async { - approximateFee = CwBasedInterface.cwWalletBase!.calculateEstimatedFee( + approximateFee = cwWalletBase!.calculateEstimatedFee( priority, amount.raw.toInt(), ); @@ -193,9 +214,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future pingCheck() async { - return await (CwBasedInterface.cwWalletBase as MoneroWalletBase?) - ?.isConnected() ?? - false; + return await cwWalletBase?.isConnected() ?? false; } @override @@ -212,7 +231,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { } if (_requireMutex) { await _torConnectingLock.protect(() async { - await CwBasedInterface.cwWalletBase?.connectToNode( + await cwWalletBase?.connectToNode( node: Node( uri: "$host:${node.port}", type: WalletType.monero, @@ -224,7 +243,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { ); }); } else { - await CwBasedInterface.cwWalletBase?.connectToNode( + await cwWalletBase?.connectToNode( node: Node( uri: "$host:${node.port}", type: WalletType.monero, @@ -241,11 +260,9 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future getKeys() async { - final base = (CwBasedInterface.cwWalletBase as MoneroWalletBase?); + final base = cwWalletBase; - if (base == null || - base.walletInfo.name != walletId || - CwBasedInterface.exitMutex.isLocked) { + if (base == null || base.walletInfo.name != walletId) { return null; } @@ -260,11 +277,9 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future updateTransactions() async { - final base = (CwBasedInterface.cwWalletBase as MoneroWalletBase?); + final base = cwWalletBase; - if (base == null || - base.walletInfo.name != walletId || - CwBasedInterface.exitMutex.isLocked) { + if (base == null || base.walletInfo.name != walletId) { return; } await base.updateTransactions(); @@ -311,9 +326,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; - final addressString = - (CwBasedInterface.cwWalletBase as MoneroWalletBase?) - ?.getTransactionAddress( + final addressString = cwWalletBase?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); @@ -388,12 +401,11 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future init({bool? isRestore}) async { - await CwBasedInterface.exitMutex.protect(() async {}); - - CwBasedInterface.cwWalletService = xmr_dart.monero - .createMoneroWalletService(DB.instance.moneroWalletInfoBox); + cwWalletService ??= xmr_dart.monero + .createMoneroWalletService(DB.instance.moneroWalletInfoBox) + as MoneroWalletService; - if (!(await CwBasedInterface.cwWalletService!.isWalletExit(walletId)) && + if (!(await cwWalletService!.isWalletExist(walletId)) && isRestore != true) { WalletInfo walletInfo; WalletCredentials credentials; @@ -422,10 +434,10 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { final _walletCreationService = WalletCreationService( secureStorage: secureStorageInterface, - walletService: CwBasedInterface.cwWalletService, + walletService: cwWalletService!, keyService: cwKeysStorage, + type: WalletType.monero, ); - _walletCreationService.type = WalletType.monero; // To restore from a seed final wallet = await _walletCreationService.create(credentials); @@ -459,7 +471,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { wallet.close(); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); - CwBasedInterface.cwWalletBase?.close(); + cwWalletBase?.close(); } await updateNode(); } @@ -469,17 +481,14 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future recover({required bool isRescan}) async { - await CwBasedInterface.exitMutex.protect(() async {}); - if (isRescan) { await refreshMutex.protect(() async { // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); - final restoreHeight = - CwBasedInterface.cwWalletBase?.walletInfo.restoreHeight; + final restoreHeight = cwWalletBase?.walletInfo.restoreHeight; highestPercentCached = 0; - await CwBasedInterface.cwWalletBase?.rescan(height: restoreHeight ?? 0); + await cwWalletBase?.rescan(height: restoreHeight ?? 0); }); unawaited(refresh()); return; @@ -501,8 +510,9 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { isar: mainDB.isar, ); - CwBasedInterface.cwWalletService = xmr_dart.monero - .createMoneroWalletService(DB.instance.moneroWalletInfoBox); + cwWalletService = xmr_dart.monero + .createMoneroWalletService(DB.instance.moneroWalletInfoBox) + as MoneroWalletService; WalletInfo walletInfo; WalletCredentials credentials; final String name = walletId; @@ -531,16 +541,21 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { final cwWalletCreationService = WalletCreationService( secureStorage: secureStorageInterface, - walletService: CwBasedInterface.cwWalletService, + walletService: cwWalletService!, keyService: cwKeysStorage, + type: WalletType.monero, ); - cwWalletCreationService.type = WalletType.monero; // 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; + cwWalletBase?.onNewBlock = onNewBlock; + cwWalletBase?.onNewTransaction = onNewTransaction; + cwWalletBase?.syncStatusChanged = syncStatusChanged; if (walletInfo.address != null) { final newReceivingAddress = await getCurrentReceivingAddress() ?? Address( @@ -559,15 +574,12 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { isar: mainDB.isar, ); } - CwBasedInterface.cwWalletBase?.close(); - CwBasedInterface.cwWalletBase = wallet as MoneroWalletBase; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); } await updateNode(); - await CwBasedInterface.cwWalletBase?.rescan(height: credentials.height); - CwBasedInterface.cwWalletBase?.close(); + await cwWalletBase?.rescan(height: credentials.height); } catch (e, s) { Logging.instance.log( "Exception rethrown from recoverFromMnemonic(): $e\n$s", @@ -610,7 +622,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { final List outputs = []; for (final recipient in txData.recipients!) { - final output = monero_output.Output(CwBasedInterface.cwWalletBase!); + final output = monero_output.Output(cwWalletBase!); output.address = recipient.address; output.sendAll = isSendAll; final String amountToSend = recipient.amount.decimal.toString(); @@ -624,9 +636,31 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { priority: feePriority, ); + final height = await chainHeight; + final inputs = txData.utxos + ?.map( + (e) => cw.UTXO( + address: e.address!, + hash: e.txid, + keyImage: e.keyImage!, + value: e.value, + isFrozen: e.isBlocked, + isUnlocked: e.blockHeight != null && + (height - (e.blockHeight ?? 0)) >= + cryptoCurrency.minConfirms, + height: e.blockHeight ?? 0, + vout: e.vout, + spent: e.used ?? false, + coinbase: e.isCoinbase, + ), + ) + .toList(); + await prepareSendMutex.protect(() async { - awaitPendingTransaction = - CwBasedInterface.cwWalletBase!.createTransaction(tmp); + awaitPendingTransaction = cwWalletBase!.createTransaction( + tmp, + inputs: inputs, + ); }); } catch (e, s) { Logging.instance.log( @@ -694,13 +728,8 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future get availableBalance async { try { - if (CwBasedInterface.exitMutex.isLocked) { - throw Exception("Exit in progress"); - } int runningBalance = 0; - for (final entry in (CwBasedInterface.cwWalletBase as MoneroWalletBase?)! - .balance! - .entries) { + for (final entry in cwWalletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Amount( @@ -715,13 +744,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future get totalBalance async { try { - if (CwBasedInterface.exitMutex.isLocked) { - throw Exception("Exit in progress"); - } - final balanceEntries = - (CwBasedInterface.cwWalletBase as MoneroWalletBase?) - ?.balance - ?.entries; + final balanceEntries = cwWalletBase?.balance?.entries; if (balanceEntries != null) { int bal = 0; for (final element in balanceEntries) { @@ -732,10 +755,7 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { fractionDigits: cryptoCurrency.fractionDigits, ); } else { - final transactions = - (CwBasedInterface.cwWalletBase as MoneroWalletBase?)! - .transactionHistory! - .transactions; + final transactions = cwWalletBase!.transactionHistory!.transactions; int transactionBalance = 0; for (final tx in transactions!.entries) { if (tx.value.direction == TransactionDirection.incoming) { diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index a3ef7d04b..40c8cd644 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -7,14 +7,15 @@ 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/utxo.dart' as cw; 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_type.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; -import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:cw_wownero/wownero_wallet_service.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' @@ -28,6 +29,7 @@ import 'package:tuple/tuple.dart'; import '../../../db/hive/db.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../models/keys/cw_key_data.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../../services/event_bus/events/global/tor_status_changed_event.dart'; @@ -74,11 +76,37 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { await updateNode(); }, ); + + // Potentially dangerous hack. See comments in _startInit() + _startInit(); } + // cw based wallet listener to handle synchronization of utxo frozen states + late final StreamSubscription> _streamSub; + Future _startInit() async { + // Delay required as `mainDB` is not initialized in constructor. + // This is a hack and could lead to a race condition. + Future.delayed(const Duration(seconds: 2), () { + _streamSub = mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .watch(fireImmediately: true) + .listen((utxos) async { + await onUTXOsCHanged(utxos); + await updateBalance(shouldUpdateUtxos: false); + }); + }); + } + + @override + WowneroWalletBase? cwWalletBase; + + @override + WowneroWalletService? cwWalletService; + @override Address addressFor({required int index, int account = 0}) { - final String address = (CwBasedInterface.cwWalletBase as WowneroWalletBase) + final String address = (cwWalletBase as WowneroWalletBase) .getTransactionAddress(account, index); final newReceivingAddress = Address( @@ -96,8 +124,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future estimateFeeFor(Amount amount, int feeRate) async { - if (CwBasedInterface.cwWalletBase == null || - CwBasedInterface.cwWalletBase?.syncStatus is! SyncedSyncStatus) { + if (cwWalletBase == null || cwWalletBase?.syncStatus is! SyncedSyncStatus) { return Amount.zeroWith( fractionDigits: cryptoCurrency.fractionDigits, ); @@ -152,7 +179,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { // unsure why this delay? await Future.delayed(const Duration(milliseconds: 500)); } catch (e) { - approximateFee = CwBasedInterface.cwWalletBase!.calculateEstimatedFee( + approximateFee = cwWalletBase!.calculateEstimatedFee( priority, amount.raw.toInt(), ); @@ -172,9 +199,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future pingCheck() async { - return await (CwBasedInterface.cwWalletBase as WowneroWalletBase?) - ?.isConnected() ?? - false; + return await cwWalletBase?.isConnected() ?? false; } @override @@ -188,7 +213,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { } if (_requireMutex) { await _torConnectingLock.protect(() async { - await CwBasedInterface.cwWalletBase?.connectToNode( + await cwWalletBase?.connectToNode( node: Node( uri: "$host:${node.port}", type: WalletType.wownero, @@ -200,7 +225,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { ); }); } else { - await CwBasedInterface.cwWalletBase?.connectToNode( + await cwWalletBase?.connectToNode( node: Node( uri: "$host:${node.port}", type: WalletType.wownero, @@ -217,11 +242,9 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future getKeys() async { - final base = (CwBasedInterface.cwWalletBase as WowneroWalletBase?); + final base = cwWalletBase; - if (base == null || - base.walletInfo.name != walletId || - CwBasedInterface.exitMutex.isLocked) { + if (base == null || base.walletInfo.name != walletId) { return null; } @@ -236,11 +259,9 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future updateTransactions() async { - final base = (CwBasedInterface.cwWalletBase as WowneroWalletBase?); + final base = cwWalletBase; - if (base == null || - base.walletInfo.name != walletId || - CwBasedInterface.exitMutex.isLocked) { + if (base == null || base.walletInfo.name != walletId) { return; } await base.updateTransactions(); @@ -287,9 +308,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; - final addressString = - (CwBasedInterface.cwWalletBase as WowneroWalletBase?) - ?.getTransactionAddress( + final addressString = cwWalletBase?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); @@ -364,11 +383,11 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future init({bool? isRestore}) async { - await CwBasedInterface.exitMutex.protect(() async {}); - CwBasedInterface.cwWalletService = wow_dart.wownero - .createWowneroWalletService(DB.instance.moneroWalletInfoBox); + cwWalletService = wow_dart.wownero + .createWowneroWalletService(DB.instance.moneroWalletInfoBox) + as WowneroWalletService; - if (!(await CwBasedInterface.cwWalletService!.isWalletExit(walletId)) && + if (!(await cwWalletService!.isWalletExist(walletId)) && isRestore != true) { WalletInfo walletInfo; WalletCredentials credentials; @@ -398,15 +417,16 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { final _walletCreationService = WalletCreationService( secureStorage: secureStorageInterface, - walletService: CwBasedInterface.cwWalletService, + walletService: cwWalletService!, keyService: cwKeysStorage, + type: WalletType.wownero, ); - // _walletCreationService.changeWalletType(); - _walletCreationService.type = WalletType.wownero; // To restore from a seed - final wallet = await _walletCreationService.create(credentials); + final wallet = await _walletCreationService.create(credentials) + as WowneroWalletBase; - final height = wownerodart.Wallet_getRefreshFromBlockHeight(wptr!); + final height = + wownerodart.Wallet_getRefreshFromBlockHeight(wallet.wallet.wptr); await info.updateRestoreHeight( newRestoreHeight: height, @@ -433,7 +453,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { wallet.close(); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); - CwBasedInterface.cwWalletBase?.close(); + cwWalletBase?.close(); } await updateNode(); } @@ -443,9 +463,6 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future open() async { - // await any previous exit - await CwBasedInterface.exitMutex.protect(() async {}); - String? password; try { password = await cwKeysStorage.getWalletPassword(walletName: walletId); @@ -453,18 +470,20 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { throw Exception("Password not found $e, $s"); } - CwBasedInterface.cwWalletBase?.close(); - CwBasedInterface.cwWalletBase = (await CwBasedInterface.cwWalletService! - .openWallet(walletId, password)) as WowneroWalletBase; + bool wasNull = false; - (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.onNewBlock = - onNewBlock; - (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.onNewTransaction = - onNewTransaction; - (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.syncStatusChanged = - syncStatusChanged; + if (cwWalletBase == null) { + wasNull = true; + // cwWalletBaseT?.close(); + cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) + as WowneroWalletBase; - await updateNode(); + cwWalletBase?.onNewBlock = onNewBlock; + cwWalletBase?.onNewTransaction = onNewTransaction; + cwWalletBase?.syncStatusChanged = syncStatusChanged; + + await updateNode(); + } Address? currentAddress = await getCurrentReceivingAddress(); if (currentAddress == null) { @@ -478,39 +497,29 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { ); } - await (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.startSync(); + if (wasNull) { + await cwWalletBase?.startSync(); + } else { + cwWalletBase?.wallet.startListeners(); + } unawaited(refresh()); - autoSaveTimer?.cancel(); + autoSaveTimer = Timer.periodic( const Duration(seconds: 193), - (_) async => await CwBasedInterface.cwWalletBase?.save(), + (_) async => await cwWalletBase?.save(), ); } - @override - Future exitCwWallet() async { - (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.onNewBlock = null; - (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.onNewTransaction = - null; - (CwBasedInterface.cwWalletBase as WowneroWalletBase?)?.syncStatusChanged = - null; - await (CwBasedInterface.cwWalletBase as WowneroWalletBase?) - ?.save(prioritySave: true); - } - @override Future recover({required bool isRescan}) async { - await CwBasedInterface.exitMutex.protect(() async {}); - if (isRescan) { await refreshMutex.protect(() async { // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); - final restoreHeight = - CwBasedInterface.cwWalletBase?.walletInfo.restoreHeight; + final restoreHeight = cwWalletBase?.walletInfo.restoreHeight; highestPercentCached = 0; - await CwBasedInterface.cwWalletBase?.rescan(height: restoreHeight ?? 0); + await cwWalletBase?.rescan(height: restoreHeight ?? 0); }); unawaited(refresh()); return; @@ -534,12 +543,14 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { height = max(height, 0); } - // TODO: info.updateRestoreHeight - // await DB.instance - // .put(boxName: walletId, key: "restoreHeight", value: height); + await info.updateRestoreHeight( + newRestoreHeight: height, + isar: mainDB.isar, + ); - CwBasedInterface.cwWalletService = wow_dart.wownero - .createWowneroWalletService(DB.instance.moneroWalletInfoBox); + cwWalletService = wow_dart.wownero + .createWowneroWalletService(DB.instance.moneroWalletInfoBox) + as WowneroWalletService; WalletInfo walletInfo; WalletCredentials credentials; final String name = walletId; @@ -569,14 +580,15 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { final cwWalletCreationService = WalletCreationService( secureStorage: secureStorageInterface, - walletService: CwBasedInterface.cwWalletService, + walletService: cwWalletService!, keyService: cwKeysStorage, + type: WalletType.wownero, ); - cwWalletCreationService.type = WalletType.wownero; // To restore from a seed final wallet = await cwWalletCreationService .restoreFromSeed(credentials) as WowneroWalletBase; - height = wownerodart.Wallet_getRefreshFromBlockHeight(wptr!); + height = + wownerodart.Wallet_getRefreshFromBlockHeight(wallet.wallet.wptr); walletInfo.address = wallet.walletAddresses.address; walletInfo.restoreHeight = height; await info.updateRestoreHeight( @@ -585,8 +597,11 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { ); await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); - CwBasedInterface.cwWalletBase?.close(); - CwBasedInterface.cwWalletBase = wallet; + cwWalletBase?.close(); + cwWalletBase = wallet; + cwWalletBase?.onNewBlock = onNewBlock; + cwWalletBase?.onNewTransaction = onNewTransaction; + cwWalletBase?.syncStatusChanged = syncStatusChanged; if (walletInfo.address != null) { final newReceivingAddress = await getCurrentReceivingAddress() ?? Address( @@ -610,8 +625,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { } await updateNode(); - await CwBasedInterface.cwWalletBase?.rescan(height: credentials.height); - CwBasedInterface.cwWalletBase?.close(); + await cwWalletBase?.rescan(height: credentials.height); } catch (e, s) { Logging.instance.log( "Exception rethrown from recoverFromMnemonic(): $e\n$s", @@ -654,8 +668,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { final List outputs = []; for (final recipient in txData.recipients!) { - final output = - wownero_output.Output(CwBasedInterface.cwWalletBase!); + final output = wownero_output.Output(cwWalletBase!); output.address = recipient.address; output.sendAll = isSendAll; final String amountToSend = recipient.amount.decimal.toString(); @@ -669,9 +682,31 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { priority: feePriority, ); + final height = await chainHeight; + final inputs = txData.utxos + ?.map( + (e) => cw.UTXO( + address: e.address!, + hash: e.txid, + keyImage: e.keyImage!, + value: e.value, + isFrozen: e.isBlocked, + isUnlocked: e.blockHeight != null && + (height - (e.blockHeight ?? 0)) >= + cryptoCurrency.minConfirms, + height: e.blockHeight ?? 0, + vout: e.vout, + spent: e.used ?? false, + coinbase: e.isCoinbase, + ), + ) + .toList(); + await prepareSendMutex.protect(() async { - awaitPendingTransaction = - CwBasedInterface.cwWalletBase!.createTransaction(tmp); + awaitPendingTransaction = cwWalletBase!.createTransaction( + tmp, + inputs: inputs, + ); }); } catch (e, s) { Logging.instance.log( @@ -739,14 +774,8 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future get availableBalance async { try { - if (CwBasedInterface.exitMutex.isLocked) { - throw Exception("Exit in progress"); - } - int runningBalance = 0; - for (final entry in (CwBasedInterface.cwWalletBase as WowneroWalletBase?)! - .balance! - .entries) { + for (final entry in cwWalletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Amount( @@ -761,13 +790,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { @override Future get totalBalance async { try { - if (CwBasedInterface.exitMutex.isLocked) { - throw Exception("Exit in progress"); - } - final balanceEntries = - (CwBasedInterface.cwWalletBase as WowneroWalletBase?) - ?.balance - ?.entries; + final balanceEntries = cwWalletBase?.balance?.entries; if (balanceEntries != null) { int bal = 0; for (final element in balanceEntries) { @@ -778,8 +801,7 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { fractionDigits: cryptoCurrency.fractionDigits, ); } else { - final transactions = - CwBasedInterface.cwWalletBase!.transactionHistory!.transactions; + final transactions = cwWalletBase!.transactionHistory!.transactions; int transactionBalance = 0; for (final tx in transactions!.entries) { if (tx.value.direction == TransactionDirection.incoming) { diff --git a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart index aabee237b..131bf8f04 100644 --- a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart +++ b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart @@ -1,51 +1,9 @@ -import 'dart:async'; - import '../../crypto_currency/intermediate/cryptonote_currency.dart'; -import '../../models/tx_data.dart'; import '../wallet.dart'; +import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/mnemonic_interface.dart'; abstract class CryptonoteWallet extends Wallet - with MnemonicInterface { + with MnemonicInterface, CoinControlInterface { CryptonoteWallet(super.currency); - - Completer? walletOpenCompleter; - - void resetWalletOpenCompleter() { - if (walletOpenCompleter == null || walletOpenCompleter!.isCompleted) { - walletOpenCompleter = Completer(); - } - } - - Future waitForWalletOpen() async { - if (walletOpenCompleter != null && !walletOpenCompleter!.isCompleted) { - await walletOpenCompleter!.future; - } - } - - // ========== Overrides ====================================================== - - @override - Future confirmSend({required TxData txData}) { - // TODO: implement confirmSend - throw UnimplementedError(); - } - - @override - Future prepareSend({required TxData txData}) { - // TODO: implement prepareSend - throw UnimplementedError(); - } - - @override - Future recover({required bool isRescan}) { - // TODO: implement recover - throw UnimplementedError(); - } - - @override - Future updateUTXOs() async { - // do nothing for now - return false; - } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart index 36cd710fd..f72ec36b2 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart @@ -1,7 +1,7 @@ -import '../../crypto_currency/intermediate/bip39_hd_currency.dart'; -import '../intermediate/bip39_hd_wallet.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../wallet.dart'; -mixin CoinControlInterface on Bip39HDWallet { +mixin CoinControlInterface on Wallet { // any required here? // currently only used to id which wallets support coin control } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart index 661b10bd3..ba054fa1f 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utxo.dart' as cw; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; @@ -14,6 +16,8 @@ import 'package:mutex/mutex.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../models/keys/cw_key_data.dart'; import '../../../models/paymint/fee_object_model.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; @@ -29,7 +33,8 @@ import '../../isar/models/wallet_info.dart'; import '../intermediate/cryptonote_wallet.dart'; import 'multi_address_interface.dart'; -mixin CwBasedInterface on CryptonoteWallet +mixin CwBasedInterface on CryptonoteWallet implements MultiAddressInterface { final prepareSendMutex = Mutex(); final estimateFeeMutex = Mutex(); @@ -38,10 +43,6 @@ mixin CwBasedInterface on CryptonoteWallet KeyService get cwKeysStorage => _cwKeysStorageCached ??= KeyService(secureStorageInterface); - static WalletService? cwWalletService; - static WalletBase? cwWalletBase; - - bool _hasCalledExit = false; bool _txRefreshLock = false; int _lastCheckedHeight = -1; int _txCount = 0; @@ -79,6 +80,44 @@ mixin CwBasedInterface on CryptonoteWallet _refreshTxDataHelper(); } + final _utxosUpdateLock = Mutex(); + Future onUTXOsCHanged(List utxos) async { + await _utxosUpdateLock.protect(() async { + final cwUtxos = cwWalletBase?.utxos ?? []; + + bool changed = false; + + for (final cw in cwUtxos) { + final match = utxos.where( + (e) => + e.keyImage != null && + e.keyImage!.isNotEmpty && + e.keyImage == cw.keyImage, + ); + + if (match.isNotEmpty) { + final u = match.first; + + if (u.isBlocked) { + if (!cw.isFrozen) { + await cwWalletBase?.freeze(cw.keyImage); + changed = true; + } + } else { + if (cw.isFrozen) { + await cwWalletBase?.thaw(cw.keyImage); + changed = true; + } + } + } + } + + if (changed) { + await cwWalletBase?.updateUTXOs(); + } + }); + } + void onNewTransaction() { // TODO: [prio=low] get rid of UpdatedInBackgroundEvent and move to // adding the v2 tx to the db which would update ui automagically since the @@ -94,6 +133,7 @@ mixin CwBasedInterface on CryptonoteWallet void syncStatusChanged() async { final syncStatus = cwWalletBase?.syncStatus; + if (syncStatus != null) { if (syncStatus.progress() == 1 && refreshMutex.isLocked) { refreshMutex.release(); @@ -188,11 +228,12 @@ mixin CwBasedInterface on CryptonoteWallet // ============ Interface ==================================================== + U? get cwWalletBase; + V? get cwWalletService; + Future get availableBalance; Future get totalBalance; - Future exitCwWallet(); - Future open(); Address addressFor({required int index, int account = 0}); @@ -247,7 +288,64 @@ mixin CwBasedInterface on CryptonoteWallet FilterOperation? get receivingAddressFilterOperation => null; @override - Future updateBalance() async { + Future updateUTXOs() async { + await cwWalletBase?.updateUTXOs(); + + final List outputArray = []; + for (final rawUTXO in (cwWalletBase?.utxos ?? [])) { + if (!rawUTXO.spent) { + final current = await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .voutEqualTo(rawUTXO.vout) + .and() + .txidEqualTo(rawUTXO.hash) + .findFirst(); + final tx = await mainDB.isar.transactions + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(rawUTXO.hash) + .findFirst(); + + final otherDataMap = { + "keyImage": rawUTXO.keyImage, + "spent": rawUTXO.spent, + }; + + final utxo = UTXO( + address: rawUTXO.address, + walletId: walletId, + txid: rawUTXO.hash, + vout: rawUTXO.vout, + value: rawUTXO.value, + name: current?.name ?? "", + isBlocked: current?.isBlocked ?? rawUTXO.isFrozen, + blockedReason: current?.blockedReason ?? "", + isCoinbase: rawUTXO.coinbase, + blockHash: "", + blockHeight: + tx?.height ?? (rawUTXO.height > 0 ? rawUTXO.height : null), + blockTime: tx?.timestamp, + otherData: jsonEncode(otherDataMap), + ); + + outputArray.add(utxo); + } + } + + await mainDB.updateUTXOs(walletId, outputArray); + + return true; + } + + @override + Future updateBalance({bool shouldUpdateUtxos = true}) async { + if (shouldUpdateUtxos) { + await updateUTXOs(); + } + final total = await totalBalance; final available = await availableBalance; @@ -303,20 +401,11 @@ mixin CwBasedInterface on CryptonoteWallet } } - static Mutex exitMutex = Mutex(); - @override Future exit() async { - if (!_hasCalledExit) { - await exitMutex.protect(() async { - _hasCalledExit = true; - autoSaveTimer?.cancel(); - await exitCwWallet(); - cwWalletBase?.close(); - cwWalletBase = null; - cwWalletService = null; - }); - } + autoSaveTimer?.cancel(); + await cwWalletBase?.save(); + cwWalletBase?.close(); } @override