Line data Source code
1 : // Copyright (c) 2011-2021 The Bitcoin Core developers
2 : // Distributed under the MIT software license, see the accompanying
3 : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 :
5 : #include <core_io.h>
6 : #include <key_io.h>
7 : #include <rpc/util.h>
8 : #include <util/vector.h>
9 : #include <wallet/receive.h>
10 : #include <wallet/rpc/util.h>
11 : #include <wallet/wallet.h>
12 :
13 : using interfaces::FoundBlock;
14 :
15 : namespace wallet {
16 0 : static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue& entry)
17 : EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
18 : {
19 0 : AssertLockHeld(wallet.cs_wallet);
20 :
21 0 : interfaces::Chain& chain = wallet.chain();
22 0 : int confirms = wallet.GetTxDepthInMainChain(wtx);
23 0 : bool fLocked = chain.isInstantSendLockedTx(wtx.GetHash());
24 0 : bool chainlock = false;
25 0 : if (confirms > 0) {
26 0 : chainlock = wallet.IsTxChainLocked(wtx);
27 0 : }
28 0 : entry.pushKV("confirmations", confirms);
29 0 : entry.pushKV("instantlock", fLocked || chainlock);
30 0 : entry.pushKV("instantlock_internal", fLocked);
31 0 : entry.pushKV("chainlock", chainlock);
32 0 : if (wtx.IsCoinBase())
33 0 : entry.pushKV("generated", true);
34 0 : if (wtx.IsPlatformTransfer())
35 0 : entry.pushKV("platform-transfer", true);
36 0 : if (auto* conf = wtx.state<TxStateConfirmed>())
37 : {
38 0 : entry.pushKV("blockhash", conf->confirmed_block_hash.GetHex());
39 0 : entry.pushKV("blockheight", conf->confirmed_block_height);
40 0 : entry.pushKV("blockindex", conf->position_in_block);
41 : int64_t block_time;
42 0 : CHECK_NONFATAL(chain.findBlock(conf->confirmed_block_hash, FoundBlock().time(block_time)));
43 0 : entry.pushKV("blocktime", block_time);
44 0 : } else {
45 0 : entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx));
46 : }
47 0 : uint256 hash = wtx.GetHash();
48 0 : entry.pushKV("txid", hash.GetHex());
49 0 : UniValue conflicts(UniValue::VARR);
50 0 : for (const uint256& conflict : wallet.GetTxConflicts(wtx))
51 0 : conflicts.push_back(conflict.GetHex());
52 0 : entry.pushKV("walletconflicts", conflicts);
53 0 : entry.pushKV("time", wtx.GetTxTime());
54 0 : entry.pushKV("timereceived", int64_t{wtx.nTimeReceived});
55 :
56 0 : for (const std::pair<const std::string, std::string>& item : wtx.mapValue)
57 0 : entry.pushKV(item.first, item.second);
58 0 : }
59 :
60 : struct tallyitem
61 : {
62 0 : CAmount nAmount{0};
63 0 : int nConf{std::numeric_limits<int>::max()};
64 : std::vector<uint256> txids;
65 0 : bool fIsWatchonly{false};
66 0 : tallyitem() = default;
67 : };
68 :
69 0 : static UniValue ListReceived(const CWallet& wallet, const UniValue& params, const bool by_label, const bool include_immature_coinbase) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
70 : {
71 : // Minimum confirmations
72 0 : int nMinDepth = 1;
73 0 : if (!params[0].isNull())
74 0 : nMinDepth = params[0].getInt<int>();
75 0 : bool fAddLocked = false;
76 0 : if (!params[1].isNull())
77 0 : fAddLocked = params[1].get_bool();
78 :
79 : // Whether to include empty labels
80 0 : bool fIncludeEmpty = false;
81 0 : if (!params[2].isNull())
82 0 : fIncludeEmpty = params[2].get_bool();
83 :
84 0 : isminefilter filter = ISMINE_SPENDABLE;
85 :
86 0 : if (ParseIncludeWatchonly(params[3], wallet)) {
87 0 : filter |= ISMINE_WATCH_ONLY;
88 0 : }
89 :
90 0 : std::optional<CTxDestination> filtered_address{std::nullopt};
91 0 : if (!by_label && !params[4].isNull() && !params[4].get_str().empty()) {
92 0 : if (!IsValidDestinationString(params[4].get_str())) {
93 0 : throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid");
94 : }
95 0 : filtered_address = DecodeDestination(params[4].get_str());
96 0 : }
97 :
98 : // Excluding coinbase outputs is deprecated
99 : // It can be enabled by setting deprecatedrpc=exclude_coinbase
100 0 : const bool include_coinbase{!wallet.chain().rpcEnableDeprecated("exclude_coinbase")};
101 :
102 0 : if (include_immature_coinbase && !include_coinbase) {
103 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "include_immature_coinbase is incompatible with deprecated exclude_coinbase");
104 : }
105 :
106 : // Tally
107 0 : std::map<CTxDestination, tallyitem> mapTally;
108 0 : for (const std::pair<const uint256, CWalletTx>& pairWtx : wallet.mapWallet) {
109 0 : const CWalletTx& wtx = pairWtx.second;
110 :
111 0 : int nDepth = wallet.GetTxDepthInMainChain(wtx);
112 0 : if ((nDepth < nMinDepth) && !(fAddLocked && wallet.IsTxLockedByInstantSend(wtx)))
113 0 : continue;
114 :
115 : // Coinbase with less than 1 confirmation is no longer in the main chain
116 0 : if ((wtx.IsCoinBase() && (nDepth < 1 || !include_coinbase))
117 0 : || (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase))
118 : {
119 0 : continue;
120 : }
121 :
122 0 : for (const CTxOut& txout : wtx.tx->vout) {
123 0 : CTxDestination address;
124 0 : if (!ExtractDestination(txout.scriptPubKey, address))
125 0 : continue;
126 :
127 0 : if (filtered_address && !(filtered_address == address)) {
128 0 : continue;
129 : }
130 :
131 0 : isminefilter mine = wallet.IsMine(address);
132 0 : if (!(mine & filter))
133 0 : continue;
134 :
135 0 : tallyitem& item = mapTally[address];
136 0 : item.nAmount += txout.nValue;
137 0 : item.nConf = std::min(item.nConf, nDepth);
138 0 : item.txids.push_back(wtx.GetHash());
139 0 : if (mine & ISMINE_WATCH_ONLY)
140 0 : item.fIsWatchonly = true;
141 : }
142 : }
143 :
144 : // Reply
145 0 : UniValue ret(UniValue::VARR);
146 0 : std::map<std::string, tallyitem> label_tally;
147 :
148 0 : const auto& func = [&](const CTxDestination& address, const std::string& label, const std::string& purpose, bool is_change)
149 : {
150 0 : if (is_change) return; // no change addresses
151 0 : auto it = mapTally.find(address);
152 0 : if (it == mapTally.end() && !fIncludeEmpty)
153 0 : return;
154 :
155 0 : CAmount nAmount = 0;
156 0 : int nConf = std::numeric_limits<int>::max();
157 0 : bool fIsWatchonly = false;
158 0 : if (it != mapTally.end()) {
159 0 : nAmount = (*it).second.nAmount;
160 0 : nConf = (*it).second.nConf;
161 0 : fIsWatchonly = (*it).second.fIsWatchonly;
162 0 : }
163 :
164 0 : if (by_label) {
165 0 : tallyitem& _item = label_tally[label];
166 0 : _item.nAmount += nAmount;
167 0 : _item.nConf = std::min(_item.nConf, nConf);
168 0 : _item.fIsWatchonly = fIsWatchonly;
169 0 : } else {
170 0 : UniValue obj(UniValue::VOBJ);
171 0 : if(fIsWatchonly) obj.pushKV("involvesWatchonly", true);
172 0 : obj.pushKV("address", EncodeDestination(address));
173 0 : obj.pushKV("amount", ValueFromAmount(nAmount));
174 0 : obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
175 0 : obj.pushKV("label", label);
176 0 : UniValue transactions(UniValue::VARR);
177 0 : if (it != mapTally.end()) {
178 0 : for (const uint256& _item : (*it).second.txids) {
179 0 : transactions.push_back(_item.GetHex());
180 : }
181 0 : }
182 0 : obj.pushKV("txids", transactions);
183 0 : ret.push_back(obj);
184 0 : }
185 0 : };
186 :
187 0 : if (filtered_address) {
188 0 : const auto& entry = wallet.FindAddressBookEntry(*filtered_address, /*allow_change=*/false);
189 0 : if (entry) func(*filtered_address, entry->GetLabel(), entry->purpose, /*is_change=*/false);
190 0 : } else {
191 : // No filtered addr, walk-through the addressbook entry
192 0 : wallet.ForEachAddrBookEntry(func);
193 : }
194 :
195 0 : if (by_label) {
196 0 : for (const auto& entry : label_tally) {
197 0 : CAmount nAmount = entry.second.nAmount;
198 0 : int nConf = entry.second.nConf;
199 0 : UniValue obj(UniValue::VOBJ);
200 0 : if (entry.second.fIsWatchonly)
201 0 : obj.pushKV("involvesWatchonly", true);
202 0 : obj.pushKV("amount", ValueFromAmount(nAmount));
203 0 : obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
204 0 : obj.pushKV("label", entry.first);
205 0 : ret.push_back(obj);
206 0 : }
207 0 : }
208 :
209 0 : return ret;
210 0 : }
211 :
212 8 : RPCHelpMan listreceivedbyaddress()
213 : {
214 16 : return RPCHelpMan{"listreceivedbyaddress",
215 8 : "\nList balances by receiving address.\n",
216 56 : {
217 8 : {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
218 8 : {"addlocked", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include transactions locked via InstantSend."},
219 8 : {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include addresses that haven't received any payments."},
220 8 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
221 8 : {"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If present and non-empty, only return information on this address."},
222 8 : {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
223 : },
224 8 : RPCResult{
225 8 : RPCResult::Type::ARR, "", "",
226 16 : {
227 16 : {RPCResult::Type::OBJ, "", "",
228 56 : {
229 8 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
230 8 : {RPCResult::Type::STR, "address", "The receiving address"},
231 8 : {RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received by the address"},
232 8 : {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included.\n"
233 : "If 'addlocked' is true, the number of confirmations can be less than\n"
234 : "configured for transactions locked via InstantSend"},
235 8 : {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""},
236 16 : {RPCResult::Type::ARR, "txids", "",
237 16 : {
238 8 : {RPCResult::Type::STR_HEX, "txid", "The ids of transactions received with the address"},
239 : }},
240 : }},
241 : }
242 : },
243 8 : RPCExamples{
244 8 : HelpExampleCli("listreceivedbyaddress", "")
245 8 : + HelpExampleCli("listreceivedbyaddress", "6 false true")
246 8 : + HelpExampleCli("listreceivedbyaddress", "6 false true true \"\" true")
247 8 : + HelpExampleRpc("listreceivedbyaddress", "6, false, true, true")
248 8 : + HelpExampleRpc("listreceivedbyaddress", "6, false, true, true, \"" + EXAMPLE_ADDRESS[0] + "\"")
249 8 : + HelpExampleRpc("listreceivedbyaddress", "6, false true, true, \"" + EXAMPLE_ADDRESS[0] + "\", true")
250 : },
251 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
252 : {
253 0 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
254 0 : if (!pwallet) return UniValue::VNULL;
255 :
256 : // Make sure the results are valid at least up to the most recent block
257 : // the user could have gotten from another RPC command prior to now
258 0 : pwallet->BlockUntilSyncedToCurrentChain();
259 :
260 0 : const bool include_immature_coinbase{request.params[5].isNull() ? false : request.params[5].get_bool()};
261 :
262 0 : LOCK(pwallet->cs_wallet);
263 :
264 0 : return ListReceived(*pwallet, request.params, false, include_immature_coinbase);
265 0 : },
266 : };
267 0 : }
268 :
269 8 : RPCHelpMan listreceivedbylabel()
270 : {
271 16 : return RPCHelpMan{"listreceivedbylabel",
272 8 : "\nList received transactions by label.\n",
273 48 : {
274 8 : {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
275 8 : {"addlocked", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include transactions locked via InstantSend."},
276 8 : {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include labels that haven't received any payments."},
277 8 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
278 8 : {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
279 : },
280 8 : RPCResult{
281 8 : RPCResult::Type::ARR, "", "",
282 16 : {
283 16 : {RPCResult::Type::OBJ, "", "",
284 40 : {
285 8 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
286 8 : {RPCResult::Type::STR_AMOUNT, "amount", "The total amount received by addresses with this label"},
287 8 : {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"},
288 8 : {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""},
289 : }},
290 : }
291 : },
292 8 : RPCExamples{
293 8 : HelpExampleCli("listreceivedbylabel", "")
294 8 : + HelpExampleCli("listreceivedbylabel", "6 true")
295 8 : + HelpExampleRpc("listreceivedbylabel", "6, true, true")
296 8 : + HelpExampleRpc("listreceivedbylabel", "6, true, true, true")
297 : },
298 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
299 : {
300 0 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
301 0 : if (!pwallet) return UniValue::VNULL;
302 :
303 : // Make sure the results are valid at least up to the most recent block
304 : // the user could have gotten from another RPC command prior to now
305 0 : pwallet->BlockUntilSyncedToCurrentChain();
306 :
307 0 : const bool include_immature_coinbase{request.params[4].isNull() ? false : request.params[4].get_bool()};
308 :
309 0 : LOCK(pwallet->cs_wallet);
310 :
311 0 : return ListReceived(*pwallet, request.params, true, include_immature_coinbase);
312 0 : },
313 : };
314 0 : }
315 :
316 0 : static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
317 : {
318 0 : if (IsValidDestination(dest)) {
319 0 : entry.pushKV("address", EncodeDestination(dest));
320 0 : }
321 0 : }
322 :
323 : /**
324 : * List transactions based on the given criteria.
325 : *
326 : * @param wallet The wallet.
327 : * @param wtx The wallet transaction.
328 : * @param nMinDepth The minimum confirmation depth.
329 : * @param fLong Whether to include the JSON version of the transaction.
330 : * @param ret The vector into which the result is stored.
331 : * @param filter_ismine The "is mine" filter flags.
332 : * @param filter_label Optional label string to filter incoming transactions.
333 : */
334 : template <class Vec>
335 0 : 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)
336 : {
337 : CAmount nFee;
338 0 : std::list<COutputEntry> listReceived;
339 0 : std::list<COutputEntry> listSent;
340 :
341 0 : CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine);
342 :
343 0 : bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY);
344 :
345 : // Sent
346 0 : if (!filter_label)
347 : {
348 0 : for (const COutputEntry& s : listSent)
349 : {
350 0 : UniValue entry(UniValue::VOBJ);
351 0 : if (involvesWatchonly || (wallet.IsMine(s.destination) & ISMINE_WATCH_ONLY)) {
352 0 : entry.pushKV("involvesWatchonly", true);
353 0 : }
354 0 : MaybePushAddress(entry, s.destination);
355 0 : std::map<std::string, std::string>::const_iterator it = wtx.mapValue.find("DS");
356 0 : entry.pushKV("category", (it != wtx.mapValue.end() && it->second == "1") ? "coinjoin" : "send");
357 0 : entry.pushKV("amount", ValueFromAmount(-s.amount));
358 0 : const auto* address_book_entry = wallet.FindAddressBookEntry(s.destination);
359 0 : if (address_book_entry) {
360 0 : entry.pushKV("label", address_book_entry->GetLabel());
361 0 : }
362 0 : entry.pushKV("vout", s.vout);
363 0 : entry.pushKV("fee", ValueFromAmount(-nFee));
364 0 : if (fLong)
365 0 : WalletTxToJSON(wallet, wtx, entry);
366 0 : entry.pushKV("abandoned", wtx.isAbandoned());
367 0 : ret.push_back(entry);
368 0 : }
369 0 : }
370 :
371 : // Received
372 0 : if (listReceived.size() > 0 && ((wallet.GetTxDepthInMainChain(wtx) >= nMinDepth) || wallet.IsTxLockedByInstantSend(wtx))) {
373 0 : for (const COutputEntry& r : listReceived)
374 : {
375 0 : std::string label;
376 0 : const auto* address_book_entry = wallet.FindAddressBookEntry(r.destination);
377 0 : if (address_book_entry) {
378 0 : label = address_book_entry->GetLabel();
379 0 : }
380 0 : if (filter_label && label != *filter_label) {
381 0 : continue;
382 : }
383 0 : UniValue entry(UniValue::VOBJ);
384 0 : if (involvesWatchonly || (wallet.IsMine(r.destination) & ISMINE_WATCH_ONLY)) {
385 0 : entry.pushKV("involvesWatchonly", true);
386 0 : }
387 0 : MaybePushAddress(entry, r.destination);
388 0 : if (wtx.IsCoinBase())
389 : {
390 0 : if (wallet.GetTxDepthInMainChain(wtx) < 1)
391 0 : entry.pushKV("category", "orphan");
392 0 : else if (wallet.IsTxImmatureCoinBase(wtx))
393 0 : entry.pushKV("category", "immature");
394 : else
395 0 : entry.pushKV("category", "generate");
396 0 : }
397 0 : else if (wtx.IsPlatformTransfer())
398 : {
399 0 : entry.pushKV("category", "platform-transfer");
400 0 : }
401 : else
402 : {
403 0 : entry.pushKV("category", "receive");
404 : }
405 0 : entry.pushKV("amount", ValueFromAmount(r.amount));
406 0 : if (address_book_entry) {
407 0 : entry.pushKV("label", label);
408 0 : }
409 0 : entry.pushKV("vout", r.vout);
410 0 : entry.pushKV("abandoned", wtx.isAbandoned());
411 0 : if (fLong)
412 0 : WalletTxToJSON(wallet, wtx, entry);
413 0 : ret.push_back(entry);
414 0 : }
415 0 : }
416 0 : }
417 :
418 24 : static std::vector<RPCResult> TransactionDescriptionString()
419 : {
420 336 : return{{RPCResult::Type::NUM, "confirmations", "The number of blockchain confirmations for the transaction. Available for 'send' and\n"
421 : "'receive' category of transactions. Negative confirmations indicate the\n"
422 : "transaction conflicts with the block chain"},
423 24 : {RPCResult::Type::BOOL, "instantlock", "Current transaction lock state. Available for 'send' and 'receive' category of transactions"},
424 24 : {RPCResult::Type::BOOL, "instantlock-internal", "Current internal transaction lock state. Available for 'send' and 'receive' category of transactions"},
425 24 : {RPCResult::Type::BOOL, "chainlock", "The state of the corresponding block ChainLock"},
426 24 : {RPCResult::Type::BOOL, "generated", /*optional=*/true, "Only present if the transaction's only input is a coinbase one."},
427 24 : {RPCResult::Type::BOOL, "trusted", /*optional=*/true, "Whether we consider the transaction to be trusted and safe to spend from.\n"
428 : "Only present when the transaction has 0 confirmations (or negative confirmations, if conflicted)."},
429 24 : {RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block hash containing the transaction. Available for 'send' and 'receive'\n"
430 : "category of transactions."},
431 24 : {RPCResult::Type::STR_HEX, "blockheight", /*optional=*/true, "The block height containing the transaction."},
432 24 : {RPCResult::Type::NUM, "blockindex", /*optional=*/true, "The index of the transaction in the block that includes it. Available for 'send' and 'receive'\n"
433 : "category of transactions."},
434 24 : {RPCResult::Type::NUM_TIME, "blocktime", /*optional=*/true, "The block time expressed in " + UNIX_EPOCH_TIME + "."},
435 24 : {RPCResult::Type::STR_HEX, "txid", "The transaction id. Available for 'send' and 'receive' category of transactions."},
436 24 : {RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."},
437 24 : {RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + ". Available \n"
438 : "for 'send' and 'receive' category of transactions."},
439 24 : {RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction."}};
440 0 : }
441 :
442 8 : RPCHelpMan listtransactions()
443 : {
444 16 : return RPCHelpMan{"listtransactions",
445 8 : "\nIf a label name is provided, this will return only incoming transactions paying to addresses with the specified label.\n"
446 : "\nReturns up to 'count' most recent transactions skipping the first 'from' transactions.\n",
447 40 : {
448 8 : {"label|dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, should be a valid label name to return only incoming transactions\n"
449 : "with the specified label, or \"*\" to disable filtering and return all transactions."},
450 8 : {"count", RPCArg::Type::NUM, RPCArg::Default{10}, "The number of transactions to return"},
451 8 : {"skip", RPCArg::Type::NUM, RPCArg::Default{0}, "The number of transactions to skip"},
452 8 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
453 : },
454 8 : RPCResult{
455 8 : RPCResult::Type::ARR, "", "",
456 16 : {
457 24 : {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
458 64 : {
459 8 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
460 8 : {RPCResult::Type::STR, "address", /*optional=*/true, "The Dash address of the transaction. Not present for\n"
461 : "move transactions (category = move)."},
462 8 : {RPCResult::Type::STR, "category", "The transaction category.\n"
463 : "\"send\" Transactions sent.\n"
464 : "\"coinjoin\" Transactions sent using CoinJoin funds.\n"
465 : "\"receive\" Non-coinbase transactions received.\n"
466 : "\"generate\" Coinbase transactions received with more than 100 confirmations.\n"
467 : "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n"
468 : "\"orphan\" Orphaned coinbase transactions received.\n"
469 : "\"platform-transfer\" Platform Transfer transactions received.\n"},
470 8 : {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n"
471 : "for all other categories"},
472 8 : {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
473 8 : {RPCResult::Type::NUM, "vout", "the vout value"},
474 8 : {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n"
475 : "'send' category of transactions."},
476 : },
477 8 : TransactionDescriptionString()),
478 16 : {
479 8 : {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
480 : })},
481 : }
482 : },
483 8 : RPCExamples{
484 : "\nList the most recent 10 transactions in the systems\n"
485 8 : + HelpExampleCli("listtransactions", "") +
486 : "\nList transactions 100 to 120\n"
487 8 : + HelpExampleCli("listtransactions", "\"\" 20 100") +
488 : "\nAs a json rpc call\n"
489 8 : + HelpExampleRpc("listtransactions", "\"\", 20, 100")
490 : },
491 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
492 : {
493 0 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
494 0 : if (!pwallet) return UniValue::VNULL;
495 :
496 : // Make sure the results are valid at least up to the most recent block
497 : // the user could have gotten from another RPC command prior to now
498 0 : pwallet->BlockUntilSyncedToCurrentChain();
499 :
500 0 : const std::string* filter_label = nullptr;
501 0 : if (!request.params[0].isNull() && request.params[0].get_str() != "*") {
502 0 : filter_label = &request.params[0].get_str();
503 0 : if (filter_label->empty()) {
504 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Label argument must be a valid label name or \"*\".");
505 : }
506 0 : }
507 0 : int nCount = 10;
508 0 : if (!request.params[1].isNull())
509 0 : nCount = request.params[1].getInt<int>();
510 0 : int nFrom = 0;
511 0 : if (!request.params[2].isNull())
512 0 : nFrom = request.params[2].getInt<int>();
513 0 : isminefilter filter = ISMINE_SPENDABLE;
514 :
515 0 : if (ParseIncludeWatchonly(request.params[3], *pwallet)) {
516 0 : filter |= ISMINE_WATCH_ONLY;
517 0 : }
518 :
519 0 : if (nCount < 0)
520 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count");
521 0 : if (nFrom < 0)
522 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative from");
523 :
524 0 : std::vector<UniValue> ret;
525 : {
526 0 : LOCK(pwallet->cs_wallet);
527 :
528 0 : const CWallet::TxItems & txOrdered = pwallet->wtxOrdered;
529 :
530 : // iterate backwards until we have nCount items to return:
531 0 : for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it)
532 : {
533 0 : CWalletTx *const pwtx = (*it).second;
534 0 : ListTransactions(*pwallet, *pwtx, 0, true, ret, filter, filter_label);
535 0 : if ((int)ret.size() >= (nCount+nFrom)) break;
536 0 : }
537 0 : }
538 :
539 : // ret is newest to oldest
540 :
541 0 : if (nFrom > (int)ret.size())
542 0 : nFrom = ret.size();
543 0 : if ((nFrom + nCount) > (int)ret.size())
544 0 : nCount = ret.size() - nFrom;
545 :
546 0 : auto txs_rev_it{std::make_move_iterator(ret.rend())};
547 0 : UniValue result{UniValue::VARR};
548 0 : result.push_backV(txs_rev_it - nFrom - nCount, txs_rev_it - nFrom); // Return oldest to newest
549 0 : return result;
550 0 : },
551 : };
552 0 : }
553 :
554 8 : RPCHelpMan listsinceblock()
555 : {
556 16 : return RPCHelpMan{"listsinceblock",
557 8 : "\nGet all transactions in blocks since block [blockhash], or all transactions if omitted.\n"
558 : "If \"blockhash\" is no longer a part of the main chain, transactions from the fork point onward are included.\n"
559 : "Additionally, if include_removed is set, transactions affecting the wallet which were removed are returned in the \"removed\" array.\n",
560 40 : {
561 8 : {"blockhash", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, the block hash to list transactions since, otherwise list all transactions."},
562 8 : {"target_confirmations", RPCArg::Type::NUM, RPCArg::Default{1}, "Return the nth block hash from the main chain. e.g. 1 would mean the best block hash. Note: this is not used as a filter, but only affects [lastblock] in the return value"},
563 8 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
564 8 : {"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n"
565 : "(not guaranteed to work on pruned nodes)"},
566 : },
567 8 : RPCResult{
568 8 : RPCResult::Type::OBJ, "", "",
569 32 : {
570 16 : {RPCResult::Type::ARR, "transactions", "",
571 16 : {
572 24 : {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
573 56 : {
574 8 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
575 8 : {RPCResult::Type::STR, "address", /*optional=*/true, "The Dash address of the transaction."},
576 8 : {RPCResult::Type::STR, "category", "The transaction category.\n"
577 : "\"send\" Transactions sent.\n"
578 : "\"coinjoin\" Transactions sent using CoinJoin funds.\n"
579 : "\"receive\" Non-coinbase transactions received.\n"
580 : "\"generate\" Coinbase transactions received with more than 100 confirmations.\n"
581 : "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n"
582 : "\"orphan\" Orphaned coinbase transactions received.\n"
583 : "\"platform-transfer\" Platform Transfer transactions received.\n"},
584 8 : {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n"
585 : "for all other categories"},
586 8 : {RPCResult::Type::NUM, "vout", "the vout value"},
587 8 : {RPCResult::Type::NUM, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the 'send' category of transactions."},
588 : },
589 8 : TransactionDescriptionString()),
590 32 : {
591 8 : {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
592 8 : {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any."},
593 8 : {RPCResult::Type::STR, "to", "If a comment to is associated with the transaction."},
594 : })},
595 : }},
596 16 : {RPCResult::Type::ARR, "removed", /*optional=*/true, "<structure is the same as \"transactions\" above, only present if include_removed=true>\n"
597 : "Note: transactions that were re-added in the active chain will appear as-is in this array, and may thus have a positive confirmation count."
598 8 : , {{RPCResult::Type::ELISION, "", ""},}},
599 8 : {RPCResult::Type::STR_HEX, "lastblockhash", "The hash of the block (target_confirmations-1) from the best block on the main chain, or the genesis hash if the referenced block does not exist yet. This is typically used to feed back into listsinceblock the next time you call it. So you would generally use a target_confirmations of say 6, so you will be continually re-notified of transactions until they've reached 6 confirmations plus any new ones."}
600 : }
601 : },
602 8 : RPCExamples{
603 8 : HelpExampleCli("listsinceblock", "")
604 8 : + HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6")
605 8 : + HelpExampleRpc("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\", 6")
606 : },
607 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
608 : {
609 0 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
610 0 : if (!pwallet) return UniValue::VNULL;
611 :
612 0 : const CWallet& wallet = *pwallet;
613 : // Make sure the results are valid at least up to the most recent block
614 : // the user could have gotten from another RPC command prior to now
615 0 : wallet.BlockUntilSyncedToCurrentChain();
616 :
617 0 : LOCK(wallet.cs_wallet);
618 :
619 0 : std::optional<int> height; // Height of the specified block or the common ancestor, if the block provided was in a deactivated chain.
620 0 : std::optional<int> altheight; // Height of the specified block, even if it's in a deactivated chain.
621 0 : int target_confirms = 1;
622 0 : isminefilter filter = ISMINE_SPENDABLE;
623 :
624 0 : uint256 blockId;
625 0 : if (!request.params[0].isNull() && !request.params[0].get_str().empty()) {
626 0 : blockId = ParseHashV(request.params[0], "blockhash");
627 0 : height = int{};
628 0 : altheight = int{};
629 0 : if (!wallet.chain().findCommonAncestor(blockId, wallet.GetLastBlockHash(), /* ancestor out */ FoundBlock().height(*height), /* blockId out */ FoundBlock().height(*altheight))) {
630 0 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
631 : }
632 0 : }
633 :
634 0 : if (!request.params[1].isNull()) {
635 0 : target_confirms = request.params[1].getInt<int>();
636 :
637 0 : if (target_confirms < 1) {
638 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter");
639 : }
640 0 : }
641 :
642 0 : if (ParseIncludeWatchonly(request.params[2], wallet)) {
643 0 : filter |= ISMINE_WATCH_ONLY;
644 0 : }
645 :
646 0 : bool include_removed = (request.params[3].isNull() || request.params[3].get_bool());
647 :
648 0 : int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1;
649 :
650 0 : UniValue transactions(UniValue::VARR);
651 :
652 0 : for (const std::pair<const uint256, CWalletTx>& pairWtx : wallet.mapWallet) {
653 0 : const CWalletTx& tx = pairWtx.second;
654 :
655 0 : if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) {
656 0 : ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */);
657 0 : }
658 : }
659 :
660 : // when a reorg'd block is requested, we also list any relevant transactions
661 : // in the blocks of the chain that was detached
662 0 : UniValue removed(UniValue::VARR);
663 0 : while (include_removed && altheight && *altheight > *height) {
664 0 : CBlock block;
665 0 : if (!wallet.chain().findBlock(blockId, FoundBlock().data(block)) || block.IsNull()) {
666 0 : throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk");
667 : }
668 0 : for (const CTransactionRef& tx : block.vtx) {
669 0 : auto it = wallet.mapWallet.find(tx->GetHash());
670 0 : if (it != wallet.mapWallet.end()) {
671 : // We want all transactions regardless of confirmation count to appear here,
672 : // even negative confirmation ones, hence the big negative.
673 0 : ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */);
674 0 : }
675 : }
676 0 : blockId = block.hashPrevBlock;
677 0 : --*altheight;
678 0 : }
679 :
680 0 : uint256 lastblock;
681 0 : target_confirms = std::min(target_confirms, wallet.GetLastBlockHeight() + 1);
682 0 : CHECK_NONFATAL(wallet.chain().findAncestorByHeight(wallet.GetLastBlockHash(), wallet.GetLastBlockHeight() + 1 - target_confirms, FoundBlock().hash(lastblock)));
683 :
684 0 : UniValue ret(UniValue::VOBJ);
685 0 : ret.pushKV("transactions", transactions);
686 0 : if (include_removed) ret.pushKV("removed", removed);
687 0 : ret.pushKV("lastblock", lastblock.GetHex());
688 :
689 0 : return ret;
690 0 : },
691 : };
692 0 : }
693 :
694 8 : RPCHelpMan gettransaction()
695 : {
696 16 : return RPCHelpMan{"gettransaction",
697 8 : "\nGet detailed information about in-wallet transaction <txid>\n",
698 32 : {
699 8 : {"txid", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id"},
700 8 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses in balance calculation and details[]"},
701 8 : {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction)"},
702 : },
703 8 : RPCResult{
704 24 : RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
705 24 : {
706 8 : {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT},
707 8 : {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n"
708 : "'send' category of transactions."},
709 : },
710 8 : TransactionDescriptionString()),
711 32 : {
712 16 : {RPCResult::Type::ARR, "details", "",
713 16 : {
714 16 : {RPCResult::Type::OBJ, "", "",
715 72 : {
716 8 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
717 8 : {RPCResult::Type::STR, "address", /*optional=*/true, "The Dash address involved in the transaction."},
718 8 : {RPCResult::Type::STR, "category", "The transaction category.\n"
719 : "\"send\" Transactions sent.\n"
720 : "\"coinjoin\" Transactions sent using CoinJoin funds.\n"
721 : "\"receive\" Non-coinbase transactions received.\n"
722 : "\"generate\" Coinbase transactions received with more than 100 confirmations.\n"
723 : "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n"
724 : "\"orphan\" Orphaned coinbase transactions received.\n"
725 : "\"platform-transfer\" Platform Transfer transactions received.\n"},
726 8 : {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT},
727 8 : {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
728 8 : {RPCResult::Type::NUM, "vout", "the vout value"},
729 8 : {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n"
730 : "'send' category of transactions."},
731 8 : {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
732 : }},
733 : }},
734 8 : {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"},
735 16 : {RPCResult::Type::OBJ, "decoded", /*optional=*/true, "the decoded transaction (only present when `verbose` is passed), equivalent to the",
736 16 : {
737 8 : {RPCResult::Type::ELISION, "", "RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."},
738 : }},
739 8 : RESULT_LAST_PROCESSED_BLOCK,
740 : }),
741 : },
742 8 : RPCExamples{
743 8 : HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
744 8 : + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true")
745 8 : + HelpExampleRpc("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
746 : },
747 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
748 : {
749 0 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
750 0 : if (!pwallet) return UniValue::VNULL;
751 :
752 : // Make sure the results are valid at least up to the most recent block
753 : // the user could have gotten from another RPC command prior to now
754 0 : pwallet->BlockUntilSyncedToCurrentChain();
755 :
756 0 : LOCK(pwallet->cs_wallet);
757 :
758 0 : uint256 hash(ParseHashV(request.params[0], "txid"));
759 :
760 0 : isminefilter filter = ISMINE_SPENDABLE;
761 :
762 0 : if (ParseIncludeWatchonly(request.params[1], *pwallet)) {
763 0 : filter |= ISMINE_WATCH_ONLY;
764 0 : }
765 :
766 0 : bool verbose = request.params[2].isNull() ? false : request.params[2].get_bool();
767 :
768 0 : UniValue entry(UniValue::VOBJ);
769 0 : auto it = pwallet->mapWallet.find(hash);
770 0 : if (it == pwallet->mapWallet.end()) {
771 0 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
772 : }
773 0 : const CWalletTx& wtx = it->second;
774 :
775 0 : CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter);
776 0 : CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter);
777 0 : CAmount nNet = nCredit - nDebit;
778 0 : CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0);
779 :
780 0 : entry.pushKV("amount", ValueFromAmount(nNet - nFee));
781 0 : if (CachedTxIsFromMe(*pwallet, wtx, filter))
782 0 : entry.pushKV("fee", ValueFromAmount(nFee));
783 :
784 0 : WalletTxToJSON(*pwallet, wtx, entry);
785 :
786 0 : UniValue details(UniValue::VARR);
787 0 : ListTransactions(*pwallet, wtx, 0, false, details, filter, nullptr /* filter_label */);
788 0 : entry.pushKV("details", details);
789 :
790 0 : std::string strHex = EncodeHexTx(*wtx.tx);
791 0 : entry.pushKV("hex", strHex);
792 :
793 0 : if (verbose) {
794 0 : UniValue decoded(UniValue::VOBJ);
795 0 : TxToUniv(*wtx.tx, /*block_hash=*/uint256(), /*entry=*/decoded, /*include_hex=*/false);
796 0 : entry.pushKV("decoded", decoded);
797 0 : }
798 :
799 0 : AppendLastProcessedBlock(entry, *pwallet);
800 0 : return entry;
801 0 : },
802 : };
803 0 : }
804 :
805 8 : RPCHelpMan abandontransaction()
806 : {
807 16 : return RPCHelpMan{"abandontransaction",
808 8 : "\nMark in-wallet transaction <txid> as abandoned\n"
809 : "This will mark this transaction and all its in-wallet descendants as abandoned which will allow\n"
810 : "for their inputs to be respent. It can be used to replace \"stuck\" or evicted transactions.\n"
811 : "It only works on transactions which are not included in a block and are not currently in the mempool.\n"
812 : "It has no effect on transactions which are already abandoned.\n",
813 16 : {
814 8 : {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
815 : },
816 8 : RPCResult{RPCResult::Type::NONE, "", ""},
817 8 : RPCExamples{
818 8 : HelpExampleCli("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
819 8 : + HelpExampleRpc("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
820 : },
821 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
822 : {
823 0 : std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
824 0 : if (!pwallet) return UniValue::VNULL;
825 :
826 : // Make sure the results are valid at least up to the most recent block
827 : // the user could have gotten from another RPC command prior to now
828 0 : pwallet->BlockUntilSyncedToCurrentChain();
829 :
830 0 : LOCK(pwallet->cs_wallet);
831 :
832 0 : uint256 hash(ParseHashV(request.params[0], "txid"));
833 :
834 0 : if (!pwallet->mapWallet.count(hash)) {
835 0 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
836 : }
837 0 : if (!pwallet->AbandonTransaction(hash)) {
838 0 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not eligible for abandonment");
839 : }
840 :
841 0 : return UniValue::VNULL;
842 0 : },
843 : };
844 0 : }
845 :
846 8 : RPCHelpMan rescanblockchain()
847 : {
848 16 : return RPCHelpMan{"rescanblockchain",
849 8 : "\nRescan the local blockchain for wallet related transactions.\n"
850 : "Note: Use \"getwalletinfo\" to query the scanning progress.\n"
851 : "The rescan is significantly faster when used on a descriptor wallet\n"
852 : "and block filters are available (using startup option \"-blockfilterindex=1\").\n",
853 24 : {
854 8 : {"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "block height where the rescan should start"},
855 8 : {"stop_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED_NAMED_ARG, "the last block height that should be scanned. If none is provided it will rescan up to the tip at return time of this call."},
856 : },
857 8 : RPCResult{
858 8 : RPCResult::Type::OBJ, "", "",
859 24 : {
860 8 : {RPCResult::Type::NUM, "start_height", "The block height where the rescan started (the requested height or 0)"},
861 8 : {RPCResult::Type::NUM, "stop_height", "The height of the last rescanned block. May be null in rare cases if there was a reorg and the call didn't scan any blocks because they were already scanned in the background."},
862 : }
863 : },
864 8 : RPCExamples{
865 8 : HelpExampleCli("rescanblockchain", "100000 120000")
866 8 : + HelpExampleRpc("rescanblockchain", "100000, 120000")
867 : },
868 8 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
869 : {
870 0 : std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
871 0 : if (!pwallet) return UniValue::VNULL;
872 0 : CWallet& wallet{*pwallet};
873 :
874 : // Make sure the results are valid at least up to the most recent block
875 : // the user could have gotten from another RPC command prior to now
876 0 : wallet.BlockUntilSyncedToCurrentChain();
877 :
878 0 : WalletRescanReserver reserver(*pwallet);
879 0 : if (!reserver.reserve()) {
880 0 : throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
881 : }
882 :
883 0 : int start_height = 0;
884 0 : std::optional<int> stop_height;
885 0 : uint256 start_block;
886 : {
887 0 : LOCK(pwallet->cs_wallet);
888 0 : int tip_height = pwallet->GetLastBlockHeight();
889 :
890 0 : if (!request.params[0].isNull()) {
891 0 : start_height = request.params[0].getInt<int>();
892 0 : if (start_height < 0 || start_height > tip_height) {
893 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height");
894 : }
895 0 : }
896 :
897 0 : if (!request.params[1].isNull()) {
898 0 : stop_height = request.params[1].getInt<int>();
899 0 : if (*stop_height < 0 || *stop_height > tip_height) {
900 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height");
901 0 : } else if (*stop_height < start_height) {
902 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "stop_height must be greater than start_height");
903 : }
904 0 : }
905 :
906 : // We can't rescan beyond non-pruned blocks, stop and throw an error
907 0 : if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(), start_height, stop_height)) {
908 0 : throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height.");
909 : }
910 :
911 0 : CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block)));
912 0 : }
913 :
914 : CWallet::ScanResult result =
915 0 : pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, /*fUpdate=*/true, /*save_progress=*/false);
916 0 : switch (result.status) {
917 : case CWallet::ScanResult::SUCCESS:
918 0 : break;
919 : case CWallet::ScanResult::FAILURE:
920 0 : throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corrupted data files.");
921 : case CWallet::ScanResult::USER_ABORT:
922 0 : throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted.");
923 : // no default case, so the compiler can warn about missing cases
924 : }
925 0 : UniValue response(UniValue::VOBJ);
926 0 : response.pushKV("start_height", start_height);
927 0 : response.pushKV("stop_height", result.last_scanned_height ? *result.last_scanned_height : UniValue());
928 0 : return response;
929 0 : },
930 : };
931 0 : }
932 : } // namespace wallet
|