Skip to content
Draft
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
5 changes: 4 additions & 1 deletion src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,14 @@ struct WalletBalances
CAmount balance = 0;
CAmount unconfirmed_balance = 0;
CAmount immature_balance = 0;
CAmount used_balance = 0;
CAmount nonmempool_balance = 0;

bool balanceChanged(const WalletBalances& prev) const
{
return balance != prev.balance || unconfirmed_balance != prev.unconfirmed_balance ||
immature_balance != prev.immature_balance;
immature_balance != prev.immature_balance ||
used_balance != prev.used_balance || nonmempool_balance != prev.nonmempool_balance;
}
};

Expand Down
1 change: 0 additions & 1 deletion src/qt/bitcoinunits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ QString BitcoinUnits::formatHtmlWithUnit(Unit unit, const CAmount& amount, bool

QString BitcoinUnits::formatWithPrivacy(Unit unit, const CAmount& amount, SeparatorStyle separators, bool privacy)
{
assert(amount >= 0);
QString value;
if (privacy) {
value = format(unit, 0, false, separators, true).replace('0', '#');
Expand Down
32 changes: 29 additions & 3 deletions src/qt/forms/overviewpage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,14 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="labelTotalText">
<property name="text">
<string>Total:</string>
Expand Down Expand Up @@ -183,7 +183,33 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1">
<widget class="QLabel" name="labelNonMempool">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="toolTip">
<string>Balance for wallet transactions not in the mempool</string>
</property>
<property name="text">
<string notr="true">0.00000000 BTC</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelNonMempoolText">
<property name="text">
<string>Non-mempool:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="labelTotal">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
Expand Down
10 changes: 9 additions & 1 deletion src/qt/overviewpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,20 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances)
ui->labelBalance->setText(BitcoinUnits::formatWithPrivacy(unit, balances.balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
ui->labelUnconfirmed->setText(BitcoinUnits::formatWithPrivacy(unit, balances.unconfirmed_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
ui->labelImmature->setText(BitcoinUnits::formatWithPrivacy(unit, balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
ui->labelTotal->setText(BitcoinUnits::formatWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
ui->labelNonMempool->setText(BitcoinUnits::formatWithPrivacy(unit, balances.nonmempool_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
ui->labelTotal->setText(BitcoinUnits::formatWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance + balances.nonmempool_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy));
// only show immature (newly mined) balance if it's non-zero, so as not to complicate things
// for the non-mining users
bool showImmature = balances.immature_balance != 0;

ui->labelImmature->setVisible(showImmature);
ui->labelImmatureText->setVisible(showImmature);

// likewise for non-mempool balances
bool showNonMempool = balances.nonmempool_balance != 0;

ui->labelNonMempool->setVisible(showNonMempool);
ui->labelNonMempoolText->setVisible(showNonMempool);
}

void OverviewPage::setClientModel(ClientModel *model)
Expand Down Expand Up @@ -296,5 +303,6 @@ void OverviewPage::setMonospacedFont(const QFont& f)
ui->labelBalance->setFont(f);
ui->labelUnconfirmed->setFont(f);
ui->labelImmature->setFont(f);
ui->labelNonMempool->setFont(f);
ui->labelTotal->setFont(f);
}
2 changes: 1 addition & 1 deletion src/qt/test/wallettests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
OverviewPage overviewPage(platformStyle.get());
overviewPage.setWalletModel(&walletModel);
walletModel.pollBalanceChanged(); // Manual balance polling update
CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild<QLabel*>("labelBalance"));
CompareBalance(walletModel, walletModel.wallet().getBalances().balance, overviewPage.findChild<QLabel*>("labelBalance"));

// Check Request Payment button
ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());
Expand Down
4 changes: 3 additions & 1 deletion src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,11 +383,13 @@ class WalletImpl : public Wallet
}
WalletBalances getBalances() override
{
const auto bal = GetBalance(*m_wallet);
const auto bal = GetBalance(*m_wallet, /*min_depth=*/0, /*avoid_reuse=*/true, /*include_nonmempool=*/true);
WalletBalances result;
result.balance = bal.m_mine_trusted;
result.unconfirmed_balance = bal.m_mine_untrusted_pending;
result.immature_balance = bal.m_mine_immature;
result.used_balance = bal.m_mine_used;
result.nonmempool_balance = bal.m_mine_nonmempool;
return result;
}
bool tryGetBalances(WalletBalances& balances, uint256& block_hash) override
Expand Down
35 changes: 28 additions & 7 deletions src/wallet/receive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx)
return CachedTxIsTrusted(wallet, wtx, trusted_parents);
}

Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse, bool include_nonmempool)
{
Balance ret;
bool allow_used_addresses = !avoid_reuse || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
Expand All @@ -255,17 +255,38 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)};
const int tx_depth{wallet.GetTxDepthInMainChain(wtx)};

if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) {
// Get the amounts for mine
CAmount credit_mine = txo.GetTxOut().nValue;
bool nonmempool_spent = false;
switch (wallet.HowSpent(outpoint)) {
case CWallet::SpendType::CONFIRMED:
case CWallet::SpendType::MEMPOOL:
// treat as spent; ignore
break;
case CWallet::SpendType::NONMEMPOOL:
if (!include_nonmempool || !allow_used_addresses) break;
nonmempool_spent = true;
[[fallthrough]];
case CWallet::SpendType::UNSPENT:
CAmount* bucket = nullptr;

// Set the amounts in the return object
if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
ret.m_mine_immature += credit_mine;
bucket = &ret.m_mine_immature;
} else if (is_trusted && tx_depth >= min_depth) {
ret.m_mine_trusted += credit_mine;
bucket = &ret.m_mine_trusted;
} else if (!is_trusted && wtx.InMempool()) {
ret.m_mine_untrusted_pending += credit_mine;
bucket = &ret.m_mine_untrusted_pending;
}
if (bucket) {
// Get the amounts for mine
CAmount credit_mine = txo.GetTxOut().nValue;

if (!allow_used_addresses && wallet.IsSpentKey(txo.GetTxOut().scriptPubKey)) {
bucket = &ret.m_mine_used;
}
*bucket += credit_mine;
if (nonmempool_spent) {
ret.m_mine_nonmempool -= credit_mine;
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/wallet/receive.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ struct Balance {
CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more
CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)
CAmount m_mine_immature{0}; //!< Immature coinbases in the main chain
CAmount m_mine_used{0}; //!< Trusted/untrusted/immature funds in utxos that have already been spent from (only populated if AVOID REUSE wallet flag is set)
CAmount m_mine_nonmempool{0}; //!< Coins spent by wallet txs that are not in the mempool
};
Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true);
Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true, bool include_nonmempool = false);

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
10 changes: 5 additions & 5 deletions src/wallet/rpc/coins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ RPCHelpMan getbalances()
{RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"},
{RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"},
{RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"},
{RPCResult::Type::STR_AMOUNT, "nonmempool", "sum of coins that are locked or spent by transactions not in the mempool (usually an over-estimate due to not accounting for change or spends that conflict with each other)"},
{RPCResult::Type::STR_AMOUNT, "used", /*optional=*/true, "(only present if avoid_reuse is set) balance from coins sent to addresses that were previously spent from (potentially privacy violating)"},
}},
RESULT_LAST_PROCESSED_BLOCK,
Expand All @@ -433,18 +434,17 @@ RPCHelpMan getbalances()

LOCK(wallet.cs_wallet);

const auto bal = GetBalance(wallet);
const auto bal = GetBalance(wallet, /*min_depth=*/0, /*avoid_reuse=*/true, /*include_nonmempool=*/true);

UniValue balances{UniValue::VOBJ};
{
UniValue balances_mine{UniValue::VOBJ};
balances_mine.pushKV("trusted", ValueFromAmount(bal.m_mine_trusted));
balances_mine.pushKV("untrusted_pending", ValueFromAmount(bal.m_mine_untrusted_pending));
balances_mine.pushKV("immature", ValueFromAmount(bal.m_mine_immature));
balances_mine.pushKV("nonmempool", ValueFromAmount(bal.m_mine_nonmempool));
if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
// If the AVOID_REUSE flag is set, bal has been set to just the un-reused address balance. Get
// the total balance, and then subtract bal to get the reused address balance.
const auto full_bal = GetBalance(wallet, 0, false);
balances_mine.pushKV("used", ValueFromAmount(full_bal.m_mine_trusted + full_bal.m_mine_untrusted_pending - bal.m_mine_trusted - bal.m_mine_untrusted_pending));
balances_mine.pushKV("used", ValueFromAmount(bal.m_mine_used));
}
balances.pushKV("mine", std::move(balances_mine));
}
Expand Down
23 changes: 23 additions & 0 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,29 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
return false;
}

CWallet::SpendType CWallet::HowSpent(const COutPoint& outpoint) const
{
SpendType st{SpendType::UNSPENT};

std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range;
range = mapTxSpends.equal_range(outpoint);

for (TxSpends::const_iterator it = range.first; it != range.second; ++it) {
const Txid& txid = it->second;
const auto mit = mapWallet.find(txid);
if (mit != mapWallet.end()) {
const auto& wtx = mit->second;
if (wtx.isConfirmed()) return SpendType::CONFIRMED;
if (wtx.InMempool()) {
st = SpendType::MEMPOOL;
} else if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted()) {
if (st == SpendType::UNSPENT) st = SpendType::NONMEMPOOL;
}
}
}
return st;
}

void CWallet::AddToSpends(const COutPoint& outpoint, const Txid& txid)
{
mapTxSpends.insert(std::make_pair(outpoint, txid));
Expand Down
7 changes: 7 additions & 0 deletions src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,13 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
int GetTxBlocksToMaturity(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsTxImmatureCoinBase(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

enum class SpendType {
UNSPENT,
CONFIRMED,
MEMPOOL,
NONMEMPOOL,
};
SpendType HowSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

// Whether this or any known scriptPubKey with the same single key has been spent.
Expand Down
5 changes: 2 additions & 3 deletions test/functional/wallet_abandonconflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,9 @@ def run_test(self):
# inputs are still spent, but change not received
newbalance = alice.getbalance()
assert_equal(newbalance, balance - signed3_change)
# Unconfirmed received funds that are not in mempool, also shouldn't show
# up in unconfirmed balance
# Unconfirmed received funds that are not in mempool
balances = alice.getbalances()['mine']
assert_equal(balances['untrusted_pending'] + balances['trusted'], newbalance)
assert_equal(balances['untrusted_pending'] + balances['trusted'] + balances['nonmempool'], newbalance)
# Also shouldn't show up in listunspent
assert not txABC2 in [utxo["txid"] for utxo in alice.listunspent(0)]
balance = newbalance
Expand Down
6 changes: 4 additions & 2 deletions test/functional/wallet_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,12 @@ def test_balances(*, fee_node_1=0):
# getbalances
expected_balances_0 = {'mine': {'immature': Decimal('0E-8'),
'trusted': Decimal('9.99'), # change from node 0's send
'untrusted_pending': Decimal('60.0')}}
'untrusted_pending': Decimal('60.0'),
'nonmempool': Decimal('0.0')}}
expected_balances_1 = {'mine': {'immature': Decimal('0E-8'),
'trusted': Decimal('0E-8'), # node 1's send had an unsafe input
'untrusted_pending': Decimal('30.0') - fee_node_1}} # Doesn't include output of node 0's send since it was spent
'untrusted_pending': Decimal('30.0') - fee_node_1, # Doesn't include output of node 0's send since it was spent
'nonmempool': Decimal('0.0')}}
balances_0 = self.nodes[0].getbalances()
balances_1 = self.nodes[1].getbalances()
# remove lastprocessedblock keys (they will be tested later)
Expand Down
5 changes: 3 additions & 2 deletions test/functional/wallet_conflicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,9 @@ def test_mempool_and_block_conflicts(self):
bob.sendrawtransaction(tx1_conflict_conflict) # kick tx1_conflict out of the mempool
bob.sendrawtransaction(raw_tx1) #re-broadcast tx1 because it is no longer conflicted

# Now bob has no pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet
assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
# Now bob has pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet
bob_bal = bob.getbalances()["mine"]
assert_equal(bob_bal["untrusted_pending"], -bob_bal["nonmempool"])

bob.sendrawtransaction(raw_tx3)
assert_equal(len(bob.getrawmempool()), 4) # The mempool contains: tx1, tx2, tx1_conflict_conflict, tx3
Expand Down
Loading
Loading