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