Skip to content

Commit a7d4127

Browse files
achow101vijaydasmp
authored andcommitted
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
1 parent dc1e8a4 commit a7d4127

File tree

6 files changed

+110
-95
lines changed

6 files changed

+110
-95
lines changed

src/interfaces/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class Wallet
134134
std::string* purpose) = 0;
135135

136136
//! Get wallet address list.
137-
virtual std::vector<WalletAddress> getAddresses() = 0;
137+
virtual std::vector<WalletAddress> getAddresses() const = 0;
138138

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

src/wallet/interfaces.cpp

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -205,29 +205,27 @@ class WalletImpl : public Wallet
205205
std::string* purpose) override
206206
{
207207
LOCK(m_wallet->cs_wallet);
208-
auto it = m_wallet->m_address_book.find(dest);
209-
if (it == m_wallet->m_address_book.end() || it->second.IsChange()) {
210-
return false;
211-
}
208+
const auto& entry = m_wallet->FindAddressBookEntry(dest, /*allow_change=*/false);
209+
if (!entry) return false; // addr not found
212210
if (name) {
213-
*name = it->second.GetLabel();
211+
*name = entry->GetLabel();
214212
}
215213
if (is_mine) {
216214
*is_mine = m_wallet->IsMine(dest);
217215
}
218216
if (purpose) {
219-
*purpose = it->second.purpose;
217+
*purpose = entry->purpose;
220218
}
221219
return true;
222220
}
223-
std::vector<WalletAddress> getAddresses() override
221+
std::vector<WalletAddress> getAddresses() const override
224222
{
225223
LOCK(m_wallet->cs_wallet);
226224
std::vector<WalletAddress> result;
227-
for (const auto& item : m_wallet->m_address_book) {
228-
if (item.second.IsChange()) continue;
229-
result.emplace_back(item.first, m_wallet->IsMine(item.first), item.second.GetLabel(), item.second.purpose);
230-
}
225+
m_wallet->ForEachAddrBookEntry([&](const CTxDestination& dest, const std::string& label, const std::string& purpose, bool is_change) EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet) {
226+
if (is_change) return;
227+
result.emplace_back(dest, m_wallet->IsMine(dest), label, purpose);
228+
});
231229
return result;
232230
}
233231
std::vector<std::string> getAddressReceiveRequests() override {

src/wallet/rpcwallet.cpp

Lines changed: 38 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -540,12 +540,12 @@ static RPCHelpMan listaddressbalances()
540540

541541
static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool by_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
542542
{
543-
std::set<CTxDestination> address_set;
543+
std::vector<CTxDestination> address_vector;
544544

545545
if (by_label) {
546546
// Get the set of addresses assigned to label
547547
std::string label = LabelFromValue(params[0]);
548-
address_set = wallet.GetLabelAddresses(label);
548+
address_vector = wallet.ListAddrBookAddresses(CWallet::AddrBookFilter{label});
549549
} else {
550550
// Get the address
551551
CTxDestination dest = DecodeDestination(params[0].get_str());
@@ -556,7 +556,7 @@ static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool b
556556
if (!wallet.IsMine(script_pub_key)) {
557557
throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet");
558558
}
559-
address_set.insert(dest);
559+
address_vector.emplace_back(dest);
560560
}
561561

562562
// Minimum confirmations
@@ -590,7 +590,7 @@ static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool b
590590

591591
for (const CTxOut& txout : wtx.tx->vout) {
592592
CTxDestination address;
593-
if (ExtractDestination(txout.scriptPubKey, address) && wallet.IsMine(address) && address_set.count(address)) {
593+
if (ExtractDestination(txout.scriptPubKey, address) && wallet.IsMine(address) && std::find(address_vector.begin(), address_vector.end(), address) != address_vector.end()) {
594594
amount += txout.nValue;
595595
}
596596
}
@@ -971,14 +971,12 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons
971971
filter |= ISMINE_WATCH_ONLY;
972972
}
973973

974-
bool has_filtered_address = false;
975-
CTxDestination filtered_address = CNoDestination();
974+
std::optional<CTxDestination> filtered_address{std::nullopt};
976975
if (!by_label && !params[4].isNull() && !params[4].get_str().empty()) {
977976
if (!IsValidDestinationString(params[4].get_str())) {
978977
throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid");
979978
}
980979
filtered_address = DecodeDestination(params[4].get_str());
981-
has_filtered_address = true;
982980
}
983981

984982
// Excluding coinbase outputs is deprecated
@@ -1005,18 +1003,17 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons
10051003
continue;
10061004
}
10071005

1008-
for (const CTxOut& txout : wtx.tx->vout)
1009-
{
1006+
for (const CTxOut& txout : wtx.tx->vout) {
10101007
CTxDestination address;
10111008
if (!ExtractDestination(txout.scriptPubKey, address))
10121009
continue;
10131010

1014-
if (has_filtered_address && !(filtered_address == address)) {
1011+
if (filtered_address && !(filtered_address == address)) {
10151012
continue;
10161013
}
10171014

10181015
isminefilter mine = wallet.IsMine(address);
1019-
if(!(mine & filter))
1016+
if (!(mine & filter))
10201017
continue;
10211018

10221019
tallyitem& item = mapTally[address];
@@ -1032,74 +1029,58 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, cons
10321029
UniValue ret(UniValue::VARR);
10331030
std::map<std::string, tallyitem> label_tally;
10341031

1035-
// Create m_address_book iterator
1036-
// If we aren't filtering, go from begin() to end()
1037-
auto start = wallet.m_address_book.begin();
1038-
auto end = wallet.m_address_book.end();
1039-
// If we are filtering, find() the applicable entry
1040-
if (has_filtered_address) {
1041-
start = wallet.m_address_book.find(filtered_address);
1042-
if (start != end) {
1043-
end = std::next(start);
1044-
}
1045-
}
1046-
1047-
for (auto item_it = start; item_it != end; ++item_it)
1048-
{
1049-
if (item_it->second.IsChange()) continue;
1050-
const CTxDestination& address = item_it->first;
1051-
const std::string& label = item_it->second.GetLabel();
1032+
const auto& func = [&](const CTxDestination& address, const std::string& label, const std::string& purpose, bool is_change) {
1033+
if (is_change) return; // no change addresses
10521034
auto it = mapTally.find(address);
10531035
if (it == mapTally.end() && !fIncludeEmpty)
1054-
continue;
1036+
return;
10551037

10561038
isminefilter mine = wallet.IsMine(address);
1057-
if(!(mine & filter))
1058-
continue;
1039+
if (!(mine & filter))
1040+
return;
10591041

10601042
CAmount nAmount = 0;
10611043
int nConf = std::numeric_limits<int>::max();
10621044
bool fIsWatchonly = false;
1063-
if (it != mapTally.end())
1064-
{
1045+
if (it != mapTally.end()) {
10651046
nAmount = (*it).second.nAmount;
10661047
nConf = (*it).second.nConf;
10671048
fIsWatchonly = (*it).second.fIsWatchonly;
10681049
}
10691050

1070-
if (by_label)
1071-
{
1051+
if (by_label) {
10721052
tallyitem& _item = label_tally[label];
10731053
_item.nAmount += nAmount;
10741054
_item.nConf = std::min(_item.nConf, nConf);
10751055
_item.fIsWatchonly = fIsWatchonly;
1076-
}
1077-
else
1078-
{
1056+
} else {
10791057
UniValue obj(UniValue::VOBJ);
1080-
if(fIsWatchonly)
1081-
obj.pushKV("involvesWatchonly", true);
1058+
if(fIsWatchonly) obj.pushKV("involvesWatchonly", true);
10821059
obj.pushKV("address", EncodeDestination(address));
10831060
obj.pushKV("amount", ValueFromAmount(nAmount));
10841061
obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
10851062
obj.pushKV("label", label);
10861063
UniValue transactions(UniValue::VARR);
1087-
if (it != mapTally.end())
1088-
{
1089-
for (const uint256& _item : (*it).second.txids)
1090-
{
1064+
if (it != mapTally.end()) {
1065+
for (const uint256& _item : (*it).second.txids) {
10911066
transactions.push_back(_item.GetHex());
10921067
}
10931068
}
10941069
obj.pushKV("txids", transactions);
10951070
ret.push_back(obj);
10961071
}
1072+
};
1073+
1074+
if (filtered_address) {
1075+
const auto& entry = wallet.FindAddressBookEntry(*filtered_address, /*allow_change=*/false);
1076+
if (entry) func(*filtered_address, entry->GetLabel(), entry->purpose, /*is_change=*/false);
1077+
} else {
1078+
// No filtered addr, walk-through the addressbook entry
1079+
wallet.ForEachAddrBookEntry(func);
10971080
}
10981081

1099-
if (by_label)
1100-
{
1101-
for (const auto& entry : label_tally)
1102-
{
1082+
if (by_label) {
1083+
for (const auto& entry : label_tally) {
11031084
CAmount nAmount = entry.second.nAmount;
11041085
int nConf = entry.second.nConf;
11051086
UniValue obj(UniValue::VOBJ);
@@ -3849,17 +3830,6 @@ static UniValue DescribeWalletAddress(const CWallet& wallet, const CTxDestinatio
38493830
return ret;
38503831
}
38513832

3852-
/** Convert CAddressBookData to JSON record. */
3853-
static UniValue AddressBookDataToJSON(const CAddressBookData& data, const bool verbose)
3854-
{
3855-
UniValue ret(UniValue::VOBJ);
3856-
if (verbose) {
3857-
ret.pushKV("name", data.GetLabel());
3858-
}
3859-
ret.pushKV("purpose", data.purpose);
3860-
return ret;
3861-
}
3862-
38633833
RPCHelpMan getaddressinfo()
38643834
{
38653835
return RPCHelpMan{"getaddressinfo",
@@ -4030,10 +4000,10 @@ static RPCHelpMan getaddressesbylabel()
40304000
// Find all addresses that have the given label
40314001
UniValue ret(UniValue::VOBJ);
40324002
std::set<std::string> addresses;
4033-
for (const std::pair<const CTxDestination, CAddressBookData>& item : pwallet->m_address_book) {
4034-
if (item.second.IsChange()) continue;
4035-
if (item.second.GetLabel() == label) {
4036-
std::string address = EncodeDestination(item.first);
4003+
pwallet->ForEachAddrBookEntry([&](const CTxDestination& _dest, const std::string& _label, const std::string& _purpose, bool _is_change) {
4004+
if (_is_change) return;
4005+
if (_label == label) {
4006+
std::string address = EncodeDestination(_dest);
40374007
// CWallet::m_address_book is not expected to contain duplicate
40384008
// address strings, but build a separate set as a precaution just in
40394009
// case it does.
@@ -4043,9 +4013,11 @@ static RPCHelpMan getaddressesbylabel()
40434013
// and since duplicate addresses are unexpected (checked with
40444014
// std::set in O(log(N))), UniValue::__pushKV is used instead,
40454015
// which currently is O(1).
4046-
ret.__pushKV(address, AddressBookDataToJSON(item.second, false));
4016+
UniValue value(UniValue::VOBJ);
4017+
value.pushKV("purpose", _purpose);
4018+
ret.__pushKV(address, value);
40474019
}
4048-
}
4020+
});
40494021

40504022
if (ret.empty()) {
40514023
throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME, std::string("No addresses with label " + label));
@@ -4092,13 +4064,7 @@ static RPCHelpMan listlabels()
40924064
}
40934065

40944066
// Add to a set to sort by label name, then insert into Univalue array
4095-
std::set<std::string> label_set;
4096-
for (const std::pair<const CTxDestination, CAddressBookData>& entry : pwallet->m_address_book) {
4097-
if (entry.second.IsChange()) continue;
4098-
if (purpose.empty() || entry.second.purpose == purpose) {
4099-
label_set.insert(entry.second.GetLabel());
4100-
}
4101-
}
4067+
std::set<std::string> label_set = pwallet->ListAddrBookLabels(purpose);
41024068

41034069
UniValue ret(UniValue::VARR);
41044070
for (const std::string& name : label_set) {

src/wallet/wallet.cpp

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2903,21 +2903,44 @@ void CWallet::MarkDestinationsDirty(const std::set<CTxDestination>& destinations
29032903
}
29042904
}
29052905

2906-
std::set<CTxDestination> CWallet::GetLabelAddresses(const std::string& label) const
2906+
void CWallet::ForEachAddrBookEntry(const ListAddrBookFunc& func) const
29072907
{
2908-
LOCK(cs_wallet);
2909-
std::set<CTxDestination> result;
2910-
for (const std::pair<const CTxDestination, CAddressBookData>& item : m_address_book)
2911-
{
2912-
if (item.second.IsChange()) continue;
2913-
const CTxDestination& address = item.first;
2914-
const std::string& strName = item.second.GetLabel();
2915-
if (strName == label)
2916-
result.insert(address);
2908+
for (const std::pair<const CTxDestination, CAddressBookData>& item : m_address_book) {
2909+
const auto& entry = item.second;
2910+
func(item.first, entry.GetLabel(), entry.purpose, entry.IsChange());
29172911
}
2912+
}
2913+
2914+
std::vector<CTxDestination> CWallet::ListAddrBookAddresses(const std::optional<AddrBookFilter>& _filter) const
2915+
{
2916+
AssertLockHeld(cs_wallet);
2917+
std::vector<CTxDestination> result;
2918+
AddrBookFilter filter = _filter ? *_filter : AddrBookFilter();
2919+
ForEachAddrBookEntry([&result, &filter](const CTxDestination& dest, const std::string& label, const std::string& purpose, bool is_change) {
2920+
// Filter by change
2921+
if (filter.ignore_change && is_change) return;
2922+
// Filter by label
2923+
if (filter.m_op_label && *filter.m_op_label != label) return;
2924+
// All good
2925+
result.emplace_back(dest);
2926+
});
29182927
return result;
29192928
}
29202929

2930+
std::set<std::string> CWallet::ListAddrBookLabels(const std::string& purpose) const
2931+
{
2932+
AssertLockHeld(cs_wallet);
2933+
std::set<std::string> label_set;
2934+
ForEachAddrBookEntry([&](const CTxDestination& _dest, const std::string& _label,
2935+
const std::string& _purpose, bool _is_change) {
2936+
if (_is_change) return;
2937+
if (purpose.empty() || _purpose == purpose) {
2938+
label_set.insert(_label);
2939+
}
2940+
});
2941+
return label_set;
2942+
}
2943+
29212944
bool ReserveDestination::GetReservedDestination(CTxDestination& dest, bool fInternalIn)
29222945
{
29232946
m_spk_man = pwallet->GetScriptPubKeyMan(fInternalIn);

src/wallet/wallet.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,30 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
791791
std::set<std::set<CTxDestination>> GetAddressGroupings() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
792792
std::map<CTxDestination, CAmount> GetAddressBalances() const;
793793

794-
std::set<CTxDestination> GetLabelAddresses(const std::string& label) const;
794+
// Filter struct for 'ListAddrBookAddresses'
795+
struct AddrBookFilter {
796+
// Fetch addresses with the provided label
797+
std::optional<std::string> m_op_label{std::nullopt};
798+
// Don't include change addresses by default
799+
bool ignore_change{true};
800+
};
801+
802+
/**
803+
* Filter and retrieve destinations stored in the addressbook
804+
*/
805+
std::vector<CTxDestination> ListAddrBookAddresses(const std::optional<AddrBookFilter>& filter) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
806+
807+
/**
808+
* Retrieve all the known labels in the address book
809+
*/
810+
std::set<std::string> ListAddrBookLabels(const std::string& purpose) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
811+
812+
/**
813+
* Walk-through the address book entries.
814+
* Stops when the provided 'ListAddrBookFunc' returns false.
815+
*/
816+
using ListAddrBookFunc = std::function<void(const CTxDestination& dest, const std::string& label, const std::string& purpose, bool is_change)>;
817+
void ForEachAddrBookEntry(const ListAddrBookFunc& func) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
795818

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

test/functional/wallet_listreceivedby.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ def run_test(self):
6363
{"address": empty_addr},
6464
{"address": empty_addr, "label": "", "amount": 0, "confirmations": 0, "txids": []})
6565

66+
# No returned addy should be a change addr
67+
for node in self.nodes:
68+
for addr_obj in node.listreceivedbyaddress():
69+
assert_equal(node.getaddressinfo(addr_obj["address"])["ischange"], False)
70+
6671
# Test Address filtering
6772
# Only on addr
6873
expected = {"address": addr, "label": "", "amount": Decimal("0.1"), "confirmations": 10, "txids": [txid, ]}

0 commit comments

Comments
 (0)