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 5383 : static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue& entry)
17 : EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
18 : {
19 5383 : AssertLockHeld(wallet.cs_wallet);
20 :
21 5383 : interfaces::Chain& chain = wallet.chain();
22 5383 : int confirms = wallet.GetTxDepthInMainChain(wtx);
23 5383 : bool fLocked = chain.isInstantSendLockedTx(wtx.GetHash());
24 5383 : bool chainlock = false;
25 5383 : if (confirms > 0) {
26 3675 : chainlock = wallet.IsTxChainLocked(wtx);
27 3675 : }
28 5383 : entry.pushKV("confirmations", confirms);
29 5383 : entry.pushKV("instantlock", fLocked || chainlock);
30 5383 : entry.pushKV("instantlock_internal", fLocked);
31 5383 : entry.pushKV("chainlock", chainlock);
32 5383 : if (wtx.IsCoinBase())
33 3186 : entry.pushKV("generated", true);
34 5383 : if (wtx.IsPlatformTransfer())
35 12 : entry.pushKV("platform-transfer", true);
36 5383 : if (auto* conf = wtx.state<TxStateConfirmed>())
37 : {
38 3675 : entry.pushKV("blockhash", conf->confirmed_block_hash.GetHex());
39 3675 : entry.pushKV("blockheight", conf->confirmed_block_height);
40 3675 : entry.pushKV("blockindex", conf->position_in_block);
41 : int64_t block_time;
42 3675 : CHECK_NONFATAL(chain.findBlock(conf->confirmed_block_hash, FoundBlock().time(block_time)));
43 3675 : entry.pushKV("blocktime", block_time);
44 3675 : } else {
45 1708 : entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx));
46 : }
47 5383 : uint256 hash = wtx.GetHash();
48 5383 : entry.pushKV("txid", hash.GetHex());
49 5383 : UniValue conflicts(UniValue::VARR);
50 5521 : for (const uint256& conflict : wallet.GetTxConflicts(wtx))
51 138 : conflicts.push_back(conflict.GetHex());
52 5383 : entry.pushKV("walletconflicts", conflicts);
53 5383 : entry.pushKV("time", wtx.GetTxTime());
54 5383 : entry.pushKV("timereceived", int64_t{wtx.nTimeReceived});
55 :
56 5383 : for (const std::pair<const std::string, std::string>& item : wtx.mapValue)
57 0 : entry.pushKV(item.first, item.second);
58 5383 : }
59 :
60 : struct tallyitem
61 : {
62 550 : CAmount nAmount{0};
63 550 : int nConf{std::numeric_limits<int>::max()};
64 : std::vector<uint256> txids;
65 550 : bool fIsWatchonly{false};
66 2200 : tallyitem() = default;
67 : };
68 :
69 400 : 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 400 : int nMinDepth = 1;
73 400 : if (!params[0].isNull())
74 324 : nMinDepth = params[0].getInt<int>();
75 400 : bool fAddLocked = false;
76 400 : if (!params[1].isNull())
77 44 : fAddLocked = params[1].get_bool();
78 :
79 : // Whether to include empty labels
80 400 : bool fIncludeEmpty = false;
81 400 : if (!params[2].isNull())
82 52 : fIncludeEmpty = params[2].get_bool();
83 :
84 400 : isminefilter filter = ISMINE_SPENDABLE;
85 :
86 400 : if (ParseIncludeWatchonly(params[3], wallet)) {
87 296 : filter |= ISMINE_WATCH_ONLY;
88 296 : }
89 :
90 400 : std::optional<CTxDestination> filtered_address{std::nullopt};
91 400 : if (!by_label && !params[4].isNull() && !params[4].get_str().empty()) {
92 280 : if (!IsValidDestinationString(params[4].get_str())) {
93 8 : throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid");
94 : }
95 276 : filtered_address = DecodeDestination(params[4].get_str());
96 276 : }
97 :
98 : // Excluding coinbase outputs is deprecated
99 : // It can be enabled by setting deprecatedrpc=exclude_coinbase
100 396 : const bool include_coinbase{!wallet.chain().rpcEnableDeprecated("exclude_coinbase")};
101 :
102 396 : if (include_immature_coinbase && !include_coinbase) {
103 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "include_immature_coinbase is incompatible with deprecated exclude_coinbase");
104 : }
105 :
106 : // Tally
107 392 : std::map<CTxDestination, tallyitem> mapTally;
108 17914 : for (const std::pair<const uint256, CWalletTx>& pairWtx : wallet.mapWallet) {
109 17522 : const CWalletTx& wtx = pairWtx.second;
110 :
111 17522 : int nDepth = wallet.GetTxDepthInMainChain(wtx);
112 17522 : if ((nDepth < nMinDepth) && !(fAddLocked && wallet.IsTxLockedByInstantSend(wtx)))
113 68 : continue;
114 :
115 : // Coinbase with less than 1 confirmation is no longer in the main chain
116 19346 : if ((wtx.IsCoinBase() && (nDepth < 1 || !include_coinbase))
117 16646 : || (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase))
118 : {
119 8532 : continue;
120 : }
121 :
122 21010 : for (const CTxOut& txout : wtx.tx->vout) {
123 12088 : CTxDestination address;
124 12088 : if (!ExtractDestination(txout.scriptPubKey, address))
125 0 : continue;
126 :
127 12088 : if (filtered_address && !(filtered_address == address)) {
128 9426 : continue;
129 : }
130 :
131 2662 : isminefilter mine = wallet.IsMine(address);
132 2662 : if (!(mine & filter))
133 272 : continue;
134 :
135 2390 : tallyitem& item = mapTally[address];
136 2390 : item.nAmount += txout.nValue;
137 2390 : item.nConf = std::min(item.nConf, nDepth);
138 2390 : item.txids.push_back(wtx.GetHash());
139 2390 : if (mine & ISMINE_WATCH_ONLY)
140 140 : item.fIsWatchonly = true;
141 : }
142 : }
143 :
144 : // Reply
145 392 : UniValue ret(UniValue::VARR);
146 392 : std::map<std::string, tallyitem> label_tally;
147 :
148 1104 : const auto& func = [&](const CTxDestination& address, const std::string& label, const std::string& purpose, bool is_change)
149 : {
150 712 : if (is_change) return; // no change addresses
151 712 : auto it = mapTally.find(address);
152 712 : if (it == mapTally.end() && !fIncludeEmpty)
153 308 : return;
154 :
155 404 : CAmount nAmount = 0;
156 404 : int nConf = std::numeric_limits<int>::max();
157 404 : bool fIsWatchonly = false;
158 404 : if (it != mapTally.end()) {
159 350 : nAmount = (*it).second.nAmount;
160 350 : nConf = (*it).second.nConf;
161 350 : fIsWatchonly = (*it).second.fIsWatchonly;
162 350 : }
163 :
164 404 : if (by_label) {
165 114 : tallyitem& _item = label_tally[label];
166 114 : _item.nAmount += nAmount;
167 114 : _item.nConf = std::min(_item.nConf, nConf);
168 114 : _item.fIsWatchonly = fIsWatchonly;
169 114 : } else {
170 290 : UniValue obj(UniValue::VOBJ);
171 290 : if(fIsWatchonly) obj.pushKV("involvesWatchonly", true);
172 290 : obj.pushKV("address", EncodeDestination(address));
173 290 : obj.pushKV("amount", ValueFromAmount(nAmount));
174 290 : obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
175 290 : obj.pushKV("label", label);
176 290 : UniValue transactions(UniValue::VARR);
177 290 : if (it != mapTally.end()) {
178 1492 : for (const uint256& _item : (*it).second.txids) {
179 1238 : transactions.push_back(_item.GetHex());
180 : }
181 254 : }
182 290 : obj.pushKV("txids", transactions);
183 290 : ret.push_back(obj);
184 290 : }
185 712 : };
186 :
187 392 : if (filtered_address) {
188 276 : const auto& entry = wallet.FindAddressBookEntry(*filtered_address, /*allow_change=*/false);
189 276 : if (entry) func(*filtered_address, entry->GetLabel(), entry->purpose, /*is_change=*/false);
190 276 : } else {
191 : // No filtered addr, walk-through the addressbook entry
192 116 : wallet.ForEachAddrBookEntry(func);
193 : }
194 :
195 392 : if (by_label) {
196 112 : for (const auto& entry : label_tally) {
197 64 : CAmount nAmount = entry.second.nAmount;
198 64 : int nConf = entry.second.nConf;
199 64 : UniValue obj(UniValue::VOBJ);
200 64 : if (entry.second.fIsWatchonly)
201 4 : obj.pushKV("involvesWatchonly", true);
202 64 : obj.pushKV("amount", ValueFromAmount(nAmount));
203 64 : obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
204 64 : obj.pushKV("label", entry.first);
205 64 : ret.push_back(obj);
206 64 : }
207 48 : }
208 :
209 392 : return ret;
210 400 : }
211 :
212 3248 : RPCHelpMan listreceivedbyaddress()
213 : {
214 6496 : return RPCHelpMan{"listreceivedbyaddress",
215 3248 : "\nList balances by receiving address.\n",
216 22736 : {
217 3248 : {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
218 3248 : {"addlocked", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include transactions locked via InstantSend."},
219 3248 : {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include addresses that haven't received any payments."},
220 3248 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
221 3248 : {"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If present and non-empty, only return information on this address."},
222 3248 : {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
223 : },
224 3248 : RPCResult{
225 3248 : RPCResult::Type::ARR, "", "",
226 6496 : {
227 6496 : {RPCResult::Type::OBJ, "", "",
228 22736 : {
229 3248 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
230 3248 : {RPCResult::Type::STR, "address", "The receiving address"},
231 3248 : {RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received by the address"},
232 3248 : {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 3248 : {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""},
236 6496 : {RPCResult::Type::ARR, "txids", "",
237 6496 : {
238 3248 : {RPCResult::Type::STR_HEX, "txid", "The ids of transactions received with the address"},
239 : }},
240 : }},
241 : }
242 : },
243 3248 : RPCExamples{
244 3248 : HelpExampleCli("listreceivedbyaddress", "")
245 3248 : + HelpExampleCli("listreceivedbyaddress", "6 false true")
246 3248 : + HelpExampleCli("listreceivedbyaddress", "6 false true true \"\" true")
247 3248 : + HelpExampleRpc("listreceivedbyaddress", "6, false, true, true")
248 3248 : + HelpExampleRpc("listreceivedbyaddress", "6, false, true, true, \"" + EXAMPLE_ADDRESS[0] + "\"")
249 3248 : + HelpExampleRpc("listreceivedbyaddress", "6, false true, true, \"" + EXAMPLE_ADDRESS[0] + "\", true")
250 : },
251 3600 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
252 : {
253 352 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
254 352 : 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 352 : pwallet->BlockUntilSyncedToCurrentChain();
259 :
260 352 : const bool include_immature_coinbase{request.params[5].isNull() ? false : request.params[5].get_bool()};
261 :
262 352 : LOCK(pwallet->cs_wallet);
263 :
264 352 : return ListReceived(*pwallet, request.params, false, include_immature_coinbase);
265 352 : },
266 : };
267 0 : }
268 :
269 2944 : RPCHelpMan listreceivedbylabel()
270 : {
271 5888 : return RPCHelpMan{"listreceivedbylabel",
272 2944 : "\nList received transactions by label.\n",
273 17664 : {
274 2944 : {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
275 2944 : {"addlocked", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include transactions locked via InstantSend."},
276 2944 : {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include labels that haven't received any payments."},
277 2944 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
278 2944 : {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
279 : },
280 2944 : RPCResult{
281 2944 : RPCResult::Type::ARR, "", "",
282 5888 : {
283 5888 : {RPCResult::Type::OBJ, "", "",
284 14720 : {
285 2944 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
286 2944 : {RPCResult::Type::STR_AMOUNT, "amount", "The total amount received by addresses with this label"},
287 2944 : {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"},
288 2944 : {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""},
289 : }},
290 : }
291 : },
292 2944 : RPCExamples{
293 2944 : HelpExampleCli("listreceivedbylabel", "")
294 2944 : + HelpExampleCli("listreceivedbylabel", "6 true")
295 2944 : + HelpExampleRpc("listreceivedbylabel", "6, true, true")
296 2944 : + HelpExampleRpc("listreceivedbylabel", "6, true, true, true")
297 : },
298 2992 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
299 : {
300 48 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
301 48 : 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 48 : pwallet->BlockUntilSyncedToCurrentChain();
306 :
307 48 : const bool include_immature_coinbase{request.params[4].isNull() ? false : request.params[4].get_bool()};
308 :
309 48 : LOCK(pwallet->cs_wallet);
310 :
311 48 : return ListReceived(*pwallet, request.params, true, include_immature_coinbase);
312 48 : },
313 : };
314 0 : }
315 :
316 5597 : static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
317 : {
318 5597 : if (IsValidDestination(dest)) {
319 5582 : entry.pushKV("address", EncodeDestination(dest));
320 5582 : }
321 5597 : }
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 12673 : 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 12673 : std::list<COutputEntry> listReceived;
339 12673 : std::list<COutputEntry> listSent;
340 :
341 12673 : CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine);
342 :
343 12673 : bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY);
344 :
345 : // Sent
346 12673 : if (!filter_label)
347 : {
348 6015 : for (const COutputEntry& s : listSent)
349 : {
350 1158 : UniValue entry(UniValue::VOBJ);
351 1158 : if (involvesWatchonly || (wallet.IsMine(s.destination) & ISMINE_WATCH_ONLY)) {
352 1158 : entry.pushKV("involvesWatchonly", true);
353 2 : }
354 2 : MaybePushAddress(entry, s.destination);
355 1158 : std::map<std::string, std::string>::const_iterator it = wtx.mapValue.find("DS");
356 1158 : entry.pushKV("category", (it != wtx.mapValue.end() && it->second == "1") ? "coinjoin" : "send");
357 1158 : entry.pushKV("amount", ValueFromAmount(-s.amount));
358 1158 : const auto* address_book_entry = wallet.FindAddressBookEntry(s.destination);
359 1158 : if (address_book_entry) {
360 397 : entry.pushKV("label", address_book_entry->GetLabel());
361 397 : }
362 1158 : entry.pushKV("vout", s.vout);
363 1158 : entry.pushKV("fee", ValueFromAmount(-nFee));
364 1158 : if (fLong)
365 694 : WalletTxToJSON(wallet, wtx, entry);
366 1158 : entry.pushKV("abandoned", wtx.isAbandoned());
367 1158 : ret.push_back(entry);
368 1158 : }
369 4857 : }
370 :
371 : // Received
372 12673 : if (listReceived.size() > 0 && ((wallet.GetTxDepthInMainChain(wtx) >= nMinDepth) || wallet.IsTxLockedByInstantSend(wtx))) {
373 24006 : for (const COutputEntry& r : listReceived)
374 : {
375 12043 : std::string label;
376 12043 : const auto* address_book_entry = wallet.FindAddressBookEntry(r.destination);
377 12043 : if (address_book_entry) {
378 11923 : label = address_book_entry->GetLabel();
379 11923 : }
380 12043 : if (filter_label && label != *filter_label) {
381 7604 : continue;
382 : }
383 4439 : UniValue entry(UniValue::VOBJ);
384 4439 : if (involvesWatchonly || (wallet.IsMine(r.destination) & ISMINE_WATCH_ONLY)) {
385 4439 : entry.pushKV("involvesWatchonly", true);
386 184 : }
387 184 : MaybePushAddress(entry, r.destination);
388 4439 : if (wtx.IsCoinBase())
389 : {
390 3186 : if (wallet.GetTxDepthInMainChain(wtx) < 1)
391 812 : entry.pushKV("category", "orphan");
392 2374 : else if (wallet.IsTxImmatureCoinBase(wtx))
393 2094 : entry.pushKV("category", "immature");
394 : else
395 280 : entry.pushKV("category", "generate");
396 3186 : }
397 1253 : else if (wtx.IsPlatformTransfer())
398 : {
399 12 : entry.pushKV("category", "platform-transfer");
400 12 : }
401 : else
402 : {
403 1241 : entry.pushKV("category", "receive");
404 : }
405 4439 : entry.pushKV("amount", ValueFromAmount(r.amount));
406 4439 : if (address_book_entry) {
407 4319 : entry.pushKV("label", label);
408 4319 : }
409 4439 : entry.pushKV("vout", r.vout);
410 4439 : entry.pushKV("abandoned", wtx.isAbandoned());
411 4439 : if (fLong)
412 4186 : WalletTxToJSON(wallet, wtx, entry);
413 4439 : ret.push_back(entry);
414 12043 : }
415 11963 : }
416 23495 : }
417 :
418 9829 : static std::vector<RPCResult> TransactionDescriptionString()
419 : {
420 137606 : 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 9829 : {RPCResult::Type::BOOL, "instantlock", "Current transaction lock state. Available for 'send' and 'receive' category of transactions"},
424 9829 : {RPCResult::Type::BOOL, "instantlock-internal", "Current internal transaction lock state. Available for 'send' and 'receive' category of transactions"},
425 9829 : {RPCResult::Type::BOOL, "chainlock", "The state of the corresponding block ChainLock"},
426 9829 : {RPCResult::Type::BOOL, "generated", /*optional=*/true, "Only present if the transaction's only input is a coinbase one."},
427 9829 : {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 9829 : {RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block hash containing the transaction. Available for 'send' and 'receive'\n"
430 : "category of transactions."},
431 9829 : {RPCResult::Type::STR_HEX, "blockheight", /*optional=*/true, "The block height containing the transaction."},
432 9829 : {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 9829 : {RPCResult::Type::NUM_TIME, "blocktime", /*optional=*/true, "The block time expressed in " + UNIX_EPOCH_TIME + "."},
435 9829 : {RPCResult::Type::STR_HEX, "txid", "The transaction id. Available for 'send' and 'receive' category of transactions."},
436 9829 : {RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."},
437 9829 : {RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + ". Available \n"
438 : "for 'send' and 'receive' category of transactions."},
439 9829 : {RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction."}};
440 0 : }
441 :
442 3400 : RPCHelpMan listtransactions()
443 : {
444 6800 : return RPCHelpMan{"listtransactions",
445 3400 : "\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 17000 : {
448 3400 : {"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 3400 : {"count", RPCArg::Type::NUM, RPCArg::Default{10}, "The number of transactions to return"},
451 3400 : {"skip", RPCArg::Type::NUM, RPCArg::Default{0}, "The number of transactions to skip"},
452 3400 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
453 : },
454 3400 : RPCResult{
455 3400 : RPCResult::Type::ARR, "", "",
456 6800 : {
457 10200 : {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
458 27200 : {
459 3400 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
460 3400 : {RPCResult::Type::STR, "address", /*optional=*/true, "The Dash address of the transaction. Not present for\n"
461 : "move transactions (category = move)."},
462 3400 : {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 3400 : {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 3400 : {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
473 3400 : {RPCResult::Type::NUM, "vout", "the vout value"},
474 3400 : {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 3400 : TransactionDescriptionString()),
478 6800 : {
479 3400 : {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
480 : })},
481 : }
482 : },
483 3400 : RPCExamples{
484 : "\nList the most recent 10 transactions in the systems\n"
485 3400 : + HelpExampleCli("listtransactions", "") +
486 : "\nList transactions 100 to 120\n"
487 3400 : + HelpExampleCli("listtransactions", "\"\" 20 100") +
488 : "\nAs a json rpc call\n"
489 3400 : + HelpExampleRpc("listtransactions", "\"\", 20, 100")
490 : },
491 3904 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
492 : {
493 504 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
494 504 : 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 504 : pwallet->BlockUntilSyncedToCurrentChain();
499 :
500 504 : const std::string* filter_label = nullptr;
501 504 : if (!request.params[0].isNull() && request.params[0].get_str() != "*") {
502 270 : filter_label = &request.params[0].get_str();
503 270 : if (filter_label->empty()) {
504 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Label argument must be a valid label name or \"*\".");
505 : }
506 266 : }
507 500 : int nCount = 10;
508 500 : if (!request.params[1].isNull())
509 292 : nCount = request.params[1].getInt<int>();
510 500 : int nFrom = 0;
511 500 : if (!request.params[2].isNull())
512 20 : nFrom = request.params[2].getInt<int>();
513 500 : isminefilter filter = ISMINE_SPENDABLE;
514 :
515 500 : if (ParseIncludeWatchonly(request.params[3], *pwallet)) {
516 314 : filter |= ISMINE_WATCH_ONLY;
517 314 : }
518 :
519 500 : if (nCount < 0)
520 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count");
521 496 : if (nFrom < 0)
522 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative from");
523 :
524 492 : std::vector<UniValue> ret;
525 : {
526 492 : LOCK(pwallet->cs_wallet);
527 :
528 492 : const CWallet::TxItems & txOrdered = pwallet->wtxOrdered;
529 :
530 : // iterate backwards until we have nCount items to return:
531 11050 : for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it)
532 : {
533 10672 : CWalletTx *const pwtx = (*it).second;
534 10672 : ListTransactions(*pwallet, *pwtx, 0, true, ret, filter, filter_label);
535 10672 : if ((int)ret.size() >= (nCount+nFrom)) break;
536 10558 : }
537 492 : }
538 :
539 : // ret is newest to oldest
540 :
541 492 : if (nFrom > (int)ret.size())
542 0 : nFrom = ret.size();
543 492 : if ((nFrom + nCount) > (int)ret.size())
544 378 : nCount = ret.size() - nFrom;
545 :
546 492 : auto txs_rev_it{std::make_move_iterator(ret.rend())};
547 492 : UniValue result{UniValue::VARR};
548 492 : result.push_backV(txs_rev_it - nFrom - nCount, txs_rev_it - nFrom); // Return oldest to newest
549 492 : return result;
550 516 : },
551 : };
552 0 : }
553 :
554 3000 : RPCHelpMan listsinceblock()
555 : {
556 6000 : return RPCHelpMan{"listsinceblock",
557 3000 : "\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 15000 : {
561 3000 : {"blockhash", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, the block hash to list transactions since, otherwise list all transactions."},
562 3000 : {"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 3000 : {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
564 3000 : {"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 3000 : RPCResult{
568 3000 : RPCResult::Type::OBJ, "", "",
569 12000 : {
570 6000 : {RPCResult::Type::ARR, "transactions", "",
571 6000 : {
572 9000 : {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
573 21000 : {
574 3000 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
575 3000 : {RPCResult::Type::STR, "address", /*optional=*/true, "The Dash address of the transaction."},
576 3000 : {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 3000 : {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 3000 : {RPCResult::Type::NUM, "vout", "the vout value"},
587 3000 : {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 3000 : TransactionDescriptionString()),
590 12000 : {
591 3000 : {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
592 3000 : {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any."},
593 3000 : {RPCResult::Type::STR, "to", "If a comment to is associated with the transaction."},
594 : })},
595 : }},
596 6000 : {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 3000 : , {{RPCResult::Type::ELISION, "", ""},}},
599 3000 : {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 3000 : RPCExamples{
603 3000 : HelpExampleCli("listsinceblock", "")
604 3000 : + HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6")
605 3000 : + HelpExampleRpc("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\", 6")
606 : },
607 3104 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
608 : {
609 104 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
610 104 : if (!pwallet) return UniValue::VNULL;
611 :
612 104 : 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 104 : wallet.BlockUntilSyncedToCurrentChain();
616 :
617 104 : LOCK(wallet.cs_wallet);
618 :
619 104 : std::optional<int> height; // Height of the specified block or the common ancestor, if the block provided was in a deactivated chain.
620 104 : std::optional<int> altheight; // Height of the specified block, even if it's in a deactivated chain.
621 104 : int target_confirms = 1;
622 104 : isminefilter filter = ISMINE_SPENDABLE;
623 :
624 104 : uint256 blockId;
625 104 : if (!request.params[0].isNull() && !request.params[0].get_str().empty()) {
626 68 : blockId = ParseHashV(request.params[0], "blockhash");
627 60 : height = int{};
628 60 : altheight = int{};
629 60 : if (!wallet.chain().findCommonAncestor(blockId, wallet.GetLastBlockHash(), /* ancestor out */ FoundBlock().height(*height), /* blockId out */ FoundBlock().height(*altheight))) {
630 8 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
631 : }
632 52 : }
633 :
634 88 : if (!request.params[1].isNull()) {
635 12 : target_confirms = request.params[1].getInt<int>();
636 :
637 12 : if (target_confirms < 1) {
638 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter");
639 : }
640 8 : }
641 :
642 84 : if (ParseIncludeWatchonly(request.params[2], wallet)) {
643 4 : filter |= ISMINE_WATCH_ONLY;
644 4 : }
645 :
646 84 : bool include_removed = (request.params[3].isNull() || request.params[3].get_bool());
647 :
648 84 : int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1;
649 :
650 84 : UniValue transactions(UniValue::VARR);
651 :
652 3556 : for (const std::pair<const uint256, CWalletTx>& pairWtx : wallet.mapWallet) {
653 3472 : const CWalletTx& tx = pairWtx.second;
654 :
655 3472 : if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) {
656 1490 : ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */);
657 1490 : }
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 84 : UniValue removed(UniValue::VARR);
663 132 : while (include_removed && altheight && *altheight > *height) {
664 48 : CBlock block;
665 48 : 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 104 : for (const CTransactionRef& tx : block.vtx) {
669 56 : auto it = wallet.mapWallet.find(tx->GetHash());
670 56 : 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 8 : ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */);
674 8 : }
675 : }
676 48 : blockId = block.hashPrevBlock;
677 48 : --*altheight;
678 48 : }
679 :
680 84 : uint256 lastblock;
681 84 : target_confirms = std::min(target_confirms, wallet.GetLastBlockHeight() + 1);
682 84 : CHECK_NONFATAL(wallet.chain().findAncestorByHeight(wallet.GetLastBlockHash(), wallet.GetLastBlockHeight() + 1 - target_confirms, FoundBlock().hash(lastblock)));
683 :
684 84 : UniValue ret(UniValue::VOBJ);
685 84 : ret.pushKV("transactions", transactions);
686 84 : if (include_removed) ret.pushKV("removed", removed);
687 84 : ret.pushKV("lastblock", lastblock.GetHex());
688 :
689 84 : return ret;
690 116 : },
691 : };
692 0 : }
693 :
694 3429 : RPCHelpMan gettransaction()
695 : {
696 6858 : return RPCHelpMan{"gettransaction",
697 3429 : "\nGet detailed information about in-wallet transaction <txid>\n",
698 13716 : {
699 3429 : {"txid", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id"},
700 3429 : {"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 3429 : {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction)"},
702 : },
703 3429 : RPCResult{
704 10287 : RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
705 10287 : {
706 3429 : {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT},
707 3429 : {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 3429 : TransactionDescriptionString()),
711 13716 : {
712 6858 : {RPCResult::Type::ARR, "details", "",
713 6858 : {
714 6858 : {RPCResult::Type::OBJ, "", "",
715 30861 : {
716 3429 : {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
717 3429 : {RPCResult::Type::STR, "address", /*optional=*/true, "The Dash address involved in the transaction."},
718 3429 : {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 3429 : {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT},
727 3429 : {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
728 3429 : {RPCResult::Type::NUM, "vout", "the vout value"},
729 3429 : {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 3429 : {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
732 : }},
733 : }},
734 3429 : {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"},
735 6858 : {RPCResult::Type::OBJ, "decoded", /*optional=*/true, "the decoded transaction (only present when `verbose` is passed), equivalent to the",
736 6858 : {
737 3429 : {RPCResult::Type::ELISION, "", "RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."},
738 : }},
739 3429 : RESULT_LAST_PROCESSED_BLOCK,
740 : }),
741 : },
742 3429 : RPCExamples{
743 3429 : HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
744 3429 : + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true")
745 3429 : + HelpExampleRpc("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
746 : },
747 3962 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
748 : {
749 533 : const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
750 533 : 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 533 : pwallet->BlockUntilSyncedToCurrentChain();
755 :
756 533 : LOCK(pwallet->cs_wallet);
757 :
758 533 : uint256 hash(ParseHashV(request.params[0], "txid"));
759 :
760 533 : isminefilter filter = ISMINE_SPENDABLE;
761 :
762 533 : if (ParseIncludeWatchonly(request.params[1], *pwallet)) {
763 18 : filter |= ISMINE_WATCH_ONLY;
764 18 : }
765 :
766 533 : bool verbose = request.params[2].isNull() ? false : request.params[2].get_bool();
767 :
768 533 : UniValue entry(UniValue::VOBJ);
769 533 : auto it = pwallet->mapWallet.find(hash);
770 533 : if (it == pwallet->mapWallet.end()) {
771 30 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
772 : }
773 503 : const CWalletTx& wtx = it->second;
774 :
775 503 : CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter);
776 503 : CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter);
777 503 : CAmount nNet = nCredit - nDebit;
778 503 : CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0);
779 :
780 503 : entry.pushKV("amount", ValueFromAmount(nNet - nFee));
781 503 : if (CachedTxIsFromMe(*pwallet, wtx, filter))
782 407 : entry.pushKV("fee", ValueFromAmount(nFee));
783 :
784 503 : WalletTxToJSON(*pwallet, wtx, entry);
785 :
786 503 : UniValue details(UniValue::VARR);
787 503 : ListTransactions(*pwallet, wtx, 0, false, details, filter, nullptr /* filter_label */);
788 503 : entry.pushKV("details", details);
789 :
790 503 : std::string strHex = EncodeHexTx(*wtx.tx);
791 503 : entry.pushKV("hex", strHex);
792 :
793 503 : if (verbose) {
794 46 : UniValue decoded(UniValue::VOBJ);
795 46 : TxToUniv(*wtx.tx, /*block_hash=*/uint256(), /*entry=*/decoded, /*include_hex=*/false);
796 46 : entry.pushKV("decoded", decoded);
797 46 : }
798 :
799 503 : AppendLastProcessedBlock(entry, *pwallet);
800 503 : return entry;
801 563 : },
802 : };
803 0 : }
804 :
805 2916 : RPCHelpMan abandontransaction()
806 : {
807 5832 : return RPCHelpMan{"abandontransaction",
808 2916 : "\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 5832 : {
814 2916 : {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
815 : },
816 2916 : RPCResult{RPCResult::Type::NONE, "", ""},
817 2916 : RPCExamples{
818 2916 : HelpExampleCli("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
819 2916 : + HelpExampleRpc("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
820 : },
821 2936 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
822 : {
823 20 : std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
824 20 : 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 20 : pwallet->BlockUntilSyncedToCurrentChain();
829 :
830 20 : LOCK(pwallet->cs_wallet);
831 :
832 20 : uint256 hash(ParseHashV(request.params[0], "txid"));
833 :
834 20 : if (!pwallet->mapWallet.count(hash)) {
835 4 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
836 : }
837 16 : if (!pwallet->AbandonTransaction(hash)) {
838 4 : throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not eligible for abandonment");
839 : }
840 :
841 12 : return UniValue::VNULL;
842 28 : },
843 : };
844 0 : }
845 :
846 2966 : RPCHelpMan rescanblockchain()
847 : {
848 5932 : return RPCHelpMan{"rescanblockchain",
849 2966 : "\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 8898 : {
854 2966 : {"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "block height where the rescan should start"},
855 2966 : {"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 2966 : RPCResult{
858 2966 : RPCResult::Type::OBJ, "", "",
859 8898 : {
860 2966 : {RPCResult::Type::NUM, "start_height", "The block height where the rescan started (the requested height or 0)"},
861 2966 : {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 2966 : RPCExamples{
865 2966 : HelpExampleCli("rescanblockchain", "100000 120000")
866 2966 : + HelpExampleRpc("rescanblockchain", "100000, 120000")
867 : },
868 3036 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
869 : {
870 70 : std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
871 70 : if (!pwallet) return UniValue::VNULL;
872 70 : 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 70 : wallet.BlockUntilSyncedToCurrentChain();
877 :
878 70 : WalletRescanReserver reserver(*pwallet);
879 70 : if (!reserver.reserve()) {
880 0 : throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
881 : }
882 :
883 70 : int start_height = 0;
884 70 : std::optional<int> stop_height;
885 70 : uint256 start_block;
886 : {
887 70 : LOCK(pwallet->cs_wallet);
888 70 : int tip_height = pwallet->GetLastBlockHeight();
889 :
890 70 : if (!request.params[0].isNull()) {
891 22 : start_height = request.params[0].getInt<int>();
892 22 : if (start_height < 0 || start_height > tip_height) {
893 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height");
894 : }
895 18 : }
896 :
897 66 : if (!request.params[1].isNull()) {
898 14 : stop_height = request.params[1].getInt<int>();
899 14 : if (*stop_height < 0 || *stop_height > tip_height) {
900 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height");
901 10 : } else if (*stop_height < start_height) {
902 4 : throw JSONRPCError(RPC_INVALID_PARAMETER, "stop_height must be greater than start_height");
903 : }
904 6 : }
905 :
906 : // We can't rescan beyond non-pruned blocks, stop and throw an error
907 58 : 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 58 : CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block)));
912 70 : }
913 :
914 : CWallet::ScanResult result =
915 58 : pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, /*fUpdate=*/true, /*save_progress=*/false);
916 58 : switch (result.status) {
917 : case CWallet::ScanResult::SUCCESS:
918 58 : 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 58 : UniValue response(UniValue::VOBJ);
926 58 : response.pushKV("start_height", start_height);
927 58 : response.pushKV("stop_height", result.last_scanned_height ? *result.last_scanned_height : UniValue());
928 58 : return response;
929 82 : },
930 : };
931 0 : }
932 : } // namespace wallet
|