wallet: use in-memory SQLite for temporary wallet in exportwatchonlywallet

The intermediate watchonly wallet created during exportwatchonlywallet is
a pure build artifact — it is always discarded once BackupWallet() copies
it to the destination. Creating it as an in-memory SQLiteDatabase
(SQLITE_OPEN_MEMORY) removes the need to write files to the wallets
directory and eliminates the cleanup handler that deleted those files on
both success and failure paths.

Introduces InMemoryWalletDatabase (a minimal SQLiteDatabase subclass) and
MakeInMemoryWalletDatabase() factory in sqlite.h/cpp, following the same
pattern as MockableSQLiteDatabase / CreateMockableWalletDatabase() in the
test utilities. MockableSQLiteDatabase now derives from InMemoryWalletDatabase,
removing its redundant Files() override.

The wallet is named after the source wallet ("<name>_watchonly_temp") so
concurrent exports of different wallets use distinct names and log lines
remain traceable to the source wallet.
This commit is contained in:
Pablo Martin
2026-07-04 15:30:37 -03:00
parent ee43743f12
commit d1e7f8c986
5 changed files with 26 additions and 31 deletions

View File

@@ -9,6 +9,7 @@
#include <util/expected.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/context.h>
#include <wallet/sqlite.h>
#include <wallet/wallet.h>
#include <fstream>
@@ -69,40 +70,15 @@ util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs:
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;
const uint64_t create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS;
// Make the watchonly wallet
DatabaseStatus status;
// Create the temporary watchonly wallet in memory to avoid leaving files on disk
std::vector<bilingual_str> warnings;
std::string wallet_name = wallet.GetName() + "_watchonly_temp";
bilingual_str error;
std::unique_ptr<WalletDatabase> 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<CWallet> watchonly_wallet;
fs::path wallet_path = fs::PathFromString(database->Filename()).parent_path();
std::vector<fs::path> 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);
std::shared_ptr<CWallet> watchonly_wallet = CWallet::CreateNew(empty_context, /*name=*/wallet.GetName() + "_watchonly_temp", MakeInMemoryWalletDatabase(), create_flags, /*born_encrypted=*/false, error, warnings);
if (!watchonly_wallet) {
return util::Error{strprintf(_("Error: Failed to create new watchonly wallet. %s"), error)};
}

View File

@@ -722,6 +722,15 @@ std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const D
}
}
InMemoryWalletDatabase::InMemoryWalletDatabase()
: SQLiteDatabase(fs::path{}, fs::path{":memory:"}, DatabaseOptions(), SQLITE_OPEN_MEMORY)
{}
std::unique_ptr<WalletDatabase> MakeInMemoryWalletDatabase()
{
return std::make_unique<InMemoryWalletDatabase>();
}
std::string SQLiteDatabaseVersion()
{
return std::string(sqlite3_libversion());

View File

@@ -175,8 +175,19 @@ public:
bool m_use_unsafe_sync;
};
/** An in-memory SQLiteDatabase. Used as a temporary build artifact where no
* on-disk persistence is needed. */
class InMemoryWalletDatabase : public SQLiteDatabase
{
public:
InMemoryWalletDatabase();
std::vector<fs::path> Files() override { return {}; }
};
std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
std::unique_ptr<WalletDatabase> MakeInMemoryWalletDatabase();
std::string SQLiteDatabaseVersion();
} // namespace wallet

View File

@@ -116,7 +116,7 @@ CTxDestination getNewDestination(CWallet& w, OutputType output_type)
}
MockableSQLiteDatabase::MockableSQLiteDatabase()
: SQLiteDatabase(fs::PathFromString("mock/"), fs::PathFromString("mock/wallet.dat"), DatabaseOptions(), SQLITE_OPEN_MEMORY)
: InMemoryWalletDatabase()
{}
std::unique_ptr<WalletDatabase> CreateMockableWalletDatabase()

View File

@@ -56,7 +56,7 @@ public:
/** A WalletDatabase whose contents and return values can be modified as needed for testing
**/
class MockableSQLiteDatabase : public SQLiteDatabase
class MockableSQLiteDatabase : public InMemoryWalletDatabase
{
public:
MockableSQLiteDatabase();
@@ -64,7 +64,6 @@ public:
bool Backup(const std::string& strDest) const override { return true; }
std::string Filename() override { return "mockable"; }
std::vector<fs::path> Files() override { return {}; }
std::string Format() override { return "sqlite-mock"; }
std::unique_ptr<DatabaseBatch> MakeBatch() override { return std::make_unique<MockableSQLiteBatch>(*this); }
};