mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-02-09 02:59:31 +08:00
9c7e4771b1test: Test listdescs with priv works even with missing priv keys (Novo)ed945a6854walletrpc: reject listdes with priv key on w-only wallets (Novo)9e5e9824f1descriptor: ToPrivateString() pass if at least 1 priv key exists (Novo)5c4db25b61descriptor: refactor ToPrivateString for providers (Novo)2dc74e3f4ewallet/migration: use HavePrivateKeys in place of ToPrivateString (Novo)e842eb90bbdescriptors: add HavePrivateKeys() (Novo) Pull request description: _TLDR: Currently, `listdescriptors [private=true]` will fail for a non-watch-only wallet if any descriptor has a missing private key(e.g `tr()`, `multi()`, etc.). This PR changes that while making sure `listdescriptors [private=true]` still fails if there no private keys. Closes #32078_ In non-watch-only wallets, it's possible to import descriptors as long as at least one private key is included. It's important that users can still view these descriptors when they need to create a backup—even if some private keys are missing ([#32078 (comment)](https://github.com/bitcoin/bitcoin/issues/32078#issuecomment-2781428475)). This change makes it possible to do so. This change also helps prevent `listdescriptors true` from failing completely, because one descriptor is missing some private keys. ### Notes - The new behaviour is applied to all descriptors including miniscript descriptors - `listdescriptors true` still fails for watch-only wallets to preserve existing behaviour https://github.com/bitcoin/bitcoin/pull/24361#discussion_r920801352 - Wallet migration logic previously used `Descriptor::ToPrivateString()` to determine which descriptor was watchonly. This means that modifying the `ToPrivateString()` behaviour caused descriptors that were previously recognized as "watchonly" to be "non-watchonly". **In order to keep the scope of this PR limited to the RPC behaviour, this PR uses a different method to determine `watchonly` descriptors for the purpose of wallet migration.** A follow-up PR can be opened to update migration logic to exclude descriptors with some private keys from the `watchonly` migration wallet. ### Relevant PRs https://github.com/bitcoin/bitcoin/pull/24361 https://github.com/bitcoin/bitcoin/pull/32186 ### Testing Functional tests were added to test the new behaviour EDIT **`listdescriptors [private=true]` will still fail when there are no private keys because non-watchonly wallets must have private keys and calling `listdescriptors [private=true]` for watchonly wallet returns an error** ACKs for top commit: Sjors: ACK9c7e4771b1achow101: ACK9c7e4771b1w0xlt: reACK9c7e4771b1with minor nits rkrux: re-ACK9c7e4771b1Tree-SHA512: f9b3b2c3e5425a26e158882e39e82e15b7cb13ffbfb6a5fa2868c79526e9b178fcc3cd88d3e2e286f64819d041f687353780bbcf5a355c63a136fb8179698b60
667 lines
31 KiB
C++
667 lines
31 KiB
C++
// Copyright (c) 2009-present 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 <chain.h>
|
|
#include <clientversion.h>
|
|
#include <core_io.h>
|
|
#include <hash.h>
|
|
#include <interfaces/chain.h>
|
|
#include <key_io.h>
|
|
#include <merkleblock.h>
|
|
#include <node/types.h>
|
|
#include <rpc/util.h>
|
|
#include <script/descriptor.h>
|
|
#include <script/script.h>
|
|
#include <script/solver.h>
|
|
#include <sync.h>
|
|
#include <uint256.h>
|
|
#include <util/bip32.h>
|
|
#include <util/check.h>
|
|
#include <util/fs.h>
|
|
#include <util/time.h>
|
|
#include <util/translation.h>
|
|
#include <wallet/rpc/util.h>
|
|
#include <wallet/wallet.h>
|
|
|
|
#include <cstdint>
|
|
#include <fstream>
|
|
#include <tuple>
|
|
#include <string>
|
|
|
|
#include <univalue.h>
|
|
|
|
|
|
|
|
using interfaces::FoundBlock;
|
|
|
|
namespace wallet {
|
|
RPCHelpMan importprunedfunds()
|
|
{
|
|
return RPCHelpMan{
|
|
"importprunedfunds",
|
|
"Imports funds without rescan. Corresponding address or script must previously be included in wallet. Aimed towards pruned wallets. The end-user is responsible to import additional transactions that subsequently spend the imported outputs or rescan after the point in the blockchain the transaction is included.\n",
|
|
{
|
|
{"rawtransaction", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A raw transaction in hex funding an already-existing address in wallet"},
|
|
{"txoutproof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex output from gettxoutproof that contains the transaction"},
|
|
},
|
|
RPCResult{RPCResult::Type::NONE, "", ""},
|
|
RPCExamples{""},
|
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
|
{
|
|
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
|
|
if (!pwallet) return UniValue::VNULL;
|
|
|
|
CMutableTransaction tx;
|
|
if (!DecodeHexTx(tx, request.params[0].get_str())) {
|
|
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed. Make sure the tx has at least one input.");
|
|
}
|
|
|
|
DataStream ssMB{ParseHexV(request.params[1], "proof")};
|
|
CMerkleBlock merkleBlock;
|
|
ssMB >> merkleBlock;
|
|
|
|
//Search partial merkle tree in proof for our transaction and index in valid block
|
|
std::vector<Txid> vMatch;
|
|
std::vector<unsigned int> vIndex;
|
|
if (merkleBlock.txn.ExtractMatches(vMatch, vIndex) != merkleBlock.header.hashMerkleRoot) {
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Something wrong with merkleblock");
|
|
}
|
|
|
|
LOCK(pwallet->cs_wallet);
|
|
int height;
|
|
if (!pwallet->chain().findAncestorByHash(pwallet->GetLastBlockHash(), merkleBlock.header.GetHash(), FoundBlock().height(height))) {
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain");
|
|
}
|
|
|
|
std::vector<Txid>::const_iterator it;
|
|
if ((it = std::find(vMatch.begin(), vMatch.end(), tx.GetHash())) == vMatch.end()) {
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction given doesn't exist in proof");
|
|
}
|
|
|
|
unsigned int txnIndex = vIndex[it - vMatch.begin()];
|
|
|
|
CTransactionRef tx_ref = MakeTransactionRef(tx);
|
|
if (pwallet->IsMine(*tx_ref)) {
|
|
pwallet->AddToWallet(std::move(tx_ref), TxStateConfirmed{merkleBlock.header.GetHash(), height, static_cast<int>(txnIndex)});
|
|
return UniValue::VNULL;
|
|
}
|
|
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No addresses in wallet correspond to included transaction");
|
|
},
|
|
};
|
|
}
|
|
|
|
RPCHelpMan removeprunedfunds()
|
|
{
|
|
return RPCHelpMan{
|
|
"removeprunedfunds",
|
|
"Deletes the specified transaction from the wallet. Meant for use with pruned wallets and as a companion to importprunedfunds. This will affect wallet balances.\n",
|
|
{
|
|
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded id of the transaction you are deleting"},
|
|
},
|
|
RPCResult{RPCResult::Type::NONE, "", ""},
|
|
RPCExamples{
|
|
HelpExampleCli("removeprunedfunds", "\"a8d0c0184dde994a09ec054286f1ce581bebf46446a512166eae7628734ea0a5\"") +
|
|
"\nAs a JSON-RPC call\n"
|
|
+ HelpExampleRpc("removeprunedfunds", "\"a8d0c0184dde994a09ec054286f1ce581bebf46446a512166eae7628734ea0a5\"")
|
|
},
|
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
|
{
|
|
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
|
|
if (!pwallet) return UniValue::VNULL;
|
|
|
|
LOCK(pwallet->cs_wallet);
|
|
|
|
Txid hash{Txid::FromUint256(ParseHashV(request.params[0], "txid"))};
|
|
std::vector<Txid> vHash;
|
|
vHash.push_back(hash);
|
|
if (auto res = pwallet->RemoveTxs(vHash); !res) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
|
|
}
|
|
|
|
return UniValue::VNULL;
|
|
},
|
|
};
|
|
}
|
|
|
|
static int64_t GetImportTimestamp(const UniValue& data, int64_t now)
|
|
{
|
|
if (data.exists("timestamp")) {
|
|
const UniValue& timestamp = data["timestamp"];
|
|
if (timestamp.isNum()) {
|
|
return timestamp.getInt<int64_t>();
|
|
} else if (timestamp.isStr() && timestamp.get_str() == "now") {
|
|
return now;
|
|
}
|
|
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected number or \"now\" timestamp value for key. got type %s", uvTypeName(timestamp.type())));
|
|
}
|
|
throw JSONRPCError(RPC_TYPE_ERROR, "Missing required timestamp field for key");
|
|
}
|
|
|
|
static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
|
|
{
|
|
UniValue warnings(UniValue::VARR);
|
|
UniValue result(UniValue::VOBJ);
|
|
|
|
try {
|
|
if (!data.exists("desc")) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
|
|
}
|
|
|
|
const std::string& descriptor = data["desc"].get_str();
|
|
const bool active = data.exists("active") ? data["active"].get_bool() : false;
|
|
const std::string label{LabelFromValue(data["label"])};
|
|
|
|
// Parse descriptor string
|
|
FlatSigningProvider keys;
|
|
std::string error;
|
|
auto parsed_descs = Parse(descriptor, keys, error, /* require_checksum = */ true);
|
|
if (parsed_descs.empty()) {
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
|
|
}
|
|
std::optional<bool> internal;
|
|
if (data.exists("internal")) {
|
|
if (parsed_descs.size() > 1) {
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot have multipath descriptor while also specifying \'internal\'");
|
|
}
|
|
internal = data["internal"].get_bool();
|
|
}
|
|
|
|
// Range check
|
|
std::optional<bool> is_ranged;
|
|
int64_t range_start = 0, range_end = 1, next_index = 0;
|
|
if (!parsed_descs.at(0)->IsRange() && data.exists("range")) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
|
|
} else if (parsed_descs.at(0)->IsRange()) {
|
|
if (data.exists("range")) {
|
|
auto range = ParseDescriptorRange(data["range"]);
|
|
range_start = range.first;
|
|
range_end = range.second + 1; // Specified range end is inclusive, but we need range end as exclusive
|
|
} else {
|
|
warnings.push_back("Range not given, using default keypool range");
|
|
range_start = 0;
|
|
range_end = wallet.m_keypool_size;
|
|
}
|
|
next_index = range_start;
|
|
is_ranged = true;
|
|
|
|
if (data.exists("next_index")) {
|
|
next_index = data["next_index"].getInt<int64_t>();
|
|
// bound checks
|
|
if (next_index < range_start || next_index >= range_end) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "next_index is out of range");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Active descriptors must be ranged
|
|
if (active && !parsed_descs.at(0)->IsRange()) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Active descriptors must be ranged");
|
|
}
|
|
|
|
// Multipath descriptors should not have a label
|
|
if (parsed_descs.size() > 1 && data.exists("label")) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Multipath descriptors should not have a label");
|
|
}
|
|
|
|
// Ranged descriptors should not have a label
|
|
if (is_ranged.has_value() && is_ranged.value() && data.exists("label")) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptors should not have a label");
|
|
}
|
|
|
|
bool desc_internal = internal.has_value() && internal.value();
|
|
// Internal addresses should not have a label either
|
|
if (desc_internal && data.exists("label")) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label");
|
|
}
|
|
|
|
// Combo descriptor check
|
|
if (active && !parsed_descs.at(0)->IsSingleType()) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Combo descriptors cannot be set to active");
|
|
}
|
|
|
|
// If the wallet disabled private keys, abort if private keys exist
|
|
if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled");
|
|
}
|
|
|
|
for (size_t j = 0; j < parsed_descs.size(); ++j) {
|
|
auto parsed_desc = std::move(parsed_descs[j]);
|
|
if (parsed_descs.size() == 2) {
|
|
desc_internal = j == 1;
|
|
} else if (parsed_descs.size() > 2) {
|
|
CHECK_NONFATAL(!desc_internal);
|
|
}
|
|
// Need to ExpandPrivate to check if private keys are available for all pubkeys
|
|
FlatSigningProvider expand_keys;
|
|
std::vector<CScript> scripts;
|
|
if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot expand descriptor. Probably because of hardened derivations without private keys provided");
|
|
}
|
|
parsed_desc->ExpandPrivate(0, keys, expand_keys);
|
|
|
|
for (const auto& w : parsed_desc->Warnings()) {
|
|
warnings.push_back(w);
|
|
}
|
|
|
|
// Check if all private keys are provided
|
|
bool have_all_privkeys = !expand_keys.keys.empty();
|
|
for (const auto& entry : expand_keys.origins) {
|
|
const CKeyID& key_id = entry.first;
|
|
CKey key;
|
|
if (!expand_keys.GetKey(key_id, key)) {
|
|
have_all_privkeys = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If private keys are enabled, check some things.
|
|
if (!wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
|
if (keys.keys.empty()) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import descriptor without private keys to a wallet with private keys enabled");
|
|
}
|
|
if (!have_all_privkeys) {
|
|
warnings.push_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
|
|
}
|
|
}
|
|
|
|
WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
|
|
|
|
// Add descriptor to the wallet
|
|
auto spk_manager_res = wallet.AddWalletDescriptor(w_desc, keys, label, desc_internal);
|
|
|
|
if (!spk_manager_res) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Could not add descriptor '%s': %s", descriptor, util::ErrorString(spk_manager_res).original));
|
|
}
|
|
|
|
auto& spk_manager = spk_manager_res.value().get();
|
|
|
|
// Set descriptor as active if necessary
|
|
if (active) {
|
|
if (!w_desc.descriptor->GetOutputType()) {
|
|
warnings.push_back("Unknown output type, cannot set descriptor to active.");
|
|
} else {
|
|
wallet.AddActiveScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
|
|
}
|
|
} else {
|
|
if (w_desc.descriptor->GetOutputType()) {
|
|
wallet.DeactivateScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
|
|
}
|
|
}
|
|
}
|
|
|
|
result.pushKV("success", UniValue(true));
|
|
} catch (const UniValue& e) {
|
|
result.pushKV("success", UniValue(false));
|
|
result.pushKV("error", e);
|
|
}
|
|
PushWarnings(warnings, result);
|
|
return result;
|
|
}
|
|
|
|
RPCHelpMan importdescriptors()
|
|
{
|
|
return RPCHelpMan{
|
|
"importdescriptors",
|
|
"Import descriptors. This will trigger a rescan of the blockchain based on the earliest timestamp of all descriptors being imported. Requires a new wallet backup.\n"
|
|
"When importing descriptors with multipath key expressions, if the multipath specifier contains exactly two elements, the descriptor produced from the second element will be imported as an internal descriptor.\n"
|
|
"\nNote: This call can take over an hour to complete if using an early timestamp; during that time, other rpc calls\n"
|
|
"may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n"
|
|
"The rescan is significantly faster if block filters are available (using startup option \"-blockfilterindex=1\").\n",
|
|
{
|
|
{"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported",
|
|
{
|
|
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
|
{
|
|
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "Descriptor to import."},
|
|
{"active", RPCArg::Type::BOOL, RPCArg::Default{false}, "Set this descriptor to be the active descriptor for the corresponding output type/externality"},
|
|
{"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED, "If a ranged descriptor is used, this specifies the end or the range (in the form [begin,end]) to import"},
|
|
{"next_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "If a ranged descriptor is set to active, this specifies the next index to generate addresses from"},
|
|
{"timestamp", RPCArg::Type::NUM, RPCArg::Optional::NO, "Time from which to start rescanning the blockchain for this descriptor, in " + UNIX_EPOCH_TIME + "\n"
|
|
"Use the string \"now\" to substitute the current synced blockchain time.\n"
|
|
"\"now\" can be specified to bypass scanning, for outputs which are known to never have been used, and\n"
|
|
"0 can be specified to scan the entire blockchain. Blocks up to 2 hours before the earliest timestamp\n"
|
|
"of all descriptors being imported will be scanned as well as the mempool.",
|
|
RPCArgOptions{.type_str={"timestamp | \"now\"", "integer / string"}}
|
|
},
|
|
{"internal", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether matching outputs should be treated as not incoming payments (e.g. change)"},
|
|
{"label", RPCArg::Type::STR, RPCArg::Default{""}, "Label to assign to the address, only allowed with internal=false. Disabled for ranged descriptors"},
|
|
},
|
|
},
|
|
},
|
|
RPCArgOptions{.oneline_description="requests"}},
|
|
},
|
|
RPCResult{
|
|
RPCResult::Type::ARR, "", "Response is an array with the same size as the input that has the execution result",
|
|
{
|
|
{RPCResult::Type::OBJ, "", "",
|
|
{
|
|
{RPCResult::Type::BOOL, "success", ""},
|
|
{RPCResult::Type::ARR, "warnings", /*optional=*/true, "",
|
|
{
|
|
{RPCResult::Type::STR, "", ""},
|
|
}},
|
|
{RPCResult::Type::OBJ, "error", /*optional=*/true, "",
|
|
{
|
|
{RPCResult::Type::ELISION, "", "JSONRPC error"},
|
|
}},
|
|
}},
|
|
}
|
|
},
|
|
RPCExamples{
|
|
HelpExampleCli("importdescriptors", "'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, \"internal\": true }, "
|
|
"{ \"desc\": \"<my descriptor 2>\", \"label\": \"example 2\", \"timestamp\": 1455191480 }]'") +
|
|
HelpExampleCli("importdescriptors", "'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, \"active\": true, \"range\": [0,100], \"label\": \"<my bech32 wallet>\" }]'")
|
|
},
|
|
[&](const RPCHelpMan& self, const JSONRPCRequest& main_request) -> UniValue
|
|
{
|
|
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(main_request);
|
|
if (!pwallet) return UniValue::VNULL;
|
|
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(/*with_passphrase=*/true)) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
|
|
}
|
|
|
|
// Ensure that the wallet is not locked for the remainder of this RPC, as
|
|
// the passphrase is used to top up the keypool.
|
|
LOCK(pwallet->m_relock_mutex);
|
|
|
|
const UniValue& requests = main_request.params[0];
|
|
const int64_t minimum_timestamp = 1;
|
|
int64_t now = 0;
|
|
int64_t lowest_timestamp = 0;
|
|
bool rescan = false;
|
|
UniValue response(UniValue::VARR);
|
|
{
|
|
LOCK(pwallet->cs_wallet);
|
|
EnsureWalletIsUnlocked(*pwallet);
|
|
|
|
CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(lowest_timestamp).mtpTime(now)));
|
|
|
|
// Get all timestamps and extract the lowest timestamp
|
|
for (const UniValue& request : requests.getValues()) {
|
|
// This throws an error if "timestamp" doesn't exist
|
|
const int64_t timestamp = std::max(GetImportTimestamp(request, now), minimum_timestamp);
|
|
const UniValue result = ProcessDescriptorImport(*pwallet, request, timestamp);
|
|
response.push_back(result);
|
|
|
|
if (lowest_timestamp > timestamp ) {
|
|
lowest_timestamp = timestamp;
|
|
}
|
|
|
|
// If we know the chain tip, and at least one request was successful then allow rescan
|
|
if (!rescan && result["success"].get_bool()) {
|
|
rescan = true;
|
|
}
|
|
}
|
|
pwallet->ConnectScriptPubKeyManNotifiers();
|
|
pwallet->RefreshAllTXOs();
|
|
}
|
|
|
|
// Rescan the blockchain using the lowest timestamp
|
|
if (rescan) {
|
|
int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, /*update=*/true);
|
|
pwallet->ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
|
|
|
|
if (pwallet->IsAbortingRescan()) {
|
|
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
|
|
}
|
|
|
|
if (scanned_time > lowest_timestamp) {
|
|
std::vector<UniValue> results = response.getValues();
|
|
response.clear();
|
|
response.setArray();
|
|
|
|
// Compose the response
|
|
for (unsigned int i = 0; i < requests.size(); ++i) {
|
|
const UniValue& request = requests.getValues().at(i);
|
|
|
|
// If the descriptor timestamp is within the successfully scanned
|
|
// range, or if the import result already has an error set, let
|
|
// the result stand unmodified. Otherwise replace the result
|
|
// with an error message.
|
|
if (scanned_time <= GetImportTimestamp(request, now) || results.at(i).exists("error")) {
|
|
response.push_back(results.at(i));
|
|
} else {
|
|
std::string error_msg{strprintf("Rescan failed for descriptor with timestamp %d. There "
|
|
"was an error reading a block from time %d, which is after or within %d seconds "
|
|
"of key creation, and could contain transactions pertaining to the desc. As a "
|
|
"result, transactions and coins using this desc may not appear in the wallet.",
|
|
GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW)};
|
|
if (pwallet->chain().havePruned()) {
|
|
error_msg += strprintf(" This error could be caused by pruning or data corruption "
|
|
"(see bitcoind log for details) and could be dealt with by downloading and "
|
|
"rescanning the relevant blocks (see -reindex option and rescanblockchain RPC).");
|
|
} else if (pwallet->chain().hasAssumedValidChain()) {
|
|
error_msg += strprintf(" This error is likely caused by an in-progress assumeutxo "
|
|
"background sync. Check logs or getchainstates RPC for assumeutxo background "
|
|
"sync progress and try again later.");
|
|
} else {
|
|
error_msg += strprintf(" This error could potentially caused by data corruption. If "
|
|
"the issue persists you may want to reindex (see -reindex option).");
|
|
}
|
|
|
|
UniValue result = UniValue(UniValue::VOBJ);
|
|
result.pushKV("success", UniValue(false));
|
|
result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, error_msg));
|
|
response.push_back(std::move(result));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return response;
|
|
},
|
|
};
|
|
}
|
|
|
|
RPCHelpMan listdescriptors()
|
|
{
|
|
return RPCHelpMan{
|
|
"listdescriptors",
|
|
"List all descriptors present in a wallet.\n",
|
|
{
|
|
{"private", RPCArg::Type::BOOL, RPCArg::Default{false}, "Show private descriptors."}
|
|
},
|
|
RPCResult{RPCResult::Type::OBJ, "", "", {
|
|
{RPCResult::Type::STR, "wallet_name", "Name of wallet this operation was performed on"},
|
|
{RPCResult::Type::ARR, "descriptors", "Array of descriptor objects (sorted by descriptor string representation)",
|
|
{
|
|
{RPCResult::Type::OBJ, "", "", {
|
|
{RPCResult::Type::STR, "desc", "Descriptor string representation"},
|
|
{RPCResult::Type::NUM, "timestamp", "The creation time of the descriptor"},
|
|
{RPCResult::Type::BOOL, "active", "Whether this descriptor is currently used to generate new addresses"},
|
|
{RPCResult::Type::BOOL, "internal", /*optional=*/true, "True if this descriptor is used to generate change addresses. False if this descriptor is used to generate receiving addresses; defined only for active descriptors"},
|
|
{RPCResult::Type::ARR_FIXED, "range", /*optional=*/true, "Defined only for ranged descriptors", {
|
|
{RPCResult::Type::NUM, "", "Range start inclusive"},
|
|
{RPCResult::Type::NUM, "", "Range end inclusive"},
|
|
}},
|
|
{RPCResult::Type::NUM, "next", /*optional=*/true, "Same as next_index field. Kept for compatibility reason."},
|
|
{RPCResult::Type::NUM, "next_index", /*optional=*/true, "The next index to generate addresses from; defined only for ranged descriptors"},
|
|
}},
|
|
}}
|
|
}},
|
|
RPCExamples{
|
|
HelpExampleCli("listdescriptors", "") + HelpExampleRpc("listdescriptors", "")
|
|
+ HelpExampleCli("listdescriptors", "true") + HelpExampleRpc("listdescriptors", "true")
|
|
},
|
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
|
{
|
|
const std::shared_ptr<const CWallet> wallet = GetWalletForJSONRPCRequest(request);
|
|
if (!wallet) return UniValue::VNULL;
|
|
|
|
const bool priv = !request.params[0].isNull() && request.params[0].get_bool();
|
|
if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && priv) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Can't get private descriptor string for watch-only wallets");
|
|
}
|
|
if (priv) {
|
|
EnsureWalletIsUnlocked(*wallet);
|
|
}
|
|
|
|
LOCK(wallet->cs_wallet);
|
|
|
|
const auto active_spk_mans = wallet->GetActiveScriptPubKeyMans();
|
|
|
|
struct WalletDescInfo {
|
|
std::string descriptor;
|
|
uint64_t creation_time;
|
|
bool active;
|
|
std::optional<bool> internal;
|
|
std::optional<std::pair<int64_t,int64_t>> range;
|
|
int64_t next_index;
|
|
};
|
|
|
|
std::vector<WalletDescInfo> wallet_descriptors;
|
|
for (const auto& spk_man : wallet->GetAllScriptPubKeyMans()) {
|
|
const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man);
|
|
if (!desc_spk_man) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Unexpected ScriptPubKey manager type.");
|
|
}
|
|
LOCK(desc_spk_man->cs_desc_man);
|
|
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
|
|
std::string descriptor;
|
|
CHECK_NONFATAL(desc_spk_man->GetDescriptorString(descriptor, priv));
|
|
const bool is_range = wallet_descriptor.descriptor->IsRange();
|
|
wallet_descriptors.push_back({
|
|
descriptor,
|
|
wallet_descriptor.creation_time,
|
|
active_spk_mans.contains(desc_spk_man),
|
|
wallet->IsInternalScriptPubKeyMan(desc_spk_man),
|
|
is_range ? std::optional(std::make_pair(wallet_descriptor.range_start, wallet_descriptor.range_end)) : std::nullopt,
|
|
wallet_descriptor.next_index
|
|
});
|
|
}
|
|
|
|
std::sort(wallet_descriptors.begin(), wallet_descriptors.end(), [](const auto& a, const auto& b) {
|
|
return a.descriptor < b.descriptor;
|
|
});
|
|
|
|
UniValue descriptors(UniValue::VARR);
|
|
for (const WalletDescInfo& info : wallet_descriptors) {
|
|
UniValue spk(UniValue::VOBJ);
|
|
spk.pushKV("desc", info.descriptor);
|
|
spk.pushKV("timestamp", info.creation_time);
|
|
spk.pushKV("active", info.active);
|
|
if (info.internal.has_value()) {
|
|
spk.pushKV("internal", info.internal.value());
|
|
}
|
|
if (info.range.has_value()) {
|
|
UniValue range(UniValue::VARR);
|
|
range.push_back(info.range->first);
|
|
range.push_back(info.range->second - 1);
|
|
spk.pushKV("range", std::move(range));
|
|
spk.pushKV("next", info.next_index);
|
|
spk.pushKV("next_index", info.next_index);
|
|
}
|
|
descriptors.push_back(std::move(spk));
|
|
}
|
|
|
|
UniValue response(UniValue::VOBJ);
|
|
response.pushKV("wallet_name", wallet->GetName());
|
|
response.pushKV("descriptors", std::move(descriptors));
|
|
|
|
return response;
|
|
},
|
|
};
|
|
}
|
|
|
|
RPCHelpMan backupwallet()
|
|
{
|
|
return RPCHelpMan{
|
|
"backupwallet",
|
|
"Safely copies the current wallet file to the specified destination, which can either be a directory or a path with a filename.\n",
|
|
{
|
|
{"destination", RPCArg::Type::STR, RPCArg::Optional::NO, "The destination directory or file"},
|
|
},
|
|
RPCResult{RPCResult::Type::NONE, "", ""},
|
|
RPCExamples{
|
|
HelpExampleCli("backupwallet", "\"backup.dat\"")
|
|
+ HelpExampleRpc("backupwallet", "\"backup.dat\"")
|
|
},
|
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
|
{
|
|
const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
|
|
if (!pwallet) return UniValue::VNULL;
|
|
|
|
// 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);
|
|
|
|
std::string strDest = request.params[0].get_str();
|
|
if (!pwallet->BackupWallet(strDest)) {
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Wallet backup failed!");
|
|
}
|
|
|
|
return UniValue::VNULL;
|
|
},
|
|
};
|
|
}
|
|
|
|
|
|
RPCHelpMan restorewallet()
|
|
{
|
|
return RPCHelpMan{
|
|
"restorewallet",
|
|
"Restores and loads a wallet from backup.\n"
|
|
"\nThe rescan is significantly faster if block filters are available"
|
|
"\n(using startup option \"-blockfilterindex=1\").\n",
|
|
{
|
|
{"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"},
|
|
{"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."},
|
|
{"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."},
|
|
},
|
|
RPCResult{
|
|
RPCResult::Type::OBJ, "", "",
|
|
{
|
|
{RPCResult::Type::STR, "name", "The wallet name if restored successfully."},
|
|
{RPCResult::Type::ARR, "warnings", /*optional=*/true, "Warning messages, if any, related to restoring and loading the wallet.",
|
|
{
|
|
{RPCResult::Type::STR, "", ""},
|
|
}},
|
|
}
|
|
},
|
|
RPCExamples{
|
|
HelpExampleCli("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"")
|
|
+ HelpExampleRpc("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"")
|
|
+ HelpExampleCliNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}})
|
|
+ HelpExampleRpcNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}})
|
|
},
|
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
|
{
|
|
|
|
WalletContext& context = EnsureWalletContext(request.context);
|
|
|
|
auto backup_file = fs::u8path(request.params[1].get_str());
|
|
|
|
std::string wallet_name = request.params[0].get_str();
|
|
|
|
std::optional<bool> load_on_start = request.params[2].isNull() ? std::nullopt : std::optional<bool>(request.params[2].get_bool());
|
|
|
|
DatabaseStatus status;
|
|
bilingual_str error;
|
|
std::vector<bilingual_str> warnings;
|
|
|
|
const std::shared_ptr<CWallet> wallet = RestoreWallet(context, backup_file, wallet_name, load_on_start, status, error, warnings);
|
|
|
|
HandleWalletError(wallet, status, error);
|
|
|
|
UniValue obj(UniValue::VOBJ);
|
|
obj.pushKV("name", wallet->GetName());
|
|
PushWarnings(warnings, obj);
|
|
|
|
return obj;
|
|
|
|
},
|
|
};
|
|
}
|
|
} // namespace wallet
|