From f7646b407ff209c8909157f592aeef79b0be7cb1 Mon Sep 17 00:00:00 2001 From: Samuel Dobson Date: Wed, 1 Dec 2021 15:06:45 +1300 Subject: [PATCH 1/7] MOVEONLY: Move transaction related wallet RPCs to transactions.cpp --- src/Makefile.am | 1 + src/wallet/rpc/transactions.cpp | 933 ++++++++++++++++++++++++++++++++ src/wallet/rpcwallet.cpp | 929 +------------------------------ 3 files changed, 943 insertions(+), 920 deletions(-) create mode 100644 src/wallet/rpc/transactions.cpp diff --git a/src/Makefile.am b/src/Makefile.am index 72f548c1921..5c6b872e96b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -413,6 +413,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/rpc/backup.cpp \ wallet/rpc/encrypt.cpp \ wallet/rpc/signmessage.cpp \ + wallet/rpc/transactions.cpp \ wallet/rpc/util.cpp \ wallet/rpcwallet.cpp \ wallet/scriptpubkeyman.cpp \ diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp new file mode 100644 index 00000000000..36acbdc9f6f --- /dev/null +++ b/src/wallet/rpc/transactions.cpp @@ -0,0 +1,933 @@ +// Copyright (c) 2011-2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include + +using interfaces::FoundBlock; + +static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue& entry) +{ + interfaces::Chain& chain = wallet.chain(); + int confirms = wallet.GetTxDepthInMainChain(wtx); + entry.pushKV("confirmations", confirms); + if (wtx.IsCoinBase()) + entry.pushKV("generated", true); + if (auto* conf = wtx.state()) + { + entry.pushKV("blockhash", conf->confirmed_block_hash.GetHex()); + entry.pushKV("blockheight", conf->confirmed_block_height); + entry.pushKV("blockindex", conf->position_in_block); + int64_t block_time; + CHECK_NONFATAL(chain.findBlock(conf->confirmed_block_hash, FoundBlock().time(block_time))); + entry.pushKV("blocktime", block_time); + } else { + entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx)); + } + uint256 hash = wtx.GetHash(); + entry.pushKV("txid", hash.GetHex()); + UniValue conflicts(UniValue::VARR); + for (const uint256& conflict : wallet.GetTxConflicts(wtx)) + conflicts.push_back(conflict.GetHex()); + entry.pushKV("walletconflicts", conflicts); + entry.pushKV("time", wtx.GetTxTime()); + entry.pushKV("timereceived", int64_t{wtx.nTimeReceived}); + + // Add opt-in RBF status + std::string rbfStatus = "no"; + if (confirms <= 0) { + RBFTransactionState rbfState = chain.isRBFOptIn(*wtx.tx); + if (rbfState == RBFTransactionState::UNKNOWN) + rbfStatus = "unknown"; + else if (rbfState == RBFTransactionState::REPLACEABLE_BIP125) + rbfStatus = "yes"; + } + entry.pushKV("bip125-replaceable", rbfStatus); + + for (const std::pair& item : wtx.mapValue) + entry.pushKV(item.first, item.second); +} + +struct tallyitem +{ + CAmount nAmount{0}; + int nConf{std::numeric_limits::max()}; + std::vector txids; + bool fIsWatchonly{false}; + tallyitem() + { + } +}; + +static UniValue ListReceived(const CWallet& wallet, const UniValue& params, const bool by_label, const bool include_immature_coinbase) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +{ + // Minimum confirmations + int nMinDepth = 1; + if (!params[0].isNull()) + nMinDepth = params[0].get_int(); + + // Whether to include empty labels + bool fIncludeEmpty = false; + if (!params[1].isNull()) + fIncludeEmpty = params[1].get_bool(); + + isminefilter filter = ISMINE_SPENDABLE; + + if (ParseIncludeWatchonly(params[2], wallet)) { + filter |= ISMINE_WATCH_ONLY; + } + + bool has_filtered_address = false; + CTxDestination filtered_address = CNoDestination(); + if (!by_label && !params[3].isNull() && !params[3].get_str().empty()) { + if (!IsValidDestinationString(params[3].get_str())) { + throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid"); + } + filtered_address = DecodeDestination(params[3].get_str()); + has_filtered_address = true; + } + + // Excluding coinbase outputs is deprecated + // It can be enabled by setting deprecatedrpc=exclude_coinbase + const bool include_coinbase{!wallet.chain().rpcEnableDeprecated("exclude_coinbase")}; + + if (include_immature_coinbase && !include_coinbase) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "include_immature_coinbase is incompatible with deprecated exclude_coinbase"); + } + + // Tally + std::map mapTally; + for (const std::pair& pairWtx : wallet.mapWallet) { + const CWalletTx& wtx = pairWtx.second; + + int nDepth = wallet.GetTxDepthInMainChain(wtx); + if (nDepth < nMinDepth) + continue; + + // Coinbase with less than 1 confirmation is no longer in the main chain + if ((wtx.IsCoinBase() && (nDepth < 1 || !include_coinbase)) + || (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase) + || !wallet.chain().checkFinalTx(*wtx.tx)) { + continue; + } + + for (const CTxOut& txout : wtx.tx->vout) + { + CTxDestination address; + if (!ExtractDestination(txout.scriptPubKey, address)) + continue; + + if (has_filtered_address && !(filtered_address == address)) { + continue; + } + + isminefilter mine = wallet.IsMine(address); + if(!(mine & filter)) + continue; + + tallyitem& item = mapTally[address]; + item.nAmount += txout.nValue; + item.nConf = std::min(item.nConf, nDepth); + item.txids.push_back(wtx.GetHash()); + if (mine & ISMINE_WATCH_ONLY) + item.fIsWatchonly = true; + } + } + + // Reply + UniValue ret(UniValue::VARR); + std::map label_tally; + + // Create m_address_book iterator + // If we aren't filtering, go from begin() to end() + auto start = wallet.m_address_book.begin(); + auto end = wallet.m_address_book.end(); + // If we are filtering, find() the applicable entry + if (has_filtered_address) { + start = wallet.m_address_book.find(filtered_address); + if (start != end) { + end = std::next(start); + } + } + + for (auto item_it = start; item_it != end; ++item_it) + { + if (item_it->second.IsChange()) continue; + const CTxDestination& address = item_it->first; + const std::string& label = item_it->second.GetLabel(); + auto it = mapTally.find(address); + if (it == mapTally.end() && !fIncludeEmpty) + continue; + + CAmount nAmount = 0; + int nConf = std::numeric_limits::max(); + bool fIsWatchonly = false; + if (it != mapTally.end()) + { + nAmount = (*it).second.nAmount; + nConf = (*it).second.nConf; + fIsWatchonly = (*it).second.fIsWatchonly; + } + + if (by_label) + { + tallyitem& _item = label_tally[label]; + _item.nAmount += nAmount; + _item.nConf = std::min(_item.nConf, nConf); + _item.fIsWatchonly = fIsWatchonly; + } + else + { + UniValue obj(UniValue::VOBJ); + if(fIsWatchonly) + obj.pushKV("involvesWatchonly", true); + obj.pushKV("address", EncodeDestination(address)); + obj.pushKV("amount", ValueFromAmount(nAmount)); + obj.pushKV("confirmations", (nConf == std::numeric_limits::max() ? 0 : nConf)); + obj.pushKV("label", label); + UniValue transactions(UniValue::VARR); + if (it != mapTally.end()) + { + for (const uint256& _item : (*it).second.txids) + { + transactions.push_back(_item.GetHex()); + } + } + obj.pushKV("txids", transactions); + ret.push_back(obj); + } + } + + if (by_label) + { + for (const auto& entry : label_tally) + { + CAmount nAmount = entry.second.nAmount; + int nConf = entry.second.nConf; + UniValue obj(UniValue::VOBJ); + if (entry.second.fIsWatchonly) + obj.pushKV("involvesWatchonly", true); + obj.pushKV("amount", ValueFromAmount(nAmount)); + obj.pushKV("confirmations", (nConf == std::numeric_limits::max() ? 0 : nConf)); + obj.pushKV("label", entry.first); + ret.push_back(obj); + } + } + + return ret; +} + +RPCHelpMan listreceivedbyaddress() +{ + return RPCHelpMan{"listreceivedbyaddress", + "\nList balances by receiving address.\n", + { + {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."}, + {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include addresses that haven't received any payments."}, + {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"}, + {"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If present and non-empty, only return information on this address."}, + {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction"}, + {RPCResult::Type::STR, "address", "The receiving address"}, + {RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received by the address"}, + {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"}, + {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""}, + {RPCResult::Type::ARR, "txids", "", + { + {RPCResult::Type::STR_HEX, "txid", "The ids of transactions received with the address"}, + }}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("listreceivedbyaddress", "") + + HelpExampleCli("listreceivedbyaddress", "6 true") + + HelpExampleCli("listreceivedbyaddress", "6 true true \"\" true") + + HelpExampleRpc("listreceivedbyaddress", "6, true, true") + + HelpExampleRpc("listreceivedbyaddress", "6, true, true, \"" + EXAMPLE_ADDRESS[0] + "\", true") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + const bool include_immature_coinbase{request.params[4].isNull() ? false : request.params[4].get_bool()}; + + LOCK(pwallet->cs_wallet); + + return ListReceived(*pwallet, request.params, false, include_immature_coinbase); +}, + }; +} + +RPCHelpMan listreceivedbylabel() +{ + return RPCHelpMan{"listreceivedbylabel", + "\nList received transactions by label.\n", + { + {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."}, + {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include labels that haven't received any payments."}, + {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"}, + {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction"}, + {RPCResult::Type::STR_AMOUNT, "amount", "The total amount received by addresses with this label"}, + {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"}, + {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("listreceivedbylabel", "") + + HelpExampleCli("listreceivedbylabel", "6 true") + + HelpExampleRpc("listreceivedbylabel", "6, true, true, true") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + const bool include_immature_coinbase{request.params[3].isNull() ? false : request.params[3].get_bool()}; + + LOCK(pwallet->cs_wallet); + + return ListReceived(*pwallet, request.params, true, include_immature_coinbase); +}, + }; +} + +static void MaybePushAddress(UniValue & entry, const CTxDestination &dest) +{ + if (IsValidDestination(dest)) { + entry.pushKV("address", EncodeDestination(dest)); + } +} + +/** + * List transactions based on the given criteria. + * + * @param wallet The wallet. + * @param wtx The wallet transaction. + * @param nMinDepth The minimum confirmation depth. + * @param fLong Whether to include the JSON version of the transaction. + * @param ret The UniValue into which the result is stored. + * @param filter_ismine The "is mine" filter flags. + * @param filter_label Optional label string to filter incoming transactions. + */ +static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter_ismine, const std::string* filter_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +{ + CAmount nFee; + std::list listReceived; + std::list listSent; + + CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine); + + bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); + + // Sent + if (!filter_label) + { + for (const COutputEntry& s : listSent) + { + UniValue entry(UniValue::VOBJ); + if (involvesWatchonly || (wallet.IsMine(s.destination) & ISMINE_WATCH_ONLY)) { + entry.pushKV("involvesWatchonly", true); + } + MaybePushAddress(entry, s.destination); + entry.pushKV("category", "send"); + entry.pushKV("amount", ValueFromAmount(-s.amount)); + const auto* address_book_entry = wallet.FindAddressBookEntry(s.destination); + if (address_book_entry) { + entry.pushKV("label", address_book_entry->GetLabel()); + } + entry.pushKV("vout", s.vout); + entry.pushKV("fee", ValueFromAmount(-nFee)); + if (fLong) + WalletTxToJSON(wallet, wtx, entry); + entry.pushKV("abandoned", wtx.isAbandoned()); + ret.push_back(entry); + } + } + + // Received + if (listReceived.size() > 0 && wallet.GetTxDepthInMainChain(wtx) >= nMinDepth) { + for (const COutputEntry& r : listReceived) + { + std::string label; + const auto* address_book_entry = wallet.FindAddressBookEntry(r.destination); + if (address_book_entry) { + label = address_book_entry->GetLabel(); + } + if (filter_label && label != *filter_label) { + continue; + } + UniValue entry(UniValue::VOBJ); + if (involvesWatchonly || (wallet.IsMine(r.destination) & ISMINE_WATCH_ONLY)) { + entry.pushKV("involvesWatchonly", true); + } + MaybePushAddress(entry, r.destination); + if (wtx.IsCoinBase()) + { + if (wallet.GetTxDepthInMainChain(wtx) < 1) + entry.pushKV("category", "orphan"); + else if (wallet.IsTxImmatureCoinBase(wtx)) + entry.pushKV("category", "immature"); + else + entry.pushKV("category", "generate"); + } + else + { + entry.pushKV("category", "receive"); + } + entry.pushKV("amount", ValueFromAmount(r.amount)); + if (address_book_entry) { + entry.pushKV("label", label); + } + entry.pushKV("vout", r.vout); + if (fLong) + WalletTxToJSON(wallet, wtx, entry); + ret.push_back(entry); + } + } +} + + +static const std::vector TransactionDescriptionString() +{ + return{{RPCResult::Type::NUM, "confirmations", "The number of confirmations for the transaction. Negative confirmations means the\n" + "transaction conflicted that many blocks ago."}, + {RPCResult::Type::BOOL, "generated", /* optional */ true, "Only present if the transaction's only input is a coinbase one."}, + {RPCResult::Type::BOOL, "trusted", /* optional */ true, "Whether we consider the transaction to be trusted and safe to spend from.\n" + "Only present when the transaction has 0 confirmations (or negative confirmations, if conflicted)."}, + {RPCResult::Type::STR_HEX, "blockhash", /* optional */ true, "The block hash containing the transaction."}, + {RPCResult::Type::NUM, "blockheight", /* optional */ true, "The block height containing the transaction."}, + {RPCResult::Type::NUM, "blockindex", /* optional */ true, "The index of the transaction in the block that includes it."}, + {RPCResult::Type::NUM_TIME, "blocktime", /* optional */ true, "The block time expressed in " + UNIX_EPOCH_TIME + "."}, + {RPCResult::Type::STR_HEX, "txid", "The transaction id."}, + {RPCResult::Type::ARR, "walletconflicts", "Conflicting transaction ids.", + { + {RPCResult::Type::STR_HEX, "txid", "The transaction id."}, + }}, + {RPCResult::Type::STR_HEX, "replaced_by_txid", /* optional */ true, "The txid if this tx was replaced."}, + {RPCResult::Type::STR_HEX, "replaces_txid", /* optional */ true, "The txid if the tx replaces one."}, + {RPCResult::Type::STR, "comment", /* optional */ true, ""}, + {RPCResult::Type::STR, "to", /* optional */ true, "If a comment to is associated with the transaction."}, + {RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."}, + {RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."}, + {RPCResult::Type::STR, "comment", /* optional */ true, "If a comment is associated with the transaction, only present if not empty."}, + {RPCResult::Type::STR, "bip125-replaceable", "(\"yes|no|unknown\") Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n" + "may be unknown for unconfirmed transactions not in the mempool."}}; +} + +RPCHelpMan listtransactions() +{ + return RPCHelpMan{"listtransactions", + "\nIf a label name is provided, this will return only incoming transactions paying to addresses with the specified label.\n" + "\nReturns up to 'count' most recent transactions skipping the first 'from' transactions.\n", + { + {"label|dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, should be a valid label name to return only incoming transactions\n" + "with the specified label, or \"*\" to disable filtering and return all transactions."}, + {"count", RPCArg::Type::NUM, RPCArg::Default{10}, "The number of transactions to return"}, + {"skip", RPCArg::Type::NUM, RPCArg::Default{0}, "The number of transactions to skip"}, + {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", Cat(Cat>( + { + {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction."}, + {RPCResult::Type::STR, "address", "The bitcoin address of the transaction."}, + {RPCResult::Type::STR, "category", "The transaction category.\n" + "\"send\" Transactions sent.\n" + "\"receive\" Non-coinbase transactions received.\n" + "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase transactions received."}, + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" + "for all other categories"}, + {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any"}, + {RPCResult::Type::NUM, "vout", "the vout value"}, + {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" + "'send' category of transactions."}, + }, + TransactionDescriptionString()), + { + {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" + "'send' category of transactions."}, + })}, + } + }, + RPCExamples{ + "\nList the most recent 10 transactions in the systems\n" + + HelpExampleCli("listtransactions", "") + + "\nList transactions 100 to 120\n" + + HelpExampleCli("listtransactions", "\"*\" 20 100") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("listtransactions", "\"*\", 20, 100") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + const std::string* filter_label = nullptr; + if (!request.params[0].isNull() && request.params[0].get_str() != "*") { + filter_label = &request.params[0].get_str(); + if (filter_label->empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Label argument must be a valid label name or \"*\"."); + } + } + int nCount = 10; + if (!request.params[1].isNull()) + nCount = request.params[1].get_int(); + int nFrom = 0; + if (!request.params[2].isNull()) + nFrom = request.params[2].get_int(); + isminefilter filter = ISMINE_SPENDABLE; + + if (ParseIncludeWatchonly(request.params[3], *pwallet)) { + filter |= ISMINE_WATCH_ONLY; + } + + if (nCount < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count"); + if (nFrom < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative from"); + + UniValue ret(UniValue::VARR); + + { + LOCK(pwallet->cs_wallet); + + const CWallet::TxItems & txOrdered = pwallet->wtxOrdered; + + // iterate backwards until we have nCount items to return: + for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) + { + CWalletTx *const pwtx = (*it).second; + ListTransactions(*pwallet, *pwtx, 0, true, ret, filter, filter_label); + if ((int)ret.size() >= (nCount+nFrom)) break; + } + } + + // ret is newest to oldest + + if (nFrom > (int)ret.size()) + nFrom = ret.size(); + if ((nFrom + nCount) > (int)ret.size()) + nCount = ret.size() - nFrom; + + const std::vector& txs = ret.getValues(); + UniValue result{UniValue::VARR}; + result.push_backV({ txs.rend() - nFrom - nCount, txs.rend() - nFrom }); // Return oldest to newest + return result; +}, + }; +} + +RPCHelpMan listsinceblock() +{ + return RPCHelpMan{"listsinceblock", + "\nGet all transactions in blocks since block [blockhash], or all transactions if omitted.\n" + "If \"blockhash\" is no longer a part of the main chain, transactions from the fork point onward are included.\n" + "Additionally, if include_removed is set, transactions affecting the wallet which were removed are returned in the \"removed\" array.\n", + { + {"blockhash", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, the block hash to list transactions since, otherwise list all transactions."}, + {"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"}, + {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"}, + {"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n" + "(not guaranteed to work on pruned nodes)"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::ARR, "transactions", "", + { + {RPCResult::Type::OBJ, "", "", Cat(Cat>( + { + {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction."}, + {RPCResult::Type::STR, "address", "The bitcoin address of the transaction."}, + {RPCResult::Type::STR, "category", "The transaction category.\n" + "\"send\" Transactions sent.\n" + "\"receive\" Non-coinbase transactions received.\n" + "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase transactions received."}, + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" + "for all other categories"}, + {RPCResult::Type::NUM, "vout", "the vout value"}, + {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" + "'send' category of transactions."}, + }, + TransactionDescriptionString()), + { + {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" + "'send' category of transactions."}, + {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any"}, + })}, + }}, + {RPCResult::Type::ARR, "removed", /* optional */ true, "\n" + "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." + , {{RPCResult::Type::ELISION, "", ""},}}, + {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"}, + } + }, + RPCExamples{ + HelpExampleCli("listsinceblock", "") + + HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6") + + HelpExampleRpc("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\", 6") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + const CWallet& wallet = *pwallet; + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + wallet.BlockUntilSyncedToCurrentChain(); + + LOCK(wallet.cs_wallet); + + std::optional height; // Height of the specified block or the common ancestor, if the block provided was in a deactivated chain. + std::optional altheight; // Height of the specified block, even if it's in a deactivated chain. + int target_confirms = 1; + isminefilter filter = ISMINE_SPENDABLE; + + uint256 blockId; + if (!request.params[0].isNull() && !request.params[0].get_str().empty()) { + blockId = ParseHashV(request.params[0], "blockhash"); + height = int{}; + altheight = int{}; + if (!wallet.chain().findCommonAncestor(blockId, wallet.GetLastBlockHash(), /* ancestor out */ FoundBlock().height(*height), /* blockId out */ FoundBlock().height(*altheight))) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + } + + if (!request.params[1].isNull()) { + target_confirms = request.params[1].get_int(); + + if (target_confirms < 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter"); + } + } + + if (ParseIncludeWatchonly(request.params[2], wallet)) { + filter |= ISMINE_WATCH_ONLY; + } + + bool include_removed = (request.params[3].isNull() || request.params[3].get_bool()); + + int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1; + + UniValue transactions(UniValue::VARR); + + for (const std::pair& pairWtx : wallet.mapWallet) { + const CWalletTx& tx = pairWtx.second; + + if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) { + ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */); + } + } + + // when a reorg'd block is requested, we also list any relevant transactions + // in the blocks of the chain that was detached + UniValue removed(UniValue::VARR); + while (include_removed && altheight && *altheight > *height) { + CBlock block; + if (!wallet.chain().findBlock(blockId, FoundBlock().data(block)) || block.IsNull()) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); + } + for (const CTransactionRef& tx : block.vtx) { + auto it = wallet.mapWallet.find(tx->GetHash()); + if (it != wallet.mapWallet.end()) { + // We want all transactions regardless of confirmation count to appear here, + // even negative confirmation ones, hence the big negative. + ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */); + } + } + blockId = block.hashPrevBlock; + --*altheight; + } + + uint256 lastblock; + target_confirms = std::min(target_confirms, wallet.GetLastBlockHeight() + 1); + CHECK_NONFATAL(wallet.chain().findAncestorByHeight(wallet.GetLastBlockHash(), wallet.GetLastBlockHeight() + 1 - target_confirms, FoundBlock().hash(lastblock))); + + UniValue ret(UniValue::VOBJ); + ret.pushKV("transactions", transactions); + if (include_removed) ret.pushKV("removed", removed); + ret.pushKV("lastblock", lastblock.GetHex()); + + return ret; +}, + }; +} + +RPCHelpMan gettransaction() +{ + return RPCHelpMan{"gettransaction", + "\nGet detailed information about in-wallet transaction \n", + { + {"txid", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id"}, + {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, + "Whether to include watch-only addresses in balance calculation and details[]"}, + {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, + "Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction)"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", Cat(Cat>( + { + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, + {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" + "'send' category of transactions."}, + }, + TransactionDescriptionString()), + { + {RPCResult::Type::ARR, "details", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction."}, + {RPCResult::Type::STR, "address", /* optional */ true, "The bitcoin address involved in the transaction."}, + {RPCResult::Type::STR, "category", "The transaction category.\n" + "\"send\" Transactions sent.\n" + "\"receive\" Non-coinbase transactions received.\n" + "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" + "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" + "\"orphan\" Orphaned coinbase transactions received."}, + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, + {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any"}, + {RPCResult::Type::NUM, "vout", "the vout value"}, + {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n" + "'send' category of transactions."}, + {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" + "'send' category of transactions."}, + }}, + }}, + {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"}, + {RPCResult::Type::OBJ, "decoded", /* optional */ true, "The decoded transaction (only present when `verbose` is passed)", + { + {RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."}, + }}, + }) + }, + RPCExamples{ + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true") + + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" false true") + + HelpExampleRpc("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + uint256 hash(ParseHashV(request.params[0], "txid")); + + isminefilter filter = ISMINE_SPENDABLE; + + if (ParseIncludeWatchonly(request.params[1], *pwallet)) { + filter |= ISMINE_WATCH_ONLY; + } + + bool verbose = request.params[2].isNull() ? false : request.params[2].get_bool(); + + UniValue entry(UniValue::VOBJ); + auto it = pwallet->mapWallet.find(hash); + if (it == pwallet->mapWallet.end()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + } + const CWalletTx& wtx = it->second; + + CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter); + CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter); + CAmount nNet = nCredit - nDebit; + CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0); + + entry.pushKV("amount", ValueFromAmount(nNet - nFee)); + if (CachedTxIsFromMe(*pwallet, wtx, filter)) + entry.pushKV("fee", ValueFromAmount(nFee)); + + WalletTxToJSON(*pwallet, wtx, entry); + + UniValue details(UniValue::VARR); + ListTransactions(*pwallet, wtx, 0, false, details, filter, nullptr /* filter_label */); + entry.pushKV("details", details); + + std::string strHex = EncodeHexTx(*wtx.tx, pwallet->chain().rpcSerializationFlags()); + entry.pushKV("hex", strHex); + + if (verbose) { + UniValue decoded(UniValue::VOBJ); + TxToUniv(*wtx.tx, uint256(), decoded, false); + entry.pushKV("decoded", decoded); + } + + return entry; +}, + }; +} + +RPCHelpMan abandontransaction() +{ + return RPCHelpMan{"abandontransaction", + "\nMark in-wallet transaction as abandoned\n" + "This will mark this transaction and all its in-wallet descendants as abandoned which will allow\n" + "for their inputs to be respent. It can be used to replace \"stuck\" or evicted transactions.\n" + "It only works on transactions which are not included in a block and are not currently in the mempool.\n" + "It has no effect on transactions which are already abandoned.\n", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + + HelpExampleRpc("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + uint256 hash(ParseHashV(request.params[0], "txid")); + + if (!pwallet->mapWallet.count(hash)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + } + if (!pwallet->AbandonTransaction(hash)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not eligible for abandonment"); + } + + return NullUniValue; +}, + }; +} + +RPCHelpMan rescanblockchain() +{ + return RPCHelpMan{"rescanblockchain", + "\nRescan the local blockchain for wallet related transactions.\n" + "Note: Use \"getwalletinfo\" to query the scanning progress.\n", + { + {"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "block height where the rescan should start"}, + {"stop_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED_NAMED_ARG, "the last block height that should be scanned. If none is provided it will rescan up to the tip at return time of this call."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "start_height", "The block height where the rescan started (the requested height or 0)"}, + {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."}, + } + }, + RPCExamples{ + HelpExampleCli("rescanblockchain", "100000 120000") + + HelpExampleRpc("rescanblockchain", "100000, 120000") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + CWallet& wallet{*pwallet}; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + wallet.BlockUntilSyncedToCurrentChain(); + + WalletRescanReserver reserver(*pwallet); + if (!reserver.reserve()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait."); + } + + int start_height = 0; + std::optional stop_height; + uint256 start_block; + { + LOCK(pwallet->cs_wallet); + int tip_height = pwallet->GetLastBlockHeight(); + + if (!request.params[0].isNull()) { + start_height = request.params[0].get_int(); + if (start_height < 0 || start_height > tip_height) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height"); + } + } + + if (!request.params[1].isNull()) { + stop_height = request.params[1].get_int(); + if (*stop_height < 0 || *stop_height > tip_height) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height"); + } else if (*stop_height < start_height) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "stop_height must be greater than start_height"); + } + } + + // We can't rescan beyond non-pruned blocks, stop and throw an error + if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(), start_height, stop_height)) { + throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height."); + } + + CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block))); + } + + CWallet::ScanResult result = + pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, true /* fUpdate */); + switch (result.status) { + case CWallet::ScanResult::SUCCESS: + break; + case CWallet::ScanResult::FAILURE: + throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corrupted data files."); + case CWallet::ScanResult::USER_ABORT: + throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted."); + // no default case, so the compiler can warn about missing cases + } + UniValue response(UniValue::VOBJ); + response.pushKV("start_height", start_height); + response.pushKV("stop_height", result.last_scanned_height ? *result.last_scanned_height : UniValue()); + return response; +}, + }; +} diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 057f000a2c5..6b2cc23fd7c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -45,8 +45,6 @@ #include -using interfaces::FoundBlock; - /** Checks if a CKey is in the given CWallet compressed or otherwise*/ bool HaveKey(const SigningProvider& wallet, const CKey& key) @@ -56,48 +54,6 @@ bool HaveKey(const SigningProvider& wallet, const CKey& key) return wallet.HaveKey(key.GetPubKey().GetID()) || wallet.HaveKey(key2.GetPubKey().GetID()); } -static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue& entry) -{ - interfaces::Chain& chain = wallet.chain(); - int confirms = wallet.GetTxDepthInMainChain(wtx); - entry.pushKV("confirmations", confirms); - if (wtx.IsCoinBase()) - entry.pushKV("generated", true); - if (auto* conf = wtx.state()) - { - entry.pushKV("blockhash", conf->confirmed_block_hash.GetHex()); - entry.pushKV("blockheight", conf->confirmed_block_height); - entry.pushKV("blockindex", conf->position_in_block); - int64_t block_time; - CHECK_NONFATAL(chain.findBlock(conf->confirmed_block_hash, FoundBlock().time(block_time))); - entry.pushKV("blocktime", block_time); - } else { - entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx)); - } - uint256 hash = wtx.GetHash(); - entry.pushKV("txid", hash.GetHex()); - UniValue conflicts(UniValue::VARR); - for (const uint256& conflict : wallet.GetTxConflicts(wtx)) - conflicts.push_back(conflict.GetHex()); - entry.pushKV("walletconflicts", conflicts); - entry.pushKV("time", wtx.GetTxTime()); - entry.pushKV("timereceived", int64_t{wtx.nTimeReceived}); - - // Add opt-in RBF status - std::string rbfStatus = "no"; - if (confirms <= 0) { - RBFTransactionState rbfState = chain.isRBFOptIn(*wtx.tx); - if (rbfState == RBFTransactionState::UNKNOWN) - rbfStatus = "unknown"; - else if (rbfState == RBFTransactionState::REPLACEABLE_BIP125) - rbfStatus = "yes"; - } - entry.pushKV("bip125-replaceable", rbfStatus); - - for (const std::pair& item : wtx.mapValue) - entry.pushKV(item.first, item.second); -} - /** * Update coin control with fee estimation based on the given parameters @@ -904,797 +860,6 @@ static RPCHelpMan addmultisigaddress() }; } -struct tallyitem -{ - CAmount nAmount{0}; - int nConf{std::numeric_limits::max()}; - std::vector txids; - bool fIsWatchonly{false}; - tallyitem() - { - } -}; - -static UniValue ListReceived(const CWallet& wallet, const UniValue& params, const bool by_label, const bool include_immature_coinbase) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) -{ - // Minimum confirmations - int nMinDepth = 1; - if (!params[0].isNull()) - nMinDepth = params[0].get_int(); - - // Whether to include empty labels - bool fIncludeEmpty = false; - if (!params[1].isNull()) - fIncludeEmpty = params[1].get_bool(); - - isminefilter filter = ISMINE_SPENDABLE; - - if (ParseIncludeWatchonly(params[2], wallet)) { - filter |= ISMINE_WATCH_ONLY; - } - - bool has_filtered_address = false; - CTxDestination filtered_address = CNoDestination(); - if (!by_label && !params[3].isNull() && !params[3].get_str().empty()) { - if (!IsValidDestinationString(params[3].get_str())) { - throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid"); - } - filtered_address = DecodeDestination(params[3].get_str()); - has_filtered_address = true; - } - - // Excluding coinbase outputs is deprecated - // It can be enabled by setting deprecatedrpc=exclude_coinbase - const bool include_coinbase{!wallet.chain().rpcEnableDeprecated("exclude_coinbase")}; - - if (include_immature_coinbase && !include_coinbase) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "include_immature_coinbase is incompatible with deprecated exclude_coinbase"); - } - - // Tally - std::map mapTally; - for (const std::pair& pairWtx : wallet.mapWallet) { - const CWalletTx& wtx = pairWtx.second; - - int nDepth = wallet.GetTxDepthInMainChain(wtx); - if (nDepth < nMinDepth) - continue; - - // Coinbase with less than 1 confirmation is no longer in the main chain - if ((wtx.IsCoinBase() && (nDepth < 1 || !include_coinbase)) - || (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase) - || !wallet.chain().checkFinalTx(*wtx.tx)) { - continue; - } - - for (const CTxOut& txout : wtx.tx->vout) - { - CTxDestination address; - if (!ExtractDestination(txout.scriptPubKey, address)) - continue; - - if (has_filtered_address && !(filtered_address == address)) { - continue; - } - - isminefilter mine = wallet.IsMine(address); - if(!(mine & filter)) - continue; - - tallyitem& item = mapTally[address]; - item.nAmount += txout.nValue; - item.nConf = std::min(item.nConf, nDepth); - item.txids.push_back(wtx.GetHash()); - if (mine & ISMINE_WATCH_ONLY) - item.fIsWatchonly = true; - } - } - - // Reply - UniValue ret(UniValue::VARR); - std::map label_tally; - - // Create m_address_book iterator - // If we aren't filtering, go from begin() to end() - auto start = wallet.m_address_book.begin(); - auto end = wallet.m_address_book.end(); - // If we are filtering, find() the applicable entry - if (has_filtered_address) { - start = wallet.m_address_book.find(filtered_address); - if (start != end) { - end = std::next(start); - } - } - - for (auto item_it = start; item_it != end; ++item_it) - { - if (item_it->second.IsChange()) continue; - const CTxDestination& address = item_it->first; - const std::string& label = item_it->second.GetLabel(); - auto it = mapTally.find(address); - if (it == mapTally.end() && !fIncludeEmpty) - continue; - - CAmount nAmount = 0; - int nConf = std::numeric_limits::max(); - bool fIsWatchonly = false; - if (it != mapTally.end()) - { - nAmount = (*it).second.nAmount; - nConf = (*it).second.nConf; - fIsWatchonly = (*it).second.fIsWatchonly; - } - - if (by_label) - { - tallyitem& _item = label_tally[label]; - _item.nAmount += nAmount; - _item.nConf = std::min(_item.nConf, nConf); - _item.fIsWatchonly = fIsWatchonly; - } - else - { - UniValue obj(UniValue::VOBJ); - if(fIsWatchonly) - obj.pushKV("involvesWatchonly", true); - obj.pushKV("address", EncodeDestination(address)); - obj.pushKV("amount", ValueFromAmount(nAmount)); - obj.pushKV("confirmations", (nConf == std::numeric_limits::max() ? 0 : nConf)); - obj.pushKV("label", label); - UniValue transactions(UniValue::VARR); - if (it != mapTally.end()) - { - for (const uint256& _item : (*it).second.txids) - { - transactions.push_back(_item.GetHex()); - } - } - obj.pushKV("txids", transactions); - ret.push_back(obj); - } - } - - if (by_label) - { - for (const auto& entry : label_tally) - { - CAmount nAmount = entry.second.nAmount; - int nConf = entry.second.nConf; - UniValue obj(UniValue::VOBJ); - if (entry.second.fIsWatchonly) - obj.pushKV("involvesWatchonly", true); - obj.pushKV("amount", ValueFromAmount(nAmount)); - obj.pushKV("confirmations", (nConf == std::numeric_limits::max() ? 0 : nConf)); - obj.pushKV("label", entry.first); - ret.push_back(obj); - } - } - - return ret; -} - -static RPCHelpMan listreceivedbyaddress() -{ - return RPCHelpMan{"listreceivedbyaddress", - "\nList balances by receiving address.\n", - { - {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."}, - {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include addresses that haven't received any payments."}, - {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"}, - {"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If present and non-empty, only return information on this address."}, - {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, - }, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction"}, - {RPCResult::Type::STR, "address", "The receiving address"}, - {RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received by the address"}, - {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"}, - {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""}, - {RPCResult::Type::ARR, "txids", "", - { - {RPCResult::Type::STR_HEX, "txid", "The ids of transactions received with the address"}, - }}, - }}, - } - }, - RPCExamples{ - HelpExampleCli("listreceivedbyaddress", "") - + HelpExampleCli("listreceivedbyaddress", "6 true") - + HelpExampleCli("listreceivedbyaddress", "6 true true \"\" true") - + HelpExampleRpc("listreceivedbyaddress", "6, true, true") - + HelpExampleRpc("listreceivedbyaddress", "6, true, true, \"" + EXAMPLE_ADDRESS[0] + "\", true") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - const bool include_immature_coinbase{request.params[4].isNull() ? false : request.params[4].get_bool()}; - - LOCK(pwallet->cs_wallet); - - return ListReceived(*pwallet, request.params, false, include_immature_coinbase); -}, - }; -} - -static RPCHelpMan listreceivedbylabel() -{ - return RPCHelpMan{"listreceivedbylabel", - "\nList received transactions by label.\n", - { - {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."}, - {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include labels that haven't received any payments."}, - {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"}, - {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, - }, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction"}, - {RPCResult::Type::STR_AMOUNT, "amount", "The total amount received by addresses with this label"}, - {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"}, - {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""}, - }}, - } - }, - RPCExamples{ - HelpExampleCli("listreceivedbylabel", "") - + HelpExampleCli("listreceivedbylabel", "6 true") - + HelpExampleRpc("listreceivedbylabel", "6, true, true, true") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - const bool include_immature_coinbase{request.params[3].isNull() ? false : request.params[3].get_bool()}; - - LOCK(pwallet->cs_wallet); - - return ListReceived(*pwallet, request.params, true, include_immature_coinbase); -}, - }; -} - -static void MaybePushAddress(UniValue & entry, const CTxDestination &dest) -{ - if (IsValidDestination(dest)) { - entry.pushKV("address", EncodeDestination(dest)); - } -} - -/** - * List transactions based on the given criteria. - * - * @param wallet The wallet. - * @param wtx The wallet transaction. - * @param nMinDepth The minimum confirmation depth. - * @param fLong Whether to include the JSON version of the transaction. - * @param ret The UniValue into which the result is stored. - * @param filter_ismine The "is mine" filter flags. - * @param filter_label Optional label string to filter incoming transactions. - */ -static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter_ismine, const std::string* filter_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) -{ - CAmount nFee; - std::list listReceived; - std::list listSent; - - CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine); - - bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); - - // Sent - if (!filter_label) - { - for (const COutputEntry& s : listSent) - { - UniValue entry(UniValue::VOBJ); - if (involvesWatchonly || (wallet.IsMine(s.destination) & ISMINE_WATCH_ONLY)) { - entry.pushKV("involvesWatchonly", true); - } - MaybePushAddress(entry, s.destination); - entry.pushKV("category", "send"); - entry.pushKV("amount", ValueFromAmount(-s.amount)); - const auto* address_book_entry = wallet.FindAddressBookEntry(s.destination); - if (address_book_entry) { - entry.pushKV("label", address_book_entry->GetLabel()); - } - entry.pushKV("vout", s.vout); - entry.pushKV("fee", ValueFromAmount(-nFee)); - if (fLong) - WalletTxToJSON(wallet, wtx, entry); - entry.pushKV("abandoned", wtx.isAbandoned()); - ret.push_back(entry); - } - } - - // Received - if (listReceived.size() > 0 && wallet.GetTxDepthInMainChain(wtx) >= nMinDepth) { - for (const COutputEntry& r : listReceived) - { - std::string label; - const auto* address_book_entry = wallet.FindAddressBookEntry(r.destination); - if (address_book_entry) { - label = address_book_entry->GetLabel(); - } - if (filter_label && label != *filter_label) { - continue; - } - UniValue entry(UniValue::VOBJ); - if (involvesWatchonly || (wallet.IsMine(r.destination) & ISMINE_WATCH_ONLY)) { - entry.pushKV("involvesWatchonly", true); - } - MaybePushAddress(entry, r.destination); - if (wtx.IsCoinBase()) - { - if (wallet.GetTxDepthInMainChain(wtx) < 1) - entry.pushKV("category", "orphan"); - else if (wallet.IsTxImmatureCoinBase(wtx)) - entry.pushKV("category", "immature"); - else - entry.pushKV("category", "generate"); - } - else - { - entry.pushKV("category", "receive"); - } - entry.pushKV("amount", ValueFromAmount(r.amount)); - if (address_book_entry) { - entry.pushKV("label", label); - } - entry.pushKV("vout", r.vout); - if (fLong) - WalletTxToJSON(wallet, wtx, entry); - ret.push_back(entry); - } - } -} - -static const std::vector TransactionDescriptionString() -{ - return{{RPCResult::Type::NUM, "confirmations", "The number of confirmations for the transaction. Negative confirmations means the\n" - "transaction conflicted that many blocks ago."}, - {RPCResult::Type::BOOL, "generated", /* optional */ true, "Only present if the transaction's only input is a coinbase one."}, - {RPCResult::Type::BOOL, "trusted", /* optional */ true, "Whether we consider the transaction to be trusted and safe to spend from.\n" - "Only present when the transaction has 0 confirmations (or negative confirmations, if conflicted)."}, - {RPCResult::Type::STR_HEX, "blockhash", /* optional */ true, "The block hash containing the transaction."}, - {RPCResult::Type::NUM, "blockheight", /* optional */ true, "The block height containing the transaction."}, - {RPCResult::Type::NUM, "blockindex", /* optional */ true, "The index of the transaction in the block that includes it."}, - {RPCResult::Type::NUM_TIME, "blocktime", /* optional */ true, "The block time expressed in " + UNIX_EPOCH_TIME + "."}, - {RPCResult::Type::STR_HEX, "txid", "The transaction id."}, - {RPCResult::Type::ARR, "walletconflicts", "Conflicting transaction ids.", - { - {RPCResult::Type::STR_HEX, "txid", "The transaction id."}, - }}, - {RPCResult::Type::STR_HEX, "replaced_by_txid", /* optional */ true, "The txid if this tx was replaced."}, - {RPCResult::Type::STR_HEX, "replaces_txid", /* optional */ true, "The txid if the tx replaces one."}, - {RPCResult::Type::STR, "comment", /* optional */ true, ""}, - {RPCResult::Type::STR, "to", /* optional */ true, "If a comment to is associated with the transaction."}, - {RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."}, - {RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."}, - {RPCResult::Type::STR, "comment", /* optional */ true, "If a comment is associated with the transaction, only present if not empty."}, - {RPCResult::Type::STR, "bip125-replaceable", "(\"yes|no|unknown\") Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n" - "may be unknown for unconfirmed transactions not in the mempool."}}; -} - -static RPCHelpMan listtransactions() -{ - return RPCHelpMan{"listtransactions", - "\nIf a label name is provided, this will return only incoming transactions paying to addresses with the specified label.\n" - "\nReturns up to 'count' most recent transactions skipping the first 'from' transactions.\n", - { - {"label|dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, should be a valid label name to return only incoming transactions\n" - "with the specified label, or \"*\" to disable filtering and return all transactions."}, - {"count", RPCArg::Type::NUM, RPCArg::Default{10}, "The number of transactions to return"}, - {"skip", RPCArg::Type::NUM, RPCArg::Default{0}, "The number of transactions to skip"}, - {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"}, - }, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::OBJ, "", "", Cat(Cat>( - { - {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction."}, - {RPCResult::Type::STR, "address", "The bitcoin address of the transaction."}, - {RPCResult::Type::STR, "category", "The transaction category.\n" - "\"send\" Transactions sent.\n" - "\"receive\" Non-coinbase transactions received.\n" - "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" - "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" - "\"orphan\" Orphaned coinbase transactions received."}, - {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" - "for all other categories"}, - {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any"}, - {RPCResult::Type::NUM, "vout", "the vout value"}, - {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" - "'send' category of transactions."}, - }, - TransactionDescriptionString()), - { - {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" - "'send' category of transactions."}, - })}, - } - }, - RPCExamples{ - "\nList the most recent 10 transactions in the systems\n" - + HelpExampleCli("listtransactions", "") + - "\nList transactions 100 to 120\n" - + HelpExampleCli("listtransactions", "\"*\" 20 100") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("listtransactions", "\"*\", 20, 100") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - const std::string* filter_label = nullptr; - if (!request.params[0].isNull() && request.params[0].get_str() != "*") { - filter_label = &request.params[0].get_str(); - if (filter_label->empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Label argument must be a valid label name or \"*\"."); - } - } - int nCount = 10; - if (!request.params[1].isNull()) - nCount = request.params[1].get_int(); - int nFrom = 0; - if (!request.params[2].isNull()) - nFrom = request.params[2].get_int(); - isminefilter filter = ISMINE_SPENDABLE; - - if (ParseIncludeWatchonly(request.params[3], *pwallet)) { - filter |= ISMINE_WATCH_ONLY; - } - - if (nCount < 0) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count"); - if (nFrom < 0) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative from"); - - UniValue ret(UniValue::VARR); - - { - LOCK(pwallet->cs_wallet); - - const CWallet::TxItems & txOrdered = pwallet->wtxOrdered; - - // iterate backwards until we have nCount items to return: - for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) - { - CWalletTx *const pwtx = (*it).second; - ListTransactions(*pwallet, *pwtx, 0, true, ret, filter, filter_label); - if ((int)ret.size() >= (nCount+nFrom)) break; - } - } - - // ret is newest to oldest - - if (nFrom > (int)ret.size()) - nFrom = ret.size(); - if ((nFrom + nCount) > (int)ret.size()) - nCount = ret.size() - nFrom; - - const std::vector& txs = ret.getValues(); - UniValue result{UniValue::VARR}; - result.push_backV({ txs.rend() - nFrom - nCount, txs.rend() - nFrom }); // Return oldest to newest - return result; -}, - }; -} - -static RPCHelpMan listsinceblock() -{ - return RPCHelpMan{"listsinceblock", - "\nGet all transactions in blocks since block [blockhash], or all transactions if omitted.\n" - "If \"blockhash\" is no longer a part of the main chain, transactions from the fork point onward are included.\n" - "Additionally, if include_removed is set, transactions affecting the wallet which were removed are returned in the \"removed\" array.\n", - { - {"blockhash", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If set, the block hash to list transactions since, otherwise list all transactions."}, - {"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"}, - {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"}, - {"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n" - "(not guaranteed to work on pruned nodes)"}, - }, - RPCResult{ - RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::ARR, "transactions", "", - { - {RPCResult::Type::OBJ, "", "", Cat(Cat>( - { - {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction."}, - {RPCResult::Type::STR, "address", "The bitcoin address of the transaction."}, - {RPCResult::Type::STR, "category", "The transaction category.\n" - "\"send\" Transactions sent.\n" - "\"receive\" Non-coinbase transactions received.\n" - "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" - "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" - "\"orphan\" Orphaned coinbase transactions received."}, - {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n" - "for all other categories"}, - {RPCResult::Type::NUM, "vout", "the vout value"}, - {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" - "'send' category of transactions."}, - }, - TransactionDescriptionString()), - { - {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" - "'send' category of transactions."}, - {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any"}, - })}, - }}, - {RPCResult::Type::ARR, "removed", /* optional */ true, "\n" - "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." - , {{RPCResult::Type::ELISION, "", ""},}}, - {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"}, - } - }, - RPCExamples{ - HelpExampleCli("listsinceblock", "") - + HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6") - + HelpExampleRpc("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\", 6") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - const CWallet& wallet = *pwallet; - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - wallet.BlockUntilSyncedToCurrentChain(); - - LOCK(wallet.cs_wallet); - - std::optional height; // Height of the specified block or the common ancestor, if the block provided was in a deactivated chain. - std::optional altheight; // Height of the specified block, even if it's in a deactivated chain. - int target_confirms = 1; - isminefilter filter = ISMINE_SPENDABLE; - - uint256 blockId; - if (!request.params[0].isNull() && !request.params[0].get_str().empty()) { - blockId = ParseHashV(request.params[0], "blockhash"); - height = int{}; - altheight = int{}; - if (!wallet.chain().findCommonAncestor(blockId, wallet.GetLastBlockHash(), /* ancestor out */ FoundBlock().height(*height), /* blockId out */ FoundBlock().height(*altheight))) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); - } - } - - if (!request.params[1].isNull()) { - target_confirms = request.params[1].get_int(); - - if (target_confirms < 1) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter"); - } - } - - if (ParseIncludeWatchonly(request.params[2], wallet)) { - filter |= ISMINE_WATCH_ONLY; - } - - bool include_removed = (request.params[3].isNull() || request.params[3].get_bool()); - - int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1; - - UniValue transactions(UniValue::VARR); - - for (const std::pair& pairWtx : wallet.mapWallet) { - const CWalletTx& tx = pairWtx.second; - - if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) { - ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */); - } - } - - // when a reorg'd block is requested, we also list any relevant transactions - // in the blocks of the chain that was detached - UniValue removed(UniValue::VARR); - while (include_removed && altheight && *altheight > *height) { - CBlock block; - if (!wallet.chain().findBlock(blockId, FoundBlock().data(block)) || block.IsNull()) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); - } - for (const CTransactionRef& tx : block.vtx) { - auto it = wallet.mapWallet.find(tx->GetHash()); - if (it != wallet.mapWallet.end()) { - // We want all transactions regardless of confirmation count to appear here, - // even negative confirmation ones, hence the big negative. - ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */); - } - } - blockId = block.hashPrevBlock; - --*altheight; - } - - uint256 lastblock; - target_confirms = std::min(target_confirms, wallet.GetLastBlockHeight() + 1); - CHECK_NONFATAL(wallet.chain().findAncestorByHeight(wallet.GetLastBlockHash(), wallet.GetLastBlockHeight() + 1 - target_confirms, FoundBlock().hash(lastblock))); - - UniValue ret(UniValue::VOBJ); - ret.pushKV("transactions", transactions); - if (include_removed) ret.pushKV("removed", removed); - ret.pushKV("lastblock", lastblock.GetHex()); - - return ret; -}, - }; -} - -static RPCHelpMan gettransaction() -{ - return RPCHelpMan{"gettransaction", - "\nGet detailed information about in-wallet transaction \n", - { - {"txid", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id"}, - {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, - "Whether to include watch-only addresses in balance calculation and details[]"}, - {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, - "Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction)"}, - }, - RPCResult{ - RPCResult::Type::OBJ, "", "", Cat(Cat>( - { - {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, - {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n" - "'send' category of transactions."}, - }, - TransactionDescriptionString()), - { - {RPCResult::Type::ARR, "details", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::BOOL, "involvesWatchonly", /* optional */ true, "Only returns true if imported addresses were involved in transaction."}, - {RPCResult::Type::STR, "address", /* optional */ true, "The bitcoin address involved in the transaction."}, - {RPCResult::Type::STR, "category", "The transaction category.\n" - "\"send\" Transactions sent.\n" - "\"receive\" Non-coinbase transactions received.\n" - "\"generate\" Coinbase transactions received with more than 100 confirmations.\n" - "\"immature\" Coinbase transactions received with 100 or fewer confirmations.\n" - "\"orphan\" Orphaned coinbase transactions received."}, - {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, - {RPCResult::Type::STR, "label", /* optional */ true, "A comment for the address/transaction, if any"}, - {RPCResult::Type::NUM, "vout", "the vout value"}, - {RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n" - "'send' category of transactions."}, - {RPCResult::Type::BOOL, "abandoned", /* optional */ true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n" - "'send' category of transactions."}, - }}, - }}, - {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"}, - {RPCResult::Type::OBJ, "decoded", /* optional */ true, "The decoded transaction (only present when `verbose` is passed)", - { - {RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."}, - }}, - }) - }, - RPCExamples{ - HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") - + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true") - + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" false true") - + HelpExampleRpc("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - LOCK(pwallet->cs_wallet); - - uint256 hash(ParseHashV(request.params[0], "txid")); - - isminefilter filter = ISMINE_SPENDABLE; - - if (ParseIncludeWatchonly(request.params[1], *pwallet)) { - filter |= ISMINE_WATCH_ONLY; - } - - bool verbose = request.params[2].isNull() ? false : request.params[2].get_bool(); - - UniValue entry(UniValue::VOBJ); - auto it = pwallet->mapWallet.find(hash); - if (it == pwallet->mapWallet.end()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); - } - const CWalletTx& wtx = it->second; - - CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter); - CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter); - CAmount nNet = nCredit - nDebit; - CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0); - - entry.pushKV("amount", ValueFromAmount(nNet - nFee)); - if (CachedTxIsFromMe(*pwallet, wtx, filter)) - entry.pushKV("fee", ValueFromAmount(nFee)); - - WalletTxToJSON(*pwallet, wtx, entry); - - UniValue details(UniValue::VARR); - ListTransactions(*pwallet, wtx, 0, false, details, filter, nullptr /* filter_label */); - entry.pushKV("details", details); - - std::string strHex = EncodeHexTx(*wtx.tx, pwallet->chain().rpcSerializationFlags()); - entry.pushKV("hex", strHex); - - if (verbose) { - UniValue decoded(UniValue::VOBJ); - TxToUniv(*wtx.tx, uint256(), decoded, false); - entry.pushKV("decoded", decoded); - } - - return entry; -}, - }; -} - -static RPCHelpMan abandontransaction() -{ - return RPCHelpMan{"abandontransaction", - "\nMark in-wallet transaction as abandoned\n" - "This will mark this transaction and all its in-wallet descendants as abandoned which will allow\n" - "for their inputs to be respent. It can be used to replace \"stuck\" or evicted transactions.\n" - "It only works on transactions which are not included in a block and are not currently in the mempool.\n" - "It has no effect on transactions which are already abandoned.\n", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - }, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - HelpExampleCli("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") - + HelpExampleRpc("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - LOCK(pwallet->cs_wallet); - - uint256 hash(ParseHashV(request.params[0], "txid")); - - if (!pwallet->mapWallet.count(hash)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); - } - if (!pwallet->AbandonTransaction(hash)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not eligible for abandonment"); - } - - return NullUniValue; -}, - }; -} - static RPCHelpMan keypoolrefill() { return RPCHelpMan{"keypoolrefill", @@ -3319,91 +2484,6 @@ static RPCHelpMan bumpfee_helper(std::string method_name) static RPCHelpMan bumpfee() { return bumpfee_helper("bumpfee"); } static RPCHelpMan psbtbumpfee() { return bumpfee_helper("psbtbumpfee"); } -static RPCHelpMan rescanblockchain() -{ - return RPCHelpMan{"rescanblockchain", - "\nRescan the local blockchain for wallet related transactions.\n" - "Note: Use \"getwalletinfo\" to query the scanning progress.\n", - { - {"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "block height where the rescan should start"}, - {"stop_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED_NAMED_ARG, "the last block height that should be scanned. If none is provided it will rescan up to the tip at return time of this call."}, - }, - RPCResult{ - RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::NUM, "start_height", "The block height where the rescan started (the requested height or 0)"}, - {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."}, - } - }, - RPCExamples{ - HelpExampleCli("rescanblockchain", "100000 120000") - + HelpExampleRpc("rescanblockchain", "100000, 120000") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - CWallet& wallet{*pwallet}; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - wallet.BlockUntilSyncedToCurrentChain(); - - WalletRescanReserver reserver(*pwallet); - if (!reserver.reserve()) { - throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait."); - } - - int start_height = 0; - std::optional stop_height; - uint256 start_block; - { - LOCK(pwallet->cs_wallet); - int tip_height = pwallet->GetLastBlockHeight(); - - if (!request.params[0].isNull()) { - start_height = request.params[0].get_int(); - if (start_height < 0 || start_height > tip_height) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height"); - } - } - - if (!request.params[1].isNull()) { - stop_height = request.params[1].get_int(); - if (*stop_height < 0 || *stop_height > tip_height) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height"); - } else if (*stop_height < start_height) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "stop_height must be greater than start_height"); - } - } - - // We can't rescan beyond non-pruned blocks, stop and throw an error - if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(), start_height, stop_height)) { - throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height."); - } - - CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block))); - } - - CWallet::ScanResult result = - pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, true /* fUpdate */); - switch (result.status) { - case CWallet::ScanResult::SUCCESS: - break; - case CWallet::ScanResult::FAILURE: - throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corrupted data files."); - case CWallet::ScanResult::USER_ABORT: - throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted."); - // no default case, so the compiler can warn about missing cases - } - UniValue response(UniValue::VOBJ); - response.pushKV("start_height", start_height); - response.pushKV("stop_height", result.last_scanned_height ? *result.last_scanned_height : UniValue()); - return response; -}, - }; -} - class DescribeWalletAddressVisitor { public: @@ -4383,6 +3463,15 @@ RPCHelpMan walletpassphrasechange(); RPCHelpMan walletlock(); RPCHelpMan encryptwallet(); +// transactions +RPCHelpMan listreceivedbyaddress(); +RPCHelpMan listreceivedbylabel(); +RPCHelpMan listtransactions(); +RPCHelpMan listsinceblock(); +RPCHelpMan gettransaction(); +RPCHelpMan abandontransaction(); +RPCHelpMan rescanblockchain(); + Span GetWalletRPCCommands() { // clang-format off From 7b45f5c0591935ef195fa4a8a7bbc38c7d7c5a76 Mon Sep 17 00:00:00 2001 From: Samuel Dobson Date: Wed, 1 Dec 2021 15:40:40 +1300 Subject: [PATCH 2/7] MOVEONLY: Move address related functions from rpcwallet to addresses.cpp --- src/Makefile.am | 1 + src/wallet/rpc/addresses.cpp | 787 ++++++++++++++++++++++++++++++++++ src/wallet/rpcwallet.cpp | 789 +---------------------------------- 3 files changed, 802 insertions(+), 775 deletions(-) create mode 100644 src/wallet/rpc/addresses.cpp diff --git a/src/Makefile.am b/src/Makefile.am index 5c6b872e96b..b1483e3b3fb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -410,6 +410,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/interfaces.cpp \ wallet/load.cpp \ wallet/receive.cpp \ + wallet/rpc/addresses.cpp \ wallet/rpc/backup.cpp \ wallet/rpc/encrypt.cpp \ wallet/rpc/signmessage.cpp \ diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp new file mode 100644 index 00000000000..edc4c26eeb2 --- /dev/null +++ b/src/wallet/rpc/addresses.cpp @@ -0,0 +1,787 @@ +// Copyright (c) 2011-2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +RPCHelpMan getnewaddress() +{ + return RPCHelpMan{"getnewaddress", + "\nReturns a new Bitcoin address for receiving payments.\n" + "If 'label' is specified, it is added to the address book \n" + "so payments received with the address will be associated with 'label'.\n", + { + {"label", RPCArg::Type::STR, RPCArg::Default{""}, "The label name for the address to be linked to. It can also be set to the empty string \"\" to represent the default label. The label does not need to exist, it will be created if there is no label by the given name."}, + {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -addresstype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", \"bech32\", and \"bech32m\"."}, + }, + RPCResult{ + RPCResult::Type::STR, "address", "The new bitcoin address" + }, + RPCExamples{ + HelpExampleCli("getnewaddress", "") + + HelpExampleRpc("getnewaddress", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + if (!pwallet->CanGetAddresses()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error: This wallet has no available keys"); + } + + // Parse the label first so we don't generate a key if there's an error + std::string label; + if (!request.params[0].isNull()) + label = LabelFromValue(request.params[0]); + + OutputType output_type = pwallet->m_default_address_type; + if (!request.params[1].isNull()) { + std::optional parsed = ParseOutputType(request.params[1].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[1].get_str())); + } else if (parsed.value() == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Legacy wallets cannot provide bech32m addresses"); + } + output_type = parsed.value(); + } + + CTxDestination dest; + bilingual_str error; + if (!pwallet->GetNewDestination(output_type, label, dest, error)) { + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error.original); + } + + return EncodeDestination(dest); +}, + }; +} + +RPCHelpMan getrawchangeaddress() +{ + return RPCHelpMan{"getrawchangeaddress", + "\nReturns a new Bitcoin address, for receiving change.\n" + "This is for use with raw transactions, NOT normal use.\n", + { + {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", \"bech32\", and \"bech32m\"."}, + }, + RPCResult{ + RPCResult::Type::STR, "address", "The address" + }, + RPCExamples{ + HelpExampleCli("getrawchangeaddress", "") + + HelpExampleRpc("getrawchangeaddress", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + if (!pwallet->CanGetAddresses(true)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error: This wallet has no available keys"); + } + + OutputType output_type = pwallet->m_default_change_type.value_or(pwallet->m_default_address_type); + if (!request.params[0].isNull()) { + std::optional parsed = ParseOutputType(request.params[0].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[0].get_str())); + } else if (parsed.value() == OutputType::BECH32M && pwallet->GetLegacyScriptPubKeyMan()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Legacy wallets cannot provide bech32m addresses"); + } + output_type = parsed.value(); + } + + CTxDestination dest; + bilingual_str error; + if (!pwallet->GetNewChangeDestination(output_type, dest, error)) { + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error.original); + } + return EncodeDestination(dest); +}, + }; +} + + +RPCHelpMan setlabel() +{ + return RPCHelpMan{"setlabel", + "\nSets the label associated with the given address.\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to be associated with a label."}, + {"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The label to assign to the address."}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("setlabel", "\"" + EXAMPLE_ADDRESS[0] + "\" \"tabby\"") + + HelpExampleRpc("setlabel", "\"" + EXAMPLE_ADDRESS[0] + "\", \"tabby\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + CTxDestination dest = DecodeDestination(request.params[0].get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); + } + + std::string label = LabelFromValue(request.params[1]); + + if (pwallet->IsMine(dest)) { + pwallet->SetAddressBook(dest, label, "receive"); + } else { + pwallet->SetAddressBook(dest, label, "send"); + } + + return NullUniValue; +}, + }; +} + +RPCHelpMan listaddressgroupings() +{ + return RPCHelpMan{"listaddressgroupings", + "\nLists groups of addresses which have had their common ownership\n" + "made public by common use as inputs or as the resulting change\n" + "in past transactions\n", + {}, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::ARR_FIXED, "", "", + { + {RPCResult::Type::STR, "address", "The bitcoin address"}, + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, + {RPCResult::Type::STR, "label", /* optional */ true, "The label"}, + }}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("listaddressgroupings", "") + + HelpExampleRpc("listaddressgroupings", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + UniValue jsonGroupings(UniValue::VARR); + std::map balances = GetAddressBalances(*pwallet); + for (const std::set& grouping : GetAddressGroupings(*pwallet)) { + UniValue jsonGrouping(UniValue::VARR); + for (const CTxDestination& address : grouping) + { + UniValue addressInfo(UniValue::VARR); + addressInfo.push_back(EncodeDestination(address)); + addressInfo.push_back(ValueFromAmount(balances[address])); + { + const auto* address_book_entry = pwallet->FindAddressBookEntry(address); + if (address_book_entry) { + addressInfo.push_back(address_book_entry->GetLabel()); + } + } + jsonGrouping.push_back(addressInfo); + } + jsonGroupings.push_back(jsonGrouping); + } + return jsonGroupings; +}, + }; +} + +RPCHelpMan addmultisigaddress() +{ + return RPCHelpMan{"addmultisigaddress", + "\nAdd an nrequired-to-sign multisignature address to the wallet. Requires a new wallet backup.\n" + "Each key is a Bitcoin address or hex-encoded public key.\n" + "This functionality is only intended for use with non-watchonly addresses.\n" + "See `importaddress` for watchonly p2sh address support.\n" + "If 'label' is specified, assign address to that label.\n", + { + {"nrequired", RPCArg::Type::NUM, RPCArg::Optional::NO, "The number of required signatures out of the n keys or addresses."}, + {"keys", RPCArg::Type::ARR, RPCArg::Optional::NO, "The bitcoin addresses or hex-encoded public keys", + { + {"key", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "bitcoin address or hex-encoded public key"}, + }, + }, + {"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "A label to assign the addresses to."}, + {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -addresstype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", "The value of the new multisig address"}, + {RPCResult::Type::STR_HEX, "redeemScript", "The string value of the hex-encoded redemption script"}, + {RPCResult::Type::STR, "descriptor", "The descriptor for this multisig"}, + } + }, + RPCExamples{ + "\nAdd a multisig address from 2 addresses\n" + + HelpExampleCli("addmultisigaddress", "2 \"[\\\"" + EXAMPLE_ADDRESS[0] + "\\\",\\\"" + EXAMPLE_ADDRESS[1] + "\\\"]\"") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("addmultisigaddress", "2, \"[\\\"" + EXAMPLE_ADDRESS[0] + "\\\",\\\"" + EXAMPLE_ADDRESS[1] + "\\\"]\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet); + + LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore); + + std::string label; + if (!request.params[2].isNull()) + label = LabelFromValue(request.params[2]); + + int required = request.params[0].get_int(); + + // Get the public keys + const UniValue& keys_or_addrs = request.params[1].get_array(); + std::vector pubkeys; + for (unsigned int i = 0; i < keys_or_addrs.size(); ++i) { + if (IsHex(keys_or_addrs[i].get_str()) && (keys_or_addrs[i].get_str().length() == 66 || keys_or_addrs[i].get_str().length() == 130)) { + pubkeys.push_back(HexToPubKey(keys_or_addrs[i].get_str())); + } else { + pubkeys.push_back(AddrToPubKey(spk_man, keys_or_addrs[i].get_str())); + } + } + + OutputType output_type = pwallet->m_default_address_type; + if (!request.params[3].isNull()) { + std::optional parsed = ParseOutputType(request.params[3].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[3].get_str())); + } else if (parsed.value() == OutputType::BECH32M) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m multisig addresses cannot be created with legacy wallets"); + } + output_type = parsed.value(); + } + + // Construct using pay-to-script-hash: + CScript inner; + CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, spk_man, inner); + pwallet->SetAddressBook(dest, label, "send"); + + // Make the descriptor + std::unique_ptr descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man); + + UniValue result(UniValue::VOBJ); + result.pushKV("address", EncodeDestination(dest)); + result.pushKV("redeemScript", HexStr(inner)); + result.pushKV("descriptor", descriptor->ToString()); + return result; +}, + }; +} + +RPCHelpMan keypoolrefill() +{ + return RPCHelpMan{"keypoolrefill", + "\nFills the keypool."+ + HELP_REQUIRING_PASSPHRASE, + { + {"newsize", RPCArg::Type::NUM, RPCArg::DefaultHint{strprintf("%u, or as set by -keypool", DEFAULT_KEYPOOL_SIZE)}, "The new keypool size"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("keypoolrefill", "") + + HelpExampleRpc("keypoolrefill", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + if (pwallet->IsLegacy() && pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet"); + } + + LOCK(pwallet->cs_wallet); + + // 0 is interpreted by TopUpKeyPool() as the default keypool size given by -keypool + unsigned int kpSize = 0; + if (!request.params[0].isNull()) { + if (request.params[0].get_int() < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected valid size."); + kpSize = (unsigned int)request.params[0].get_int(); + } + + EnsureWalletIsUnlocked(*pwallet); + pwallet->TopUpKeyPool(kpSize); + + if (pwallet->GetKeyPoolSize() < kpSize) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool."); + } + + return NullUniValue; +}, + }; +} + +RPCHelpMan newkeypool() +{ + return RPCHelpMan{"newkeypool", + "\nEntirely clears and refills the keypool."+ + HELP_REQUIRING_PASSPHRASE, + {}, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("newkeypool", "") + + HelpExampleRpc("newkeypool", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); + spk_man.NewKeyPool(); + + return NullUniValue; +}, + }; +} + + +class DescribeWalletAddressVisitor +{ +public: + const SigningProvider * const provider; + + void ProcessSubScript(const CScript& subscript, UniValue& obj) const + { + // Always present: script type and redeemscript + std::vector> solutions_data; + TxoutType which_type = Solver(subscript, solutions_data); + obj.pushKV("script", GetTxnOutputType(which_type)); + obj.pushKV("hex", HexStr(subscript)); + + CTxDestination embedded; + if (ExtractDestination(subscript, embedded)) { + // Only when the script corresponds to an address. + UniValue subobj(UniValue::VOBJ); + UniValue detail = DescribeAddress(embedded); + subobj.pushKVs(detail); + UniValue wallet_detail = std::visit(*this, embedded); + subobj.pushKVs(wallet_detail); + subobj.pushKV("address", EncodeDestination(embedded)); + subobj.pushKV("scriptPubKey", HexStr(subscript)); + // Always report the pubkey at the top level, so that `getnewaddress()['pubkey']` always works. + if (subobj.exists("pubkey")) obj.pushKV("pubkey", subobj["pubkey"]); + obj.pushKV("embedded", std::move(subobj)); + } else if (which_type == TxoutType::MULTISIG) { + // Also report some information on multisig scripts (which do not have a corresponding address). + obj.pushKV("sigsrequired", solutions_data[0][0]); + UniValue pubkeys(UniValue::VARR); + for (size_t i = 1; i < solutions_data.size() - 1; ++i) { + CPubKey key(solutions_data[i].begin(), solutions_data[i].end()); + pubkeys.push_back(HexStr(key)); + } + obj.pushKV("pubkeys", std::move(pubkeys)); + } + } + + explicit DescribeWalletAddressVisitor(const SigningProvider* _provider) : provider(_provider) {} + + UniValue operator()(const CNoDestination& dest) const { return UniValue(UniValue::VOBJ); } + + UniValue operator()(const PKHash& pkhash) const + { + CKeyID keyID{ToKeyID(pkhash)}; + UniValue obj(UniValue::VOBJ); + CPubKey vchPubKey; + if (provider && provider->GetPubKey(keyID, vchPubKey)) { + obj.pushKV("pubkey", HexStr(vchPubKey)); + obj.pushKV("iscompressed", vchPubKey.IsCompressed()); + } + return obj; + } + + UniValue operator()(const ScriptHash& scripthash) const + { + CScriptID scriptID(scripthash); + UniValue obj(UniValue::VOBJ); + CScript subscript; + if (provider && provider->GetCScript(scriptID, subscript)) { + ProcessSubScript(subscript, obj); + } + return obj; + } + + UniValue operator()(const WitnessV0KeyHash& id) const + { + UniValue obj(UniValue::VOBJ); + CPubKey pubkey; + if (provider && provider->GetPubKey(ToKeyID(id), pubkey)) { + obj.pushKV("pubkey", HexStr(pubkey)); + } + return obj; + } + + UniValue operator()(const WitnessV0ScriptHash& id) const + { + UniValue obj(UniValue::VOBJ); + CScript subscript; + CRIPEMD160 hasher; + uint160 hash; + hasher.Write(id.begin(), 32).Finalize(hash.begin()); + if (provider && provider->GetCScript(CScriptID(hash), subscript)) { + ProcessSubScript(subscript, obj); + } + return obj; + } + + UniValue operator()(const WitnessV1Taproot& id) const { return UniValue(UniValue::VOBJ); } + UniValue operator()(const WitnessUnknown& id) const { return UniValue(UniValue::VOBJ); } +}; + +static UniValue DescribeWalletAddress(const CWallet& wallet, const CTxDestination& dest) +{ + UniValue ret(UniValue::VOBJ); + UniValue detail = DescribeAddress(dest); + CScript script = GetScriptForDestination(dest); + std::unique_ptr provider = nullptr; + provider = wallet.GetSolvingProvider(script); + ret.pushKVs(detail); + ret.pushKVs(std::visit(DescribeWalletAddressVisitor(provider.get()), dest)); + return ret; +} + +RPCHelpMan getaddressinfo() +{ + return RPCHelpMan{"getaddressinfo", + "\nReturn information about the given bitcoin address.\n" + "Some of the information will only be present if the address is in the active wallet.\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address for which to get information."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", "The bitcoin address validated."}, + {RPCResult::Type::STR_HEX, "scriptPubKey", "The hex-encoded scriptPubKey generated by the address."}, + {RPCResult::Type::BOOL, "ismine", "If the address is yours."}, + {RPCResult::Type::BOOL, "iswatchonly", "If the address is watchonly."}, + {RPCResult::Type::BOOL, "solvable", "If we know how to spend coins sent to this address, ignoring the possible lack of private keys."}, + {RPCResult::Type::STR, "desc", /* optional */ true, "A descriptor for spending coins sent to this address (only when solvable)."}, + {RPCResult::Type::STR, "parent_desc", /* optional */ true, "The descriptor used to derive this address if this is a descriptor wallet"}, + {RPCResult::Type::BOOL, "isscript", "If the key is a script."}, + {RPCResult::Type::BOOL, "ischange", "If the address was used for change output."}, + {RPCResult::Type::BOOL, "iswitness", "If the address is a witness address."}, + {RPCResult::Type::NUM, "witness_version", /* optional */ true, "The version number of the witness program."}, + {RPCResult::Type::STR_HEX, "witness_program", /* optional */ true, "The hex value of the witness program."}, + {RPCResult::Type::STR, "script", /* optional */ true, "The output script type. Only if isscript is true and the redeemscript is known. Possible\n" + "types: nonstandard, pubkey, pubkeyhash, scripthash, multisig, nulldata, witness_v0_keyhash,\n" + "witness_v0_scripthash, witness_unknown."}, + {RPCResult::Type::STR_HEX, "hex", /* optional */ true, "The redeemscript for the p2sh address."}, + {RPCResult::Type::ARR, "pubkeys", /* optional */ true, "Array of pubkeys associated with the known redeemscript (only if script is multisig).", + { + {RPCResult::Type::STR, "pubkey", ""}, + }}, + {RPCResult::Type::NUM, "sigsrequired", /* optional */ true, "The number of signatures required to spend multisig output (only if script is multisig)."}, + {RPCResult::Type::STR_HEX, "pubkey", /* optional */ true, "The hex value of the raw public key for single-key addresses (possibly embedded in P2SH or P2WSH)."}, + {RPCResult::Type::OBJ, "embedded", /* optional */ true, "Information about the address embedded in P2SH or P2WSH, if relevant and known.", + { + {RPCResult::Type::ELISION, "", "Includes all getaddressinfo output fields for the embedded address, excluding metadata (timestamp, hdkeypath, hdseedid)\n" + "and relation to the wallet (ismine, iswatchonly)."}, + }}, + {RPCResult::Type::BOOL, "iscompressed", /* optional */ true, "If the pubkey is compressed."}, + {RPCResult::Type::NUM_TIME, "timestamp", /* optional */ true, "The creation time of the key, if available, expressed in " + UNIX_EPOCH_TIME + "."}, + {RPCResult::Type::STR, "hdkeypath", /* optional */ true, "The HD keypath, if the key is HD and available."}, + {RPCResult::Type::STR_HEX, "hdseedid", /* optional */ true, "The Hash160 of the HD seed."}, + {RPCResult::Type::STR_HEX, "hdmasterfingerprint", /* optional */ true, "The fingerprint of the master key."}, + {RPCResult::Type::ARR, "labels", "Array of labels associated with the address. Currently limited to one label but returned\n" + "as an array to keep the API stable if multiple labels are enabled in the future.", + { + {RPCResult::Type::STR, "label name", "Label name (defaults to \"\")."}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("getaddressinfo", "\"" + EXAMPLE_ADDRESS[0] + "\"") + + HelpExampleRpc("getaddressinfo", "\"" + EXAMPLE_ADDRESS[0] + "\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + std::string error_msg; + CTxDestination dest = DecodeDestination(request.params[0].get_str(), error_msg); + + // Make sure the destination is valid + if (!IsValidDestination(dest)) { + // Set generic error message in case 'DecodeDestination' didn't set it + if (error_msg.empty()) error_msg = "Invalid address"; + + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error_msg); + } + + UniValue ret(UniValue::VOBJ); + + std::string currentAddress = EncodeDestination(dest); + ret.pushKV("address", currentAddress); + + CScript scriptPubKey = GetScriptForDestination(dest); + ret.pushKV("scriptPubKey", HexStr(scriptPubKey)); + + std::unique_ptr provider = pwallet->GetSolvingProvider(scriptPubKey); + + isminetype mine = pwallet->IsMine(dest); + ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE)); + + if (provider) { + auto inferred = InferDescriptor(scriptPubKey, *provider); + bool solvable = inferred->IsSolvable() || IsSolvable(*provider, scriptPubKey); + ret.pushKV("solvable", solvable); + if (solvable) { + ret.pushKV("desc", inferred->ToString()); + } + } else { + ret.pushKV("solvable", false); + } + + const auto& spk_mans = pwallet->GetScriptPubKeyMans(scriptPubKey); + // In most cases there is only one matching ScriptPubKey manager and we can't resolve ambiguity in a better way + ScriptPubKeyMan* spk_man{nullptr}; + if (spk_mans.size()) spk_man = *spk_mans.begin(); + + DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast(spk_man); + if (desc_spk_man) { + std::string desc_str; + if (desc_spk_man->GetDescriptorString(desc_str, /* priv */ false)) { + ret.pushKV("parent_desc", desc_str); + } + } + + ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY)); + + UniValue detail = DescribeWalletAddress(*pwallet, dest); + ret.pushKVs(detail); + + ret.pushKV("ischange", ScriptIsChange(*pwallet, scriptPubKey)); + + if (spk_man) { + if (const std::unique_ptr meta = spk_man->GetMetadata(dest)) { + ret.pushKV("timestamp", meta->nCreateTime); + if (meta->has_key_origin) { + ret.pushKV("hdkeypath", WriteHDKeypath(meta->key_origin.path)); + ret.pushKV("hdseedid", meta->hd_seed_id.GetHex()); + ret.pushKV("hdmasterfingerprint", HexStr(meta->key_origin.fingerprint)); + } + } + } + + // Return a `labels` array containing the label associated with the address, + // equivalent to the `label` field above. Currently only one label can be + // associated with an address, but we return an array so the API remains + // stable if we allow multiple labels to be associated with an address in + // the future. + UniValue labels(UniValue::VARR); + const auto* address_book_entry = pwallet->FindAddressBookEntry(dest); + if (address_book_entry) { + labels.push_back(address_book_entry->GetLabel()); + } + ret.pushKV("labels", std::move(labels)); + + return ret; +}, + }; +} + +/** Convert CAddressBookData to JSON record. */ +static UniValue AddressBookDataToJSON(const CAddressBookData& data, const bool verbose) +{ + UniValue ret(UniValue::VOBJ); + if (verbose) { + ret.pushKV("name", data.GetLabel()); + } + ret.pushKV("purpose", data.purpose); + return ret; +} + +RPCHelpMan getaddressesbylabel() +{ + return RPCHelpMan{"getaddressesbylabel", + "\nReturns the list of addresses assigned the specified label.\n", + { + {"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The label."}, + }, + RPCResult{ + RPCResult::Type::OBJ_DYN, "", "json object with addresses as keys", + { + {RPCResult::Type::OBJ, "address", "json object with information about address", + { + {RPCResult::Type::STR, "purpose", "Purpose of address (\"send\" for sending address, \"receive\" for receiving address)"}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("getaddressesbylabel", "\"tabby\"") + + HelpExampleRpc("getaddressesbylabel", "\"tabby\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + std::string label = LabelFromValue(request.params[0]); + + // Find all addresses that have the given label + UniValue ret(UniValue::VOBJ); + std::set addresses; + for (const std::pair& item : pwallet->m_address_book) { + if (item.second.IsChange()) continue; + if (item.second.GetLabel() == label) { + std::string address = EncodeDestination(item.first); + // CWallet::m_address_book is not expected to contain duplicate + // address strings, but build a separate set as a precaution just in + // case it does. + bool unique = addresses.emplace(address).second; + CHECK_NONFATAL(unique); + // UniValue::pushKV checks if the key exists in O(N) + // and since duplicate addresses are unexpected (checked with + // std::set in O(log(N))), UniValue::__pushKV is used instead, + // which currently is O(1). + ret.__pushKV(address, AddressBookDataToJSON(item.second, false)); + } + } + + if (ret.empty()) { + throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME, std::string("No addresses with label " + label)); + } + + return ret; +}, + }; +} + +RPCHelpMan listlabels() +{ + return RPCHelpMan{"listlabels", + "\nReturns the list of all labels, or labels that are assigned to addresses with a specific purpose.\n", + { + {"purpose", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "Address purpose to list labels for ('send','receive'). An empty string is the same as not providing this argument."}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::STR, "label", "Label name"}, + } + }, + RPCExamples{ + "\nList all labels\n" + + HelpExampleCli("listlabels", "") + + "\nList labels that have receiving addresses\n" + + HelpExampleCli("listlabels", "receive") + + "\nList labels that have sending addresses\n" + + HelpExampleCli("listlabels", "send") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("listlabels", "receive") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + std::string purpose; + if (!request.params[0].isNull()) { + purpose = request.params[0].get_str(); + } + + // Add to a set to sort by label name, then insert into Univalue array + std::set label_set; + for (const std::pair& entry : pwallet->m_address_book) { + if (entry.second.IsChange()) continue; + if (purpose.empty() || entry.second.purpose == purpose) { + label_set.insert(entry.second.GetLabel()); + } + } + + UniValue ret(UniValue::VARR); + for (const std::string& name : label_set) { + ret.push_back(name); + } + + return ret; +}, + }; +} + + +#ifdef ENABLE_EXTERNAL_SIGNER +RPCHelpMan walletdisplayaddress() +{ + return RPCHelpMan{ + "walletdisplayaddress", + "Display address on an external signer for verification.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "bitcoin address to display"}, + }, + RPCResult{ + RPCResult::Type::OBJ,"","", + { + {RPCResult::Type::STR, "address", "The address as confirmed by the signer"}, + } + }, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + + LOCK(pwallet->cs_wallet); + + CTxDestination dest = DecodeDestination(request.params[0].get_str()); + + // Make sure the destination is valid + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); + } + + if (!pwallet->DisplayAddress(dest)) { + throw JSONRPCError(RPC_MISC_ERROR, "Failed to display address"); + } + + UniValue result(UniValue::VOBJ); + result.pushKV("address", request.params[0].get_str()); + return result; + } + }; +} +#endif // ENABLE_EXTERNAL_SIGNER diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 6b2cc23fd7c..1334e536c56 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -18,7 +18,6 @@ #include #include