From 3f94ec0649f935d00de49ad7b55026317dc64a27 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 16:14:19 -0300 Subject: wallet: make SQLite the default backend for new wallets A fresh install with no wallet.dat now starts on the SQLite backend and creates wallet.sqlite, instead of a Berkeley DB wallet.dat. The choice is recorded in a plain-text /wallet-backend marker so it survives a restart without a command-line flag. Backend resolution when no -walletbackend is given: an existing wallet.sqlite (or a "sqlite" marker) stays on SQLite; an existing wallet.dat stays on Berkeley DB -- nothing is converted behind the user; a fresh datadir starts on SQLite. An explicit -walletbackend=bdb or a "bdb" marker forces Berkeley DB and overrides the default. Adds CreateNewSQLiteWallet() (WalletSQLiteRuntimeOpen gains fMustExist so the create path can make a new file) which mints the first default key straight into the SQLite store via the same FinishLoadedWallet() call the Berkeley DB create path uses, then cleans up on failure. Marker read/write helpers are plain text, hand-editable. New wallet-backend-default self-test proves the resolution in child processes (fresh->SQLite+marker, existing wallet.dat->BDB, "bdb" marker->BDB, explicit flag->wallet.dat). The three wallet.dat-specific suites now ask for -walletbackend=bdb explicitly, since a flagless fresh datadir is SQLite now. All wallet suites pass. --- src/selftest.cpp | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) (limited to 'src/selftest.cpp') diff --git a/src/selftest.cpp b/src/selftest.cpp index 90a30d2..1960003 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -911,6 +911,7 @@ static int RunWalletStorageSanitySelfTest() vCreateArgs.push_back("-datadir=" + strScenarioDir); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && @@ -1474,6 +1475,7 @@ static int RunWalletSQLiteMigrationSelfTest() vCreateArgs.push_back("-datadir=" + strWalletDir); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && @@ -2534,6 +2536,134 @@ static int RunWalletSQLiteEncryptSelfTest() return nFail == 0 ? 0 : 1; } +// Backend resolution when no -walletbackend flag is given: a fresh install +// starts on SQLite (the new default), an existing wallet.dat stays on Berkeley +// DB, the persistent marker overrides the default, and an explicit flag +// overrides everything. Each scenario runs a real child process. +static int RunWalletBackendDefaultSelfTest() +{ + fflush(stdout); + printf("wallet-backend-default self-test\n"); + + std::string tmp, cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + + try + { + // 1. Fresh datadir, no flag -> new wallet.sqlite, marker "sqlite", no wallet.dat. + string d1 = tmp + "/fresh"; + nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; + vector a1; + a1.push_back("-datadir=" + d1); + a1.push_back("-nomanagedtor"); + a1.push_back("-nogui"); + a1.push_back("-newaddress"); + int r1 = RunBitflashChild(strExe, a1); + nFail += Check(r1 == 0 && + FileExists((d1 + "/wallet.sqlite").c_str()) && + !FileExists((d1 + "/wallet.dat").c_str()) && + FileContainsText(d1 + "/wallet-backend", "sqlite"), + "a fresh install starts on SQLite (new default) and records the marker") ? 0 : 1; + + // 1b. Second run reuses it, still SQLite, still no wallet.dat. + int r1b = RunBitflashChild(strExe, a1); + nFail += Check(r1b == 0 && + FileExists((d1 + "/wallet.sqlite").c_str()) && + !FileExists((d1 + "/wallet.dat").c_str()), + "the recorded SQLite choice survives a restart") ? 0 : 1; + + // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. + string d2 = tmp + "/existing-bdb"; + nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1; + vector a2c; + a2c.push_back("-datadir=" + d2); + a2c.push_back("-nomanagedtor"); + a2c.push_back("-nogui"); + a2c.push_back("-walletbackend=bdb"); + a2c.push_back("-newphrase"); + RunBitflashChild(strExe, a2c); + vector a2; + a2.push_back("-datadir=" + d2); + a2.push_back("-nomanagedtor"); + a2.push_back("-nogui"); + a2.push_back("-newaddress"); + int r2 = RunBitflashChild(strExe, a2); + nFail += Check(r2 == 0 && + FileExists((d2 + "/wallet.dat").c_str()) && + !FileExists((d2 + "/wallet.sqlite").c_str()), + "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1; + + // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir. + string d3 = tmp + "/marker-bdb"; + nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1; + nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"), + "a bdb marker can be written") ? 0 : 1; + vector a3; + a3.push_back("-datadir=" + d3); + a3.push_back("-nomanagedtor"); + a3.push_back("-nogui"); + a3.push_back("-newaddress"); + int r3 = RunBitflashChild(strExe, a3); + nFail += Check(r3 == 0 && + FileExists((d3 + "/wallet.dat").c_str()) && + !FileExists((d3 + "/wallet.sqlite").c_str()), + "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1; + + // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat. + string d4 = tmp + "/explicit-bdb"; + nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1; + vector a4; + a4.push_back("-datadir=" + d4); + a4.push_back("-nomanagedtor"); + a4.push_back("-nogui"); + a4.push_back("-walletbackend=bdb"); + a4.push_back("-newaddress"); + int r4 = RunBitflashChild(strExe, a4); + nFail += Check(r4 == 0 && + FileExists((d4 + "/wallet.dat").c_str()) && + !FileExists((d4 + "/wallet.sqlite").c_str()), + "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static int RunWalletPortabilitySelfTest() { fflush(stdout); @@ -2576,6 +2706,7 @@ static int RunWalletPortabilitySelfTest() vCreateArgs.push_back("-datadir=" + strOriginal); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && @@ -3265,6 +3396,8 @@ int RunSelfTest(const std::string& name) return RunWalletEncryptSelfTest(); if (name == "wallet-sqlite-encrypt") return RunWalletSQLiteEncryptSelfTest(); + if (name == "wallet-backend-default") + return RunWalletBackendDefaultSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") @@ -3281,6 +3414,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } -- cgit v1.2.3 From 477d57b9a01102b6f79ad9b10b2dcfd280faa1cb Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 16:26:34 -0300 Subject: gui: offer to convert a Berkeley DB wallet to SQLite on first run A first-run dialog for an existing wallet.dat with no backend choice yet: "Convert to SQLite" exports the wallet and records the sqlite marker so the next start opens it; "Keep Berkeley DB" records that choice so the offer does not return; "Decide later" leaves no marker and asks again. wallet.dat is never touched, and the change takes effect on restart -- matching the restart-to-apply model. The conversion itself is DoConvertWalletToSQLite(), a headless-testable function the button calls: it reuses the export path (extracted as ExportActiveWalletToSQLite, shared with -walletsqliteexport, which keeps its own specific messages), refuses if already on SQLite or if a wallet.sqlite exists, and writes the marker only after a verified export. New wallet-convert self-test proves the action end to end: build a Berkeley DB wallet, convert, and confirm wallet.sqlite + the marker are written, wallet.dat is left untouched, a second convert refuses, and a flagless restart opens the converted SQLite wallet with its phrase intact. All wallet suites pass. The modal itself is thin UI over this tested action. --- src/gui.cpp | 90 ++++++++++++++++++++++++++++++++++++++++ src/makefile.mingw | 1 + src/selftest.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 119 +++++++++++++++++++++++++++++++++++++---------------- src/walletcmd.h | 9 ++++ 5 files changed, 290 insertions(+), 37 deletions(-) (limited to 'src/selftest.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 8dcb854..89a7ab6 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1391,6 +1391,84 @@ static void DrawAboutDialog() ImGui::End(); } +// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the +// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite +// and records the backend marker (wallet.dat is left untouched, so a restart +// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB" +// just records that choice so the offer does not return; "Decide later" leaves +// no marker and asks again next start. +static bool g_showBackendWizard = false; +static std::string g_wizardStatus; +static bool g_wizardConverted = false; + +static void DrawBackendWizardDialog() +{ + if (!g_showBackendWizard) return; + ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Wallet Storage", &g_showBackendWizard, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + if (g_wizardConverted) + { + ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite."); + ImGui::TextWrapped("%s", g_wizardStatus.c_str()); + ImGui::Spacing(); + ImGui::TextWrapped( + "Restart Bitflash to start using the SQLite wallet. Your Berkeley " + "DB wallet.dat is kept untouched as a fallback."); + ImGui::Spacing(); + if (ImGui::Button("Close", ImVec2(120.0f, 0.0f))) + g_showBackendWizard = false; + } + else + { + ImGui::TextWrapped( + "Bitflash can store your wallet in a new SQLite format: a single " + "file that is easier to back up and does not depend on the " + "database/ directory beside it."); + ImGui::Spacing(); + ImGui::TextWrapped( + "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it " + "is -- nothing is deleted, and you can go back to it at any time. " + "The change takes effect after you restart."); + ImGui::Spacing(); + if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f))) + { + int nCopied = 0; + std::string strError; + if (DoConvertWalletToSQLite(nCopied, strError)) + { + g_wizardStatus = + strprintf("%d record(s) copied to wallet.sqlite.", nCopied); + g_wizardConverted = true; + } + else + g_wizardStatus = "Conversion failed: " + strError; + } + ImGui::SameLine(); + if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f))) + { + WriteWalletBackendMarker("bdb"); + g_showBackendWizard = false; + } + ImGui::SameLine(); + if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f))) + g_showBackendWizard = false; + + if (!g_wizardStatus.empty()) + { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f)); + ImGui::TextWrapped("%s", g_wizardStatus.c_str()); + ImGui::PopStyleColor(); + } + } + } + ImGui::End(); +} + // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- @@ -1457,6 +1535,17 @@ int RunGUI(int argc, char* argv[]) g_mineRadio = nMineMode; strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); + // First run on a Berkeley DB wallet with no recorded choice yet: offer to + // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists, + // or once a choice has been recorded in the backend marker. + if (!WalletSQLiteRuntimeActive() && + FileExists((GetAppDir() + "/wallet.dat").c_str()) && + !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) && + ReadWalletBackendMarker().empty()) + { + g_showBackendWizard = true; + } + int frame = 0; while (!glfwWindowShouldClose(window) && !fShutdown) { @@ -1480,6 +1569,7 @@ int RunGUI(int argc, char* argv[]) DrawRestorePhraseDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); + DrawBackendWizardDialog(); DrawStatusBar(); ImGui::Render(); diff --git a/src/makefile.mingw b/src/makefile.mingw index 06c9211..55f186b 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -114,6 +114,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=wallet-sqlite-encrypt ./bitflash.exe -selftest=wallet-backend-default + ./bitflash.exe -selftest=wallet-convert ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum diff --git a/src/selftest.cpp b/src/selftest.cpp index 1960003..a9c73fe 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -2664,6 +2664,110 @@ static int RunWalletBackendDefaultSelfTest() return nFail == 0 ? 0 : 1; } +// The wizard's conversion action, headless: build a Berkeley DB wallet, run +// DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a +// usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then +// that a flagless restart opens the converted SQLite wallet. +static int RunWalletConvertSelfTest() +{ + fflush(stdout); + printf("wallet-convert self-test\n"); + + std::string tmp, cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + string strError; + nFail += Check(SetHDSeedFromMnemonic( + "abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon about", strError), + "a phrase can be installed before conversion") ? 0 : 1; + TopUpKeyPool(); + + vector vchDefaultPubKey = keyUser.GetPubKey(); + CPrivKey vchDefaultPrivKey; + nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), + "the default private key is readable before conversion") ? 0 : 1; + vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), + vchDefaultPrivKey.end()); + + // The conversion action the GUI button invokes. + int nCopied = 0; + string strConvertError; + bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError); + nFail += Check(fConverted && nCopied > 0, + "DoConvertWalletToSQLite exports the wallet") ? 0 : 1; + nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) && + FileContainsText(tmp + "/wallet-backend", "sqlite"), + "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1; + nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes), + "wallet.dat is left untouched by the conversion") ? 0 : 1; + + // Idempotence guard: a second conversion refuses rather than overwrite. + int nCopied2 = 0; + string strSecondError; + nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError), + "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1; + + DBFlush(true); + + // A flagless restart now opens the converted SQLite wallet (the marker + // routes it) and finds the same recovery phrase. + string strAudit = tmp + "/converted-recovery-audit.txt"; + vector vAuditArgs; + vAuditArgs.push_back("-datadir=" + tmp); + vAuditArgs.push_back("-nomanagedtor"); + vAuditArgs.push_back("-nogui"); + vAuditArgs.push_back("-recoveryaudit"); + int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); + nFail += Check(nAuditRet == 0 && + FileContainsText(strAudit, "recovery phrase: present"), + "a flagless restart opens the converted SQLite wallet") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static int RunWalletPortabilitySelfTest() { fflush(stdout); @@ -3398,6 +3502,8 @@ int RunSelfTest(const std::string& name) return RunWalletSQLiteEncryptSelfTest(); if (name == "wallet-backend-default") return RunWalletBackendDefaultSelfTest(); + if (name == "wallet-convert") + return RunWalletConvertSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") @@ -3414,6 +3520,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-convert, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 374b2cc..1f1fd27 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -1070,79 +1070,126 @@ public: } // namespace -int CmdWalletSQLiteExport(const std::string& strDest) +// Export the currently-loaded wallet to a SQLite file. Shared by the +// -walletsqliteexport command and the GUI conversion wizard: opens the +// destination, copies every record inside one transaction, checkpoints, and +// verifies the stored count matches. Returns false with strErrorRet set on any +// failure and leaves no partial file behind; the source wallet is only read. +bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, + std::string& strErrorRet) { - AttachTerminal(); - + nCopiedRet = 0; if (strDest.empty()) { - fprintf(stderr, "Missing SQLite wallet export path.\n"); - return 1; + strErrorRet = "no SQLite export path was given"; + return false; } if (FileExists(strDest.c_str())) { - fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", - strDest.c_str()); - return 1; + strErrorRet = strprintf("refusing to overwrite existing file: %s", strDest.c_str()); + return false; } - string strError; CWalletDBSQLite db; - if (!db.Open(strDest, strError)) + if (!db.Open(strDest, strErrorRet)) { RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.BeginTransaction(strError)) + if (!db.BeginTransaction(strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - int nCopied = 0; - CWalletSQLiteExportVisitor visitor(db, nCopied); - if (!ScanWalletRecords(visitor, strError)) + CWalletSQLiteExportVisitor visitor(db, nCopiedRet); + if (!ScanWalletRecords(visitor, strErrorRet)) { string strRollbackError; db.RollbackTransaction(strRollbackError); db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.CommitTransaction(strError)) + if (!db.CommitTransaction(strErrorRet) || !db.Checkpoint(strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.Checkpoint(strError)) + + int nCount = 0; + if (!db.CountRecords(nCount, strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - - int nCount = 0; - if (!db.CountRecords(nCount, strError)) + if (nCount != nCopiedRet) { + strErrorRet = strprintf("export count mismatch: copied %d, stored %d", + nCopiedRet, nCount); db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", - strError.c_str()); + return false; + } + db.Close(); + return true; +} + +// Convert the running Berkeley DB wallet to SQLite for the GUI wizard: export +// wallet.dat to /wallet.sqlite, then record the backend marker so the +// next start opens SQLite. wallet.dat is never modified -- it stays as the +// fallback. Refuses if already on SQLite or if a wallet.sqlite already exists. +bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet) +{ + nCopiedRet = 0; + if (WalletSQLiteRuntimeActive()) + { + strErrorRet = "this wallet is already on the SQLite backend"; + return false; + } + string strDest = GetAppDir() + "/wallet.sqlite"; + if (FileExists(strDest.c_str())) + { + strErrorRet = strprintf("a SQLite wallet already exists at %s", strDest.c_str()); + return false; + } + if (!ExportActiveWalletToSQLite(strDest, nCopiedRet, strErrorRet)) + return false; + if (!WriteWalletBackendMarker("sqlite")) + { + strErrorRet = "the wallet was exported but the backend choice could not be saved"; + return false; + } + return true; +} + +int CmdWalletSQLiteExport(const std::string& strDest) +{ + AttachTerminal(); + + // Keep the command's own specific messages for the two conditions the + // shared helper folds into a generic error, so scripts and the tests that + // read this output see the same wording as before. + if (strDest.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); return 1; } - if (nCount != nCopied) + if (FileExists(strDest.c_str())) { - db.Close(); - RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", - nCopied, nCount); + fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", + strDest.c_str()); + return 1; + } + + int nCopied = 0; + string strError; + if (!ExportActiveWalletToSQLite(strDest, nCopied, strError)) + { + fprintf(stderr, "Cannot export wallet to SQLite: %s\n", strError.c_str()); return 1; } diff --git a/src/walletcmd.h b/src/walletcmd.h index d993856..38cbfdb 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -75,6 +75,15 @@ int CmdWalletStorageCheck(); // not change which backend the runtime wallet opens. int CmdWalletSQLiteExport(const std::string& strDest); +// Export the currently-loaded wallet to a SQLite file (shared by the command +// above and the GUI wizard). Returns false with an error on any failure. +bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, + std::string& strErrorRet); + +// Convert the running Berkeley DB wallet to SQLite and record the backend +// marker so the next start opens it. wallet.dat is left untouched as fallback. +bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet); + // Diagnostic/migration staging: compare a SQLite wallet export with the current // wallet.dat raw records. Prints counts only, never record keys or values. int CmdWalletSQLiteVerify(const std::string& strPath); -- cgit v1.2.3