Skip to content

Commit 1a5e481

Browse files
committed
wallet: Add separate balance info for non-mempool wallet txs
1 parent 81e763f commit 1a5e481

File tree

13 files changed

+84
-26
lines changed

13 files changed

+84
-26
lines changed

src/interfaces/wallet.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,12 +370,13 @@ struct WalletBalances
370370
CAmount unconfirmed_balance = 0;
371371
CAmount immature_balance = 0;
372372
CAmount used_balance = 0;
373+
CAmount nonmempool_balance = 0;
373374

374375
bool balanceChanged(const WalletBalances& prev) const
375376
{
376377
return balance != prev.balance || unconfirmed_balance != prev.unconfirmed_balance ||
377378
immature_balance != prev.immature_balance ||
378-
used_balance != prev.used_balance;
379+
used_balance != prev.used_balance || nonmempool_balance != prev.nonmempool_balance;
379380
}
380381
};
381382

src/wallet/interfaces.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,13 @@ class WalletImpl : public Wallet
383383
}
384384
WalletBalances getBalances() override
385385
{
386-
const auto bal = GetBalance(*m_wallet);
386+
const auto bal = GetBalance(*m_wallet, /*min_depth=*/0, /*avoid_reuse=*/true, /*include_nonmempool=*/true);
387387
WalletBalances result;
388388
result.balance = bal.m_mine_trusted;
389389
result.unconfirmed_balance = bal.m_mine_untrusted_pending;
390390
result.immature_balance = bal.m_mine_immature;
391391
result.used_balance = bal.m_mine_used;
392+
result.nonmempool_balance = bal.m_mine_nonmempool;
392393
return result;
393394
}
394395
bool tryGetBalances(WalletBalances& balances, uint256& block_hash) override

src/wallet/receive.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx)
242242
return CachedTxIsTrusted(wallet, wtx, trusted_parents);
243243
}
244244

245-
Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
245+
Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse, bool include_nonmempool)
246246
{
247247
Balance ret;
248248
bool allow_used_addresses = !avoid_reuse || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
@@ -255,7 +255,17 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
255255
const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)};
256256
const int tx_depth{wallet.GetTxDepthInMainChain(wtx)};
257257

258-
if (!wallet.IsSpent(outpoint)) {
258+
bool nonmempool_spent = false;
259+
switch (wallet.HowSpent(outpoint)) {
260+
case CWallet::SpendType::CONFIRMED:
261+
case CWallet::SpendType::MEMPOOL:
262+
// treat as spent; ignore
263+
break;
264+
case CWallet::SpendType::NONMEMPOOL:
265+
if (!include_nonmempool || !allow_used_addresses) break;
266+
nonmempool_spent = true;
267+
[[fallthrough]];
268+
case CWallet::SpendType::UNSPENT:
259269
CAmount* bucket = nullptr;
260270

261271
// Set the amounts in the return object
@@ -274,6 +284,9 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
274284
bucket = &ret.m_mine_used;
275285
}
276286
*bucket += credit_mine;
287+
if (nonmempool_spent) {
288+
ret.m_mine_nonmempool -= credit_mine;
289+
}
277290
}
278291
}
279292
}

src/wallet/receive.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ struct Balance {
4848
CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)
4949
CAmount m_mine_immature{0}; //!< Immature coinbases in the main chain
5050
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)
51+
CAmount m_mine_nonmempool{0}; //!< Coins spent by wallet txs that are not in the mempool
5152
};
52-
Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true);
53+
Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true, bool include_nonmempool = false);
5354

5455
std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet);
5556
std::set<std::set<CTxDestination>> GetAddressGroupings(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);

src/wallet/rpc/coins.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ RPCHelpMan getbalances()
413413
{RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"},
414414
{RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"},
415415
{RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"},
416+
{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)"},
416417
{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)"},
417418
}},
418419
RESULT_LAST_PROCESSED_BLOCK,
@@ -433,14 +434,15 @@ RPCHelpMan getbalances()
433434

434435
LOCK(wallet.cs_wallet);
435436

436-
const auto bal = GetBalance(wallet, /*min_depth=*/0, /*avoid_reuse=*/true);
437+
const auto bal = GetBalance(wallet, /*min_depth=*/0, /*avoid_reuse=*/true, /*include_nonmempool=*/true);
437438

438439
UniValue balances{UniValue::VOBJ};
439440
{
440441
UniValue balances_mine{UniValue::VOBJ};
441442
balances_mine.pushKV("trusted", ValueFromAmount(bal.m_mine_trusted));
442443
balances_mine.pushKV("untrusted_pending", ValueFromAmount(bal.m_mine_untrusted_pending));
443444
balances_mine.pushKV("immature", ValueFromAmount(bal.m_mine_immature));
445+
balances_mine.pushKV("nonmempool", ValueFromAmount(bal.m_mine_nonmempool));
444446
if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
445447
balances_mine.pushKV("used", ValueFromAmount(bal.m_mine_used));
446448
}

src/wallet/wallet.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,29 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
738738
return false;
739739
}
740740

741+
CWallet::SpendType CWallet::HowSpent(const COutPoint& outpoint) const
742+
{
743+
SpendType st{SpendType::UNSPENT};
744+
745+
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range;
746+
range = mapTxSpends.equal_range(outpoint);
747+
748+
for (TxSpends::const_iterator it = range.first; it != range.second; ++it) {
749+
const Txid& txid = it->second;
750+
const auto mit = mapWallet.find(txid);
751+
if (mit != mapWallet.end()) {
752+
const auto& wtx = mit->second;
753+
if (wtx.isConfirmed()) return SpendType::CONFIRMED;
754+
if (wtx.InMempool()) {
755+
st = SpendType::MEMPOOL;
756+
} else if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted()) {
757+
if (st == SpendType::UNSPENT) st = SpendType::NONMEMPOOL;
758+
}
759+
}
760+
}
761+
return st;
762+
}
763+
741764
void CWallet::AddToSpends(const COutPoint& outpoint, const Txid& txid)
742765
{
743766
mapTxSpends.insert(std::make_pair(outpoint, txid));

src/wallet/wallet.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,13 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
556556
int GetTxBlocksToMaturity(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
557557
bool IsTxImmatureCoinBase(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
558558

559+
enum class SpendType {
560+
UNSPENT,
561+
CONFIRMED,
562+
MEMPOOL,
563+
NONMEMPOOL,
564+
};
565+
SpendType HowSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
559566
bool IsSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
560567

561568
// Whether this or any known scriptPubKey with the same single key has been spent.

test/functional/wallet_abandonconflict.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,9 @@ def run_test(self):
115115
# inputs are still spent, but change not received
116116
newbalance = alice.getbalance()
117117
assert_equal(newbalance, balance - signed3_change)
118-
# Unconfirmed received funds that are not in mempool, also shouldn't show
119-
# up in unconfirmed balance
118+
# Unconfirmed received funds that are not in mempool
120119
balances = alice.getbalances()['mine']
121-
assert_equal(balances['untrusted_pending'] + balances['trusted'], newbalance)
120+
assert_equal(balances['untrusted_pending'] + balances['trusted'] + balances['nonmempool'], newbalance)
122121
# Also shouldn't show up in listunspent
123122
assert not txABC2 in [utxo["txid"] for utxo in alice.listunspent(0)]
124123
balance = newbalance

test/functional/wallet_balance.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,12 @@ def test_balances(*, fee_node_1=0):
145145
# getbalances
146146
expected_balances_0 = {'mine': {'immature': Decimal('0E-8'),
147147
'trusted': Decimal('9.99'), # change from node 0's send
148-
'untrusted_pending': Decimal('60.0')}}
148+
'untrusted_pending': Decimal('60.0'),
149+
'nonmempool': Decimal('0.0')}}
149150
expected_balances_1 = {'mine': {'immature': Decimal('0E-8'),
150151
'trusted': Decimal('0E-8'), # node 1's send had an unsafe input
151-
'untrusted_pending': Decimal('30.0') - fee_node_1}} # Doesn't include output of node 0's send since it was spent
152+
'untrusted_pending': Decimal('30.0') - fee_node_1, # Doesn't include output of node 0's send since it was spent
153+
'nonmempool': Decimal('0.0')}}
152154
balances_0 = self.nodes[0].getbalances()
153155
balances_1 = self.nodes[1].getbalances()
154156
# remove lastprocessedblock keys (they will be tested later)

test/functional/wallet_conflicts.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,9 @@ def test_mempool_and_block_conflicts(self):
304304
bob.sendrawtransaction(tx1_conflict_conflict) # kick tx1_conflict out of the mempool
305305
bob.sendrawtransaction(raw_tx1) #re-broadcast tx1 because it is no longer conflicted
306306

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

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

0 commit comments

Comments
 (0)