Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
};

Expand Down
36 changes: 17 additions & 19 deletions src/qt/overviewpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand All @@ -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));
Expand Down Expand Up @@ -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());
});
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"));
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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),
Expand Down
1 change: 0 additions & 1 deletion src/qt/overviewpage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 5 additions & 7 deletions src/qt/sendcoinsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Coin Control
connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
if (e && !e->isHidden() && e != entry) {
Expand Down
2 changes: 1 addition & 1 deletion src/qt/sendcoinsdialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 16 additions & 15 deletions src/qt/test/wallettests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -162,15 +171,10 @@ void TestGUI(interfaces::Node& node)
sendCoinsDialog.setModel(&walletModel);
transactionView.setModel(&walletModel);

{
// Check balance in send dialog
QLabel* balanceLabel = sendCoinsDialog.findChild<QLabel*>("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<QLabel*>("labelBalance"), false);

// Send two transactions, and verify they are added to transaction list.
TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel();
Expand All @@ -187,12 +191,8 @@ void TestGUI(interfaces::Node& node)
OverviewPage overviewPage;
overviewPage.setClientModel(&clientModel);
overviewPage.setWalletModel(&walletModel);
QLabel* balanceLabel = overviewPage.findChild<QLabel*>("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<QLabel*>("labelBalance"), true);

// Check that each autobackup failure state selects its specific tooltip on the CoinJoin status label
{
Expand Down Expand Up @@ -238,6 +238,7 @@ void TestGUI(interfaces::Node& node)
QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton");
requestPaymentButton->click();
QString address;
BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit();
for (QWidget* widget : QApplication::topLevelWidgets()) {
if (widget->inherits("ReceiveRequestDialog")) {
ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget);
Expand Down
40 changes: 38 additions & 2 deletions src/qt/walletmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ WalletModel::WalletModel(std::unique_ptr<interfaces::Wallet> 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; });
}
}

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Comment on lines +656 to +657

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh CoinJoin balances when enabling CoinJoin

When CoinJoin is disabled during the initial balance poll, getBalances() leaves anonymized_balance at 0; enabling CoinJoin later only emits showCoinJoinChanged, while pollBalanceChanged() refreshes on tip/forced/round changes, so this branch keeps returning the stale 0 value. In a wallet that already has mixed funds, the mixed-funds send dialog will report 0 available or AmountExceedsBalance until another block, transaction, or rounds change forces a repoll, whereas the previous path queried wallet().getAvailableBalance() live after CoinJoin was enabled.

Useful? React with 👍 / 👎.

Comment on lines +656 to +657

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Refresh the balance cache when CoinJoin is enabled

When CoinJoin is disabled, GetBalance() skips the CoinJoin-specific calculation at src/wallet/receive.cpp:320-328, so the initial cache records zero anonymized and denominated balances. Enabling CoinJoin later emits showCoinJoinChanged, but the only wallet-view connection calls coinJoinStatus(true) and does not force WalletModel to refresh. Because pollBalanceChanged() returns at line 116 when neither the tip nor fForceCheckBalanceChanged changed, this branch continues returning zero for wallets that already contain fully mixed funds. The CoinJoin send dialog can therefore report no available balance and reject sends until another transaction, block, or rounds change triggers a poll; the converted overview also reads the same stale CoinJoin fields. Force a wallet-model balance poll when the CoinJoin enabled state changes, or keep this CoinJoin availability query live.

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Refresh the balance cache when CoinJoin is enabled no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

}
// 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();
Comment on lines +655 to +661

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor coin control's address-reuse policy

When an avoid_reuse wallet has no manually selected inputs, this cached path excludes every dirty output because getBalances() calls GetBalance() with avoid_reuse=true, even though the GUI's CCoinControl::m_avoid_address_reuse defaults to false and AvailableCoins() therefore permits those outputs. If the clean balance is insufficient but reusable funds are available, “Use available balance” underreports the amount and prepareTransaction() returns AmountExceedsBalance before coin selection can spend the permitted dirty outputs; use the live query or otherwise account for the control's reuse setting.

Useful? React with 👍 / 👎.

Comment on lines +655 to +661

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Honor coin control's address-reuse policy

For a wallet with WALLET_FLAG_AVOID_REUSE, the cached balances.balance comes from GetBalance() with avoid_reuse=true, so it excludes dirty outputs. The GUI coin control defaults m_avoid_address_reuse to false, and AvailableCoins() therefore permits those outputs. With no manually selected inputs, this branch returns only the clean cached balance. If clean funds are insufficient but reusable funds make the payment possible, “Use available balance” underreports the amount and prepareTransaction() returns AmountExceedsBalance before coin selection can use the eligible dirty outputs. Use the live query when the coin-control policy differs from the cache, or cache balances for both policies.

source: ['codex']

// 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);
}
7 changes: 7 additions & 0 deletions src/qt/walletmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<interfaces::Wallet> m_wallet;
std::unique_ptr<interfaces::Handler> m_handler_unload;
Expand Down
1 change: 1 addition & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions src/wallet/receive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint256> 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);
Comment on lines +346 to +348

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Subtract locked watch-only outputs too

In a private-key-disabled watch-only wallet without an external signer, getAvailableBalance() adds the full watch_only_balance, but this helper subtracts only ISMINE_SPENDABLE locked outputs, not ISMINE_WATCH_ONLY ones. Thus a locked solvable watch-only UTXO is still offered by “Use available balance” and counted by the send preflight even though AvailableCoins() skips it. Fresh evidence after the earlier locked-output comment is that the newly added correction explicitly filters the subtraction to ISMINE_SPENDABLE, leaving the watch-only branch uncorrected.

Useful? React with 👍 / 👎.

Comment on lines +346 to +348

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Subtract locked watch-only outputs too

GetLockedBalance() only totals ISMINE_SPENDABLE credit. For a private-key-disabled legacy watch-only wallet without an external signer, the GUI enables watch-only coin selection and WalletModel::getAvailableBalance() adds the entire cached watch_only_balance, including locked watch-only UTXOs. AvailableCoins() skips every locked outpoint before evaluating watch-only solvability, so “Use available balance” and the preflight can offer an amount transaction creation cannot use. Track and subtract trusted locked watch-only credit separately from the spendable locked balance.

source: ['codex']

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underreport is real, but it is inherited 1:1 from the backported upstream
code rather than introduced here: bitcoin-core/gui#598 caches GetBalance()
computed with avoid_reuse=true, the GUI's CCoinControl leaves
m_avoid_address_reuse at its default false, and Bitcoin Core master still
behaves this way today (WalletModel::getAvailableBalance returns the raw
cached balance). It only affects wallets created with the RPC-only
avoid_reuse flag (the GUI never sets the flag or the coin-control field), and
only when clean funds alone cannot cover the payment. To keep this backport
aligned with upstream and limit dashification to Dash features (CoinJoin,
dust-protection locking), I'd rather not diverge here; this deserves an
upstream-first fix or a separate follow-up.

}
return total;
}

std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet)
{
std::map<CTxDestination, CAmount> balances;
Expand Down
3 changes: 3 additions & 0 deletions src/wallet/receive.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet);
std::set<std::set<CTxDestination>> GetAddressGroupings(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
Expand Down
Loading
Loading