upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/gui.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/gui.cpp
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'src/gui.cpp')
-rw-r--r--src/gui.cpp275
1 files changed, 263 insertions, 12 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
index 8f1d204..ff6059a 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -11,10 +11,16 @@
11#include <ctime> 11#include <ctime>
12#include <cstdio> 12#include <cstdio>
13#include <sstream> 13#include <sstream>
14#ifdef _WIN32
15#include <shellapi.h> // ShellExecuteA, to open the website in the browser
16#else
17#include <unistd.h>
18#endif
14#include <iomanip> 19#include <iomanip>
15#include <atomic> 20#include <atomic>
16#include <mutex> 21#include <mutex>
17#include <thread> 22#include <thread>
23#include <system_error>
18#include "bip32.h" 24#include "bip32.h"
19#include "walletcmd.h" 25#include "walletcmd.h"
20#include "font_roboto.h" 26#include "font_roboto.h"
@@ -172,7 +178,11 @@ static bool PathIsInsideDataDir(const std::string& strPath)
172 178
173static void SetDefaultBackupPath() 179static void SetDefaultBackupPath()
174{ 180{
175 std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; 181 // Match the extension to the live backend: BackupWallet copies wallet.sqlite
182 // under the SQLite backend and wallet.dat under Berkeley DB, so a ".dat"
183 // default name would put SQLite contents in a misleadingly-named file.
184 const char* pszExt = WalletSQLiteRuntimeActive() ? ".sqlite" : ".dat";
185 std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + pszExt;
176 strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); 186 strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1);
177 g_backupPath[sizeof(g_backupPath)-1] = '\0'; 187 g_backupPath[sizeof(g_backupPath)-1] = '\0';
178} 188}
@@ -910,8 +920,16 @@ static void DrawOptionsDialog()
910 920
911 if (nMineMode == MINE_OPERATOR) { 921 if (nMineMode == MINE_OPERATOR) {
912 if (!gPoolServerRunning) { 922 if (!gPoolServerRunning) {
913 gPoolServerRunning = true; 923 // Claim the server is up only once it is. The flag used to
914 _beginthread(ThreadRPCServer, 0, NULL); 924 // be set first and the result never looked at, so a refused
925 // spawn left an operator with a pool that reports itself
926 // running, no RPC server for miners to reach, and nothing
927 // anywhere saying why. The flag also latches: with it set,
928 // this branch is never retried.
929 if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1)
930 printf("Error: _beginthread(ThreadRPCServer) failed -- pool server not started\n");
931 else
932 gPoolServerRunning = true;
915 } else if (!gPoolRunning) { 933 } else if (!gPoolRunning) {
916 gPoolRunning = true; 934 gPoolRunning = true;
917 } 935 }
@@ -1124,11 +1142,28 @@ static void DrawRestorePhraseDialog()
1124 } 1142 }
1125 g_restoreDerived.store(0); 1143 g_restoreDerived.store(0);
1126 g_restoreRecovered.store(0); 1144 g_restoreRecovered.store(0);
1145 // Set before the spawn, not after: RestoreThread clears this
1146 // flag itself when it finishes, and a short restore can beat
1147 // us back here and have its `false` overwritten, leaving the
1148 // dialog disabled forever.
1127 g_restoreRunning.store(true); 1149 g_restoreRunning.store(true);
1128 // On its own thread: a restore walks the whole chain once per 1150 // On its own thread: a restore walks the whole chain once per
1129 // batch and would otherwise freeze the window for as long as it 1151 // batch and would otherwise freeze the window for as long as it
1130 // takes. 1152 // takes. A refused spawn is one restore that does not happen,
1131 std::thread(RestoreThread, std::string(g_restoreInput)).detach(); 1153 // not a reason to take the window down with an uncaught
1154 // std::system_error -- and the flag has to come back off, or
1155 // the dialog sits greyed out reporting a search nobody is
1156 // running.
1157 try
1158 {
1159 std::thread(RestoreThread, std::string(g_restoreInput)).detach();
1160 }
1161 catch (const std::system_error& e)
1162 {
1163 g_restoreRunning.store(false);
1164 std::lock_guard<std::mutex> lock(g_restoreResultMutex);
1165 g_restoreResult = std::string("Could not start the search: ") + e.what();
1166 }
1132 } 1167 }
1133 } 1168 }
1134 if (fBusy) 1169 if (fBusy)
@@ -1250,15 +1285,15 @@ static void DrawWalletSafetyDialog()
1250 if (g_recoveryAudit.fHaveSeed) 1285 if (g_recoveryAudit.fHaveSeed)
1251 { 1286 {
1252 ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); 1287 ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str());
1253 ImGui::Text("BIP44 coin type: %u (provisional BITFLASH)", g_recoveryAudit.nCoinType); 1288 ImGui::Text("BIP44 coin type: %u (SLIP-0044 BITFLASH)", g_recoveryAudit.nCoinType);
1254 } 1289 }
1255 ImGui::Text("Phrase-backed spendable balance: %s BTF", 1290 ImGui::Text("Phrase-backed spendable balance: %s BTF",
1256 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); 1291 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str());
1257 ImGui::Text("Wallet.dat-only spendable balance: %s BTF", 1292 ImGui::Text("Not phrase-backed spendable balance: %s BTF",
1258 FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); 1293 FmtMoney(g_recoveryAudit.nLegacyCredit).c_str());
1259 ImGui::Text("Phrase-backed immature mining rewards: %s BTF", 1294 ImGui::Text("Phrase-backed immature mining rewards: %s BTF",
1260 FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); 1295 FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str());
1261 ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", 1296 ImGui::Text("Not phrase-backed immature mining rewards: %s BTF",
1262 FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); 1297 FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str());
1263 if (!g_recoveryAudit.fDeriveComplete) 1298 if (!g_recoveryAudit.fDeriveComplete)
1264 { 1299 {
@@ -1272,8 +1307,8 @@ static void DrawWalletSafetyDialog()
1272 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); 1307 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f));
1273 ImGui::TextWrapped( 1308 ImGui::TextWrapped(
1274 "Some coins are on keys the phrase does not reproduce. " 1309 "Some coins are on keys the phrase does not reproduce. "
1275 "Keep wallet.dat backups until that balance has been moved to a " 1310 "Keep a file backup of this wallet until that balance has been "
1276 "phrase-backed address."); 1311 "moved to a phrase-backed address.");
1277 ImGui::PopStyleColor(); 1312 ImGui::PopStyleColor();
1278 } 1313 }
1279 else if (HaveHDSeed() && 1314 else if (HaveHDSeed() &&
@@ -1369,16 +1404,38 @@ static void DrawDiagnosticsDialog()
1369 ImGui::End(); 1404 ImGui::End();
1370} 1405}
1371 1406
1407static void OpenURL(const char* pszUrl)
1408{
1409#ifdef _WIN32
1410 ShellExecuteA(NULL, "open", pszUrl, NULL, NULL, SW_SHOWNORMAL);
1411#else
1412 std::string cmd = std::string("xdg-open '") + pszUrl + "' >/dev/null 2>&1 &";
1413 int r = system(cmd.c_str());
1414 (void)r;
1415#endif
1416}
1417
1372static void DrawAboutDialog() 1418static void DrawAboutDialog()
1373{ 1419{
1374 if (!g_showAbout) return; 1420 if (!g_showAbout) return;
1375 ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); 1421 ImGui::SetNextWindowSize(ImVec2(380.0f, 205.0f), ImGuiCond_Always);
1376 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), 1422 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1377 ImGuiCond_Always, ImVec2(0.5f, 0.5f)); 1423 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1378 if (ImGui::Begin("About Bitflash", &g_showAbout, 1424 if (ImGui::Begin("About Bitflash", &g_showAbout,
1379 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) 1425 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1380 { 1426 {
1381 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); 1427 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
1428 "Bitflash BTF v" BITFLASH_VERSION_STRING);
1429 ImGui::Spacing();
1430 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, 1.0f));
1431 ImGui::TextUnformatted("https://bitflash.network");
1432 ImGui::PopStyleColor();
1433 if (ImGui::IsItemHovered())
1434 {
1435 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
1436 if (ImGui::IsMouseClicked(0))
1437 OpenURL("https://bitflash.network");
1438 }
1382 ImGui::Spacing(); 1439 ImGui::Spacing();
1383 ImGui::TextWrapped( 1440 ImGui::TextWrapped(
1384 "CPU-only cryptocurrency. RandomX proof of work. " 1441 "CPU-only cryptocurrency. RandomX proof of work. "
@@ -1391,6 +1448,184 @@ static void DrawAboutDialog()
1391 ImGui::End(); 1448 ImGui::End();
1392} 1449}
1393 1450
1451// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the
1452// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite
1453// and records the backend marker (wallet.dat is left untouched, so a restart
1454// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB"
1455// just records that choice so the offer does not return; "Decide later" leaves
1456// no marker and asks again next start.
1457static bool g_showBackendWizard = false;
1458static std::string g_wizardStatus;
1459static bool g_wizardConverted = false;
1460static int g_restartArgc = 0;
1461static char** g_restartArgv = NULL;
1462
1463#ifdef _WIN32
1464// Quote one argument so CommandLineToArgvW parses it back to exactly this
1465// string: only backslashes that immediately precede a double quote (including
1466// the closing one) are doubled, so ordinary Windows paths are not mangled.
1467static std::string QuoteArgWin(const std::string& a)
1468{
1469 if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos)
1470 return a;
1471 std::string r = "\"";
1472 size_t bs = 0;
1473 for (size_t i = 0; i < a.size(); i++)
1474 {
1475 if (a[i] == '\\') { bs++; continue; }
1476 if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; }
1477 r.append(bs, '\\'); bs = 0; r += a[i];
1478 }
1479 r.append(bs * 2, '\\');
1480 r += "\"";
1481 return r;
1482}
1483#endif
1484
1485// Relaunch Bitflash with the same arguments and datadir, then ask this instance
1486// to exit -- but only once the relaunch has actually started, so a failure never
1487// leaves the user with no node running. The new instance waits (-restartwait)
1488// before it opens the network port and the block index, so this one is gone
1489// first and they never fight over the same files.
1490static void RestartApplication()
1491{
1492 bool fLaunched = false;
1493#ifdef _WIN32
1494 char exePath[MAX_PATH];
1495 if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0)
1496 return;
1497 std::string strCmd = QuoteArgWin(exePath);
1498 bool fHasDataDir = false;
1499 for (int i = 1; i < g_restartArgc; i++)
1500 {
1501 std::string a = g_restartArgv[i] ? g_restartArgv[i] : "";
1502 if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0)
1503 fHasDataDir = true;
1504 strCmd += " " + QuoteArgWin(a);
1505 }
1506 if (!fHasDataDir)
1507 strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir());
1508 strCmd += " -restartwait";
1509
1510 std::vector<char> cmdBuf(strCmd.begin(), strCmd.end());
1511 cmdBuf.push_back('\0');
1512 STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si);
1513 PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi));
1514 if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
1515 {
1516 CloseHandle(pi.hProcess);
1517 CloseHandle(pi.hThread);
1518 fLaunched = true;
1519 }
1520#else
1521 pid_t pid = fork();
1522 if (pid == 0)
1523 {
1524 // Child: wait for the parent to release the port and files, then re-exec.
1525 // /proc/self/exe is the reliable absolute path; argv[0] may be relative
1526 // or a bare name that execv() cannot resolve.
1527 sleep(2);
1528 char exePath[4096];
1529 ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
1530 if (len > 0)
1531 {
1532 exePath[len] = '\0';
1533 execv(exePath, g_restartArgv);
1534 }
1535 if (g_restartArgv)
1536 execv(g_restartArgv[0], g_restartArgv);
1537 _exit(127);
1538 }
1539 fLaunched = (pid > 0);
1540#endif
1541
1542 if (fLaunched)
1543 {
1544 fShutdown = true;
1545 if (glfwGetCurrentContext())
1546 glfwSetWindowShouldClose(glfwGetCurrentContext(), true);
1547 }
1548 else
1549 g_wizardStatus = "Could not relaunch automatically -- please restart "
1550 "Bitflash yourself to switch to the SQLite wallet.";
1551}
1552
1553static void DrawBackendWizardDialog()
1554{
1555 if (!g_showBackendWizard) return;
1556 ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always);
1557 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1558 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1559 if (ImGui::Begin("Wallet Storage", &g_showBackendWizard,
1560 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1561 {
1562 if (g_wizardConverted)
1563 {
1564 ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite.");
1565 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1566 ImGui::Spacing();
1567 ImGui::TextWrapped(
1568 "The switch takes effect when Bitflash restarts. Until then it is "
1569 "still running on the old Berkeley DB wallet. Your wallet.dat is "
1570 "kept untouched as a fallback.");
1571 ImGui::Spacing();
1572 if (ImGui::Button("Restart Now", ImVec2(140.0f, 0.0f)))
1573 RestartApplication();
1574 ImGui::SameLine();
1575 if (ImGui::Button("Later", ImVec2(120.0f, 0.0f)))
1576 g_showBackendWizard = false;
1577 ImGui::Spacing();
1578 ImGui::TextDisabled(
1579 "If you keep mining before restarting, new coins go to the old "
1580 "wallet until you switch.");
1581 }
1582 else
1583 {
1584 ImGui::TextWrapped(
1585 "Bitflash can store your wallet in a new SQLite format: a single "
1586 "file that is easier to back up and does not depend on the "
1587 "database/ directory beside it.");
1588 ImGui::Spacing();
1589 ImGui::TextWrapped(
1590 "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it "
1591 "is -- nothing is deleted, and you can go back to it at any time. "
1592 "The change takes effect after you restart.");
1593 ImGui::Spacing();
1594 if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f)))
1595 {
1596 int nCopied = 0;
1597 std::string strError;
1598 if (DoConvertWalletToSQLite(nCopied, strError))
1599 {
1600 g_wizardStatus =
1601 strprintf("%d record(s) copied to wallet.sqlite.", nCopied);
1602 g_wizardConverted = true;
1603 }
1604 else
1605 g_wizardStatus = "Conversion failed: " + strError;
1606 }
1607 ImGui::SameLine();
1608 if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f)))
1609 {
1610 WriteWalletBackendMarker("bdb");
1611 g_showBackendWizard = false;
1612 }
1613 ImGui::SameLine();
1614 if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f)))
1615 g_showBackendWizard = false;
1616
1617 if (!g_wizardStatus.empty())
1618 {
1619 ImGui::Spacing();
1620 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f));
1621 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1622 ImGui::PopStyleColor();
1623 }
1624 }
1625 }
1626 ImGui::End();
1627}
1628
1394// --------------------------------------------------------------------------- 1629// ---------------------------------------------------------------------------
1395// Entry point 1630// Entry point
1396// --------------------------------------------------------------------------- 1631// ---------------------------------------------------------------------------
@@ -1457,6 +1692,21 @@ int RunGUI(int argc, char* argv[])
1457 g_mineRadio = nMineMode; 1692 g_mineRadio = nMineMode;
1458 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); 1693 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1);
1459 1694
1695 // Kept for the wizard's "Restart Now": relaunch with the same command line.
1696 g_restartArgc = argc;
1697 g_restartArgv = argv;
1698
1699 // First run on a Berkeley DB wallet with no recorded choice yet: offer to
1700 // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists,
1701 // or once a choice has been recorded in the backend marker.
1702 if (!WalletSQLiteRuntimeActive() &&
1703 FileExists((GetAppDir() + "/wallet.dat").c_str()) &&
1704 !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) &&
1705 ReadWalletBackendMarker().empty())
1706 {
1707 g_showBackendWizard = true;
1708 }
1709
1460 int frame = 0; 1710 int frame = 0;
1461 while (!glfwWindowShouldClose(window) && !fShutdown) 1711 while (!glfwWindowShouldClose(window) && !fShutdown)
1462 { 1712 {
@@ -1480,6 +1730,7 @@ int RunGUI(int argc, char* argv[])
1480 DrawRestorePhraseDialog(); 1730 DrawRestorePhraseDialog();
1481 DrawDiagnosticsDialog(); 1731 DrawDiagnosticsDialog();
1482 DrawAboutDialog(); 1732 DrawAboutDialog();
1733 DrawBackendWizardDialog();
1483 DrawStatusBar(); 1734 DrawStatusBar();
1484 1735
1485 ImGui::Render(); 1736 ImGui::Render();