Skip to content

Commit b9f9ed4

Browse files
committed
Merge bitcoin#25337: refactor: encapsulate wallet's address book access
d69045e test: add coverage for 'listreceivedbyaddress' no change addrs return (furszy) 324f00a refactor: 'ListReceived' use optional for filtered address (furszy) b459fc1 refactor: RPC 'ListReceived', encapsulate m_address_book access (furszy) fa9f2ab refactor: RPC 'listlabels', encapsulate 'CWallet::ListAddrBookLabels' functionality (furszy) 83e42c4 refactor: use 'ForEachAddrBookEntry' in RPC 'getaddressesbylabel' (furszy) 2b48642 refactor: use ForEachAddrBookEntry in interfaces::getAddresses (furszy) 032842a wallet: implement ForEachAddrBookEntry method (furszy) 09649bc refactor: implement general 'ListAddrBookAddresses' for addressbook destinations lookup (furszy) 192eb1e refactor: getAddress don't access m_address_book, use FindAddressEntry function (furszy) Pull request description: ### Context The wallet's `m_address_book` field is being accessed directly from several places across the sources. ### Problem Code structure wise, we shouldn't be accessing it directly. It could end up being modified by mistake (from a place that has nothing to do with the wallet like an RPC command or the GUI) and cause a bigger issue: like an address book entry 'purpose' string change, which if done badly (from 'send' to 'receive'), could end up in a user sharing a "receive" address that he/she doesn't own. ### Solution Encapsulate `m_address_book` access inside the wallet. ------------------------------------------------------- Extra Note: This is the initial step towards decoupling the address book functionality from the wallet's sources. In other words, the creation of the `AddressBookManager` (which will be coming in a follow-up PR). ACKs for top commit: achow101: ACK d69045e theStack: ACK d69045e ✅ w0xlt: ACK bitcoin@d69045e Tree-SHA512: dba17acd86f171b4e9af0223bbbcad380048570f6a2f6a92732a51f01abe8806debaf65c9e9e5569fa76a541903cbb50adcb5f56ef77858151c698ae6b218e2a
2 parents 9945737 + d69045e commit b9f9ed4

File tree

8 files changed

+108
-92
lines changed

8 files changed

+108
-92
lines changed

src/interfaces/wallet.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class Wallet
114114
std::string* purpose) = 0;
115115

116116
//! Get wallet address list.
117-
virtual std::vector<WalletAddress> getAddresses() = 0;
117+
virtual std::vector<WalletAddress> getAddresses() const = 0;
118118

119119
//! Get receive requests.
120120
virtual std::vector<std::string> getAddressReceiveRequests() = 0;

src/wallet/interfaces.cpp

+9-11
Original file line numberDiff line numberDiff line change
@@ -191,29 +191,27 @@ class WalletImpl : public Wallet
191191
std::string* purpose) override
192192
{
193193
LOCK(m_wallet->cs_wallet);
194-
auto it = m_wallet->m_address_book.find(dest);
195-
if (it == m_wallet->m_address_book.end() || it->second.IsChange()) {
196-
return false;
197-
}
194+
const auto& entry = m_wallet->FindAddressBookEntry(dest, /*allow_change=*/false);
195+
if (!entry) return false; // addr not found
198196
if (name) {
199-
*name = it->second.GetLabel();
197+
*name = entry->GetLabel();
200198
}
201199
if (is_mine) {
202200
*is_mine = m_wallet->IsMine(dest);
203201
}
204202
if (purpose) {
205-
*purpose = it->second.purpose;
203+
*purpose = entry->purpose;
206204
}
207205
return true;
208206
}
209-
std::vector<WalletAddress> getAddresses() override
207+
std::vector<WalletAddress> getAddresses() const override
210208
{
211209
LOCK(m_wallet->cs_wallet);
212210
std::vector<WalletAddress> result;
213-
for (const auto& item : m_wallet->m_address_book) {
214-
if (item.second.IsChange()) continue;
215-
result.emplace_back(item.first, m_wallet->IsMine(item.first), item.second.GetLabel(), item.second.purpose);
216-
}
211+
m_wallet->ForEachAddrBookEntry([&](const CTxDestination& dest, const std::string& label, const std::string& purpose, bool is_change) EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet) {
212+
if (is_change) return;
213+
result.emplace_back(dest, m_wallet->IsMine(dest), label, purpose);
214+
});
217215
return result;
218216
}
219217
std::vector<std::string> getAddressReceiveRequests() override {

src/wallet/rpc/addresses.cpp

+9-24
Original file line numberDiff line numberDiff line change
@@ -637,17 +637,6 @@ RPCHelpMan getaddressinfo()
637637
};
638638
}
639639

640-
/** Convert CAddressBookData to JSON record. */
641-
static UniValue AddressBookDataToJSON(const CAddressBookData& data, const bool verbose)
642-
{
643-
UniValue ret(UniValue::VOBJ);
644-
if (verbose) {
645-
ret.pushKV("name", data.GetLabel());
646-
}
647-
ret.pushKV("purpose", data.purpose);
648-
return ret;
649-
}
650-
651640
RPCHelpMan getaddressesbylabel()
652641
{
653642
return RPCHelpMan{"getaddressesbylabel",
@@ -680,10 +669,10 @@ RPCHelpMan getaddressesbylabel()
680669
// Find all addresses that have the given label
681670
UniValue ret(UniValue::VOBJ);
682671
std::set<std::string> addresses;
683-
for (const std::pair<const CTxDestination, CAddressBookData>& item : pwallet->m_address_book) {
684-
if (item.second.IsChange()) continue;
685-
if (item.second.GetLabel() == label) {
686-
std::string address = EncodeDestination(item.first);
672+
pwallet->ForEachAddrBookEntry([&](const CTxDestination& _dest, const std::string& _label, const std::string& _purpose, bool _is_change) {
673+
if (_is_change) return;
674+
if (_label == label) {
675+
std::string address = EncodeDestination(_dest);
687676
// CWallet::m_address_book is not expected to contain duplicate
688677
// address strings, but build a separate set as a precaution just in
689678
// case it does.
@@ -693,9 +682,11 @@ RPCHelpMan getaddressesbylabel()
693682
// and since duplicate addresses are unexpected (checked with
694683
// std::set in O(log(N))), UniValue::__pushKV is used instead,
695684
// which currently is O(1).
696-
ret.__pushKV(address, AddressBookDataToJSON(item.second, false));
685+
UniValue value(UniValue::VOBJ);
686+
value.pushKV("purpose", _purpose);
687+
ret.__pushKV(address, value);
697688
}
698-
}
689+
});
699690

700691
if (ret.empty()) {
701692
throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME, std::string("No addresses with label " + label));
@@ -742,13 +733,7 @@ RPCHelpMan listlabels()
742733
}
743734

744735
// Add to a set to sort by label name, then insert into Univalue array
745-
std::set<std::string> label_set;
746-
for (const std::pair<const CTxDestination, CAddressBookData>& entry : pwallet->m_address_book) {
747-
if (entry.second.IsChange()) continue;
748-
if (purpose.empty() || entry.second.purpose == purpose) {
749-
label_set.insert(entry.second.GetLabel());
750-
}
751-
}
736+
std::set<std::string> label_set = pwallet->ListAddrBookLabels(purpose);
752737

753738
UniValue ret(UniValue::VARR);
754739
for (const std::string& name : label_set) {

src/wallet/rpc/coins.cpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@
1818
namespace wallet {
1919
static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool by_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
2020
{
21-
std::set<CTxDestination> addresses;
21+
std::vector<CTxDestination> addresses;
2222
if (by_label) {
2323
// Get the set of addresses assigned to label
24-
addresses = wallet.GetLabelAddresses(LabelFromValue(params[0]));
24+
addresses = wallet.ListAddrBookAddresses(CWallet::AddrBookFilter{LabelFromValue(params[0])});
2525
if (addresses.empty()) throw JSONRPCError(RPC_WALLET_ERROR, "Label not found in wallet");
2626
} else {
2727
// Get the address
2828
CTxDestination dest = DecodeDestination(params[0].get_str());
2929
if (!IsValidDestination(dest)) {
3030
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address");
3131
}
32-
addresses.insert(dest);
32+
addresses.emplace_back(dest);
3333
}
3434

3535
// Filter by own scripts only

src/wallet/rpc/transactions.cpp

+24-43
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,12 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons
8585
filter |= ISMINE_WATCH_ONLY;
8686
}
8787

88-
bool has_filtered_address = false;
89-
CTxDestination filtered_address = CNoDestination();
88+
std::optional<CTxDestination> filtered_address{std::nullopt};
9089
if (!by_label && !params[3].isNull() && !params[3].get_str().empty()) {
9190
if (!IsValidDestinationString(params[3].get_str())) {
9291
throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid");
9392
}
9493
filtered_address = DecodeDestination(params[3].get_str());
95-
has_filtered_address = true;
9694
}
9795

9896
// Tally
@@ -106,23 +104,21 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons
106104

107105
// Coinbase with less than 1 confirmation is no longer in the main chain
108106
if ((wtx.IsCoinBase() && (nDepth < 1))
109-
|| (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase))
110-
{
107+
|| (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase)) {
111108
continue;
112109
}
113110

114-
for (const CTxOut& txout : wtx.tx->vout)
115-
{
111+
for (const CTxOut& txout : wtx.tx->vout) {
116112
CTxDestination address;
117113
if (!ExtractDestination(txout.scriptPubKey, address))
118114
continue;
119115

120-
if (has_filtered_address && !(filtered_address == address)) {
116+
if (filtered_address && !(filtered_address == address)) {
121117
continue;
122118
}
123119

124120
isminefilter mine = wallet.IsMine(address);
125-
if(!(mine & filter))
121+
if (!(mine & filter))
126122
continue;
127123

128124
tallyitem& item = mapTally[address];
@@ -138,70 +134,55 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons
138134
UniValue ret(UniValue::VARR);
139135
std::map<std::string, tallyitem> label_tally;
140136

141-
// Create m_address_book iterator
142-
// If we aren't filtering, go from begin() to end()
143-
auto start = wallet.m_address_book.begin();
144-
auto end = wallet.m_address_book.end();
145-
// If we are filtering, find() the applicable entry
146-
if (has_filtered_address) {
147-
start = wallet.m_address_book.find(filtered_address);
148-
if (start != end) {
149-
end = std::next(start);
150-
}
151-
}
137+
const auto& func = [&](const CTxDestination& address, const std::string& label, const std::string& purpose, bool is_change) {
138+
if (is_change) return; // no change addresses
152139

153-
for (auto item_it = start; item_it != end; ++item_it)
154-
{
155-
if (item_it->second.IsChange()) continue;
156-
const CTxDestination& address = item_it->first;
157-
const std::string& label = item_it->second.GetLabel();
158140
auto it = mapTally.find(address);
159141
if (it == mapTally.end() && !fIncludeEmpty)
160-
continue;
142+
return;
161143

162144
CAmount nAmount = 0;
163145
int nConf = std::numeric_limits<int>::max();
164146
bool fIsWatchonly = false;
165-
if (it != mapTally.end())
166-
{
147+
if (it != mapTally.end()) {
167148
nAmount = (*it).second.nAmount;
168149
nConf = (*it).second.nConf;
169150
fIsWatchonly = (*it).second.fIsWatchonly;
170151
}
171152

172-
if (by_label)
173-
{
153+
if (by_label) {
174154
tallyitem& _item = label_tally[label];
175155
_item.nAmount += nAmount;
176156
_item.nConf = std::min(_item.nConf, nConf);
177157
_item.fIsWatchonly = fIsWatchonly;
178-
}
179-
else
180-
{
158+
} else {
181159
UniValue obj(UniValue::VOBJ);
182-
if(fIsWatchonly)
183-
obj.pushKV("involvesWatchonly", true);
160+
if (fIsWatchonly) obj.pushKV("involvesWatchonly", true);
184161
obj.pushKV("address", EncodeDestination(address));
185162
obj.pushKV("amount", ValueFromAmount(nAmount));
186163
obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
187164
obj.pushKV("label", label);
188165
UniValue transactions(UniValue::VARR);
189-
if (it != mapTally.end())
190-
{
191-
for (const uint256& _item : (*it).second.txids)
192-
{
166+
if (it != mapTally.end()) {
167+
for (const uint256& _item : (*it).second.txids) {
193168
transactions.push_back(_item.GetHex());
194169
}
195170
}
196171
obj.pushKV("txids", transactions);
197172
ret.push_back(obj);
198173
}
174+
};
175+
176+
if (filtered_address) {
177+
const auto& entry = wallet.FindAddressBookEntry(*filtered_address, /*allow_change=*/false);
178+
if (entry) func(*filtered_address, entry->GetLabel(), entry->purpose, /*is_change=*/false);
179+
} else {
180+
// No filtered addr, walk-through the addressbook entry
181+
wallet.ForEachAddrBookEntry(func);
199182
}
200183

201-
if (by_label)
202-
{
203-
for (const auto& entry : label_tally)
204-
{
184+
if (by_label) {
185+
for (const auto& entry : label_tally) {
205186
CAmount nAmount = entry.second.nAmount;
206187
int nConf = entry.second.nConf;
207188
UniValue obj(UniValue::VOBJ);

src/wallet/wallet.cpp

+33-9
Original file line numberDiff line numberDiff line change
@@ -2368,21 +2368,45 @@ void CWallet::MarkDestinationsDirty(const std::set<CTxDestination>& destinations
23682368
}
23692369
}
23702370

2371-
std::set<CTxDestination> CWallet::GetLabelAddresses(const std::string& label) const
2371+
void CWallet::ForEachAddrBookEntry(const ListAddrBookFunc& func) const
23722372
{
23732373
AssertLockHeld(cs_wallet);
2374-
std::set<CTxDestination> result;
2375-
for (const std::pair<const CTxDestination, CAddressBookData>& item : m_address_book)
2376-
{
2377-
if (item.second.IsChange()) continue;
2378-
const CTxDestination& address = item.first;
2379-
const std::string& strName = item.second.GetLabel();
2380-
if (strName == label)
2381-
result.insert(address);
2374+
for (const std::pair<const CTxDestination, CAddressBookData>& item : m_address_book) {
2375+
const auto& entry = item.second;
2376+
func(item.first, entry.GetLabel(), entry.purpose, entry.IsChange());
23822377
}
2378+
}
2379+
2380+
std::vector<CTxDestination> CWallet::ListAddrBookAddresses(const std::optional<AddrBookFilter>& _filter) const
2381+
{
2382+
AssertLockHeld(cs_wallet);
2383+
std::vector<CTxDestination> result;
2384+
AddrBookFilter filter = _filter ? *_filter : AddrBookFilter();
2385+
ForEachAddrBookEntry([&result, &filter](const CTxDestination& dest, const std::string& label, const std::string& purpose, bool is_change) {
2386+
// Filter by change
2387+
if (filter.ignore_change && is_change) return;
2388+
// Filter by label
2389+
if (filter.m_op_label && *filter.m_op_label != label) return;
2390+
// All good
2391+
result.emplace_back(dest);
2392+
});
23832393
return result;
23842394
}
23852395

2396+
std::set<std::string> CWallet::ListAddrBookLabels(const std::string& purpose) const
2397+
{
2398+
AssertLockHeld(cs_wallet);
2399+
std::set<std::string> label_set;
2400+
ForEachAddrBookEntry([&](const CTxDestination& _dest, const std::string& _label,
2401+
const std::string& _purpose, bool _is_change) {
2402+
if (_is_change) return;
2403+
if (purpose.empty() || _purpose == purpose) {
2404+
label_set.insert(_label);
2405+
}
2406+
});
2407+
return label_set;
2408+
}
2409+
23862410
bool ReserveDestination::GetReservedDestination(CTxDestination& dest, bool internal, bilingual_str& error)
23872411
{
23882412
m_spk_man = pwallet->GetScriptPubKeyMan(type, internal);

src/wallet/wallet.h

+24-1
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,30 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
635635

636636
std::optional<int64_t> GetOldestKeyPoolTime() const;
637637

638-
std::set<CTxDestination> GetLabelAddresses(const std::string& label) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
638+
// Filter struct for 'ListAddrBookAddresses'
639+
struct AddrBookFilter {
640+
// Fetch addresses with the provided label
641+
std::optional<std::string> m_op_label{std::nullopt};
642+
// Don't include change addresses by default
643+
bool ignore_change{true};
644+
};
645+
646+
/**
647+
* Filter and retrieve destinations stored in the addressbook
648+
*/
649+
std::vector<CTxDestination> ListAddrBookAddresses(const std::optional<AddrBookFilter>& filter) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
650+
651+
/**
652+
* Retrieve all the known labels in the address book
653+
*/
654+
std::set<std::string> ListAddrBookLabels(const std::string& purpose) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
655+
656+
/**
657+
* Walk-through the address book entries.
658+
* Stops when the provided 'ListAddrBookFunc' returns false.
659+
*/
660+
using ListAddrBookFunc = std::function<void(const CTxDestination& dest, const std::string& label, const std::string& purpose, bool is_change)>;
661+
void ForEachAddrBookEntry(const ListAddrBookFunc& func) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
639662

640663
/**
641664
* Marks all outputs in each one of the destinations dirty, so their cache is

test/functional/wallet_listreceivedby.py

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ def run_test(self):
5757
{"address": empty_addr},
5858
{"address": empty_addr, "label": "", "amount": 0, "confirmations": 0, "txids": []})
5959

60+
# No returned addy should be a change addr
61+
for node in self.nodes:
62+
for addr_obj in node.listreceivedbyaddress():
63+
assert_equal(node.getaddressinfo(addr_obj["address"])["ischange"], False)
64+
6065
# Test Address filtering
6166
# Only on addr
6267
expected = {"address": addr, "label": "", "amount": Decimal("0.1"), "confirmations": 10, "txids": [txid, ]}

0 commit comments

Comments
 (0)