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: