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
This commit is contained in:
cyan 2024-11-27 16:34:36 +01:00 committed by GitHub
parent 9706243c81
commit 87178b2a54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 92 additions and 28 deletions

View file

@ -12,6 +12,18 @@ int countOfCoins() => monero.Coins_count(coins!);
monero.CoinsInfo getCoin(int index) => monero.Coins_coin(coins!, index); monero.CoinsInfo getCoin(int index) => monero.Coins_coin(coins!, index);
int? getCoinByKeyImage(String keyImage) {
final count = countOfCoins();
for (int i = 0; i < count; i++) {
final coin = getCoin(i);
final coinAddress = monero.CoinsInfo_keyImage(coin);
if (keyImage == coinAddress) {
return i;
}
}
return null;
}
void freezeCoin(int index) => monero.Coins_setFrozen(coins!, index: index); void freezeCoin(int index) => monero.Coins_setFrozen(coins!, index: index);
void thawCoin(int index) => monero.Coins_thaw(coins!, index: index); void thawCoin(int index) => monero.Coins_thaw(coins!, index: index);

View file

@ -6,6 +6,7 @@ import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/monero_output.dart';
import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart';
import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:monero/monero.dart' as monero; import 'package:monero/monero.dart' as monero;
import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
@ -101,7 +102,11 @@ Future<PendingTransactionDescription> createTransactionSync(
}); });
final address_ = address.toNativeUtf8(); final address_ = address.toNativeUtf8();
final paymentId_ = paymentId.toNativeUtf8(); final paymentId_ = paymentId.toNativeUtf8();
if (preferredInputs.isEmpty) {
throw MoneroTransactionCreationException("No inputs provided, transaction cannot be constructed");
}
final preferredInputs_ = preferredInputs.join(monero.defaultSeparatorStr).toNativeUtf8(); final preferredInputs_ = preferredInputs.join(monero.defaultSeparatorStr).toNativeUtf8();
final addraddr = address_.address; final addraddr = address_.address;

View file

@ -4,6 +4,7 @@ import 'dart:isolate';
import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
import 'package:flutter/foundation.dart';
import 'package:monero/monero.dart' as monero; import 'package:monero/monero.dart' as monero;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -129,6 +130,15 @@ Future<bool> setupNodeSync(
throw SetupWalletException(message: error); throw SetupWalletException(message: error);
} }
if (kDebugMode) {
monero.Wallet_init3(
wptr!, argv0: '',
defaultLogBaseName: 'moneroc',
console: true,
logPath: '',
);
}
return status == 0; return status == 0;
} }

View file

@ -1,10 +1,32 @@
import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_transaction_output.dart';
import 'package:cw_monero/api/coins_info.dart';
import 'package:monero/monero.dart' as monero;
class MoneroUnspent extends Unspent { class MoneroUnspent extends Unspent {
MoneroUnspent( MoneroUnspent(
String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked) String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked)
: super(address, hash, value, 0, keyImage) { : super(address, hash, value, 0, keyImage) {
this.isFrozen = isFrozen; }
@override
set isFrozen(bool freeze) {
print("set isFrozen: $freeze ($keyImage): $freeze");
final coinId = getCoinByKeyImage(keyImage!);
if (coinId == null) throw Exception("Unable to find a coin for address $address");
if (freeze) {
freezeCoin(coinId);
} else {
thawCoin(coinId);
}
}
@override
bool get isFrozen {
print("get isFrozen");
final coinId = getCoinByKeyImage(keyImage!);
if (coinId == null) throw Exception("Unable to find a coin for address $address");
final coin = getCoin(coinId);
return monero.CoinsInfo_frozen(coin);
} }
final bool isUnlocked; final bool isUnlocked;

View file

@ -309,9 +309,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
'You do not have enough XMR to send this amount.'); 'You do not have enough XMR to send this amount.');
} }
if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) { if (inputs.isEmpty) MoneroTransactionCreationException(
throw MoneroTransactionNoInputsException(inputs.length); 'No inputs selected');
}
final moneroOutputs = outputs.map((output) { final moneroOutputs = outputs.map((output) {
final outputAddress = final outputAddress =
@ -337,23 +336,18 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final formattedAmount = final formattedAmount =
output.sendAll ? null : output.formattedCryptoAmount; output.sendAll ? null : output.formattedCryptoAmount;
if ((formattedAmount != null && unlockedBalance < formattedAmount) || // if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
(formattedAmount == null && unlockedBalance <= 0)) { // (formattedAmount == null && unlockedBalance <= 0)) {
final formattedBalance = moneroAmountToString(amount: unlockedBalance); // final formattedBalance = moneroAmountToString(amount: unlockedBalance);
//
throw MoneroTransactionCreationException( // throw MoneroTransactionCreationException(
'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); // 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
} // }
final estimatedFee = final estimatedFee =
calculateEstimatedFee(_credentials.priority, formattedAmount); calculateEstimatedFee(_credentials.priority, formattedAmount);
if (!spendAllCoins && if (inputs.isEmpty) MoneroTransactionCreationException(
((formattedAmount != null && 'No inputs selected');
allInputsAmount < (formattedAmount + estimatedFee)) ||
formattedAmount == null)) {
throw MoneroTransactionNoInputsException(inputs.length);
}
pendingTransactionDescription = pendingTransactionDescription =
await transaction_history.createTransaction( await transaction_history.createTransaction(
address: address!, address: address!,
@ -363,6 +357,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
preferredInputs: inputs); preferredInputs: inputs);
} }
// final status = monero.PendingTransaction_status(pendingTransactionDescription);
return PendingMoneroTransaction(pendingTransactionDescription); return PendingMoneroTransaction(pendingTransactionDescription);
} }
@ -515,7 +511,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
for (var i = 0; i < coinCount; i++) { for (var i = 0; i < coinCount; i++) {
final coin = getCoin(i); final coin = getCoin(i);
final coinSpent = monero.CoinsInfo_spent(coin); final coinSpent = monero.CoinsInfo_spent(coin);
if (coinSpent == false) { if (coinSpent == false && monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) {
final unspent = MoneroUnspent( final unspent = MoneroUnspent(
monero.CoinsInfo_address(coin), monero.CoinsInfo_address(coin),
monero.CoinsInfo_hash(coin), monero.CoinsInfo_hash(coin),
@ -729,9 +725,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
void _askForUpdateBalance() { void _askForUpdateBalance() {
final unlockedBalance = _getUnlockedBalance(); final unlockedBalance = _getUnlockedBalance();
final fullBalance = _getFullBalance(); final fullBalance = _getUnlockedBalance() + _getFrozenBalance();
final frozenBalance = _getFrozenBalance(); final frozenBalance = 0; // this is calculated on the monero side now
if (balance[currency]!.fullBalance != fullBalance || if (balance[currency]!.fullBalance != fullBalance ||
balance[currency]!.unlockedBalance != unlockedBalance || balance[currency]!.unlockedBalance != unlockedBalance ||
balance[currency]!.frozenBalance != frozenBalance) { balance[currency]!.frozenBalance != frozenBalance) {
@ -757,9 +752,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
for (var coin in unspentCoinsInfo.values.where((element) => for (var coin in unspentCoinsInfo.values.where((element) =>
element.walletId == id && element.walletId == id &&
element.accountIndex == walletAddresses.account!.id)) { element.accountIndex == walletAddresses.account!.id)) {
if (coin.isFrozen) frozenBalance += coin.value; if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value;
} }
return frozenBalance; return frozenBalance;
} }

View file

@ -6,13 +6,16 @@ import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/api/wallet.dart';
import 'package:cw_wownero/api/wownero_output.dart'; import 'package:cw_wownero/api/wownero_output.dart';
import 'package:cw_wownero/api/structs/pending_transaction.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart';
import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:monero/wownero.dart' as wownero; import 'package:monero/wownero.dart' as wownero;
import 'package:monero/src/generated_bindings_wownero.g.dart' as wownero_gen; import 'package:monero/src/generated_bindings_wownero.g.dart' as wownero_gen;
String getTxKey(String txId) { String getTxKey(String txId) {
return wownero.Wallet_getTxKey(wptr!, txid: txId); final ret = wownero.Wallet_getTxKey(wptr!, txid: txId);
wownero.Wallet_status(wptr!);
return ret;
} }
wownero.TransactionHistory? txhistory; wownero.TransactionHistory? txhistory;
@ -86,7 +89,10 @@ Future<PendingTransactionDescription> createTransactionSync(
final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount); final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount);
final address_ = address.toNativeUtf8(); final address_ = address.toNativeUtf8();
final paymentId_ = paymentId.toNativeUtf8(); final paymentId_ = paymentId.toNativeUtf8();
if (preferredInputs.isEmpty) {
throw WowneroTransactionCreationException("No inputs provided, transaction cannot be constructed");
}
final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8(); final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8();
final waddr = wptr!.address; final waddr = wptr!.address;

View file

@ -746,6 +746,7 @@ Future<void> setup({
_transactionDescriptionBox, _transactionDescriptionBox,
getIt.get<AppStore>().wallet!.isHardwareWallet ? getIt.get<LedgerViewModel>() : null, getIt.get<AppStore>().wallet!.isHardwareWallet ? getIt.get<LedgerViewModel>() : null,
coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any,
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
), ),
); );

View file

@ -19,6 +19,7 @@ import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/exceptions.dart'; import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
@ -64,6 +65,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
wallet.type == WalletType.tron; wallet.type == WalletType.tron;
} }
UnspentCoinsListViewModel unspentCoinsListViewModel;
SendViewModelBase( SendViewModelBase(
AppStore appStore, AppStore appStore,
this.sendTemplateViewModel, this.sendTemplateViewModel,
@ -71,7 +74,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
this.balanceViewModel, this.balanceViewModel,
this.contactListViewModel, this.contactListViewModel,
this.transactionDescriptionBox, this.transactionDescriptionBox,
this.ledgerViewModel, { this.ledgerViewModel,
this.unspentCoinsListViewModel, {
this.coinTypeToSpendFrom = UnspentCoinType.any, this.coinTypeToSpendFrom = UnspentCoinType.any,
}) : state = InitialExecutionState(), }) : state = InitialExecutionState(),
currencies = appStore.wallet!.balance.keys.toList(), currencies = appStore.wallet!.balance.keys.toList(),
@ -530,6 +534,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
throw Exception('Priority is null for wallet type: ${wallet.type}'); throw Exception('Priority is null for wallet type: ${wallet.type}');
} }
if (hasCoinControl) {
bool isCoinSelected = false;
for (var coin in unspentCoinsListViewModel.items) {
isCoinSelected = isCoinSelected || (coin.isSending && !coin.isFrozen);
}
if (!isCoinSelected) {
throw Exception("No coin selected in coin control, you need to select a coin in order to spend");
}
}
switch (wallet.type) { switch (wallet.type) {
case WalletType.bitcoin: case WalletType.bitcoin:
case WalletType.litecoin: case WalletType.litecoin: