Skip to content

Menu action to export a watchonly wallet #872

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ class Wallet

//! Return pointer to internal wallet class, useful for testing.
virtual wallet::CWallet* wallet() { return nullptr; }

//! Export a watchonly wallet file. See CWallet::ExportWatchOnlyWallet
virtual util::Result<std::string> exportWatchOnlyWallet(const fs::path& destination) = 0;
};

//! Wallet chain client that in addition to having chain client methods for
Expand Down
11 changes: 11 additions & 0 deletions src/qt/bitcoingui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ void BitcoinGUI::createActions()
m_mask_values_action->setStatusTip(tr("Mask the values in the Overview tab"));
m_mask_values_action->setCheckable(true);

m_export_watchonly_action = new QAction(tr("Export watch-only wallet"), this);
m_export_watchonly_action->setEnabled(false);
m_export_watchonly_action->setStatusTip(tr("Export a watch-only version of the current wallet that can be restored onto another node."));

connect(quitAction, &QAction::triggered, this, &BitcoinGUI::quitRequested);
connect(aboutAction, &QAction::triggered, this, &BitcoinGUI::aboutClicked);
connect(aboutQtAction, &QAction::triggered, qApp, QApplication::aboutQt);
Expand Down Expand Up @@ -484,6 +488,11 @@ void BitcoinGUI::createActions()
});
connect(m_mask_values_action, &QAction::toggled, this, &BitcoinGUI::setPrivacy);
connect(m_mask_values_action, &QAction::toggled, this, &BitcoinGUI::enableHistoryAction);
connect(m_export_watchonly_action, &QAction::triggered, [this] {
QString destination = GUIUtil::getSaveFileName(this, tr("Save Watch-only Wallet Export"), QString(), QString(), nullptr);
if (destination.isEmpty()) return;
walletFrame->currentWalletModel()->wallet().exportWatchOnlyWallet(GUIUtil::QStringToPath(destination));
});
}
#endif // ENABLE_WALLET

Expand All @@ -507,6 +516,7 @@ void BitcoinGUI::createMenuBar()
file->addSeparator();
file->addAction(backupWalletAction);
file->addAction(m_restore_wallet_action);
file->addAction(m_export_watchonly_action);
file->addSeparator();
file->addAction(openAction);
file->addAction(signMessageAction);
Expand Down Expand Up @@ -715,6 +725,7 @@ void BitcoinGUI::setWalletController(WalletController* wallet_controller, bool s
m_restore_wallet_action->setEnabled(true);
m_migrate_wallet_action->setEnabled(true);
m_migrate_wallet_action->setMenu(m_migrate_wallet_menu);
m_export_watchonly_action->setEnabled(true);

GUIUtil::ExceptionSafeConnect(wallet_controller, &WalletController::walletAdded, this, &BitcoinGUI::addWallet);
connect(wallet_controller, &WalletController::walletRemoved, this, &BitcoinGUI::removeWallet);
Expand Down
1 change: 1 addition & 0 deletions src/qt/bitcoingui.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class BitcoinGUI : public QMainWindow
QAction* m_mask_values_action{nullptr};
QAction* m_migrate_wallet_action{nullptr};
QMenu* m_migrate_wallet_menu{nullptr};
QAction* m_export_watchonly_action{nullptr};

QLabel *m_wallet_selector_label = nullptr;
QComboBox* m_wallet_selector = nullptr;
Expand Down
43 changes: 43 additions & 0 deletions src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ struct PubkeyProvider

/** Make a deep copy of this PubkeyProvider */
virtual std::unique_ptr<PubkeyProvider> Clone() const = 0;

/** Whether this PubkeyProvider can always provide a public key without cache or private key arguments */
virtual bool CanSelfExpand() const = 0;
};

class OriginPubkeyProvider final : public PubkeyProvider
Expand Down Expand Up @@ -290,6 +293,7 @@ class OriginPubkeyProvider final : public PubkeyProvider
{
return std::make_unique<OriginPubkeyProvider>(m_expr_index, m_origin, m_provider->Clone(), m_apostrophe);
}
bool CanSelfExpand() const override { return m_provider->CanSelfExpand(); }
};

/** An object representing a parsed constant public key in a descriptor. */
Expand Down Expand Up @@ -350,6 +354,7 @@ class ConstPubkeyProvider final : public PubkeyProvider
{
return std::make_unique<ConstPubkeyProvider>(m_expr_index, m_pubkey, m_xonly);
}
bool CanSelfExpand() const final { return true; }
};

enum class DeriveType {
Expand Down Expand Up @@ -572,6 +577,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
{
return std::make_unique<BIP32PubkeyProvider>(m_expr_index, m_root_extkey, m_path, m_derive, m_apostrophe);
}
bool CanSelfExpand() const override { return !IsHardened(); }
};

/** Base class for all Descriptor implementations. */
Expand Down Expand Up @@ -800,6 +806,7 @@ class AddressDescriptor final : public DescriptorImpl
}
bool IsSingleType() const final { return true; }
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; }
bool CanSelfExpand() const final { return true; }

std::optional<int64_t> ScriptSize() const override { return GetScriptForDestination(m_destination).size(); }
std::unique_ptr<DescriptorImpl> Clone() const override
Expand Down Expand Up @@ -827,6 +834,7 @@ class RawDescriptor final : public DescriptorImpl
}
bool IsSingleType() const final { return true; }
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; }
bool CanSelfExpand() const final { return true; }

std::optional<int64_t> ScriptSize() const override { return m_script.size(); }

Expand Down Expand Up @@ -854,6 +862,7 @@ class PKDescriptor final : public DescriptorImpl
public:
PKDescriptor(std::unique_ptr<PubkeyProvider> prov, bool xonly = false) : DescriptorImpl(Vector(std::move(prov)), "pk"), m_xonly(xonly) {}
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override { return m_pubkey_args[0]->CanSelfExpand(); }

std::optional<int64_t> ScriptSize() const override {
return 1 + (m_xonly ? 32 : m_pubkey_args[0]->GetSize()) + 1;
Expand Down Expand Up @@ -889,6 +898,7 @@ class PKHDescriptor final : public DescriptorImpl
PKHDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), "pkh") {}
std::optional<OutputType> GetOutputType() const override { return OutputType::LEGACY; }
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override { return m_pubkey_args[0]->CanSelfExpand(); }

std::optional<int64_t> ScriptSize() const override { return 1 + 1 + 1 + 20 + 1 + 1; }

Expand Down Expand Up @@ -922,6 +932,7 @@ class WPKHDescriptor final : public DescriptorImpl
WPKHDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), "wpkh") {}
std::optional<OutputType> GetOutputType() const override { return OutputType::BECH32; }
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override { return m_pubkey_args[0]->CanSelfExpand(); }

std::optional<int64_t> ScriptSize() const override { return 1 + 1 + 20; }

Expand Down Expand Up @@ -963,6 +974,7 @@ class ComboDescriptor final : public DescriptorImpl
public:
ComboDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), "combo") {}
bool IsSingleType() const final { return false; }
bool CanSelfExpand() const override { return m_pubkey_args[0]->CanSelfExpand(); }
std::unique_ptr<DescriptorImpl> Clone() const override
{
return std::make_unique<ComboDescriptor>(m_pubkey_args.at(0)->Clone());
Expand All @@ -987,6 +999,13 @@ class MultisigDescriptor final : public DescriptorImpl
public:
MultisigDescriptor(int threshold, std::vector<std::unique_ptr<PubkeyProvider>> providers, bool sorted = false) : DescriptorImpl(std::move(providers), sorted ? "sortedmulti" : "multi"), m_threshold(threshold), m_sorted(sorted) {}
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override {
bool can_expand = true;
for (const auto& key : m_pubkey_args) {
can_expand &= key->CanSelfExpand();
}
return can_expand;
}

std::optional<int64_t> ScriptSize() const override {
const auto n_keys = m_pubkey_args.size();
Expand Down Expand Up @@ -1038,6 +1057,13 @@ class MultiADescriptor final : public DescriptorImpl
public:
MultiADescriptor(int threshold, std::vector<std::unique_ptr<PubkeyProvider>> providers, bool sorted = false) : DescriptorImpl(std::move(providers), sorted ? "sortedmulti_a" : "multi_a"), m_threshold(threshold), m_sorted(sorted) {}
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override {
bool can_expand = true;
for (const auto& key : m_pubkey_args) {
can_expand &= key->CanSelfExpand();
}
return can_expand;
}

std::optional<int64_t> ScriptSize() const override {
const auto n_keys = m_pubkey_args.size();
Expand Down Expand Up @@ -1084,6 +1110,7 @@ class SHDescriptor final : public DescriptorImpl
return OutputType::LEGACY;
}
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override { return m_subdescriptor_args[0]->CanSelfExpand(); }

std::optional<int64_t> ScriptSize() const override { return 1 + 1 + 20 + 1; }

Expand Down Expand Up @@ -1125,6 +1152,7 @@ class WSHDescriptor final : public DescriptorImpl
WSHDescriptor(std::unique_ptr<DescriptorImpl> desc) : DescriptorImpl({}, std::move(desc), "wsh") {}
std::optional<OutputType> GetOutputType() const override { return OutputType::BECH32; }
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override { return m_subdescriptor_args[0]->CanSelfExpand(); }

std::optional<int64_t> ScriptSize() const override { return 1 + 1 + 32; }

Expand Down Expand Up @@ -1202,6 +1230,13 @@ class TRDescriptor final : public DescriptorImpl
}
std::optional<OutputType> GetOutputType() const override { return OutputType::BECH32M; }
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override {
bool can_expand = m_pubkey_args[0]->CanSelfExpand();
for (const auto& sub : m_subdescriptor_args) {
can_expand &= sub->CanSelfExpand();
}
return can_expand;
}

std::optional<int64_t> ScriptSize() const override { return 1 + 1 + 32; }

Expand Down Expand Up @@ -1329,6 +1364,13 @@ class MiniscriptDescriptor final : public DescriptorImpl

bool IsSolvable() const override { return true; }
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override {
bool can_expand = true;
for (const auto& key : m_pubkey_args) {
can_expand &= key->CanSelfExpand();
}
return can_expand;
}

std::optional<int64_t> ScriptSize() const override { return m_node->ScriptSize(); }

Expand Down Expand Up @@ -1368,6 +1410,7 @@ class RawTRDescriptor final : public DescriptorImpl
RawTRDescriptor(std::unique_ptr<PubkeyProvider> output_key) : DescriptorImpl(Vector(std::move(output_key)), "rawtr") {}
std::optional<OutputType> GetOutputType() const override { return OutputType::BECH32M; }
bool IsSingleType() const final { return true; }
bool CanSelfExpand() const override { return m_pubkey_args[0]->CanSelfExpand(); }

std::optional<int64_t> ScriptSize() const override { return 1 + 1 + 32; }

Expand Down
3 changes: 3 additions & 0 deletions src/script/descriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ struct Descriptor {
/** Convert the descriptor to a normalized string. Normalized descriptors have the xpub at the last hardened step. This fails if the provided provider does not have the private keys to derive that xpub. */
virtual bool ToNormalizedString(const SigningProvider& provider, std::string& out, const DescriptorCache* cache = nullptr) const = 0;

/** Whether the descriptor can be used to get more addresses without needing a cache or private keys. */
virtual bool CanSelfExpand() const = 0;

/** Expand a descriptor at a specified position.
*
* @param[in] pos The position at which to expand the descriptor. If IsRange() is false, this is ignored.
Expand Down
5 changes: 5 additions & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,11 @@ class WalletImpl : public Wallet
}
CWallet* wallet() override { return m_wallet.get(); }

util::Result<std::string> exportWatchOnlyWallet(const fs::path& destination) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->ExportWatchOnlyWallet(destination, m_context);
}

WalletContext& m_context;
std::shared_ptr<CWallet> m_wallet;
};
Expand Down
37 changes: 4 additions & 33 deletions src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -503,40 +503,11 @@ RPCHelpMan listdescriptors()
}

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;
if (!desc_spk_man->GetDescriptorString(descriptor, priv)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Can't get descriptor string.");
}
const bool is_range = wallet_descriptor.descriptor->IsRange();
wallet_descriptors.push_back({
descriptor,
wallet_descriptor.creation_time,
active_spk_mans.count(desc_spk_man) != 0,
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
});
util::Result<std::vector<WalletDescInfo>> exported = wallet->ExportDescriptors(priv);
if (!exported) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(exported).original);
}
std::vector<WalletDescInfo> wallet_descriptors = *exported;

std::sort(wallet_descriptors.begin(), wallet_descriptors.end(), [](const auto& a, const auto& b) {
return a.descriptor < b.descriptor;
Expand Down
5 changes: 4 additions & 1 deletion src/wallet/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey,
{
UniValue parent_descs(UniValue::VARR);
for (const auto& desc: wallet.GetWalletDescriptors(script_pubkey)) {
parent_descs.push_back(desc.descriptor->ToString());
std::string desc_str;
FlatSigningProvider provider;
if (!CHECK_NONFATAL(desc.descriptor->ToNormalizedString(provider, desc_str, &desc.cache))) continue;
parent_descs.push_back(desc_str);
}
entry.pushKV("parent_descs", std::move(parent_descs));
}
Expand Down
41 changes: 41 additions & 0 deletions src/wallet/rpc/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,46 @@ static RPCHelpMan createwalletdescriptor()
};
}

static RPCHelpMan exportwatchonlywallet()
{
return RPCHelpMan{"exportwatchonlywallet",
"Creates a wallet file at the specified path and name containing a watchonly version "
"of the wallet. This watchonly wallet contains the wallet's public descriptors, "
"its transactions, and address book data. The watchonly wallet can be imported to "
"another node using 'restorewallet'.",
{
{"destination", RPCArg::Type::STR, RPCArg::Optional::NO, "The path to the filename the exported watchonly wallet will be saved to"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "exported_file", "The full path that the file has been exported to"},
},
},
RPCExamples{
HelpExampleCli("exportwatchonlywallet", "\"home\\user\\\"")
+ HelpExampleRpc("exportwatchonlywallet", "\"home\\user\\\"")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL;
WalletContext& context = EnsureWalletContext(request.context);

std::string dest = request.params[0].get_str();

LOCK(pwallet->cs_wallet);
util::Result<std::string> exported = pwallet->ExportWatchOnlyWallet(fs::PathFromString(dest), context);
if (!exported) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(exported).original);
}
UniValue out{UniValue::VOBJ};
out.pushKV("exported_file", *exported);
return out;
}
};
}

// addresses
RPCHelpMan getaddressinfo();
RPCHelpMan getnewaddress();
Expand Down Expand Up @@ -1010,6 +1050,7 @@ std::span<const CRPCCommand> GetWalletRPCCommands()
{"wallet", &createwalletdescriptor},
{"wallet", &restorewallet},
{"wallet", &encryptwallet},
{"wallet", &exportwatchonlywallet},
{"wallet", &getaddressesbylabel},
{"wallet", &getaddressinfo},
{"wallet", &getbalance},
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ bool DescriptorScriptPubKeyMan::CanGetAddresses(bool internal) const
LOCK(cs_desc_man);
return m_wallet_descriptor.descriptor->IsSingleType() &&
m_wallet_descriptor.descriptor->IsRange() &&
(HavePrivateKeys() || m_wallet_descriptor.next_index < m_wallet_descriptor.range_end);
(HavePrivateKeys() || m_wallet_descriptor.next_index < m_wallet_descriptor.range_end || m_wallet_descriptor.descriptor->CanSelfExpand());
}

bool DescriptorScriptPubKeyMan::HavePrivateKeys() const
Expand Down
1 change: 1 addition & 0 deletions src/wallet/test/walletload_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class DummyDescriptor final : public Descriptor {
std::optional<int64_t> MaxSatisfactionWeight(bool) const override { return {}; }
std::optional<int64_t> MaxSatisfactionElems() const override { return {}; }
void GetPubKeys(std::set<CPubKey>& pubkeys, std::set<CExtPubKey>& ext_pubs) const override {}
bool CanSelfExpand() const final { return false; }
};

BOOST_FIXTURE_TEST_CASE(wallet_load_descriptors, TestingSetup)
Expand Down
Loading
Loading