diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index fd9892cfa..9ae2f4d1a 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit fd9892cfa3ab85d50354f334a18a537b4af7d00e +Subproject commit 9ae2f4d1a1826ac83ef82def915e5dba0350f705 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/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/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index 85269ac90..c3c3ea02a 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -7,6 +7,7 @@ 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'; @@ -26,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'; @@ -72,6 +74,26 @@ 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 @@ -614,8 +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 = cwWalletBase!.createTransaction(tmp); + awaitPendingTransaction = cwWalletBase!.createTransaction( + tmp, + inputs: inputs, + ); }); } catch (e, s) { Logging.instance.log( diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index f79564614..40c8cd644 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -7,6 +7,7 @@ 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'; @@ -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,6 +76,26 @@ 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 @@ -660,8 +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 = cwWalletBase!.createTransaction(tmp); + awaitPendingTransaction = cwWalletBase!.createTransaction( + tmp, + inputs: inputs, + ); }); } catch (e, s) { Logging.instance.log( diff --git a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart index 3e8f835cf..131bf8f04 100644 --- a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart +++ b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart @@ -1,37 +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); - - // ========== 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 c3c21be91..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'; @@ -76,6 +80,44 @@ mixin CwBasedInterface 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 @@ -246,7 +288,64 @@ mixin CwBasedInterface 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;