Skip to content

Commit c336f81

Browse files
committed
Merge bitcoin/bitcoin#25504: RPC: allow to track coins by parent descriptors
a6b0c1f doc: add releases notes for 25504 (listsinceblock updates) (Antoine Poinsot) 0fd2d14 rpc: add an include_change parameter to listsinceblock (Antoine Poinsot) 55f98d0 rpc: output parent wallet descriptors for coins in listunspent (Antoine Poinsot) b724476 rpc: output wallet descriptors for received entries in listsinceblock (Antoine Poinsot) 55a82ea wallet: allow to fetch the wallet descriptors for a given Script (Antoine Poinsot) Pull request description: Wallet descriptors are useful for applications using the Bitcoin Core wallet as a backend for tracking coins, as they allow to track coins for multiple descriptors in a single wallet. However there is no information currently given for such applications to link a coin with an imported descriptor, severely limiting the possibilities for such applications of using multiple descriptors in a single wallet. This PR outputs the matching imported descriptor(s) for a given received coin in `listsinceblock` (and friends). It comes from a need for an application i'm working on, but i think it's something any software using `bitcoind` to track multiple descriptors in a single wallet would have eventually. For instance i'm thinking about the BDK project. Currently, the way to achieve this is to import raw addresses with labels and to have your application be responsible for wallet things like the gap limit. I'll add this to the output of `listunspent` too if this gets a few Concept ACKs. ACKs for top commit: instagibbs: ACK bitcoin/bitcoin@a6b0c1f achow101: re-ACK a6b0c1f Tree-SHA512: 7a5850e8de98b439ddede2cb72de0208944f8cda67272e8b8037678738d55b7a5272375be808b0f7d15def4904430e089dafdcc037436858ff3292c5f8b75e37
2 parents cf39913 + a6b0c1f commit c336f81

File tree

12 files changed

+158
-9
lines changed

12 files changed

+158
-9
lines changed

doc/release-notes-25504.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Updated RPCs
2+
------------
3+
4+
- The `listsinceblock`, `listtransactions` and `gettransaction` output now contain a new
5+
`parent_descs` field for every "receive" entry.
6+
- A new optional `include_change` parameter was added to the `listsinceblock` command.

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
7474
{ "listsinceblock", 1, "target_confirmations" },
7575
{ "listsinceblock", 2, "include_watchonly" },
7676
{ "listsinceblock", 3, "include_removed" },
77+
{ "listsinceblock", 4, "include_change" },
7778
{ "sendmany", 1, "amounts" },
7879
{ "sendmany", 2, "minconf" },
7980
{ "sendmany", 4, "subtractfeefrom" },

src/wallet/receive.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx,
193193

194194
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
195195
std::list<COutputEntry>& listReceived,
196-
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter)
196+
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
197+
bool include_change)
197198
{
198199
nFee = 0;
199200
listReceived.clear();
@@ -218,8 +219,7 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
218219
// 2) the output is to us (received)
219220
if (nDebit > 0)
220221
{
221-
// Don't report 'change' txouts
222-
if (OutputIsChange(wallet, txout))
222+
if (!include_change && OutputIsChange(wallet, txout))
223223
continue;
224224
}
225225
else if (!(fIsMine & filter))

src/wallet/receive.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ struct COutputEntry
4242
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
4343
std::list<COutputEntry>& listReceived,
4444
std::list<COutputEntry>& listSent,
45-
CAmount& nFee, const isminefilter& filter);
45+
CAmount& nFee, const isminefilter& filter,
46+
bool include_change);
4647
bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
4748
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
4849
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx);

src/wallet/rpc/coins.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,9 @@ RPCHelpMan listunspent()
543543
{RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"},
544544
{RPCResult::Type::BOOL, "reused", /*optional=*/true, "(only present if avoid_reuse is set) Whether this output is reused/dirty (sent to an address that was previously spent from)"},
545545
{RPCResult::Type::STR, "desc", /*optional=*/true, "(only when solvable) A descriptor for spending this output"},
546+
{RPCResult::Type::ARR, "parent_descs", /*optional=*/false, "List of parent descriptors for the scriptPubKey of this coin.", {
547+
{RPCResult::Type::STR, "desc", "The descriptor string."},
548+
}},
546549
{RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions\n"
547550
"from outside keys and unconfirmed replacement transactions are considered unsafe\n"
548551
"and are not eligible for spending by fundrawtransaction and sendtoaddress."},
@@ -722,6 +725,7 @@ RPCHelpMan listunspent()
722725
entry.pushKV("desc", descriptor->ToString());
723726
}
724727
}
728+
PushParentDescriptors(*pwallet, scriptPubKey, entry);
725729
if (avoid_reuse) entry.pushKV("reused", reused);
726730
entry.pushKV("safe", out.safe);
727731
results.push_back(entry);

src/wallet/rpc/transactions.cpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,16 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
315315
* @param filter_label Optional label string to filter incoming transactions.
316316
*/
317317
template <class Vec>
318-
static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong, Vec& ret, const isminefilter& filter_ismine, const std::string* filter_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
318+
static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong,
319+
Vec& ret, const isminefilter& filter_ismine, const std::string* filter_label,
320+
bool include_change = false)
321+
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
319322
{
320323
CAmount nFee;
321324
std::list<COutputEntry> listReceived;
322325
std::list<COutputEntry> listSent;
323326

324-
CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine);
327+
CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change);
325328

326329
bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY);
327330

@@ -367,6 +370,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM
367370
entry.pushKV("involvesWatchonly", true);
368371
}
369372
MaybePushAddress(entry, r.destination);
373+
PushParentDescriptors(wallet, wtx.tx->vout.at(r.vout).scriptPubKey, entry);
370374
if (wtx.IsCoinBase())
371375
{
372376
if (wallet.GetTxDepthInMainChain(wtx) < 1)
@@ -418,7 +422,11 @@ static const std::vector<RPCResult> TransactionDescriptionString()
418422
{RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."},
419423
{RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction, only present if not empty."},
420424
{RPCResult::Type::STR, "bip125-replaceable", "(\"yes|no|unknown\") Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n"
421-
"may be unknown for unconfirmed transactions not in the mempool."}};
425+
"may be unknown for unconfirmed transactions not in the mempool."},
426+
{RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the scriptPubKey of this coin.", {
427+
{RPCResult::Type::STR, "desc", "The descriptor string."},
428+
}},
429+
};
422430
}
423431

424432
RPCHelpMan listtransactions()
@@ -543,6 +551,7 @@ RPCHelpMan listsinceblock()
543551
{"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
544552
{"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n"
545553
"(not guaranteed to work on pruned nodes)"},
554+
{"include_change", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also add entries for change outputs.\n"},
546555
},
547556
RPCResult{
548557
RPCResult::Type::OBJ, "", "",
@@ -623,6 +632,7 @@ RPCHelpMan listsinceblock()
623632
}
624633

625634
bool include_removed = (request.params[3].isNull() || request.params[3].get_bool());
635+
bool include_change = (!request.params[4].isNull() && request.params[4].get_bool());
626636

627637
int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1;
628638

@@ -632,7 +642,7 @@ RPCHelpMan listsinceblock()
632642
const CWalletTx& tx = pairWtx.second;
633643

634644
if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) {
635-
ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */);
645+
ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */, /*include_change=*/include_change);
636646
}
637647
}
638648

@@ -649,7 +659,7 @@ RPCHelpMan listsinceblock()
649659
if (it != wallet.mapWallet.end()) {
650660
// We want all transactions regardless of confirmation count to appear here,
651661
// even negative confirmation ones, hence the big negative.
652-
ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */);
662+
ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */, /*include_change=*/include_change);
653663
}
654664
}
655665
blockId = block.hashPrevBlock;
@@ -709,6 +719,9 @@ RPCHelpMan gettransaction()
709719
"'send' category of transactions."},
710720
{RPCResult::Type::BOOL, "abandoned", /*optional=*/true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n"
711721
"'send' category of transactions."},
722+
{RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the scriptPubKey of this coin.", {
723+
{RPCResult::Type::STR, "desc", "The descriptor string."},
724+
}},
712725
}},
713726
}},
714727
{RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"},

src/wallet/rpc/util.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ std::string LabelFromValue(const UniValue& value)
123123
return label;
124124
}
125125

126+
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry)
127+
{
128+
UniValue parent_descs(UniValue::VARR);
129+
for (const auto& desc: wallet.GetWalletDescriptors(script_pubkey)) {
130+
parent_descs.push_back(desc.descriptor->ToString());
131+
}
132+
entry.pushKV("parent_descs", parent_descs);
133+
}
134+
126135
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error)
127136
{
128137
if (!wallet) {

src/wallet/rpc/util.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#ifndef BITCOIN_WALLET_RPC_UTIL_H
66
#define BITCOIN_WALLET_RPC_UTIL_H
77

8+
#include <script/script.h>
9+
810
#include <any>
911
#include <memory>
1012
#include <string>
@@ -39,6 +41,8 @@ const LegacyScriptPubKeyMan& EnsureConstLegacyScriptPubKeyMan(const CWallet& wal
3941
bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param);
4042
bool ParseIncludeWatchonly(const UniValue& include_watchonly, const CWallet& wallet);
4143
std::string LabelFromValue(const UniValue& value);
44+
//! Fetch parent descriptors of this scriptPubKey.
45+
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry);
4246

4347
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error);
4448
} // namespace wallet

src/wallet/wallet.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3336,6 +3336,18 @@ std::unique_ptr<SigningProvider> CWallet::GetSolvingProvider(const CScript& scri
33363336
return nullptr;
33373337
}
33383338

3339+
std::vector<WalletDescriptor> CWallet::GetWalletDescriptors(const CScript& script) const
3340+
{
3341+
std::vector<WalletDescriptor> descs;
3342+
for (const auto spk_man: GetScriptPubKeyMans(script)) {
3343+
if (const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man)) {
3344+
LOCK(desc_spk_man->cs_desc_man);
3345+
descs.push_back(desc_spk_man->GetWalletDescriptor());
3346+
}
3347+
}
3348+
return descs;
3349+
}
3350+
33393351
LegacyScriptPubKeyMan* CWallet::GetLegacyScriptPubKeyMan() const
33403352
{
33413353
if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {

src/wallet/wallet.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
845845
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const;
846846
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script, SignatureData& sigdata) const;
847847

848+
//! Get the wallet descriptors for a script.
849+
std::vector<WalletDescriptor> GetWalletDescriptors(const CScript& script) const;
850+
848851
//! Get the LegacyScriptPubKeyMan which is used for all types, internal, and external.
849852
LegacyScriptPubKeyMan* GetLegacyScriptPubKeyMan() const;
850853
LegacyScriptPubKeyMan* GetOrCreateLegacyScriptPubKeyMan();

0 commit comments

Comments
 (0)