diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 6bcde7e75af5..45f5046baaf9 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -325,6 +325,9 @@ class Wallet //! Get available balance. virtual CAmount getAvailableBalance(const wallet::CCoinControl& coin_control) = 0; + //! Get total value of the locked coins counted in the trusted balance. + virtual CAmount getLockedBalance() = 0; + //! Return whether transaction input belongs to wallet. virtual wallet::isminetype txinIsMine(const CTxIn& txin) = 0; @@ -511,7 +514,9 @@ struct WalletBalances return balance != prev.balance || unconfirmed_balance != prev.unconfirmed_balance || anonymized_balance != prev.anonymized_balance || immature_balance != prev.immature_balance || watch_only_balance != prev.watch_only_balance || unconfirmed_watch_only_balance != prev.unconfirmed_watch_only_balance || - immature_watch_only_balance != prev.immature_watch_only_balance; + immature_watch_only_balance != prev.immature_watch_only_balance || + denominated_untrusted_pending != prev.denominated_untrusted_pending || + denominated_trusted != prev.denominated_trusted; } }; diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 9ce945530a5b..e827820cbc25 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -163,8 +163,6 @@ OverviewPage::OverviewPage(QWidget* parent) : GUIUtil::updateFonts(); - m_balances.balance = -1; - // Recent transactions ui->listTransactions->setItemDelegate(txdelegate); // Note: minimum height of listTransactions will be set later in updateAdvancedCJUI() to reflect actual settings @@ -202,8 +200,9 @@ void OverviewPage::setPrivacy(bool privacy) { m_privacy = privacy; clientModel->getOptionsModel()->setOption(OptionsModel::OptionID::MaskValues, privacy); - if (m_balances.balance != -1) { - setBalance(m_balances); + const auto& balances = walletModel->getCachedBalance(); + if (balances.balance != -1) { + setBalance(balances); coinJoinStatus(true); } @@ -226,7 +225,6 @@ OverviewPage::~OverviewPage() void OverviewPage::setBalance(const interfaces::WalletBalances& balances) { BitcoinUnit unit = walletModel->getOptionsModel()->getDisplayUnit(); - m_balances = balances; if (walletModel->wallet().isLegacy()) { if (walletModel->wallet().privateKeysDisabled()) { ui->labelBalance->setText(BitcoinUnits::floorHtmlWithPrivacy(unit, balances.watch_only_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); @@ -312,12 +310,11 @@ void OverviewPage::setWalletModel(WalletModel *model) // update the display unit, to not use the default ("DASH") updateDisplayUnit(); // Keep up to date with wallet - interfaces::Wallet& wallet = model->wallet(); - interfaces::WalletBalances balances = wallet.getBalances(); - setBalance(balances); + setBalance(model->getCachedBalance()); connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance); - updateWatchOnlyLabels((wallet.haveWatchOnly() && !model->wallet().privateKeysDisabled()) || gArgs.GetBoolArg("-debug-ui", false)); + interfaces::Wallet& wallet = model->wallet(); + updateWatchOnlyLabels((wallet.haveWatchOnly() && !wallet.privateKeysDisabled()) || gArgs.GetBoolArg("-debug-ui", false)); connect(model, &WalletModel::notifyWatchonlyChanged, [this](bool showWatchOnly) { updateWatchOnlyLabels(showWatchOnly && !walletModel->wallet().privateKeysDisabled()); }); @@ -348,11 +345,11 @@ void OverviewPage::setWalletModel(WalletModel *model) void OverviewPage::updateDisplayUnit() { - if(walletModel && walletModel->getOptionsModel()) - { + if (walletModel && walletModel->getOptionsModel()) { m_display_bitcoin_unit = walletModel->getOptionsModel()->getDisplayUnit(); - if (m_balances.balance != -1) { - setBalance(m_balances); + const auto& balances = walletModel->getCachedBalance(); + if (balances.balance != -1) { + setBalance(balances); } // Update txdelegate->unit with the current unit @@ -404,7 +401,8 @@ void OverviewPage::updateCoinJoinProgress() QString strAmountAndRounds; QString strCoinJoinAmount = BitcoinUnits::formatHtmlWithUnit(m_display_bitcoin_unit, clientModel->coinJoinOptions().getAmount() * COIN, false, BitcoinUnits::SeparatorStyle::ALWAYS); - if(m_balances.balance == 0) + const auto& balances = walletModel->getCachedBalance(); + if(balances.balance == 0) { ui->coinJoinProgress->setValue(0); ui->coinJoinProgress->setToolTip(tr("No inputs detected")); @@ -420,7 +418,7 @@ void OverviewPage::updateCoinJoinProgress() CAmount nAnonymizableBalance = walletModel->wallet().getAnonymizableBalance(false, false); - CAmount nMaxToAnonymize = nAnonymizableBalance + m_balances.anonymized_balance; + CAmount nMaxToAnonymize = nAnonymizableBalance + balances.anonymized_balance; // If it's more than the anon threshold, limit to that. if (nMaxToAnonymize > clientModel->coinJoinOptions().getAmount() * COIN) nMaxToAnonymize = clientModel->coinJoinOptions().getAmount() * COIN; @@ -451,7 +449,6 @@ void OverviewPage::updateCoinJoinProgress() if (!fShowAdvancedCJUI) return; - const interfaces::WalletBalances balances = walletModel->wallet().getBalances(); CAmount nDenominatedConfirmedBalance = balances.denominated_trusted; CAmount nDenominatedUnconfirmedBalance = balances.denominated_untrusted_pending; CAmount nNormalizedAnonymizedBalance; @@ -477,7 +474,7 @@ void OverviewPage::updateCoinJoinProgress() anonNormPart = anonNormPart > 1 ? 1 : anonNormPart; anonNormPart *= 100; - anonFullPart = (float)m_balances.anonymized_balance / nMaxToAnonymize; + anonFullPart = (float)balances.anonymized_balance / nMaxToAnonymize; anonFullPart = anonFullPart > 1 ? 1 : anonFullPart; anonFullPart *= 100; @@ -692,7 +689,7 @@ void OverviewPage::coinJoinStatus(bool fForce) setWidgetsVisible(true); } -void OverviewPage::toggleCoinJoin(){ +void OverviewPage::toggleCoinJoin() { QSettings settings; // Popup some information on first mixing QString hasMixed = settings.value("hasMixed").toString(); @@ -707,9 +704,10 @@ void OverviewPage::toggleCoinJoin(){ bool mixing{false}; walletModel->withCoinJoin([&](auto& client) { mixing = client.isMixing(); }); if (!mixing) { + const auto& balances = walletModel->getCachedBalance(); auto& options = walletModel->node().coinJoinOptions(); const CAmount nMinAmount = options.getSmallestDenomination() + options.getMaxCollateralAmount(); - if(m_balances.balance < nMinAmount) { + if(balances.balance < nMinAmount) { QString strMinAmount(BitcoinUnits::formatWithUnit(m_display_bitcoin_unit, nMinAmount)); QMessageBox::warning(this, strCoinJoinName, tr("%1 requires at least %2 to use.").arg(strCoinJoinName).arg(strMinAmount), diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index b1a5c2f047ea..a12d74e5705b 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -54,7 +54,6 @@ public Q_SLOTS: Ui::OverviewPage *ui; ClientModel* clientModel{nullptr}; WalletModel* walletModel{nullptr}; - interfaces::WalletBalances m_balances; bool m_privacy{false}; BitcoinUnit m_display_bitcoin_unit; bool fShowAdvancedCJUI; diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 1b25c171abb5..c8073ebdc378 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -176,11 +176,9 @@ void SendCoinsDialog::setModel(WalletModel *_model) } } - interfaces::WalletBalances balances = _model->wallet().getBalances(); - setBalance(balances); connect(_model, &WalletModel::balanceChanged, this, &SendCoinsDialog::setBalance); - connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::updateDisplayUnit); - updateDisplayUnit(); + connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::refreshBalance); + refreshBalance(); // Coin Control connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::coinControlUpdateLabels); @@ -822,9 +820,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances) } } -void SendCoinsDialog::updateDisplayUnit() +void SendCoinsDialog::refreshBalance() { - setBalance(model->wallet().getBalances()); + setBalance(model->getCachedBalance()); coinControlUpdateLabels(); ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit()); updateSmartFeeLabel(); @@ -896,7 +894,7 @@ void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry) m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); // Calculate available amount to send. - CAmount amount = model->wallet().getAvailableBalance(*m_coin_control); + CAmount amount = model->getAvailableBalance(m_coin_control.get()); for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry* e = qobject_cast(ui->entries->itemAt(i)->widget()); if (e && !e->isHidden() && e != entry) { diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index d691f1555195..65a65999a579 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -99,7 +99,7 @@ private Q_SLOTS: void on_buttonMinimizeFee_clicked(); void removeEntry(SendCoinsEntry* entry); void useAvailableBalance(SendCoinsEntry* entry); - void updateDisplayUnit(); + void refreshBalance(); void coinControlFeatureChanged(bool); void coinControlButtonClicked(); void coinControlChangeChecked(int); diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 1394ac1fdea1..3be37a434f34 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -100,6 +100,15 @@ QModelIndex FindTx(const QAbstractItemModel& model, const uint256& txid) return {}; } +void CompareBalance(WalletModel& walletModel, CAmount expected_balance, QLabel* balance_label_to_check, bool use_privacy_formatting) +{ + BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit(); + QString balanceComparison = use_privacy_formatting + ? BitcoinUnits::floorHtmlWithPrivacy(unit, expected_balance, BitcoinUnits::SeparatorStyle::ALWAYS, false) + : BitcoinUnits::formatWithUnit(unit, expected_balance, false/*, BitcoinUnits::SeparatorStyle::ALWAYS*/); + QCOMPARE(balance_label_to_check->text().trimmed(), balanceComparison); +} + //! Simple qt wallet tests. // // Test widgets can be debugged interactively calling show() on them and @@ -162,15 +171,10 @@ void TestGUI(interfaces::Node& node) sendCoinsDialog.setModel(&walletModel); transactionView.setModel(&walletModel); - { - // Check balance in send dialog - QLabel* balanceLabel = sendCoinsDialog.findChild("labelBalance"); - QString balanceText = balanceLabel->text(); - BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit(); - CAmount balance = walletModel.wallet().getBalance(); - QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false /*, BitcoinUnits::SeparatorStyle::ALWAYS*/); - QCOMPARE(balanceText, balanceComparison); - } + // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel. + walletModel.pollBalanceChanged(); + // Check balance in send dialog + CompareBalance(walletModel, walletModel.wallet().getBalance(), sendCoinsDialog.findChild("labelBalance"), false); // Send two transactions, and verify they are added to transaction list. TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel(); @@ -187,12 +191,8 @@ void TestGUI(interfaces::Node& node) OverviewPage overviewPage; overviewPage.setClientModel(&clientModel); overviewPage.setWalletModel(&walletModel); - QLabel* balanceLabel = overviewPage.findChild("labelBalance"); - QString balanceText = balanceLabel->text().trimmed(); - BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit(); - CAmount balance = walletModel.wallet().getBalance(); - QString balanceComparison = BitcoinUnits::floorHtmlWithPrivacy(unit, balance, BitcoinUnits::SeparatorStyle::ALWAYS, false); - QCOMPARE(balanceText, balanceComparison); + walletModel.pollBalanceChanged(); // Manual balance polling update + CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild("labelBalance"), true); // Check that each autobackup failure state selects its specific tooltip on the CoinJoin status label { @@ -238,6 +238,7 @@ void TestGUI(interfaces::Node& node) QPushButton* requestPaymentButton = receiveCoinsDialog.findChild("receiveButton"); requestPaymentButton->click(); QString address; + BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit(); for (QWidget* widget : QApplication::topLevelWidgets()) { if (widget->inherits("ReceiveRequestDialog")) { ReceiveRequestDialog* receiveRequestDialog = qobject_cast(widget); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index d4f25cee6410..444dc42ad739 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -63,6 +63,9 @@ WalletModel::WalletModel(std::unique_ptr wallet, ClientModel connect(optionsModel, &OptionsModel::dustProtectionChanged, this, &WalletModel::lockExistingDustOutputs); // Lock existing dust on startup if dust protection is enabled lockExistingDustOutputs(); + // CoinJoin balances are calculated only while CoinJoin is enabled, + // so the cached balance must be recalculated when it is toggled + connect(optionsModel, &OptionsModel::showCoinJoinChanged, this, [this] { fForceCheckBalanceChanged = true; }); } } @@ -73,6 +76,10 @@ WalletModel::~WalletModel() void WalletModel::startPollBalance() { + // Update the cached balance right away, so every view can make use of it, + // so them don't need to waste resources recalculating it. + pollBalanceChanged(); + // This timer will be fired repeatedly to update the balance // Since the QTimer::timeout is a private signal, it cannot be used // in the GUIUtil::ExceptionSafeConnect directly. @@ -137,12 +144,17 @@ void WalletModel::pollBalanceChanged() void WalletModel::checkBalanceChanged(const interfaces::WalletBalances& new_balances) { - if(new_balances.balanceChanged(m_cached_balances)) { + if (new_balances.balanceChanged(m_cached_balances)) { m_cached_balances = new_balances; Q_EMIT balanceChanged(new_balances); } } +interfaces::WalletBalances WalletModel::getCachedBalance() const +{ + return m_cached_balances; +} + void WalletModel::updateTransaction() { // Balance and number of transactions might have changed @@ -258,7 +270,9 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact } } - CAmount nBalance = m_wallet->getAvailableBalance(coinControl); + // If no coin was manually selected, use the cached balance + // Future: can merge this call with 'createTransaction'. + CAmount nBalance = getAvailableBalance(&coinControl); if(total > nBalance) { @@ -633,3 +647,25 @@ uint256 WalletModel::getLastBlockProcessed() const { return m_client_model ? m_client_model->getBestBlockHash() : uint256{}; } + +CAmount WalletModel::getAvailableBalance(const CCoinControl* control) +{ + // No selected coins, return the cached balance + if (!control || !control->HasSelected()) { + const interfaces::WalletBalances& balances = getCachedBalance(); + if (control && control->IsUsingCoinJoin()) { + return balances.anonymized_balance; + } + // The cached balance counts locked coins, which coin selection cannot + // spend; exclude their live total, as lock changes don't trigger a repoll + CAmount available_balance = balances.balance - m_wallet->getLockedBalance(); + // if wallet private keys are disabled, this is a watch-only wallet + // so, let's include the watch-only balance. + if (balances.have_watch_only && m_wallet->privateKeysDisabled()) { + available_balance += balances.watch_only_balance; + } + return available_balance; + } + // Fetch balance from the wallet, taking into account the selected coins + return wallet().getAvailableBalance(*control); +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 262c766ae482..7c07875092c9 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -163,6 +163,13 @@ class WalletModel : public QObject uint256 getLastBlockProcessed() const; + // Retrieve the cached wallet balance + interfaces::WalletBalances getCachedBalance() const; + + // If coin control has selected outputs, searches the total amount inside the wallet. + // Otherwise, uses the wallet's cached available balance. + CAmount getAvailableBalance(const wallet::CCoinControl* control); + private: std::unique_ptr m_wallet; std::unique_ptr m_handler_unload; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 39f2080d388c..bf0fa7982c52 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -657,6 +657,7 @@ class WalletImpl : public Wallet return GetAvailableBalance(*m_wallet, &coin_control); } } + CAmount getLockedBalance() override { return GetLockedBalance(*m_wallet); } wallet::isminetype txinIsMine(const CTxIn& txin) override { LOCK(m_wallet->cs_wallet); diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp index 309c43450a0f..9b39a55c4c85 100644 --- a/src/wallet/receive.cpp +++ b/src/wallet/receive.cpp @@ -331,6 +331,25 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse, return ret; } +CAmount GetLockedBalance(const CWallet& wallet) +{ + CAmount total{0}; + LOCK(wallet.cs_wallet); + std::set trusted_parents; + for (const COutPoint& outpoint : wallet.ListLockedCoins()) { + const CWalletTx* wtx = wallet.GetWalletTx(outpoint.hash); + if (!wtx || outpoint.n >= wtx->tx->vout.size()) continue; + // Mirror the conditions under which GetBalance() counts an output + // towards m_mine_trusted + if (wallet.IsSpent(outpoint) || wallet.IsTxImmatureCoinBase(*wtx)) continue; + if (!CachedTxIsTrusted(wallet, *wtx, trusted_parents)) continue; + const CTxOut& txout{wtx->tx->vout[outpoint.n]}; + if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) && wallet.IsSpentKey(txout.scriptPubKey)) continue; + total += OutputGetCredit(wallet, txout, ISMINE_SPENDABLE); + } + return total; +} + std::map GetAddressBalances(const CWallet& wallet) { std::map balances; diff --git a/src/wallet/receive.h b/src/wallet/receive.h index 0acc4c6373e7..3b989c2cea29 100644 --- a/src/wallet/receive.h +++ b/src/wallet/receive.h @@ -60,6 +60,9 @@ struct Balance { CAmount m_denominated_untrusted_pending{0}; }; Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true, const bool fAddLocked = false); +//! Total value of the locked coins counted in GetBalance().m_mine_trusted, +//! i.e. the share of the trusted balance that is not available for spending +CAmount GetLockedBalance(const CWallet& wallet); std::map GetAddressBalances(const CWallet& wallet); std::set> GetAddressGroupings(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 9c8132804272..486f9183cee5 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -218,26 +218,18 @@ CoinsResult AvailableCoins(const CWallet& wallet, // Filter by spendable outputs only if (!spendable && only_spendable) continue; - // When parsing a scriptPubKey, Solver returns the parsed pubkeys or hashes (depending on the script) - // We don't need those here, so we are leaving them in return_values_unused - std::vector> return_values_unused; - TxoutType type; + // Obtain script type + std::vector> script_solutions; + TxoutType type = Solver(output.scriptPubKey, script_solutions); - // If the Output is P2SH and spendable, we want to know if it is + // If the output is P2SH and solvable, we want to know if it is // a P2SH (legacy). We can determine this from the redeemScript. - // If the Output is not spendable, it will be classified as a P2SH (legacy), + // If the output is not solvable, it will be classified as a P2SH (legacy), // since we have no way of knowing otherwise without the redeemScript - if (output.scriptPubKey.IsPayToScriptHash() && solvable) { - CScript redeemScript; - CTxDestination destination; - if (!ExtractDestination(output.scriptPubKey, destination)) - continue; - const CScriptID& hash = CScriptID(std::get(destination)); - if (!provider->GetCScript(hash, redeemScript)) - continue; - type = Solver(redeemScript, return_values_unused); - } else { - type = Solver(output.scriptPubKey, return_values_unused); + if (type == TxoutType::SCRIPTHASH && solvable) { + CScript script; + if (!provider->GetCScript(CScriptID(uint160(script_solutions[0])), script)) continue; + type = Solver(script, script_solutions); } COutput coin(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate);