Skip to content

Commit 0fd2d14

Browse files
committed
rpc: add an include_change parameter to listsinceblock
It's useful for an external application tracking coins to not be limited by our change detection. For instance, for a watchonly wallet with two descriptors a transaction from one to the other would be considered a change output and not be included in the result (if the address was not generated by this wallet).
1 parent 55f98d0 commit 0fd2d14

File tree

5 files changed

+37
-8
lines changed

5 files changed

+37
-8
lines changed

src/rpc/client.cpp

+1
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

+3-3
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx,
223223

224224
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
225225
std::list<COutputEntry>& listReceived,
226-
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter)
226+
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
227+
bool include_change)
227228
{
228229
nFee = 0;
229230
listReceived.clear();
@@ -248,8 +249,7 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
248249
// 2) the output is to us (received)
249250
if (nDebit > 0)
250251
{
251-
// Don't report 'change' txouts
252-
if (OutputIsChange(wallet, txout))
252+
if (!include_change && OutputIsChange(wallet, txout))
253253
continue;
254254
}
255255
else if (!(fIsMine & filter))

src/wallet/receive.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ struct COutputEntry
4444
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
4545
std::list<COutputEntry>& listReceived,
4646
std::list<COutputEntry>& listSent,
47-
CAmount& nFee, const isminefilter& filter);
47+
CAmount& nFee, const isminefilter& filter,
48+
bool include_change);
4849
bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
4950
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
5051
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx);

src/wallet/rpc/transactions.cpp

+9-4
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

@@ -548,6 +551,7 @@ RPCHelpMan listsinceblock()
548551
{"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
549552
{"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n"
550553
"(not guaranteed to work on pruned nodes)"},
554+
{"include_change", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also add entries for change outputs.\n"},
551555
},
552556
RPCResult{
553557
RPCResult::Type::OBJ, "", "",
@@ -628,6 +632,7 @@ RPCHelpMan listsinceblock()
628632
}
629633

630634
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());
631636

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

@@ -637,7 +642,7 @@ RPCHelpMan listsinceblock()
637642
const CWalletTx& tx = pairWtx.second;
638643

639644
if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) {
640-
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);
641646
}
642647
}
643648

@@ -654,7 +659,7 @@ RPCHelpMan listsinceblock()
654659
if (it != wallet.mapWallet.end()) {
655660
// We want all transactions regardless of confirmation count to appear here,
656661
// even negative confirmation ones, hence the big negative.
657-
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);
658663
}
659664
}
660665
blockId = block.hashPrevBlock;

test/functional/wallet_listsinceblock.py

+22
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def run_test(self):
4141
self.double_spends_filtered()
4242
self.test_targetconfirmations()
4343
self.test_desc()
44+
self.test_send_to_self()
4445

4546
def test_no_blockhash(self):
4647
self.log.info("Test no blockhash")
@@ -423,6 +424,27 @@ def test_desc(self):
423424
coin_b = next(c for c in coins if c["amount"] == 2)
424425
assert_equal(coin_b["parent_descs"][0], multi_b)
425426

427+
def test_send_to_self(self):
428+
"""We can make listsinceblock output our change outputs."""
429+
self.log.info("Test the inclusion of change outputs in the output.")
430+
431+
# Create a UTxO paying to one of our change addresses.
432+
block_hash = self.nodes[2].getbestblockhash()
433+
addr = self.nodes[2].getrawchangeaddress()
434+
self.nodes[2].sendtoaddress(addr, 1)
435+
436+
# If we don't list change, we won't have an entry for it.
437+
coins = self.nodes[2].listsinceblock(blockhash=block_hash)["transactions"]
438+
assert not any(c["address"] == addr for c in coins)
439+
440+
# Now if we list change, we'll get both the send (to a change address) and
441+
# the actual change.
442+
res = self.nodes[2].listsinceblock(blockhash=block_hash, include_change=True)
443+
coins = [entry for entry in res["transactions"] if entry["category"] == "receive"]
444+
assert_equal(len(coins), 2)
445+
assert any(c["address"] == addr for c in coins)
446+
assert all(self.nodes[2].getaddressinfo(c["address"])["ischange"] for c in coins)
447+
426448

427449
if __name__ == '__main__':
428450
ListSinceBlockTest().main()

0 commit comments

Comments
 (0)