transactions.cpp raw

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