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).
pull/25504/head
Antoine Poinsot 2 years ago
parent 55f98d087e
commit 0fd2d14454
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304

@ -74,6 +74,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "listsinceblock", 1, "target_confirmations" }, { "listsinceblock", 1, "target_confirmations" },
{ "listsinceblock", 2, "include_watchonly" }, { "listsinceblock", 2, "include_watchonly" },
{ "listsinceblock", 3, "include_removed" }, { "listsinceblock", 3, "include_removed" },
{ "listsinceblock", 4, "include_change" },
{ "sendmany", 1, "amounts" }, { "sendmany", 1, "amounts" },
{ "sendmany", 2, "minconf" }, { "sendmany", 2, "minconf" },
{ "sendmany", 4, "subtractfeefrom" }, { "sendmany", 4, "subtractfeefrom" },

@ -223,7 +223,8 @@ CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx,
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listReceived, std::list<COutputEntry>& listReceived,
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
bool include_change)
{ {
nFee = 0; nFee = 0;
listReceived.clear(); listReceived.clear();
@ -248,8 +249,7 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
// 2) the output is to us (received) // 2) the output is to us (received)
if (nDebit > 0) if (nDebit > 0)
{ {
// Don't report 'change' txouts if (!include_change && OutputIsChange(wallet, txout))
if (OutputIsChange(wallet, txout))
continue; continue;
} }
else if (!(fIsMine & filter)) else if (!(fIsMine & filter))

@ -44,7 +44,8 @@ struct COutputEntry
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listReceived, std::list<COutputEntry>& listReceived,
std::list<COutputEntry>& listSent, std::list<COutputEntry>& listSent,
CAmount& nFee, const isminefilter& filter); CAmount& nFee, const isminefilter& filter,
bool include_change);
bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx); bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx);

@ -315,13 +315,16 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
* @param filter_label Optional label string to filter incoming transactions. * @param filter_label Optional label string to filter incoming transactions.
*/ */
template <class Vec> template <class Vec>
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) static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong,
Vec& ret, const isminefilter& filter_ismine, const std::string* filter_label,
bool include_change = false)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{ {
CAmount nFee; CAmount nFee;
std::list<COutputEntry> listReceived; std::list<COutputEntry> listReceived;
std::list<COutputEntry> listSent; std::list<COutputEntry> listSent;
CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine); CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change);
bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY);
@ -548,6 +551,7 @@ RPCHelpMan listsinceblock()
{"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"}, {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
{"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n" {"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n"
"(not guaranteed to work on pruned nodes)"}, "(not guaranteed to work on pruned nodes)"},
{"include_change", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also add entries for change outputs.\n"},
}, },
RPCResult{ RPCResult{
RPCResult::Type::OBJ, "", "", RPCResult::Type::OBJ, "", "",
@ -628,6 +632,7 @@ RPCHelpMan listsinceblock()
} }
bool include_removed = (request.params[3].isNull() || request.params[3].get_bool()); bool include_removed = (request.params[3].isNull() || request.params[3].get_bool());
bool include_change = (!request.params[4].isNull() && request.params[4].get_bool());
int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1; int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1;
@ -637,7 +642,7 @@ RPCHelpMan listsinceblock()
const CWalletTx& tx = pairWtx.second; const CWalletTx& tx = pairWtx.second;
if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) { if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) {
ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */); ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */, /*include_change=*/include_change);
} }
} }
@ -654,7 +659,7 @@ RPCHelpMan listsinceblock()
if (it != wallet.mapWallet.end()) { if (it != wallet.mapWallet.end()) {
// We want all transactions regardless of confirmation count to appear here, // We want all transactions regardless of confirmation count to appear here,
// even negative confirmation ones, hence the big negative. // even negative confirmation ones, hence the big negative.
ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */); ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */, /*include_change=*/include_change);
} }
} }
blockId = block.hashPrevBlock; blockId = block.hashPrevBlock;

@ -41,6 +41,7 @@ class ListSinceBlockTest(BitcoinTestFramework):
self.double_spends_filtered() self.double_spends_filtered()
self.test_targetconfirmations() self.test_targetconfirmations()
self.test_desc() self.test_desc()
self.test_send_to_self()
def test_no_blockhash(self): def test_no_blockhash(self):
self.log.info("Test no blockhash") self.log.info("Test no blockhash")
@ -423,6 +424,27 @@ class ListSinceBlockTest(BitcoinTestFramework):
coin_b = next(c for c in coins if c["amount"] == 2) coin_b = next(c for c in coins if c["amount"] == 2)
assert_equal(coin_b["parent_descs"][0], multi_b) assert_equal(coin_b["parent_descs"][0], multi_b)
def test_send_to_self(self):
"""We can make listsinceblock output our change outputs."""
self.log.info("Test the inclusion of change outputs in the output.")
# Create a UTxO paying to one of our change addresses.
block_hash = self.nodes[2].getbestblockhash()
addr = self.nodes[2].getrawchangeaddress()
self.nodes[2].sendtoaddress(addr, 1)
# If we don't list change, we won't have an entry for it.
coins = self.nodes[2].listsinceblock(blockhash=block_hash)["transactions"]
assert not any(c["address"] == addr for c in coins)
# Now if we list change, we'll get both the send (to a change address) and
# the actual change.
res = self.nodes[2].listsinceblock(blockhash=block_hash, include_change=True)
coins = [entry for entry in res["transactions"] if entry["category"] == "receive"]
assert_equal(len(coins), 2)
assert any(c["address"] == addr for c in coins)
assert all(self.nodes[2].getaddressinfo(c["address"])["ischange"] for c in coins)
if __name__ == '__main__': if __name__ == '__main__':
ListSinceBlockTest().main() ListSinceBlockTest().main()

Loading…
Cancel
Save