CW-766 fix coin freezing (#1751)

* fix coin freezing * fix frozen balance * update monero_c hash * update monero_c hash (after merge) * fix test E make it ready * revert local change * throw error on wow as well * experiment with view model code * move view model into di.dart

cyan committed Nov 27, 2024 at 16:34 UTC 87178b2a54481b4c9498c093b06002f0f2e0d5b0
8 files changed +92 -28
cw_monero/lib/api/coins_info.dart
+12
@@ -12,6 +12,18 @@ int countOfCoins() => monero.Coins_count(coins!);
12
13 monero.CoinsInfo getCoin(int index) => monero.Coins_coin(coins!, index);
14
15 +int? getCoinByKeyImage(String keyImage) {
16 + final count = countOfCoins();
17 + for (int i = 0; i < count; i++) {
18 + final coin = getCoin(i);
19 + final coinAddress = monero.CoinsInfo_keyImage(coin);
20 + if (keyImage == coinAddress) {
21 + return i;
22 + }
23 + }
24 + return null;
25 +}
26 +
27 void freezeCoin(int index) => monero.Coins_setFrozen(coins!, index: index);
28
29 void thawCoin(int index) => monero.Coins_thaw(coins!, index: index);
cw_monero/lib/api/transaction_history.dart
+6 -1
@@ -6,6 +6,7 @@ import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
6 import 'package:cw_monero/api/monero_output.dart';
7 import 'package:cw_monero/api/structs/pending_transaction.dart';
8 import 'package:cw_monero/api/wallet.dart';
9 +import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
10 import 'package:ffi/ffi.dart';
11 import 'package:monero/monero.dart' as monero;
12 import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
@@ -101,7 +102,11 @@ Future<PendingTransactionDescription> createTransactionSync(
102 });
103
104 final address_ = address.toNativeUtf8();
104 - final paymentId_ = paymentId.toNativeUtf8();
105 + final paymentId_ = paymentId.toNativeUtf8();
106 + if (preferredInputs.isEmpty) {
107 + throw MoneroTransactionCreationException("No inputs provided, transaction cannot be constructed");
108 + }
109 +
110 final preferredInputs_ = preferredInputs.join(monero.defaultSeparatorStr).toNativeUtf8();
111
112 final addraddr = address_.address;
cw_monero/lib/api/wallet.dart
+10
@@ -4,6 +4,7 @@ import 'dart:isolate';
4
5 import 'package:cw_monero/api/account_list.dart';
6 import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
7 +import 'package:flutter/foundation.dart';
8 import 'package:monero/monero.dart' as monero;
9 import 'package:mutex/mutex.dart';
10
@@ -129,6 +130,15 @@ Future<bool> setupNodeSync(
130 throw SetupWalletException(message: error);
131 }
132
133 + if (kDebugMode) {
134 + monero.Wallet_init3(
135 + wptr!, argv0: '',
136 + defaultLogBaseName: 'moneroc',
137 + console: true,
138 + logPath: '',
139 + );
140 + }
141 +
142 return status == 0;
143 }
144
cw_monero/lib/monero_unspent.dart
+23 -1
@@ -1,10 +1,32 @@
1 import 'package:cw_core/unspent_transaction_output.dart';
2 +import 'package:cw_monero/api/coins_info.dart';
3 +import 'package:monero/monero.dart' as monero;
4
5 class MoneroUnspent extends Unspent {
6 MoneroUnspent(
7 String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked)
8 : super(address, hash, value, 0, keyImage) {
7 - this.isFrozen = isFrozen;
9 + }
10 +
11 + @override
12 + set isFrozen(bool freeze) {
13 + print("set isFrozen: $freeze ($keyImage): $freeze");
14 + final coinId = getCoinByKeyImage(keyImage!);
15 + if (coinId == null) throw Exception("Unable to find a coin for address $address");
16 + if (freeze) {
17 + freezeCoin(coinId);
18 + } else {
19 + thawCoin(coinId);
20 + }
21 + }
22 +
23 + @override
24 + bool get isFrozen {
25 + print("get isFrozen");
26 + final coinId = getCoinByKeyImage(keyImage!);
27 + if (coinId == null) throw Exception("Unable to find a coin for address $address");
28 + final coin = getCoin(coinId);
29 + return monero.CoinsInfo_frozen(coin);
30 }
31
32 final bool isUnlocked;
cw_monero/lib/monero_wallet.dart
+17 -23
@@ -309,9 +309,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
309 'You do not have enough XMR to send this amount.');
310 }
311
312 - if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) {
313 - throw MoneroTransactionNoInputsException(inputs.length);
314 - }
312 + if (inputs.isEmpty) MoneroTransactionCreationException(
313 + 'No inputs selected');
314
315 final moneroOutputs = outputs.map((output) {
316 final outputAddress =
@@ -337,23 +336,18 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
336 final formattedAmount =
337 output.sendAll ? null : output.formattedCryptoAmount;
338
340 - if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
341 - (formattedAmount == null && unlockedBalance <= 0)) {
342 - final formattedBalance = moneroAmountToString(amount: unlockedBalance);
343 -
344 - throw MoneroTransactionCreationException(
345 - 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
346 - }
339 + // if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
340 + // (formattedAmount == null && unlockedBalance <= 0)) {
341 + // final formattedBalance = moneroAmountToString(amount: unlockedBalance);
342 + //
343 + // throw MoneroTransactionCreationException(
344 + // 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
345 + // }
346
347 final estimatedFee =
348 calculateEstimatedFee(_credentials.priority, formattedAmount);
350 - if (!spendAllCoins &&
351 - ((formattedAmount != null &&
352 - allInputsAmount < (formattedAmount + estimatedFee)) ||
353 - formattedAmount == null)) {
354 - throw MoneroTransactionNoInputsException(inputs.length);
355 - }
356 -
349 + if (inputs.isEmpty) MoneroTransactionCreationException(
350 + 'No inputs selected');
351 pendingTransactionDescription =
352 await transaction_history.createTransaction(
353 address: address!,
@@ -363,6 +357,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
357 preferredInputs: inputs);
358 }
359
360 + // final status = monero.PendingTransaction_status(pendingTransactionDescription);
361 +
362 return PendingMoneroTransaction(pendingTransactionDescription);
363 }
364
@@ -515,7 +511,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
511 for (var i = 0; i < coinCount; i++) {
512 final coin = getCoin(i);
513 final coinSpent = monero.CoinsInfo_spent(coin);
518 - if (coinSpent == false) {
514 + if (coinSpent == false && monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) {
515 final unspent = MoneroUnspent(
516 monero.CoinsInfo_address(coin),
517 monero.CoinsInfo_hash(coin),
@@ -729,9 +725,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
725
726 void _askForUpdateBalance() {
727 final unlockedBalance = _getUnlockedBalance();
732 - final fullBalance = _getFullBalance();
733 - final frozenBalance = _getFrozenBalance();
734 -
728 + final fullBalance = _getUnlockedBalance() + _getFrozenBalance();
729 + final frozenBalance = 0; // this is calculated on the monero side now
730 if (balance[currency]!.fullBalance != fullBalance ||
731 balance[currency]!.unlockedBalance != unlockedBalance ||
732 balance[currency]!.frozenBalance != frozenBalance) {
@@ -757,9 +752,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
752 for (var coin in unspentCoinsInfo.values.where((element) =>
753 element.walletId == id &&
754 element.accountIndex == walletAddresses.account!.id)) {
760 - if (coin.isFrozen) frozenBalance += coin.value;
755 + if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value;
756 }
762 -
757 return frozenBalance;
758 }
759
cw_wownero/lib/api/transaction_history.dart
+8 -2
@@ -6,13 +6,16 @@ import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart';
6 import 'package:cw_wownero/api/wallet.dart';
7 import 'package:cw_wownero/api/wownero_output.dart';
8 import 'package:cw_wownero/api/structs/pending_transaction.dart';
9 +import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dart';
10 import 'package:ffi/ffi.dart';
11 import 'package:monero/wownero.dart' as wownero;
12 import 'package:monero/src/generated_bindings_wownero.g.dart' as wownero_gen;
13
14
15 String getTxKey(String txId) {
15 - return wownero.Wallet_getTxKey(wptr!, txid: txId);
16 + final ret = wownero.Wallet_getTxKey(wptr!, txid: txId);
17 + wownero.Wallet_status(wptr!);
18 + return ret;
19 }
20
21 wownero.TransactionHistory? txhistory;
@@ -86,7 +89,10 @@ Future<PendingTransactionDescription> createTransactionSync(
89 final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount);
90
91 final address_ = address.toNativeUtf8();
89 - final paymentId_ = paymentId.toNativeUtf8();
92 + final paymentId_ = paymentId.toNativeUtf8();
93 + if (preferredInputs.isEmpty) {
94 + throw WowneroTransactionCreationException("No inputs provided, transaction cannot be constructed");
95 + }
96 final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8();
97
98 final waddr = wptr!.address;
lib/di.dart
+1
@@ -746,6 +746,7 @@ Future<void> setup({
746 _transactionDescriptionBox,
747 getIt.get<AppStore>().wallet!.isHardwareWallet ? getIt.get<LedgerViewModel>() : null,
748 coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any,
749 + getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
750 ),
751 );
752
lib/view_model/send/send_view_model.dart
+15 -1
@@ -19,6 +19,7 @@ import 'package:cake_wallet/tron/tron.dart';
19 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
20 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
21 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
22 +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
23 import 'package:cake_wallet/wownero/wownero.dart';
24 import 'package:cw_core/exceptions.dart';
25 import 'package:cw_core/transaction_info.dart';
@@ -64,6 +65,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
65 wallet.type == WalletType.tron;
66 }
67
68 + UnspentCoinsListViewModel unspentCoinsListViewModel;
69 +
70 SendViewModelBase(
71 AppStore appStore,
72 this.sendTemplateViewModel,
@@ -71,7 +74,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
74 this.balanceViewModel,
75 this.contactListViewModel,
76 this.transactionDescriptionBox,
74 - this.ledgerViewModel, {
77 + this.ledgerViewModel,
78 + this.unspentCoinsListViewModel, {
79 this.coinTypeToSpendFrom = UnspentCoinType.any,
80 }) : state = InitialExecutionState(),
81 currencies = appStore.wallet!.balance.keys.toList(),
@@ -530,6 +534,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
534 throw Exception('Priority is null for wallet type: ${wallet.type}');
535 }
536
537 + if (hasCoinControl) {
538 + bool isCoinSelected = false;
539 + for (var coin in unspentCoinsListViewModel.items) {
540 + isCoinSelected = isCoinSelected || (coin.isSending && !coin.isFrozen);
541 + }
542 + if (!isCoinSelected) {
543 + throw Exception("No coin selected in coin control, you need to select a coin in order to spend");
544 + }
545 + }
546 +
547 switch (wallet.type) {
548 case WalletType.bitcoin:
549 case WalletType.litecoin: