diff options
author | Samuel Dobson <dobsonsa68@gmail.com> | 2021-12-01 16:09:30 +1300 |
---|---|---|
committer | Samuel Dobson <dobsonsa68@gmail.com> | 2021-12-08 11:45:19 +1300 |
commit | 9ce521a61bb7db3c881fbb3534472a60985e19d6 (patch) | |
tree | ca8754f5ae34589e8cd87e7835020707a7dbc8d0 | |
parent | 7b45f5c0591935ef195fa4a8a7bbc38c7d7c5a76 (diff) |
MOVEONLY: Move balance and utxo RPCs to coins.cpp
-rw-r--r-- | src/Makefile.am | 1 | ||||
-rw-r--r-- | src/wallet/rpc/coins.cpp | 733 | ||||
-rw-r--r-- | src/wallet/rpcwallet.cpp | 729 |
3 files changed, 744 insertions, 719 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index b1483e3b3f..272848ddaa 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -412,6 +412,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/receive.cpp \ wallet/rpc/addresses.cpp \ wallet/rpc/backup.cpp \ + wallet/rpc/coins.cpp \ wallet/rpc/encrypt.cpp \ wallet/rpc/signmessage.cpp \ wallet/rpc/transactions.cpp \ diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp new file mode 100644 index 0000000000..326f0f088b --- /dev/null +++ b/src/wallet/rpc/coins.cpp @@ -0,0 +1,733 @@ +// 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 <core_io.h> +#include <key_io.h> +#include <rpc/util.h> +#include <util/moneystr.h> +#include <wallet/coincontrol.h> +#include <wallet/receive.h> +#include <wallet/rpc/util.h> +#include <wallet/spend.h> +#include <wallet/wallet.h> + +#include <univalue.h> + + +static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool by_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +{ + std::set<CTxDestination> address_set; + + if (by_label) { + // Get the set of addresses assigned to label + std::string label = LabelFromValue(params[0]); + address_set = wallet.GetLabelAddresses(label); + } else { + // Get the address + CTxDestination dest = DecodeDestination(params[0].get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); + } + CScript script_pub_key = GetScriptForDestination(dest); + if (!wallet.IsMine(script_pub_key)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet"); + } + address_set.insert(dest); + } + + // Minimum confirmations + int min_depth = 1; + if (!params[1].isNull()) + min_depth = params[1].get_int(); + + const bool include_immature_coinbase{params[2].isNull() ? false : params[2].get_bool()}; + + // 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 + CAmount amount = 0; + for (const std::pair<const uint256, CWalletTx>& wtx_pair : wallet.mapWallet) { + const CWalletTx& wtx = wtx_pair.second; + int depth{wallet.GetTxDepthInMainChain(wtx)}; + if (depth < min_depth + // Coinbase with less than 1 confirmation is no longer in the main chain + || (wtx.IsCoinBase() && (depth < 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) && wallet.IsMine(address) && address_set.count(address)) { + amount += txout.nValue; + } + } + } + + return amount; +} + + +RPCHelpMan getreceivedbyaddress() +{ + return RPCHelpMan{"getreceivedbyaddress", + "\nReturns the total amount received by the given address in transactions with at least minconf confirmations.\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address for transactions."}, + {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "Only include transactions confirmed at least this many times."}, + {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, + }, + RPCResult{ + RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received at this address." + }, + RPCExamples{ + "\nThe amount from transactions with at least 1 confirmation\n" + + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\"") + + "\nThe amount including unconfirmed transactions, zero confirmations\n" + + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0") + + "\nThe amount with at least 6 confirmations\n" + + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 6") + + "\nThe amount with at least 6 confirmations including immature coinbase outputs\n" + + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 6 true") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\", 6") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> 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); + + return ValueFromAmount(GetReceived(*pwallet, request.params, /* by_label */ false)); +}, + }; +} + + +RPCHelpMan getreceivedbylabel() +{ + return RPCHelpMan{"getreceivedbylabel", + "\nReturns the total amount received by addresses with <label> in transactions with at least [minconf] confirmations.\n", + { + {"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The selected label, may be the default label using \"\"."}, + {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "Only include transactions confirmed at least this many times."}, + {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, + }, + RPCResult{ + RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this label." + }, + RPCExamples{ + "\nAmount received by the default label with at least 1 confirmation\n" + + HelpExampleCli("getreceivedbylabel", "\"\"") + + "\nAmount received at the tabby label including unconfirmed amounts with zero confirmations\n" + + HelpExampleCli("getreceivedbylabel", "\"tabby\" 0") + + "\nThe amount with at least 6 confirmations\n" + + HelpExampleCli("getreceivedbylabel", "\"tabby\" 6") + + "\nThe amount with at least 6 confirmations including immature coinbase outputs\n" + + HelpExampleCli("getreceivedbylabel", "\"tabby\" 6 true") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("getreceivedbylabel", "\"tabby\", 6, true") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> 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); + + return ValueFromAmount(GetReceived(*pwallet, request.params, /* by_label */ true)); +}, + }; +} + + +RPCHelpMan getbalance() +{ + return RPCHelpMan{"getbalance", + "\nReturns the total available balance.\n" + "The available balance is what the wallet considers currently spendable, and is\n" + "thus affected by options which limit spendability such as -spendzeroconfchange.\n", + { + {"dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "Remains for backward compatibility. Must be excluded or set to \"*\"."}, + {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Only include transactions confirmed at least this many times."}, + {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also include balance in watch-only addresses (see 'importaddress')"}, + {"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{true}, "(only available if avoid_reuse wallet flag is set) Do not include balance in dirty outputs; addresses are considered dirty if they have previously been used in a transaction."}, + }, + RPCResult{ + RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this wallet." + }, + RPCExamples{ + "\nThe total amount in the wallet with 0 or more confirmations\n" + + HelpExampleCli("getbalance", "") + + "\nThe total amount in the wallet with at least 6 confirmations\n" + + HelpExampleCli("getbalance", "\"*\" 6") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("getbalance", "\"*\", 6") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> 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); + + const UniValue& dummy_value = request.params[0]; + if (!dummy_value.isNull() && dummy_value.get_str() != "*") { + throw JSONRPCError(RPC_METHOD_DEPRECATED, "dummy first argument must be excluded or set to \"*\"."); + } + + int min_depth = 0; + if (!request.params[1].isNull()) { + min_depth = request.params[1].get_int(); + } + + bool include_watchonly = ParseIncludeWatchonly(request.params[2], *pwallet); + + bool avoid_reuse = GetAvoidReuseFlag(*pwallet, request.params[3]); + + const auto bal = GetBalance(*pwallet, min_depth, avoid_reuse); + + return ValueFromAmount(bal.m_mine_trusted + (include_watchonly ? bal.m_watchonly_trusted : 0)); +}, + }; +} + +RPCHelpMan getunconfirmedbalance() +{ + return RPCHelpMan{"getunconfirmedbalance", + "DEPRECATED\nIdentical to getbalances().mine.untrusted_pending\n", + {}, + RPCResult{RPCResult::Type::NUM, "", "The balance"}, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> 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); + + return ValueFromAmount(GetBalance(*pwallet).m_mine_untrusted_pending); +}, + }; +} + +RPCHelpMan lockunspent() +{ + return RPCHelpMan{"lockunspent", + "\nUpdates list of temporarily unspendable outputs.\n" + "Temporarily lock (unlock=false) or unlock (unlock=true) specified transaction outputs.\n" + "If no transaction outputs are specified when unlocking then all current locked transaction outputs are unlocked.\n" + "A locked transaction output will not be chosen by automatic coin selection, when spending bitcoins.\n" + "Manually selected coins are automatically unlocked.\n" + "Locks are stored in memory only, unless persistent=true, in which case they will be written to the\n" + "wallet database and loaded on node start. Unwritten (persistent=false) locks are always cleared\n" + "(by virtue of process exit) when a node stops or fails. Unlocking will clear both persistent and not.\n" + "Also see the listunspent call\n", + { + {"unlock", RPCArg::Type::BOOL, RPCArg::Optional::NO, "Whether to unlock (true) or lock (false) the specified transactions"}, + {"transactions", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The transaction outputs and within each, the txid (string) vout (numeric).", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + }, + }, + }, + }, + {"persistent", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to write/erase this lock in the wallet database, or keep the change in memory only. Ignored for unlocking."}, + }, + RPCResult{ + RPCResult::Type::BOOL, "", "Whether the command was successful or not" + }, + RPCExamples{ + "\nList the unspent transactions\n" + + HelpExampleCli("listunspent", "") + + "\nLock an unspent transaction\n" + + HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + + "\nList the locked transactions\n" + + HelpExampleCli("listlockunspent", "") + + "\nUnlock the transaction again\n" + + HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + + "\nLock the transaction persistently in the wallet database\n" + + HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\" true") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("lockunspent", "false, \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr<CWallet> 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); + + RPCTypeCheckArgument(request.params[0], UniValue::VBOOL); + + bool fUnlock = request.params[0].get_bool(); + + const bool persistent{request.params[2].isNull() ? false : request.params[2].get_bool()}; + + if (request.params[1].isNull()) { + if (fUnlock) { + if (!pwallet->UnlockAllCoins()) + throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coins failed"); + } + return true; + } + + RPCTypeCheckArgument(request.params[1], UniValue::VARR); + + const UniValue& output_params = request.params[1]; + + // Create and validate the COutPoints first. + + std::vector<COutPoint> outputs; + outputs.reserve(output_params.size()); + + for (unsigned int idx = 0; idx < output_params.size(); idx++) { + const UniValue& o = output_params[idx].get_obj(); + + RPCTypeCheckObj(o, + { + {"txid", UniValueType(UniValue::VSTR)}, + {"vout", UniValueType(UniValue::VNUM)}, + }); + + const uint256 txid(ParseHashO(o, "txid")); + const int nOutput = find_value(o, "vout").get_int(); + if (nOutput < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative"); + } + + const COutPoint outpt(txid, nOutput); + + const auto it = pwallet->mapWallet.find(outpt.hash); + if (it == pwallet->mapWallet.end()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, unknown transaction"); + } + + const CWalletTx& trans = it->second; + + if (outpt.n >= trans.tx->vout.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout index out of bounds"); + } + + if (pwallet->IsSpent(outpt.hash, outpt.n)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected unspent output"); + } + + const bool is_locked = pwallet->IsLockedCoin(outpt.hash, outpt.n); + + if (fUnlock && !is_locked) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output"); + } + + if (!fUnlock && is_locked && !persistent) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked"); + } + + outputs.push_back(outpt); + } + + std::unique_ptr<WalletBatch> batch = nullptr; + // Unlock is always persistent + if (fUnlock || persistent) batch = std::make_unique<WalletBatch>(pwallet->GetDatabase()); + + // Atomically set (un)locked status for the outputs. + for (const COutPoint& outpt : outputs) { + if (fUnlock) { + if (!pwallet->UnlockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed"); + } else { + if (!pwallet->LockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed"); + } + } + + return true; +}, + }; +} + +RPCHelpMan listlockunspent() +{ + return RPCHelpMan{"listlockunspent", + "\nReturns list of temporarily unspendable outputs.\n" + "See the lockunspent call to lock and unlock transactions for spending.\n", + {}, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "The transaction id locked"}, + {RPCResult::Type::NUM, "vout", "The vout value"}, + }}, + } + }, + RPCExamples{ + "\nList the unspent transactions\n" + + HelpExampleCli("listunspent", "") + + "\nLock an unspent transaction\n" + + HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + + "\nList the locked transactions\n" + + HelpExampleCli("listlockunspent", "") + + "\nUnlock the transaction again\n" + + HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("listlockunspent", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + std::vector<COutPoint> vOutpts; + pwallet->ListLockedCoins(vOutpts); + + UniValue ret(UniValue::VARR); + + for (const COutPoint& outpt : vOutpts) { + UniValue o(UniValue::VOBJ); + + o.pushKV("txid", outpt.hash.GetHex()); + o.pushKV("vout", (int)outpt.n); + ret.push_back(o); + } + + return ret; +}, + }; +} + +RPCHelpMan getbalances() +{ + return RPCHelpMan{ + "getbalances", + "Returns an object with all balances in " + CURRENCY_UNIT + ".\n", + {}, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::OBJ, "mine", "balances from outputs that the wallet can sign", + { + {RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"}, + {RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"}, + {RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"}, + {RPCResult::Type::STR_AMOUNT, "used", /* optional */ true, "(only present if avoid_reuse is set) balance from coins sent to addresses that were previously spent from (potentially privacy violating)"}, + }}, + {RPCResult::Type::OBJ, "watchonly", /* optional */ true, "watchonly balances (not present if wallet does not watch anything)", + { + {RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"}, + {RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"}, + {RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("getbalances", "") + + HelpExampleRpc("getbalances", "")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> rpc_wallet = GetWalletForJSONRPCRequest(request); + if (!rpc_wallet) return NullUniValue; + const CWallet& wallet = *rpc_wallet; + + // 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); + + const auto bal = GetBalance(wallet); + UniValue balances{UniValue::VOBJ}; + { + UniValue balances_mine{UniValue::VOBJ}; + balances_mine.pushKV("trusted", ValueFromAmount(bal.m_mine_trusted)); + balances_mine.pushKV("untrusted_pending", ValueFromAmount(bal.m_mine_untrusted_pending)); + balances_mine.pushKV("immature", ValueFromAmount(bal.m_mine_immature)); + if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) { + // If the AVOID_REUSE flag is set, bal has been set to just the un-reused address balance. Get + // the total balance, and then subtract bal to get the reused address balance. + const auto full_bal = GetBalance(wallet, 0, false); + balances_mine.pushKV("used", ValueFromAmount(full_bal.m_mine_trusted + full_bal.m_mine_untrusted_pending - bal.m_mine_trusted - bal.m_mine_untrusted_pending)); + } + balances.pushKV("mine", balances_mine); + } + auto spk_man = wallet.GetLegacyScriptPubKeyMan(); + if (spk_man && spk_man->HaveWatchOnly()) { + UniValue balances_watchonly{UniValue::VOBJ}; + balances_watchonly.pushKV("trusted", ValueFromAmount(bal.m_watchonly_trusted)); + balances_watchonly.pushKV("untrusted_pending", ValueFromAmount(bal.m_watchonly_untrusted_pending)); + balances_watchonly.pushKV("immature", ValueFromAmount(bal.m_watchonly_immature)); + balances.pushKV("watchonly", balances_watchonly); + } + return balances; +}, + }; +} + +RPCHelpMan listunspent() +{ + return RPCHelpMan{ + "listunspent", + "\nReturns array of unspent transaction outputs\n" + "with between minconf and maxconf (inclusive) confirmations.\n" + "Optionally filter to only include txouts paid to specified addresses.\n", + { + {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum confirmations to filter"}, + {"maxconf", RPCArg::Type::NUM, RPCArg::Default{9999999}, "The maximum confirmations to filter"}, + {"addresses", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The bitcoin addresses to filter", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "bitcoin address"}, + }, + }, + {"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include outputs that are not safe to spend\n" + "See description of \"safe\" attribute below."}, + {"query_options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "JSON with query options", + { + {"minimumAmount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(0)}, "Minimum value of each UTXO in " + CURRENCY_UNIT + ""}, + {"maximumAmount", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"unlimited"}, "Maximum value of each UTXO in " + CURRENCY_UNIT + ""}, + {"maximumCount", RPCArg::Type::NUM, RPCArg::DefaultHint{"unlimited"}, "Maximum number of UTXOs"}, + {"minimumSumAmount", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"unlimited"}, "Minimum sum value of all UTXOs in " + CURRENCY_UNIT + ""}, + }, + "query_options"}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "the transaction id"}, + {RPCResult::Type::NUM, "vout", "the vout value"}, + {RPCResult::Type::STR, "address", /* optional */ true, "the bitcoin address"}, + {RPCResult::Type::STR, "label", /* optional */ true, "The associated label, or \"\" for the default label"}, + {RPCResult::Type::STR, "scriptPubKey", "the script key"}, + {RPCResult::Type::STR_AMOUNT, "amount", "the transaction output amount in " + CURRENCY_UNIT}, + {RPCResult::Type::NUM, "confirmations", "The number of confirmations"}, + {RPCResult::Type::NUM, "ancestorcount", /* optional */ true, "The number of in-mempool ancestor transactions, including this one (if transaction is in the mempool)"}, + {RPCResult::Type::NUM, "ancestorsize", /* optional */ true, "The virtual transaction size of in-mempool ancestors, including this one (if transaction is in the mempool)"}, + {RPCResult::Type::STR_AMOUNT, "ancestorfees", /* optional */ true, "The total fees of in-mempool ancestors (including this one) with fee deltas used for mining priority in " + CURRENCY_ATOM + " (if transaction is in the mempool)"}, + {RPCResult::Type::STR_HEX, "redeemScript", /* optional */ true, "The redeemScript if scriptPubKey is P2SH"}, + {RPCResult::Type::STR, "witnessScript", /* optional */ true, "witnessScript if the scriptPubKey is P2WSH or P2SH-P2WSH"}, + {RPCResult::Type::BOOL, "spendable", "Whether we have the private keys to spend this output"}, + {RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"}, + {RPCResult::Type::BOOL, "reused", /* optional */ true, "(only present if avoid_reuse is set) Whether this output is reused/dirty (sent to an address that was previously spent from)"}, + {RPCResult::Type::STR, "desc", /* optional */ true, "(only when solvable) A descriptor for spending this output"}, + {RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions\n" + "from outside keys and unconfirmed replacement transactions are considered unsafe\n" + "and are not eligible for spending by fundrawtransaction and sendtoaddress."}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("listunspent", "") + + HelpExampleCli("listunspent", "6 9999999 \"[\\\"" + EXAMPLE_ADDRESS[0] + "\\\",\\\"" + EXAMPLE_ADDRESS[1] + "\\\"]\"") + + HelpExampleRpc("listunspent", "6, 9999999 \"[\\\"" + EXAMPLE_ADDRESS[0] + "\\\",\\\"" + EXAMPLE_ADDRESS[1] + "\\\"]\"") + + HelpExampleCli("listunspent", "6 9999999 '[]' true '{ \"minimumAmount\": 0.005 }'") + + HelpExampleRpc("listunspent", "6, 9999999, [] , true, { \"minimumAmount\": 0.005 } ") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + int nMinDepth = 1; + if (!request.params[0].isNull()) { + RPCTypeCheckArgument(request.params[0], UniValue::VNUM); + nMinDepth = request.params[0].get_int(); + } + + int nMaxDepth = 9999999; + if (!request.params[1].isNull()) { + RPCTypeCheckArgument(request.params[1], UniValue::VNUM); + nMaxDepth = request.params[1].get_int(); + } + + std::set<CTxDestination> destinations; + if (!request.params[2].isNull()) { + RPCTypeCheckArgument(request.params[2], UniValue::VARR); + UniValue inputs = request.params[2].get_array(); + for (unsigned int idx = 0; idx < inputs.size(); idx++) { + const UniValue& input = inputs[idx]; + CTxDestination dest = DecodeDestination(input.get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + input.get_str()); + } + if (!destinations.insert(dest).second) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + input.get_str()); + } + } + } + + bool include_unsafe = true; + if (!request.params[3].isNull()) { + RPCTypeCheckArgument(request.params[3], UniValue::VBOOL); + include_unsafe = request.params[3].get_bool(); + } + + CAmount nMinimumAmount = 0; + CAmount nMaximumAmount = MAX_MONEY; + CAmount nMinimumSumAmount = MAX_MONEY; + uint64_t nMaximumCount = 0; + + if (!request.params[4].isNull()) { + const UniValue& options = request.params[4].get_obj(); + + RPCTypeCheckObj(options, + { + {"minimumAmount", UniValueType()}, + {"maximumAmount", UniValueType()}, + {"minimumSumAmount", UniValueType()}, + {"maximumCount", UniValueType(UniValue::VNUM)}, + }, + true, true); + + if (options.exists("minimumAmount")) + nMinimumAmount = AmountFromValue(options["minimumAmount"]); + + if (options.exists("maximumAmount")) + nMaximumAmount = AmountFromValue(options["maximumAmount"]); + + if (options.exists("minimumSumAmount")) + nMinimumSumAmount = AmountFromValue(options["minimumSumAmount"]); + + if (options.exists("maximumCount")) + nMaximumCount = options["maximumCount"].get_int64(); + } + + // 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(); + + UniValue results(UniValue::VARR); + std::vector<COutput> vecOutputs; + { + CCoinControl cctl; + cctl.m_avoid_address_reuse = false; + cctl.m_min_depth = nMinDepth; + cctl.m_max_depth = nMaxDepth; + cctl.m_include_unsafe_inputs = include_unsafe; + LOCK(pwallet->cs_wallet); + AvailableCoins(*pwallet, vecOutputs, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount); + } + + LOCK(pwallet->cs_wallet); + + const bool avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); + + for (const COutput& out : vecOutputs) { + CTxDestination address; + const CScript& scriptPubKey = out.tx->tx->vout[out.i].scriptPubKey; + bool fValidAddress = ExtractDestination(scriptPubKey, address); + bool reused = avoid_reuse && pwallet->IsSpentKey(out.tx->GetHash(), out.i); + + if (destinations.size() && (!fValidAddress || !destinations.count(address))) + continue; + + UniValue entry(UniValue::VOBJ); + entry.pushKV("txid", out.tx->GetHash().GetHex()); + entry.pushKV("vout", out.i); + + if (fValidAddress) { + entry.pushKV("address", EncodeDestination(address)); + + const auto* address_book_entry = pwallet->FindAddressBookEntry(address); + if (address_book_entry) { + entry.pushKV("label", address_book_entry->GetLabel()); + } + + std::unique_ptr<SigningProvider> provider = pwallet->GetSolvingProvider(scriptPubKey); + if (provider) { + if (scriptPubKey.IsPayToScriptHash()) { + const CScriptID& hash = CScriptID(std::get<ScriptHash>(address)); + CScript redeemScript; + if (provider->GetCScript(hash, redeemScript)) { + entry.pushKV("redeemScript", HexStr(redeemScript)); + // Now check if the redeemScript is actually a P2WSH script + CTxDestination witness_destination; + if (redeemScript.IsPayToWitnessScriptHash()) { + bool extracted = ExtractDestination(redeemScript, witness_destination); + CHECK_NONFATAL(extracted); + // Also return the witness script + const WitnessV0ScriptHash& whash = std::get<WitnessV0ScriptHash>(witness_destination); + CScriptID id; + CRIPEMD160().Write(whash.begin(), whash.size()).Finalize(id.begin()); + CScript witnessScript; + if (provider->GetCScript(id, witnessScript)) { + entry.pushKV("witnessScript", HexStr(witnessScript)); + } + } + } + } else if (scriptPubKey.IsPayToWitnessScriptHash()) { + const WitnessV0ScriptHash& whash = std::get<WitnessV0ScriptHash>(address); + CScriptID id; + CRIPEMD160().Write(whash.begin(), whash.size()).Finalize(id.begin()); + CScript witnessScript; + if (provider->GetCScript(id, witnessScript)) { + entry.pushKV("witnessScript", HexStr(witnessScript)); + } + } + } + } + + entry.pushKV("scriptPubKey", HexStr(scriptPubKey)); + entry.pushKV("amount", ValueFromAmount(out.tx->tx->vout[out.i].nValue)); + entry.pushKV("confirmations", out.nDepth); + if (!out.nDepth) { + size_t ancestor_count, descendant_count, ancestor_size; + CAmount ancestor_fees; + pwallet->chain().getTransactionAncestry(out.tx->GetHash(), ancestor_count, descendant_count, &ancestor_size, &ancestor_fees); + if (ancestor_count) { + entry.pushKV("ancestorcount", uint64_t(ancestor_count)); + entry.pushKV("ancestorsize", uint64_t(ancestor_size)); + entry.pushKV("ancestorfees", uint64_t(ancestor_fees)); + } + } + entry.pushKV("spendable", out.fSpendable); + entry.pushKV("solvable", out.fSolvable); + if (out.fSolvable) { + std::unique_ptr<SigningProvider> provider = pwallet->GetSolvingProvider(scriptPubKey); + if (provider) { + auto descriptor = InferDescriptor(scriptPubKey, *provider); + entry.pushKV("desc", descriptor->ToString()); + } + } + if (avoid_reuse) entry.pushKV("reused", reused); + entry.pushKV("safe", out.fSafe); + results.push_back(entry); + } + + return results; +}, + }; +} diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 1334e536c5..7ce0195568 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -19,7 +19,6 @@ #include <script/descriptor.h> #include <script/sign.h> #include <util/fees.h> -#include <util/moneystr.h> #include <util/string.h> #include <util/system.h> #include <util/translation.h> @@ -255,228 +254,6 @@ static RPCHelpMan sendtoaddress() }; } -static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool by_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) -{ - std::set<CTxDestination> address_set; - - if (by_label) { - // Get the set of addresses assigned to label - std::string label = LabelFromValue(params[0]); - address_set = wallet.GetLabelAddresses(label); - } else { - // Get the address - CTxDestination dest = DecodeDestination(params[0].get_str()); - if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); - } - CScript script_pub_key = GetScriptForDestination(dest); - if (!wallet.IsMine(script_pub_key)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet"); - } - address_set.insert(dest); - } - - // Minimum confirmations - int min_depth = 1; - if (!params[1].isNull()) - min_depth = params[1].get_int(); - - const bool include_immature_coinbase{params[2].isNull() ? false : params[2].get_bool()}; - - // 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 - CAmount amount = 0; - for (const std::pair<const uint256, CWalletTx>& wtx_pair : wallet.mapWallet) { - const CWalletTx& wtx = wtx_pair.second; - int depth{wallet.GetTxDepthInMainChain(wtx)}; - if (depth < min_depth - // Coinbase with less than 1 confirmation is no longer in the main chain - || (wtx.IsCoinBase() && (depth < 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) && wallet.IsMine(address) && address_set.count(address)) { - amount += txout.nValue; - } - } - } - - return amount; -} - - -static RPCHelpMan getreceivedbyaddress() -{ - return RPCHelpMan{"getreceivedbyaddress", - "\nReturns the total amount received by the given address in transactions with at least minconf confirmations.\n", - { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address for transactions."}, - {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "Only include transactions confirmed at least this many times."}, - {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, - }, - RPCResult{ - RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received at this address." - }, - RPCExamples{ - "\nThe amount from transactions with at least 1 confirmation\n" - + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\"") + - "\nThe amount including unconfirmed transactions, zero confirmations\n" - + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0") + - "\nThe amount with at least 6 confirmations\n" - + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 6") + - "\nThe amount with at least 6 confirmations including immature coinbase outputs\n" - + HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 6 true") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\", 6") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> 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); - - return ValueFromAmount(GetReceived(*pwallet, request.params, /* by_label */ false)); -}, - }; -} - - -static RPCHelpMan getreceivedbylabel() -{ - return RPCHelpMan{"getreceivedbylabel", - "\nReturns the total amount received by addresses with <label> in transactions with at least [minconf] confirmations.\n", - { - {"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The selected label, may be the default label using \"\"."}, - {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "Only include transactions confirmed at least this many times."}, - {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."}, - }, - RPCResult{ - RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this label." - }, - RPCExamples{ - "\nAmount received by the default label with at least 1 confirmation\n" - + HelpExampleCli("getreceivedbylabel", "\"\"") + - "\nAmount received at the tabby label including unconfirmed amounts with zero confirmations\n" - + HelpExampleCli("getreceivedbylabel", "\"tabby\" 0") + - "\nThe amount with at least 6 confirmations\n" - + HelpExampleCli("getreceivedbylabel", "\"tabby\" 6") + - "\nThe amount with at least 6 confirmations including immature coinbase outputs\n" - + HelpExampleCli("getreceivedbylabel", "\"tabby\" 6 true") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("getreceivedbylabel", "\"tabby\", 6, true") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> 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); - - return ValueFromAmount(GetReceived(*pwallet, request.params, /* by_label */ true)); -}, - }; -} - - -static RPCHelpMan getbalance() -{ - return RPCHelpMan{"getbalance", - "\nReturns the total available balance.\n" - "The available balance is what the wallet considers currently spendable, and is\n" - "thus affected by options which limit spendability such as -spendzeroconfchange.\n", - { - {"dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "Remains for backward compatibility. Must be excluded or set to \"*\"."}, - {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Only include transactions confirmed at least this many times."}, - {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also include balance in watch-only addresses (see 'importaddress')"}, - {"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{true}, "(only available if avoid_reuse wallet flag is set) Do not include balance in dirty outputs; addresses are considered dirty if they have previously been used in a transaction."}, - }, - RPCResult{ - RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this wallet." - }, - RPCExamples{ - "\nThe total amount in the wallet with 0 or more confirmations\n" - + HelpExampleCli("getbalance", "") + - "\nThe total amount in the wallet with at least 6 confirmations\n" - + HelpExampleCli("getbalance", "\"*\" 6") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("getbalance", "\"*\", 6") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> 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); - - const UniValue& dummy_value = request.params[0]; - if (!dummy_value.isNull() && dummy_value.get_str() != "*") { - throw JSONRPCError(RPC_METHOD_DEPRECATED, "dummy first argument must be excluded or set to \"*\"."); - } - - int min_depth = 0; - if (!request.params[1].isNull()) { - min_depth = request.params[1].get_int(); - } - - bool include_watchonly = ParseIncludeWatchonly(request.params[2], *pwallet); - - bool avoid_reuse = GetAvoidReuseFlag(*pwallet, request.params[3]); - - const auto bal = GetBalance(*pwallet, min_depth, avoid_reuse); - - return ValueFromAmount(bal.m_mine_trusted + (include_watchonly ? bal.m_watchonly_trusted : 0)); -}, - }; -} - -static RPCHelpMan getunconfirmedbalance() -{ - return RPCHelpMan{"getunconfirmedbalance", - "DEPRECATED\nIdentical to getbalances().mine.untrusted_pending\n", - {}, - RPCResult{RPCResult::Type::NUM, "", "The balance"}, - RPCExamples{""}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> 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); - - return ValueFromAmount(GetBalance(*pwallet).m_mine_untrusted_pending); -}, - }; -} - - static RPCHelpMan sendmany() { return RPCHelpMan{"sendmany", @@ -570,199 +347,6 @@ static RPCHelpMan sendmany() }; } -static RPCHelpMan lockunspent() -{ - return RPCHelpMan{"lockunspent", - "\nUpdates list of temporarily unspendable outputs.\n" - "Temporarily lock (unlock=false) or unlock (unlock=true) specified transaction outputs.\n" - "If no transaction outputs are specified when unlocking then all current locked transaction outputs are unlocked.\n" - "A locked transaction output will not be chosen by automatic coin selection, when spending bitcoins.\n" - "Manually selected coins are automatically unlocked.\n" - "Locks are stored in memory only, unless persistent=true, in which case they will be written to the\n" - "wallet database and loaded on node start. Unwritten (persistent=false) locks are always cleared\n" - "(by virtue of process exit) when a node stops or fails. Unlocking will clear both persistent and not.\n" - "Also see the listunspent call\n", - { - {"unlock", RPCArg::Type::BOOL, RPCArg::Optional::NO, "Whether to unlock (true) or lock (false) the specified transactions"}, - {"transactions", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The transaction outputs and within each, the txid (string) vout (numeric).", - { - {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, - }, - }, - }, - }, - {"persistent", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to write/erase this lock in the wallet database, or keep the change in memory only. Ignored for unlocking."}, - }, - RPCResult{ - RPCResult::Type::BOOL, "", "Whether the command was successful or not" - }, - RPCExamples{ - "\nList the unspent transactions\n" - + HelpExampleCli("listunspent", "") + - "\nLock an unspent transaction\n" - + HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + - "\nList the locked transactions\n" - + HelpExampleCli("listlockunspent", "") + - "\nUnlock the transaction again\n" - + HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + - "\nLock the transaction persistently in the wallet database\n" - + HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\" true") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("lockunspent", "false, \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr<CWallet> 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); - - RPCTypeCheckArgument(request.params[0], UniValue::VBOOL); - - bool fUnlock = request.params[0].get_bool(); - - const bool persistent{request.params[2].isNull() ? false : request.params[2].get_bool()}; - - if (request.params[1].isNull()) { - if (fUnlock) { - if (!pwallet->UnlockAllCoins()) - throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coins failed"); - } - return true; - } - - RPCTypeCheckArgument(request.params[1], UniValue::VARR); - - const UniValue& output_params = request.params[1]; - - // Create and validate the COutPoints first. - - std::vector<COutPoint> outputs; - outputs.reserve(output_params.size()); - - for (unsigned int idx = 0; idx < output_params.size(); idx++) { - const UniValue& o = output_params[idx].get_obj(); - - RPCTypeCheckObj(o, - { - {"txid", UniValueType(UniValue::VSTR)}, - {"vout", UniValueType(UniValue::VNUM)}, - }); - - const uint256 txid(ParseHashO(o, "txid")); - const int nOutput = find_value(o, "vout").get_int(); - if (nOutput < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative"); - } - - const COutPoint outpt(txid, nOutput); - - const auto it = pwallet->mapWallet.find(outpt.hash); - if (it == pwallet->mapWallet.end()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, unknown transaction"); - } - - const CWalletTx& trans = it->second; - - if (outpt.n >= trans.tx->vout.size()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout index out of bounds"); - } - - if (pwallet->IsSpent(outpt.hash, outpt.n)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected unspent output"); - } - - const bool is_locked = pwallet->IsLockedCoin(outpt.hash, outpt.n); - - if (fUnlock && !is_locked) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output"); - } - - if (!fUnlock && is_locked && !persistent) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked"); - } - - outputs.push_back(outpt); - } - - std::unique_ptr<WalletBatch> batch = nullptr; - // Unlock is always persistent - if (fUnlock || persistent) batch = std::make_unique<WalletBatch>(pwallet->GetDatabase()); - - // Atomically set (un)locked status for the outputs. - for (const COutPoint& outpt : outputs) { - if (fUnlock) { - if (!pwallet->UnlockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed"); - } else { - if (!pwallet->LockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed"); - } - } - - return true; -}, - }; -} - -static RPCHelpMan listlockunspent() -{ - return RPCHelpMan{"listlockunspent", - "\nReturns list of temporarily unspendable outputs.\n" - "See the lockunspent call to lock and unlock transactions for spending.\n", - {}, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::STR_HEX, "txid", "The transaction id locked"}, - {RPCResult::Type::NUM, "vout", "The vout value"}, - }}, - } - }, - RPCExamples{ - "\nList the unspent transactions\n" - + HelpExampleCli("listunspent", "") + - "\nLock an unspent transaction\n" - + HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + - "\nList the locked transactions\n" - + HelpExampleCli("listlockunspent", "") + - "\nUnlock the transaction again\n" - + HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("listlockunspent", "") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - LOCK(pwallet->cs_wallet); - - std::vector<COutPoint> vOutpts; - pwallet->ListLockedCoins(vOutpts); - - UniValue ret(UniValue::VARR); - - for (const COutPoint& outpt : vOutpts) { - UniValue o(UniValue::VOBJ); - - o.pushKV("txid", outpt.hash.GetHex()); - o.pushKV("vout", (int)outpt.n); - ret.push_back(o); - } - - return ret; -}, - }; -} - static RPCHelpMan settxfee() { return RPCHelpMan{"settxfee", @@ -804,73 +388,6 @@ static RPCHelpMan settxfee() }; } -static RPCHelpMan getbalances() -{ - return RPCHelpMan{ - "getbalances", - "Returns an object with all balances in " + CURRENCY_UNIT + ".\n", - {}, - RPCResult{ - RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::OBJ, "mine", "balances from outputs that the wallet can sign", - { - {RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"}, - {RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"}, - {RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"}, - {RPCResult::Type::STR_AMOUNT, "used", /* optional */ true, "(only present if avoid_reuse is set) balance from coins sent to addresses that were previously spent from (potentially privacy violating)"}, - }}, - {RPCResult::Type::OBJ, "watchonly", /* optional */ true, "watchonly balances (not present if wallet does not watch anything)", - { - {RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"}, - {RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"}, - {RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"}, - }}, - } - }, - RPCExamples{ - HelpExampleCli("getbalances", "") + - HelpExampleRpc("getbalances", "")}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> rpc_wallet = GetWalletForJSONRPCRequest(request); - if (!rpc_wallet) return NullUniValue; - const CWallet& wallet = *rpc_wallet; - - // 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); - - const auto bal = GetBalance(wallet); - UniValue balances{UniValue::VOBJ}; - { - UniValue balances_mine{UniValue::VOBJ}; - balances_mine.pushKV("trusted", ValueFromAmount(bal.m_mine_trusted)); - balances_mine.pushKV("untrusted_pending", ValueFromAmount(bal.m_mine_untrusted_pending)); - balances_mine.pushKV("immature", ValueFromAmount(bal.m_mine_immature)); - if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) { - // If the AVOID_REUSE flag is set, bal has been set to just the un-reused address balance. Get - // the total balance, and then subtract bal to get the reused address balance. - const auto full_bal = GetBalance(wallet, 0, false); - balances_mine.pushKV("used", ValueFromAmount(full_bal.m_mine_trusted + full_bal.m_mine_untrusted_pending - bal.m_mine_trusted - bal.m_mine_untrusted_pending)); - } - balances.pushKV("mine", balances_mine); - } - auto spk_man = wallet.GetLegacyScriptPubKeyMan(); - if (spk_man && spk_man->HaveWatchOnly()) { - UniValue balances_watchonly{UniValue::VOBJ}; - balances_watchonly.pushKV("trusted", ValueFromAmount(bal.m_watchonly_trusted)); - balances_watchonly.pushKV("untrusted_pending", ValueFromAmount(bal.m_watchonly_untrusted_pending)); - balances_watchonly.pushKV("immature", ValueFromAmount(bal.m_watchonly_immature)); - balances.pushKV("watchonly", balances_watchonly); - } - return balances; -}, - }; -} - static RPCHelpMan getwalletinfo() { return RPCHelpMan{"getwalletinfo", @@ -1284,242 +801,6 @@ static RPCHelpMan unloadwallet() }; } -static RPCHelpMan listunspent() -{ - return RPCHelpMan{ - "listunspent", - "\nReturns array of unspent transaction outputs\n" - "with between minconf and maxconf (inclusive) confirmations.\n" - "Optionally filter to only include txouts paid to specified addresses.\n", - { - {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum confirmations to filter"}, - {"maxconf", RPCArg::Type::NUM, RPCArg::Default{9999999}, "The maximum confirmations to filter"}, - {"addresses", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The bitcoin addresses to filter", - { - {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "bitcoin address"}, - }, - }, - {"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include outputs that are not safe to spend\n" - "See description of \"safe\" attribute below."}, - {"query_options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "JSON with query options", - { - {"minimumAmount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(0)}, "Minimum value of each UTXO in " + CURRENCY_UNIT + ""}, - {"maximumAmount", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"unlimited"}, "Maximum value of each UTXO in " + CURRENCY_UNIT + ""}, - {"maximumCount", RPCArg::Type::NUM, RPCArg::DefaultHint{"unlimited"}, "Maximum number of UTXOs"}, - {"minimumSumAmount", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"unlimited"}, "Minimum sum value of all UTXOs in " + CURRENCY_UNIT + ""}, - }, - "query_options"}, - }, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::STR_HEX, "txid", "the transaction id"}, - {RPCResult::Type::NUM, "vout", "the vout value"}, - {RPCResult::Type::STR, "address", /* optional */ true, "the bitcoin address"}, - {RPCResult::Type::STR, "label", /* optional */ true, "The associated label, or \"\" for the default label"}, - {RPCResult::Type::STR, "scriptPubKey", "the script key"}, - {RPCResult::Type::STR_AMOUNT, "amount", "the transaction output amount in " + CURRENCY_UNIT}, - {RPCResult::Type::NUM, "confirmations", "The number of confirmations"}, - {RPCResult::Type::NUM, "ancestorcount", /* optional */ true, "The number of in-mempool ancestor transactions, including this one (if transaction is in the mempool)"}, - {RPCResult::Type::NUM, "ancestorsize", /* optional */ true, "The virtual transaction size of in-mempool ancestors, including this one (if transaction is in the mempool)"}, - {RPCResult::Type::STR_AMOUNT, "ancestorfees", /* optional */ true, "The total fees of in-mempool ancestors (including this one) with fee deltas used for mining priority in " + CURRENCY_ATOM + " (if transaction is in the mempool)"}, - {RPCResult::Type::STR_HEX, "redeemScript", /* optional */ true, "The redeemScript if scriptPubKey is P2SH"}, - {RPCResult::Type::STR, "witnessScript", /* optional */ true, "witnessScript if the scriptPubKey is P2WSH or P2SH-P2WSH"}, - {RPCResult::Type::BOOL, "spendable", "Whether we have the private keys to spend this output"}, - {RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"}, - {RPCResult::Type::BOOL, "reused", /* optional */ true, "(only present if avoid_reuse is set) Whether this output is reused/dirty (sent to an address that was previously spent from)"}, - {RPCResult::Type::STR, "desc", /* optional */ true, "(only when solvable) A descriptor for spending this output"}, - {RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions\n" - "from outside keys and unconfirmed replacement transactions are considered unsafe\n" - "and are not eligible for spending by fundrawtransaction and sendtoaddress."}, - }}, - } - }, - RPCExamples{ - HelpExampleCli("listunspent", "") - + HelpExampleCli("listunspent", "6 9999999 \"[\\\"" + EXAMPLE_ADDRESS[0] + "\\\",\\\"" + EXAMPLE_ADDRESS[1] + "\\\"]\"") - + HelpExampleRpc("listunspent", "6, 9999999 \"[\\\"" + EXAMPLE_ADDRESS[0] + "\\\",\\\"" + EXAMPLE_ADDRESS[1] + "\\\"]\"") - + HelpExampleCli("listunspent", "6 9999999 '[]' true '{ \"minimumAmount\": 0.005 }'") - + HelpExampleRpc("listunspent", "6, 9999999, [] , true, { \"minimumAmount\": 0.005 } ") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - int nMinDepth = 1; - if (!request.params[0].isNull()) { - RPCTypeCheckArgument(request.params[0], UniValue::VNUM); - nMinDepth = request.params[0].get_int(); - } - - int nMaxDepth = 9999999; - if (!request.params[1].isNull()) { - RPCTypeCheckArgument(request.params[1], UniValue::VNUM); - nMaxDepth = request.params[1].get_int(); - } - - std::set<CTxDestination> destinations; - if (!request.params[2].isNull()) { - RPCTypeCheckArgument(request.params[2], UniValue::VARR); - UniValue inputs = request.params[2].get_array(); - for (unsigned int idx = 0; idx < inputs.size(); idx++) { - const UniValue& input = inputs[idx]; - CTxDestination dest = DecodeDestination(input.get_str()); - if (!IsValidDestination(dest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + input.get_str()); - } - if (!destinations.insert(dest).second) { - throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + input.get_str()); - } - } - } - - bool include_unsafe = true; - if (!request.params[3].isNull()) { - RPCTypeCheckArgument(request.params[3], UniValue::VBOOL); - include_unsafe = request.params[3].get_bool(); - } - - CAmount nMinimumAmount = 0; - CAmount nMaximumAmount = MAX_MONEY; - CAmount nMinimumSumAmount = MAX_MONEY; - uint64_t nMaximumCount = 0; - - if (!request.params[4].isNull()) { - const UniValue& options = request.params[4].get_obj(); - - RPCTypeCheckObj(options, - { - {"minimumAmount", UniValueType()}, - {"maximumAmount", UniValueType()}, - {"minimumSumAmount", UniValueType()}, - {"maximumCount", UniValueType(UniValue::VNUM)}, - }, - true, true); - - if (options.exists("minimumAmount")) - nMinimumAmount = AmountFromValue(options["minimumAmount"]); - - if (options.exists("maximumAmount")) - nMaximumAmount = AmountFromValue(options["maximumAmount"]); - - if (options.exists("minimumSumAmount")) - nMinimumSumAmount = AmountFromValue(options["minimumSumAmount"]); - - if (options.exists("maximumCount")) - nMaximumCount = options["maximumCount"].get_int64(); - } - - // 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(); - - UniValue results(UniValue::VARR); - std::vector<COutput> vecOutputs; - { - CCoinControl cctl; - cctl.m_avoid_address_reuse = false; - cctl.m_min_depth = nMinDepth; - cctl.m_max_depth = nMaxDepth; - cctl.m_include_unsafe_inputs = include_unsafe; - LOCK(pwallet->cs_wallet); - AvailableCoins(*pwallet, vecOutputs, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount); - } - - LOCK(pwallet->cs_wallet); - - const bool avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); - - for (const COutput& out : vecOutputs) { - CTxDestination address; - const CScript& scriptPubKey = out.tx->tx->vout[out.i].scriptPubKey; - bool fValidAddress = ExtractDestination(scriptPubKey, address); - bool reused = avoid_reuse && pwallet->IsSpentKey(out.tx->GetHash(), out.i); - - if (destinations.size() && (!fValidAddress || !destinations.count(address))) - continue; - - UniValue entry(UniValue::VOBJ); - entry.pushKV("txid", out.tx->GetHash().GetHex()); - entry.pushKV("vout", out.i); - - if (fValidAddress) { - entry.pushKV("address", EncodeDestination(address)); - - const auto* address_book_entry = pwallet->FindAddressBookEntry(address); - if (address_book_entry) { - entry.pushKV("label", address_book_entry->GetLabel()); - } - - std::unique_ptr<SigningProvider> provider = pwallet->GetSolvingProvider(scriptPubKey); - if (provider) { - if (scriptPubKey.IsPayToScriptHash()) { - const CScriptID& hash = CScriptID(std::get<ScriptHash>(address)); - CScript redeemScript; - if (provider->GetCScript(hash, redeemScript)) { - entry.pushKV("redeemScript", HexStr(redeemScript)); - // Now check if the redeemScript is actually a P2WSH script - CTxDestination witness_destination; - if (redeemScript.IsPayToWitnessScriptHash()) { - bool extracted = ExtractDestination(redeemScript, witness_destination); - CHECK_NONFATAL(extracted); - // Also return the witness script - const WitnessV0ScriptHash& whash = std::get<WitnessV0ScriptHash>(witness_destination); - CScriptID id; - CRIPEMD160().Write(whash.begin(), whash.size()).Finalize(id.begin()); - CScript witnessScript; - if (provider->GetCScript(id, witnessScript)) { - entry.pushKV("witnessScript", HexStr(witnessScript)); - } - } - } - } else if (scriptPubKey.IsPayToWitnessScriptHash()) { - const WitnessV0ScriptHash& whash = std::get<WitnessV0ScriptHash>(address); - CScriptID id; - CRIPEMD160().Write(whash.begin(), whash.size()).Finalize(id.begin()); - CScript witnessScript; - if (provider->GetCScript(id, witnessScript)) { - entry.pushKV("witnessScript", HexStr(witnessScript)); - } - } - } - } - - entry.pushKV("scriptPubKey", HexStr(scriptPubKey)); - entry.pushKV("amount", ValueFromAmount(out.tx->tx->vout[out.i].nValue)); - entry.pushKV("confirmations", out.nDepth); - if (!out.nDepth) { - size_t ancestor_count, descendant_count, ancestor_size; - CAmount ancestor_fees; - pwallet->chain().getTransactionAncestry(out.tx->GetHash(), ancestor_count, descendant_count, &ancestor_size, &ancestor_fees); - if (ancestor_count) { - entry.pushKV("ancestorcount", uint64_t(ancestor_count)); - entry.pushKV("ancestorsize", uint64_t(ancestor_size)); - entry.pushKV("ancestorfees", uint64_t(ancestor_fees)); - } - } - entry.pushKV("spendable", out.fSpendable); - entry.pushKV("solvable", out.fSolvable); - if (out.fSolvable) { - std::unique_ptr<SigningProvider> provider = pwallet->GetSolvingProvider(scriptPubKey); - if (provider) { - auto descriptor = InferDescriptor(scriptPubKey, *provider); - entry.pushKV("desc", descriptor->ToString()); - } - } - if (avoid_reuse) entry.pushKV("reused", reused); - entry.pushKV("safe", out.fSafe); - results.push_back(entry); - } - - return results; -}, - }; -} - // Only includes key documentation where the key is snake_case in all RPC methods. MixedCase keys can be added later. static std::vector<RPCArg> FundTxDoc() { @@ -2696,6 +1977,16 @@ RPCHelpMan listlabels(); RPCHelpMan walletdisplayaddress(); #endif // ENABLE_EXTERNAL_SIGNER +// coins +RPCHelpMan getreceivedbyaddress(); +RPCHelpMan getreceivedbylabel(); +RPCHelpMan getbalance(); +RPCHelpMan getunconfirmedbalance(); +RPCHelpMan lockunspent(); +RPCHelpMan listlockunspent(); +RPCHelpMan getbalances(); +RPCHelpMan listunspent(); + // encryption RPCHelpMan walletpassphrase(); RPCHelpMan walletpassphrasechange(); |