diff --git a/src/wallet/export.cpp b/src/wallet/export.cpp index 9c042d3b99e..6f170dd728e 100644 --- a/src/wallet/export.cpp +++ b/src/wallet/export.cpp @@ -4,10 +4,15 @@ #include +#include +#include #include #include +#include #include +#include + namespace wallet { util::Expected, std::string> ExportDescriptors(const CWallet& wallet, bool export_private) { @@ -36,4 +41,182 @@ util::Expected, std::string> ExportDescriptors(const } return wallet_descriptors; } + +util::Result ExportWatchOnlyWallet(const CWallet& wallet, const fs::path& destination, WalletContext& context) +{ + AssertLockHeld(wallet.cs_wallet); + + if (destination.empty()) { + return util::Error{_("Error: Export destination cannot be empty")}; + } + if (fs::exists(destination)) { + return util::Error{strprintf(_("Error: Export destination '%s' already exists"), fs::PathToString(destination))}; + } + if (!std::ofstream{fs::PathToString(destination)}) { + return util::Error{strprintf(_("Error: Could not create file '%s'"), fs::PathToString(destination))}; + } + bool success = false; + auto cleanup_destination = interfaces::MakeCleanupHandler([&success, &destination] { + if (!success) fs::remove(destination); + }); + + // Get the descriptors from this wallet + util::Expected, std::string> exported = ExportDescriptors(wallet, /*export_private=*/false); + if (!exported) { + return util::Error{Untranslated(exported.error())}; + } + if (exported->empty()) { + return util::Error{_("Error: Wallet has no descriptors to export")}; + } + + // Setup DatabaseOptions to create a new sqlite database + DatabaseOptions options; + options.require_existing = false; + options.require_create = true; + options.require_format = DatabaseFormat::SQLITE; + + // Make the wallet with the same flags as this wallet, but without private keys + options.create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS; + + // Make the watchonly wallet + DatabaseStatus status; + std::vector warnings; + std::string wallet_name = wallet.GetName() + "_watchonly_temp"; + bilingual_str error; + std::unique_ptr database = MakeWalletDatabase(wallet_name, options, status, error); + if (!database) { + return util::Error{strprintf(_("Wallet file creation failed: %s"), error)}; + } + + // Always remove the temporary wallet files, even when returning early on error. + std::shared_ptr watchonly_wallet; + fs::path wallet_path = fs::PathFromString(database->Filename()).parent_path(); + std::vector cleanup_files = database->Files(); + auto cleanup_watchonly_wallet = interfaces::MakeCleanupHandler([&watchonly_wallet, &wallet_path, &cleanup_files] { + if (watchonly_wallet) watchonly_wallet.reset(); + for (const auto& file : cleanup_files) { + fs::remove(file); + } + fs::remove(wallet_path); + }); + + WalletContext empty_context; + empty_context.args = context.args; + watchonly_wallet = CWallet::CreateNew(empty_context, wallet_name, std::move(database), options.create_flags, /*born_encrypted=*/false, error, warnings); + if (!watchonly_wallet) { + return util::Error{strprintf(_("Error: Failed to create new watchonly wallet. %s"), error)}; + } + + { + LOCK(watchonly_wallet->cs_wallet); + + // Parse the descriptors and add them to the new wallet + for (const WalletDescInfo& desc_info : *Assert(exported)) { + // Parse the descriptor + FlatSigningProvider dummy_keys; + std::string dummy_err; + std::vector> descs = Parse(desc_info.descriptor, dummy_keys, dummy_err, /*require_checksum=*/true); + CHECK_NONFATAL(descs.size() == 1); // All of our descriptors should be valid, and not multipath + CHECK_NONFATAL(dummy_keys.keys.size() == 0); // No private keys should be present in our exported descriptors + + // Get the range if there is one + int32_t range_start = 0; + int32_t range_end = 0; + if (desc_info.range) { + range_start = desc_info.range->first; + range_end = desc_info.range->second; + } + + WalletDescriptor w_desc(std::move(descs.at(0)), desc_info.creation_time, range_start, range_end, desc_info.next_index); + + // For descriptors that cannot self expand (i.e. needs private keys or cache), retrieve the cache + uint256 desc_id = w_desc.id; + if (!w_desc.descriptor->CanSelfExpand()) { + DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast(wallet.GetScriptPubKeyMan(desc_id)); + w_desc.cache = WITH_LOCK(desc_spkm->cs_desc_man, return desc_spkm->GetWalletDescriptor().cache); + } + + // Add to the watchonly wallet + if (auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, dummy_keys, /*label=*/"", /*internal=*/false); !spkm_res) { + return util::Error{util::ErrorString(spkm_res)}; + } + + // Set active spkms as active + if (desc_info.active) { + // Determine whether this descriptor is internal + // This is only set for active spkms + bool internal = false; + if (desc_info.internal) { + internal = *desc_info.internal; + } + watchonly_wallet->AddActiveScriptPubKeyMan(desc_id, *Assert(w_desc.descriptor->GetOutputType()), internal); + } + } + + // Copy locked coins that are persisted + for (const auto& [coin, persisted] : wallet.m_locked_coins) { + if (!persisted) continue; + watchonly_wallet->LockCoin(coin, persisted); + } + + { + // Make a WalletBatch for the watchonly wallet so that everything else can be written atomically + WalletBatch watchonly_batch(watchonly_wallet->GetDatabase()); + if (!watchonly_batch.TxnBegin()) { + return util::Error{strprintf(_("Error: database transaction cannot be executed for new watchonly wallet %s"), watchonly_wallet->GetName())}; + } + + // Copy orderPosNext + watchonly_batch.WriteOrderPosNext(wallet.nOrderPosNext); + + // Write the best block locator to avoid rescanning on reload + CBlockLocator best_block_locator; + { + WalletBatch local_wallet_batch(wallet.GetDatabase()); + if (!local_wallet_batch.ReadBestBlock(best_block_locator)) { + return util::Error{_("Error: Unable to read wallet's best block locator record")}; + } + } + if (!watchonly_batch.WriteBestBlock(best_block_locator)) { + return util::Error{_("Error: Unable to write watchonly wallet best block locator record")}; + } + + // Copy the transactions + for (const auto& [txid, wtx] : wallet.mapWallet) { + if (!watchonly_wallet->LoadToWallet(txid, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(watchonly_wallet->cs_wallet) { + if (!new_tx) return false; + ins_wtx.SetTx(wtx.tx); + ins_wtx.CopyFrom(wtx); + return true; + })) { + return util::Error{strprintf(_("Error: Could not add tx %s to watchonly wallet"), txid.GetHex())}; + } + watchonly_batch.WriteTx(watchonly_wallet->mapWallet.at(txid)); + } + + // Copy address book + for (const auto& [dest, entry] : wallet.m_address_book) { + auto address{EncodeDestination(dest)}; + if (entry.purpose) watchonly_batch.WritePurpose(address, PurposeToString(*entry.purpose)); + if (entry.label) watchonly_batch.WriteName(address, *entry.label); + for (const auto& [id, request] : entry.receive_requests) { + watchonly_batch.WriteAddressReceiveRequest(dest, id, request); + } + if (entry.previously_spent) watchonly_batch.WriteAddressPreviouslySpent(dest, true); + } + + if (!watchonly_batch.TxnCommit()) { + return util::Error{_("Error: cannot commit db transaction for watchonly wallet export")}; + } + } + + // Make a backup of this wallet at the specified destination directory + if (!watchonly_wallet->BackupWallet(fs::PathToString(destination))) { + return util::Error{_("Error: Unable to write the exported wallet")}; + } + success = true; + } + + return fs::PathToString(destination); +} } // namespace wallet diff --git a/src/wallet/export.h b/src/wallet/export.h index d83600be2a6..5f2289302cc 100644 --- a/src/wallet/export.h +++ b/src/wallet/export.h @@ -27,6 +27,10 @@ struct WalletDescInfo { //! Export the descriptors from a wallet so that they can be imported elsewhere util::Expected, std::string> ExportDescriptors(const CWallet& wallet, bool export_private) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +//! Make a new watchonly wallet file containing the public descriptors from this wallet +//! The exported watchonly wallet file will be named and placed at the path specified in 'destination' +util::Result ExportWatchOnlyWallet(const CWallet& wallet, const fs::path& destination, WalletContext& context) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); } // namespace wallet #endif // BITCOIN_WALLET_EXPORT_H diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 9f964e50e62..7c65383b0ad 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -882,7 +882,7 @@ public: */ void postInitProcess(); - bool BackupWallet(const std::string& strDest) const; + [[nodiscard]] bool BackupWallet(const std::string& strDest) const; /* Returns true if HD is enabled */ bool IsHDEnabled() const;