Skip to content

Commit 8eadc7c

Browse files
committed
wallet: Add CWallet::ExportWatchOnly
ExportWatchOnly produces a watchonly wallet file from a CWallet. This can be restored onto another instance of Bitcoin Core to allow that instance to watch the same descriptors, and also have all of the same initial address book and transactions.
1 parent cb43639 commit 8eadc7c

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

src/wallet/wallet.cpp

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4510,4 +4510,170 @@ util::Result<std::vector<WalletDescInfo>> CWallet::ExportDescriptors(bool export
45104510
}
45114511
return wallet_descriptors;
45124512
}
4513+
4514+
util::Result<std::string> CWallet::ExportWatchOnlyWallet(const fs::path& destination, WalletContext& context) const
4515+
{
4516+
AssertLockHeld(cs_wallet);
4517+
4518+
if (destination.empty()) {
4519+
return util::Error{_("Error: Export destination cannot be empty")};
4520+
}
4521+
if (fs::exists(destination)) {
4522+
return util::Error{strprintf(_("Error: Export destination '%s' already exists"), fs::PathToString(destination))};
4523+
}
4524+
fs::path canonical_dest = fs::canonical(destination.parent_path());
4525+
canonical_dest /= destination.filename();
4526+
4527+
// Get the descriptors from this wallet
4528+
util::Result<std::vector<WalletDescInfo>> exported = ExportDescriptors(/*export_private=*/false);
4529+
if (!exported) {
4530+
return util::Error{util::ErrorString(exported)};
4531+
}
4532+
4533+
// Setup DatabaseOptions to create a new sqlite database
4534+
DatabaseOptions options;
4535+
options.require_existing = false;
4536+
options.require_create = true;
4537+
options.require_format = DatabaseFormat::SQLITE;
4538+
4539+
// Make the wallet with the same flags as this wallet, but without private keys
4540+
options.create_flags = GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS;
4541+
4542+
// Make the watchonly wallet
4543+
DatabaseStatus status;
4544+
std::vector<bilingual_str> warnings;
4545+
std::string wallet_name = GetName() + "_watchonly";
4546+
bilingual_str error;
4547+
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
4548+
if (!database) {
4549+
return util::Error{strprintf(_("Wallet file creation failed: %s"), error)};
4550+
}
4551+
WalletContext empty_context;
4552+
empty_context.args = context.args;
4553+
std::shared_ptr<CWallet> watchonly_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
4554+
if (!watchonly_wallet) {
4555+
return util::Error{_("Error: Failed to create new watchonly wallet")};
4556+
}
4557+
4558+
{
4559+
LOCK(watchonly_wallet->cs_wallet);
4560+
4561+
// Parse the descriptors and add them to the new wallet
4562+
for (const WalletDescInfo& desc_info : *exported) {
4563+
// Parse the descriptor
4564+
FlatSigningProvider keys;
4565+
std::string parse_err;
4566+
std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_info.descriptor, keys, parse_err, /*require_checksum=*/true);
4567+
assert(descs.size() == 1); // All of our descriptors should be valid, and not multipath
4568+
4569+
// Get the range if there is one
4570+
int32_t range_start = 0;
4571+
int32_t range_end = 0;
4572+
if (desc_info.range) {
4573+
range_start = desc_info.range->first;
4574+
range_end = desc_info.range->second;
4575+
}
4576+
4577+
WalletDescriptor w_desc(std::move(descs.at(0)), desc_info.creation_time, range_start, range_end, desc_info.next_index);
4578+
4579+
// For descriptors that cannot self expand (i.e. needs private keys or cache), retrieve the cache
4580+
uint256 desc_id = w_desc.id;
4581+
if (!w_desc.descriptor->CanSelfExpand()) {
4582+
DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(GetScriptPubKeyMan(desc_id));
4583+
w_desc.cache = WITH_LOCK(desc_spkm->cs_desc_man, return desc_spkm->GetWalletDescriptor().cache);
4584+
}
4585+
4586+
// Add to the watchonly wallet
4587+
if (auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, keys, "", false); !spkm_res) {
4588+
return util::Error{util::ErrorString(spkm_res)};
4589+
}
4590+
4591+
// Set active spkms as active
4592+
if (desc_info.active) {
4593+
// Determine whether this descriptor is internal
4594+
// This is only set for active spkms
4595+
bool internal = false;
4596+
if (desc_info.internal) {
4597+
internal = *desc_info.internal;
4598+
}
4599+
watchonly_wallet->AddActiveScriptPubKeyMan(desc_id, *w_desc.descriptor->GetOutputType(), internal);
4600+
}
4601+
}
4602+
4603+
{
4604+
// Make a WalletBatch for the watchonly_wallet so that everything else can be written atomically
4605+
WalletBatch watchonly_batch(watchonly_wallet->GetDatabase());
4606+
if (!watchonly_batch.TxnBegin()) {
4607+
return util::Error{strprintf(_("Error: database transaction cannot be executed for new watchonly wallet %s"), watchonly_wallet->GetName())};
4608+
}
4609+
4610+
// Copy minversion
4611+
// Don't use SetMinVersion to account for the newly created wallet having FEATURE_LATEST
4612+
// while the source wallet doesn't.
4613+
watchonly_wallet->LoadMinVersion(GetVersion());
4614+
watchonly_batch.WriteMinVersion(watchonly_wallet->GetVersion());
4615+
4616+
// Copy orderPosNext
4617+
watchonly_batch.WriteOrderPosNext(watchonly_wallet->nOrderPosNext);
4618+
4619+
// Write the best block locator to avoid rescanning on reload
4620+
CBlockLocator best_block_locator;
4621+
{
4622+
WalletBatch local_wallet_batch(GetDatabase());
4623+
if (!local_wallet_batch.ReadBestBlock(best_block_locator)) {
4624+
return util::Error{_("Error: Unable to read wallet's best block locator record")};
4625+
}
4626+
}
4627+
if (!watchonly_batch.WriteBestBlock(best_block_locator)) {
4628+
return util::Error{_("Error: Unable to write watchonly wallet best block locator record")};
4629+
}
4630+
4631+
// Copy the transactions
4632+
for (const auto& [txid, wtx] : mapWallet) {
4633+
const CWalletTx& to_copy_wtx = wtx;
4634+
if (!watchonly_wallet->LoadToWallet(txid, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(watchonly_wallet->cs_wallet) {
4635+
if (!new_tx) return false;
4636+
ins_wtx.SetTx(to_copy_wtx.tx);
4637+
ins_wtx.CopyFrom(to_copy_wtx);
4638+
return true;
4639+
})) {
4640+
return util::Error{strprintf(_("Error: Could not add tx %s to watchonly wallet"), txid.GetHex())};
4641+
}
4642+
watchonly_batch.WriteTx(watchonly_wallet->mapWallet.at(txid));
4643+
}
4644+
4645+
// Copy address book
4646+
for (const auto& [dest, entry] : m_address_book) {
4647+
auto address{EncodeDestination(dest)};
4648+
if (entry.purpose) watchonly_batch.WritePurpose(address, PurposeToString(*entry.purpose));
4649+
if (entry.label) watchonly_batch.WriteName(address, *entry.label);
4650+
for (const auto& [id, request] : entry.receive_requests) {
4651+
watchonly_batch.WriteAddressReceiveRequest(dest, id, request);
4652+
}
4653+
if (entry.previously_spent) watchonly_batch.WriteAddressPreviouslySpent(dest, true);
4654+
}
4655+
4656+
// Copy locked coins that are persisted
4657+
for (const auto& [coin, persisted] : m_locked_coins) {
4658+
if (!persisted) continue;
4659+
watchonly_wallet->LockCoin(coin, &watchonly_batch);
4660+
}
4661+
4662+
4663+
if (!watchonly_batch.TxnCommit()) {
4664+
return util::Error{_("Error: cannot commit db transaction for watchonly wallet export")};
4665+
}
4666+
}
4667+
4668+
// Make a backup of this wallet at the specified destination directory
4669+
watchonly_wallet->BackupWallet(fs::PathToString(canonical_dest));
4670+
}
4671+
4672+
// Delete the watchonly wallet now that it has been exported to the desired location
4673+
fs::path watchonly_path = fs::PathFromString(watchonly_wallet->GetDatabase().Filename()).parent_path();
4674+
watchonly_wallet.reset();
4675+
fs::remove_all(watchonly_path);
4676+
4677+
return fs::PathToString(canonical_dest);
4678+
}
45134679
} // namespace wallet

src/wallet/wallet.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
10681068

10691069
//! Export the descriptors from this wallet so that they can be imported elsewhere
10701070
util::Result<std::vector<WalletDescInfo>> ExportDescriptors(bool export_private) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1071+
1072+
//! Make a new watchonly wallet file containing the public descriptors from this wallet
1073+
//! The exported watchonly wallet file will be named and placed at the path specified in 'destination'
1074+
util::Result<std::string> ExportWatchOnlyWallet(const fs::path& destination, WalletContext& context) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
10711075
};
10721076

10731077
/**

0 commit comments

Comments
 (0)