// Bitflash GUI -- ImGui + GLFW + OpenGL3 // Same source on Linux and Windows. No platform ifdefs. #include "imgui/imgui.h" #include "imgui/imgui_impl_glfw.h" #include "imgui/imgui_impl_opengl3.h" #include #include "headers_core.h" #include #include #include #include #include #ifdef _WIN32 #include // ShellExecuteA, to open the website in the browser #else #include #endif #include #include #include #include #include #include "bip32.h" #include "walletcmd.h" #include "font_roboto.h" // --------------------------------------------------------------------------- // Node API // --------------------------------------------------------------------------- extern int64 nTransactionFee; extern int fGenerateBitcoins; extern int nBestHeight; extern int nMineMode; extern std::string strParticipantPool; extern CAddress addrLocalHost; void MainFrameRepaint(); int GetDiscoveredPeerCount(); int GetPeerMedianHeight(); string DateTimeStr(int64 nTime); bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); int64 GetBalance(); string PubKeyToAddress(const std::vector& vchPubKey); bool AddressToHash160(const std::string& str, uint160& hash160Ret); void ThreadBitcoinMiner(void*); int StartMinerThreads(); void ThreadRPCServer(void*); // --------------------------------------------------------------------------- // Layout constants // --------------------------------------------------------------------------- static const float STATUS_BAR_H = 22.0f; // --------------------------------------------------------------------------- // GUI state // --------------------------------------------------------------------------- static std::string g_myAddress; static int64 g_balance = 0; static int64 g_lastWalletRefresh = 0; static bool g_showSend = false; static bool g_showOptions = false; static bool g_showAbout = false; static bool g_showDiagnostics = false; static bool g_showWalletSafety = false; static bool g_needRefresh = true; static char g_sendAddr[128] = {}; static char g_sendAmount[32] = {}; static std::string g_sendStatus; static char g_backupPath[512] = {}; static std::string g_backupStatus; static int64 g_lastWalletBackup = 0; static WalletRecoveryAudit g_recoveryAudit; static int64 g_recoveryAuditTime = 0; static bool g_walletSafetyLoaded = false; static char g_participantPool[256] = {}; static char g_poolName[128] = {}; static char g_poolFee[32] = {}; static char g_poolDash[256] = {}; static int g_mineRadio = 0; struct TxRow { std::string date, desc; int64 amount; int depth; }; static std::vector g_txRows; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static std::string FmtMoney(int64 n) { bool neg = n < 0; if (neg) n = -n; char buf[64]; snprintf(buf, sizeof(buf), "%s%lld.%08lld", neg?"-":"", (long long)(n/COIN), (long long)(n%COIN)); char* dot = strchr(buf, '.'); if (dot) { char* e = buf+strlen(buf)-1; while(e>dot+2&&*e=='0') *e--='\0'; } return buf; } static std::string FmtAge(int64 createdAt) { int64 age = GetTime() - createdAt; if (age < 0) age = 0; if (age < 60) return strprintf("%llds", (long long)age); if (age < 3600) return strprintf("%lldm", (long long)(age / 60)); return strprintf("%lldh", (long long)(age / 3600)); } static ImVec4 StatusColor(const std::string& s) { if (s.find("failed") != std::string::npos || s.find("rejected") != std::string::npos || s.find("stopped") != std::string::npos) return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); // red if (s.find("hashing") != std::string::npos || s.find("accepted") != std::string::npos || s.find("connected")!= std::string::npos) return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); // green if (s.find("asking") != std::string::npos || s.find("authoriz") != std::string::npos || s.find("subscrib") != std::string::npos) return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); // amber return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); // blue-grey } static std::string PoolLabel(const BtfPoolAnnouncement& ann) { std::ostringstream o; o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%"; if (ann.connectedMiners > 0) o << " - " << ann.connectedMiners << " miners"; return o.str(); } static std::string BackupTimeSuffix() { return strprintf("%lld", (long long)GetTime()); } // Where to put a backup by default. // // Not the data directory. A copy that lives in the folder it is meant to // outlive is not a backup: same disk, same directory the README tells people to // copy wholesale, and the same directory they are eventually going to delete -- // which is the moment the backup was for. The home directory is not off-site // either, but it survives the one accident this file exists to survive. static std::string DefaultBackupDir() { #ifdef _WIN32 const char* pszHome = getenv("USERPROFILE"); #else const char* pszHome = getenv("HOME"); #endif if (pszHome && *pszHome) return std::string(pszHome); return GetAppDir(); // nothing better available; the warning below still fires } // True when a path sits inside the data directory, so the dialog can say so. static bool PathIsInsideDataDir(const std::string& strPath) { std::string strDir = GetAppDir(); if (strPath.size() < strDir.size()) return false; std::string a = strPath.substr(0, strDir.size()); // Windows paths are case-insensitive and mix separators; compare loosely. for (size_t i = 0; i < a.size(); i++) { char c1 = a[i], c2 = strDir[i]; if (c1 == '\\') c1 = '/'; if (c2 == '\\') c2 = '/'; if (tolower((unsigned char)c1) != tolower((unsigned char)c2)) return false; } return true; } static void SetDefaultBackupPath() { // Match the extension to the live backend: BackupWallet copies wallet.sqlite // under the SQLite backend and wallet.dat under Berkeley DB, so a ".dat" // default name would put SQLite contents in a misleadingly-named file. const char* pszExt = WalletSQLiteRuntimeActive() ? ".sqlite" : ".dat"; std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + pszExt; strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); g_backupPath[sizeof(g_backupPath)-1] = '\0'; } static void LoadWalletSafetyState() { if (g_walletSafetyLoaded) return; CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); SetDefaultBackupPath(); g_walletSafetyLoaded = true; } static void RefreshRecoveryAudit(bool fForce=false) { int64 nNow = GetTime(); if (!fForce && g_recoveryAuditTime != 0 && nNow - g_recoveryAuditTime < 5) return; g_recoveryAudit = GetWalletRecoveryAudit(); g_recoveryAuditTime = nNow; } static int KeyPoolCount() { int n = 0; CRITICAL_BLOCK(cs_keyPool) n = (int)mapKeyPool.size(); return n; } static void RefreshWallet() { LoadWalletSafetyState(); std::vector vchPubKey; if (CWalletDB("r").ReadDefaultKey(vchPubKey)) g_myAddress = PubKeyToAddress(vchPubKey); g_balance = GetBalance(); g_txRows.clear(); TRY_CRITICAL_BLOCK(cs_mapWallet) { // Sort: pending first, then confirmed by depth ascending (least deep = // most recent), then orphaned coinbases at the very bottom. // // Orphans have to be pushed down explicitly. They are depth 0, exactly // like a genuinely pending transaction, so sorting on depth alone put // every orphan the node had ever produced at the top of the list -- and // they never age out, because depth 0 is where they stay forever. A // miner with a week of ordinary orphaned races opened the wallet and // saw nothing but failures stacked above every block it had actually // won. The balance was right; the list was telling a different story. // // We can't sort on nTimeReceived because a resync re-stamps every // transaction with the same time. Depth is always correct. // // 0 = pending, 1 = confirmed, 2 = orphaned. std::vector,uint256>> vs; for (auto& kv : mapWallet) { int d = kv.second.GetDepthInMainChain(); bool orphaned = kv.second.IsCoinBase() && kv.second.hashBlock != 0 && d == 0; int rank = orphaned ? 2 : (d == 0 ? 0 : 1); vs.push_back({{rank, d}, kv.second.GetHash()}); } std::sort(vs.begin(), vs.end(), [](const std::pair,uint256>& a, const std::pair,uint256>& b) { return a.first < b.first; }); for (auto& sv : vs) { auto mi = mapWallet.find(sv.second); if (mi == mapWallet.end()) continue; CWalletTx& wtx = mi->second; int depth = wtx.GetDepthInMainChain(); int toMature = wtx.IsCoinBase() ? wtx.GetBlocksToMaturity() : 0; // An orphaned coinbase has hashBlock set but depth=0 (block not in main chain). // A genuinely unconfirmed tx has hashBlock==0. bool isOrphaned = wtx.IsCoinBase() && wtx.hashBlock != 0 && depth == 0; int64 net; std::string desc; if (wtx.IsCoinBase()) { if (isOrphaned) { net = 0; desc = "Generated (orphaned -- block not in main chain)"; } else if (toMature > 0) { net = wtx.GetValueOut(); char buf[64]; snprintf(buf, sizeof(buf), "Generated (matures in %d blocks)", toMature); desc = buf; } else { net = wtx.GetCredit() - wtx.GetDebit(); desc = depth < 1 ? "Generated (unconfirmed)" : "Generated"; } } else if (wtx.GetDebit() > 0) { net = wtx.GetCredit() - wtx.GetDebit(); desc = "Sent"; } else { net = wtx.GetCredit() - wtx.GetDebit(); desc = "Received"; } g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); } } g_lastWalletRefresh = GetTime(); } void MainFrameRepaint() { g_needRefresh = true; g_recoveryAuditTime = 0; } // DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is // needed by builds that have no GUI at all. // --------------------------------------------------------------------------- // Status bar -- separate fullscreen-width overlay pinned to bottom edge. // Drawn last so it always sits on top of everything else. // --------------------------------------------------------------------------- static void DrawStatusBar() { ImGuiIO& io = ImGui::GetIO(); ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y - STATUS_BAR_H)); ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, STATUS_BAR_H)); ImGui::SetNextWindowBgAlpha(1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 3.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.13f, 0.13f, 0.13f, 1.0f)); ImGui::Begin("##statusbar", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse| ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus); ImGui::PopStyleVar(3); ImGui::PopStyleColor(); int connected = (int)vNodes.size(); int discovered = GetDiscoveredPeerCount(); if (nMineMode == MINE_PARTICIPANT) { uint64 sent = 0, accepted = 0; double hashRate = 0.0; std::string st = GetParticipantMiningStatus(); GetParticipantMiningStats(sent, accepted, hashRate); if (sent > 0 || accepted > 0 || hashRate > 0.0) ImGui::TextDisabled( "Connected peers: %d Detected peers: %d Height: %d" " Shares: %llu sent / %llu accepted %.2f H/s", connected, discovered, nBestHeight, (unsigned long long)sent, (unsigned long long)accepted, hashRate); else if (!st.empty()) ImGui::TextDisabled( "Connected peers: %d Detected peers: %d Height: %d %s", connected, discovered, nBestHeight, st.c_str()); else ImGui::TextDisabled( "Connected peers: %d Detected peers: %d Height: %d", connected, discovered, nBestHeight); } else if (nMineMode == MINE_OPERATOR) { int miners = 0, blocksFound = 0; uint64 roundShares = 0; double totalHashRate = 0.0; GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); ImGui::TextDisabled( "Connected peers: %d Detected peers: %d Height: %d" " Miners: %d Round shares: %llu Blocks found: %d %.2f H/s", connected, discovered, nBestHeight, miners, (unsigned long long)roundShares, blocksFound, totalHashRate); } else { ImGui::TextDisabled( "Connected peers: %d Detected peers: %d Height: %d", connected, discovered, nBestHeight); } // A node that has stopped receiving looks exactly like one with nothing to // do. Now that peers announce their height, say the difference out loud // instead of leaving the user to guess from a number that stopped moving. int peerHeight = GetPeerMedianHeight(); if (peerHeight >= 0 && nBestHeight < peerHeight - 1) ImGui::TextColored(ImVec4(0.95f, 0.65f, 0.20f, 1.0f), "Behind the network by %d block(s) -- peers report height %d", peerHeight - nBestHeight, peerHeight); ImGui::End(); } // --------------------------------------------------------------------------- // Main window // // Layout: // [menu bar] -- ImGui built-in menu // [header strip] -- address, balance, send button, mine status // [separator] // [telemetry strip] -- mode-specific live stats (never scrolls) // [separator] // [scrollable content child] -- transactions + mode panels // // The scrollable child ends at the top of the status bar so content never // hides behind it. The child is explicitly sized to fill the remaining space. // --------------------------------------------------------------------------- static void DrawMainWindow() { ImGuiIO& io = ImGui::GetIO(); // Full display height minus the status bar at the bottom. const float winH = io.DisplaySize.y - STATUS_BAR_H; ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, winH)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 12.0f)); ImGui::Begin("##main", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoBringToFrontOnFocus); ImGui::SetWindowFontScale(0.92f); ImGui::PopStyleVar(); // ---- Menu bar ---- if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Backup Wallet")) g_showWalletSafety = true; if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); ImGui::EndMenu(); } if (ImGui::MenuItem("Options")) g_showOptions = true; if (ImGui::MenuItem("Wallet Safety")) g_showWalletSafety = true; if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; if (ImGui::MenuItem("About")) g_showAbout = true; ImGui::EndMenuBar(); } // ---- Header strip (never scrolls) ---- ImGui::Spacing(); ImGui::TextDisabled("Address:"); ImGui::SameLine(); ImGui::SetNextItemWidth(-80.0f); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 1.0f)); ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleColor(); ImGui::SameLine(); if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); ImGui::TextDisabled("Balance:"); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f, 1.0f, 0.35f, 1.0f)); ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.40f, 0.15f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.55f, 0.20f, 1.0f)); if (ImGui::Button(" Send Coins ")) g_showSend = true; ImGui::PopStyleColor(2); ImGui::SameLine(0.0f, 8.0f); if (ImGui::Button(" Backup Wallet ")) g_showWalletSafety = true; ImGui::SameLine(0.0f, 20.0f); if (fGenerateBitcoins) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); const char* modeLabel = nMineMode == MINE_OPERATOR ? "Mining (operator)" : nMineMode == MINE_PARTICIPANT ? "Mining (pool)" : nMineMode == MINE_RELAY ? "Relay" : "Mining"; ImGui::Text("* %s", modeLabel); ImGui::PopStyleColor(); if (nMineMode == MINE_PARTICIPANT) { std::string st = GetParticipantMiningStatus(); if (!st.empty()) { ImGui::SameLine(0.0f, 12.0f); ImGui::PushStyleColor(ImGuiCol_Text, StatusColor(st)); ImGui::Text("[%s]", st.c_str()); ImGui::PopStyleColor(); } } } else { ImGui::TextDisabled("Not mining"); } ImGui::Spacing(); ImGui::Separator(); // ---- Telemetry strip (never scrolls) ---- { ImGui::Spacing(); const char* modeName = nMineMode == MINE_OPERATOR ? "Operator" : nMineMode == MINE_PARTICIPANT ? "Participant" : nMineMode == MINE_RELAY ? "Relay" : "Solo"; int64 age = g_lastWalletRefresh > 0 ? GetTime() - g_lastWalletRefresh : 0; ImGui::Text("Mode: %s", modeName); ImGui::SameLine(0.0f, 16.0f); ImGui::TextDisabled("Wallet refreshed %llds ago", (long long)age); if (nMineMode == MINE_PARTICIPANT) { uint64 sent = 0, accepted = 0; double hashRate = 0.0; std::string st = GetParticipantMiningStatus(); GetParticipantMiningStats(sent, accepted, hashRate); if (sent > 0 || accepted > 0 || hashRate > 0.0) { double pct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; ImGui::TextDisabled( "Sent %llu shares, accepted %llu (%.1f%%), %.2f H/s", (unsigned long long)sent, (unsigned long long)accepted, pct, hashRate); } else if (!st.empty()) { ImGui::TextDisabled("Status: %s", st.c_str()); } else { ImGui::TextDisabled("Waiting for pool work..."); } } else if (nMineMode == MINE_OPERATOR) { int miners = 0, blocksFound = 0; uint64 roundShares = 0; double totalHashRate = 0.0; GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); if (miners > 0 || blocksFound > 0 || roundShares > 0) ImGui::TextDisabled( "Authorized miners: %d Round shares: %llu Blocks found: %d %.2f H/s", miners, (unsigned long long)roundShares, blocksFound, totalHashRate); else ImGui::TextDisabled("Pool server running, waiting for miners"); } else if (nMineMode == MINE_RELAY) { ImGui::TextDisabled("Relay mode: syncing, not mining"); } else { ImGui::TextDisabled("Solo mining"); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); } // ---- Scrollable content child ---- // Fills all remaining height in the window (cursor Y to bottom of window). // Content that overflows scrolls; nothing leaks past the window edge. // Size the scrollable child to fill exactly the remaining window height. // GetContentRegionAvail() already accounts for padding/borders. const float availH = ImGui::GetContentRegionAvail().y; ImGui::BeginChild("##scroll", ImVec2(0.0f, availH), false, 0); // ---- Transaction list ---- float txH = io.DisplaySize.y * 0.26f; if (txH < 180.0f) txH = 180.0f; if (txH > 260.0f) txH = 260.0f; if (ImGui::BeginTable("txlist", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, ImVec2(0.0f, txH))) { ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110.0f); ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130.0f); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140.0f); ImGui::TableHeadersRow(); for (auto& row : g_txRows) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); bool isImmatureCoinbase = (row.desc.find("matures in") != std::string::npos); bool isOrphaned = (row.desc.find("orphaned") != std::string::npos); if (isOrphaned) ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Orphaned"); else if (row.depth < 1) ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Unconfirmed"); else if (isImmatureCoinbase) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Immature"); else if (row.depth < 6) ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%d confirm%s", row.depth, row.depth == 1 ? "" : "s"); else ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Confirmed"); ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("%s", row.date.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(row.desc.c_str()); ImGui::TableSetColumnIndex(3); if (row.amount >= 0) ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f), "+%s BTF", FmtMoney(row.amount).c_str()); else ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s BTF", FmtMoney(row.amount).c_str()); } ImGui::EndTable(); } // ---- Mode panels ---- std::vector pools; BtfGetPoolAnnouncements(pools); if (nMineMode == MINE_PARTICIPANT) { ImGui::Spacing(); ImGui::SeparatorText("Participant Mining"); ImGui::TextDisabled("Selected pool:"); ImGui::SameLine(); if (strParticipantPool.empty()) ImGui::TextDisabled("none"); else ImGui::TextWrapped("%s", strParticipantPool.c_str()); const BtfPoolAnnouncement* sel = nullptr; for (size_t i = 0; i < pools.size(); i++) if (pools[i].btfAddress == strParticipantPool) { sel = &pools[i]; break; } if (sel) { ImGui::Text("Pool: %s Fee: %.2f%% Miners: %d Blocks: %d %.2f H/s", sel->poolName.c_str(), sel->feePercent, sel->connectedMiners, sel->blocksFound, sel->hashRate); if (!sel->dashboardUrl.empty()) ImGui::TextDisabled("Dashboard: %s", sel->dashboardUrl.c_str()); } else if (!strParticipantPool.empty()) { ImGui::TextDisabled("No live announcement for this pool yet."); } if (!pools.empty()) { ImGui::Spacing(); ImGui::SeparatorText("Live Pools"); if (ImGui::BeginTable("livepools", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 60.0f); ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 150.0f); ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 50.0f); ImGui::TableHeadersRow(); for (size_t i = 0; i < pools.size(); i++) { const BtfPoolAnnouncement& ann = pools[i]; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(ann.poolName.c_str()); ImGui::TextDisabled("%s", ann.btfAddress.c_str()); ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f%%", ann.feePercent); ImGui::TableSetColumnIndex(2); ImGui::Text("%d miners %d blocks %.2f H/s %s", ann.connectedMiners, ann.blocksFound, ann.hashRate, FmtAge(ann.createdAt).c_str()); ImGui::TableSetColumnIndex(3); if (!ann.dashboardUrl.empty()) ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); else ImGui::TextDisabled("-"); ImGui::TableSetColumnIndex(4); std::string btnId = "Use##" + ann.btfAddress; if (ImGui::SmallButton(btnId.c_str())) { strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); strParticipantPool = ann.btfAddress; CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); g_needRefresh = true; } } ImGui::EndTable(); } } } if (nMineMode == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Operator Overview"); std::string opAddr = BtfLocalAddress(); ImGui::TextDisabled("Pool .btf address:"); ImGui::SameLine(); if (opAddr.empty()) ImGui::TextDisabled("identity not ready"); else ImGui::TextWrapped("%s", opAddr.c_str()); // Pending payouts std::vector owed; GetPendingPayouts(owed); ImGui::Spacing(); ImGui::SeparatorText("Pending Payouts (owed to miners)"); if (owed.empty()) { ImGui::TextDisabled("No pending payouts"); } else if (ImGui::BeginTable("owedpayouts", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableSetupColumn("Blocks left", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); for (const PendingPayoutView& row : owed) { int left = std::max(0, row.matureAtHeight - nBestHeight); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Text("%d", row.matureAtHeight); ImGui::TableSetColumnIndex(1); ImGui::Text("%d", left); ImGui::TableSetColumnIndex(2); ImGui::Text("%d", row.recipients); ImGui::TableSetColumnIndex(3); ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); } ImGui::EndTable(); } // Workers std::vector workers; GetPoolWorkerStats(workers); ImGui::Spacing(); ImGui::SeparatorText("Current Round Workers"); if (workers.empty()) { ImGui::TextDisabled("No active workers this round"); } else if (ImGui::BeginTable("workers", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Payout Address", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Hashrate", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableSetupColumn("Round shares", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableSetupColumn("Last seen", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableHeadersRow(); for (const PoolWorkerStatView& row : workers) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(row.address.empty() ? "unknown" : row.address.c_str()); ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); ImGui::TableSetColumnIndex(2); ImGui::Text("%.2f H/s", row.hashRate); ImGui::TableSetColumnIndex(3); ImGui::Text("%llu", (unsigned long long)row.roundShares); ImGui::TableSetColumnIndex(4); ImGui::TextDisabled("%s ago", FmtAge(row.lastSeen).c_str()); } ImGui::EndTable(); } } ImGui::EndChild(); // ##scroll ImGui::SetWindowFontScale(1.0f); ImGui::End(); // ##main } // --------------------------------------------------------------------------- // Send dialog // --------------------------------------------------------------------------- static void DrawSendDialog() { if (!g_showSend) return; ImGui::SetNextWindowSize(ImVec2(500.0f, 180.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Send Coins", &g_showSend, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::Text("Recipient address:"); ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); ImGui::Text("Amount (BTF):"); ImGui::SetNextItemWidth(180.0f); ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); if (!g_sendStatus.empty()) { ImGui::Spacing(); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", g_sendStatus.c_str()); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("Send", ImVec2(90.0f, 0.0f))) { uint160 h; int64 nv = 0; if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address."; else if (!ParseMoney(g_sendAmount, nv) || nv <= 0) g_sendStatus = "Invalid amount."; else { CScript sc; sc << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; CWalletTx wtx; if (SendMoney(sc, nv, wtx)) { g_showSend = false; g_sendStatus = ""; memset(g_sendAddr, 0, sizeof(g_sendAddr)); memset(g_sendAmount, 0, sizeof(g_sendAmount)); g_needRefresh = true; } else { g_sendStatus = "Transaction failed."; } } } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) { g_showSend = false; g_sendStatus = ""; } } ImGui::End(); } // --------------------------------------------------------------------------- // Options dialog // --------------------------------------------------------------------------- static void DrawOptionsDialog() { if (!g_showOptions) return; static bool wasOpen = false; if (!wasOpen) { g_mineRadio = nMineMode; strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); strncpy(g_poolName, strPoolName.c_str(), sizeof(g_poolName)-1); snprintf(g_poolFee, sizeof(g_poolFee), "%.2f", dPoolFeePercent); strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); } wasOpen = true; ImGui::SetNextWindowSize(ImVec2(540.0f, 385.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Options", &g_showOptions, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { // Transaction fee ImGui::SeparatorText("Transaction Fee"); static char feeStr[32] = {}; if (feeStr[0] == '\0') snprintf(feeStr, sizeof(feeStr), "%s", FmtMoney(nTransactionFee).c_str()); ImGui::SetNextItemWidth(180.0f); ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); // Mining mode ImGui::SeparatorText("Mining Mode"); ImGui::RadioButton("Relay -- just run the node, don't mine", &g_mineRadio, MINE_RELAY); ImGui::RadioButton("Solo -- rewards go to your wallet", &g_mineRadio, MINE_SOLO); ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); if (g_mineRadio != MINE_RELAY && g_lastWalletBackup == 0) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); ImGui::TextWrapped("Back up this wallet before mining. The key pool covers the next %d generated addresses, but only after you save a backup that contains those keys.", KEYPOOL_SIZE); ImGui::PopStyleColor(); if (ImGui::SmallButton("Backup now")) g_showWalletSafety = true; } if (g_mineRadio == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Pool Announcement"); ImGui::Text("Pool name:"); ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); ImGui::Text("Fee %%:"); ImGui::SetNextItemWidth(120.0f); ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); ImGui::Text("Dashboard URL:"); ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); } if (g_mineRadio == MINE_PARTICIPANT) { ImGui::Spacing(); ImGui::SeparatorText("Pool Selection"); ImGui::TextDisabled("Pick from live discovered pools."); std::vector pools; BtfGetPoolAnnouncements(pools); if (!pools.empty()) { std::string current = "Select a live pool"; for (size_t i = 0; i < pools.size(); i++) { if (pools[i].btfAddress == g_participantPool) { current = PoolLabel(pools[i]); break; } } if (ImGui::BeginCombo("Discovered pools##combo", current.c_str())) { for (size_t i = 0; i < pools.size(); i++) { const BtfPoolAnnouncement& ann = pools[i]; bool selected = (ann.btfAddress == g_participantPool); if (ImGui::Selectable(PoolLabel(ann).c_str(), selected)) strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); } ImGui::EndCombo(); } } else { ImGui::TextDisabled("No live pools discovered yet."); } } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) { int64 fee = 0; if (ParseMoney(feeStr, fee)) { nTransactionFee = fee; CWalletDB().WriteSetting("nTransactionFee", nTransactionFee); } bool poolChanged = nMineMode != g_mineRadio || strPoolName != (g_poolName[0] ? g_poolName : "Bitflash Pool") || strPoolDashboardUrl != g_poolDash || dPoolFeePercent != atof(g_poolFee); nMineMode = g_mineRadio; strParticipantPool = g_participantPool; strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; strPoolDashboardUrl = g_poolDash; dPoolFeePercent = atof(g_poolFee); if (nMineMode == MINE_OPERATOR && poolChanged) gAnnounceNow = true; if (nMineMode == MINE_RELAY) { fGenerateBitcoins = 0; } else { fGenerateBitcoins = 1; if (!vfThreadRunning[3]) StartMinerThreads(); } // Remember the choice. Without this the node came back from every // restart as a plain relay, and a machine that had been mining for // days looked identical to one that had never been asked to. CWalletDB().WriteSetting("nMineMode", nMineMode); CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); if (nMineMode == MINE_OPERATOR) { if (!gPoolServerRunning) { // Claim the server is up only once it is. The flag used to // be set first and the result never looked at, so a refused // spawn left an operator with a pool that reports itself // running, no RPC server for miners to reach, and nothing // anywhere saying why. The flag also latches: with it set, // this branch is never retried. if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1) printf("Error: _beginthread(ThreadRPCServer) failed -- pool server not started\n"); else gPoolServerRunning = true; } else if (!gPoolRunning) { gPoolRunning = true; } } else { gPoolRunning = false; } g_showOptions = false; } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) g_showOptions = false; } if (!g_showOptions) wasOpen = false; ImGui::End(); } // --------------------------------------------------------------------------- // Wallet safety dialog // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Recovery phrase // // The phrase is shown once, and the seed is installed only after the user says // they have written it down. Generating it and storing it in the same breath // would hand somebody a wallet whose only backup is twelve words they may have // closed the window on -- which is worse than no phrase at all, because they // would believe they had one. // // Nothing here writes the words to disk or to debug.log. They live in one // std::string that is cleared the moment the dialog closes. // --------------------------------------------------------------------------- static bool g_showCreatePhrase = false; static bool g_showRestorePhrase = false; static std::string g_pendingMnemonic; static bool g_phraseWrittenDown = false; static std::string g_phraseStatus; static char g_restoreInput[600] = {}; static std::string g_restoreStatus; static std::atomic g_restoreRunning(false); static std::atomic g_restoreDerived(0); static std::atomic g_restoreRecovered(0); static std::string g_restoreResult; static std::mutex g_restoreResultMutex; static void RestoreProgress(void*, int nDerived, int nRecovered) { g_restoreDerived.store(nDerived); g_restoreRecovered.store(nRecovered); } static void RestoreThread(std::string strPhrase) { std::string strError; int nRecovered = 0, nDerived = 0; bool fOk = RestoreFromPhrase(strPhrase, 0, RestoreProgress, NULL, strError, nRecovered, nDerived); { std::lock_guard lock(g_restoreResultMutex); if (!fOk) g_restoreResult = "Nothing was changed. " + strError; else if (nRecovered > 0) g_restoreResult = strprintf( "Restored %d transaction(s) across %d derived addresses. " "Restart the node to see the balance.", nRecovered, nDerived); else g_restoreResult = strprintf( "No transactions found for the first %d addresses of that phrase. " "Check the words and their order.", nDerived); } g_restoreRunning.store(false); g_recoveryAuditTime = 0; g_needRefresh = true; } static void DrawCreatePhraseDialog() { if (!g_showCreatePhrase) return; ImGui::SetNextWindowSize(ImVec2(640.0f, 340.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Recovery Phrase", &g_showCreatePhrase, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Write these twelve words down, in order, on paper."); ImGui::TextWrapped( "They are the only thing that can rebuild this wallet if the file is lost. " "Anyone who reads them can spend your coins. They are not stored anywhere, " "and this is the only time they will be shown."); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.10f, 1.0f)); ImGui::BeginChild("##phrase", ImVec2(0.0f, 70.0f), true); ImGui::PushTextWrapPos(0.0f); ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "%s", g_pendingMnemonic.c_str()); ImGui::PopTextWrapPos(); ImGui::EndChild(); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::TextWrapped( "Keys already in this wallet are NOT covered by the phrase -- they existed " "before it did. Keep your file backups as well."); ImGui::Spacing(); ImGui::Checkbox("I have written these words down", &g_phraseWrittenDown); ImGui::Spacing(); // Read the condition once, before the button that changes it. // // Pressing Finish clears g_phraseWrittenDown, so testing it again after // the click asked BeginDisabled and EndDisabled two different questions: // the pair was opened while the checkbox was ticked and closed after the // handler had unticked it. ImGui asserts on the unmatched EndDisabled -- // on the success path, so every phrase created from the window hit it. const bool fFinishDisabled = !g_phraseWrittenDown; if (fFinishDisabled) ImGui::BeginDisabled(); if (ImGui::Button("Finish", ImVec2(120.0f, 0.0f))) { std::string strError; if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) { TopUpKeyPool(); g_recoveryAuditTime = 0; g_phraseStatus = "Recovery phrase created. New addresses come from it."; } else { g_phraseStatus = "Could not install the seed: " + strError; } g_pendingMnemonic.clear(); g_phraseWrittenDown = false; g_showCreatePhrase = false; g_needRefresh = true; } if (fFinishDisabled) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) { // Nothing was installed, so there is nothing to undo. The words go // away with the string. g_pendingMnemonic.clear(); g_phraseWrittenDown = false; g_showCreatePhrase = false; g_phraseStatus = "Cancelled. No recovery phrase was created."; } } ImGui::End(); // Closing with the window X is the same as cancelling. if (!g_showCreatePhrase && !g_pendingMnemonic.empty()) { g_pendingMnemonic.clear(); g_phraseWrittenDown = false; g_phraseStatus = "Cancelled. No recovery phrase was created."; } } static void DrawRestorePhraseDialog() { if (!g_showRestorePhrase) return; ImGui::SetNextWindowSize(ImVec2(640.0f, 330.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Restore From Phrase", &g_showRestorePhrase, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::TextWrapped( "Type the twelve words, in order, separated by spaces. The node will " "derive the addresses they describe and search the chain for their coins."); ImGui::Spacing(); if (HaveHDSeed()) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); ImGui::TextWrapped( "This wallet already has a recovery phrase. Restoring replaces it: " "coins on addresses from the current phrase would no longer come back " "from the words you wrote down for it. Back up wallet.dat first."); ImGui::PopStyleColor(); ImGui::Spacing(); } ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextMultiline("##phrase-in", g_restoreInput, sizeof(g_restoreInput), ImVec2(0.0f, 60.0f)); ImGui::Spacing(); bool fBusy = g_restoreRunning.load(); if (fBusy) ImGui::BeginDisabled(); if (ImGui::Button("Restore", ImVec2(140.0f, 0.0f))) { std::string strWhy; if (!CanScanWalletTransactions(strWhy)) { std::lock_guard lock(g_restoreResultMutex); g_restoreResult = strWhy; } else { { std::lock_guard lock(g_restoreResultMutex); g_restoreResult.clear(); } g_restoreDerived.store(0); g_restoreRecovered.store(0); // Set before the spawn, not after: RestoreThread clears this // flag itself when it finishes, and a short restore can beat // us back here and have its `false` overwritten, leaving the // dialog disabled forever. g_restoreRunning.store(true); // On its own thread: a restore walks the whole chain once per // batch and would otherwise freeze the window for as long as it // takes. A refused spawn is one restore that does not happen, // not a reason to take the window down with an uncaught // std::system_error -- and the flag has to come back off, or // the dialog sits greyed out reporting a search nobody is // running. try { std::thread(RestoreThread, std::string(g_restoreInput)).detach(); } catch (const std::system_error& e) { g_restoreRunning.store(false); std::lock_guard lock(g_restoreResultMutex); g_restoreResult = std::string("Could not start the search: ") + e.what(); } } } if (fBusy) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Close", ImVec2(100.0f, 0.0f)) && !fBusy) g_showRestorePhrase = false; ImGui::Spacing(); if (fBusy) { ImGui::Text("Searching: %d addresses checked, %d transaction(s) so far", g_restoreDerived.load(), g_restoreRecovered.load()); } else { std::lock_guard lock(g_restoreResultMutex); if (!g_restoreResult.empty()) ImGui::TextWrapped("%s", g_restoreResult.c_str()); } } ImGui::End(); } static void DrawWalletSafetyDialog() { if (!g_showWalletSafety) return; LoadWalletSafetyState(); RefreshRecoveryAudit(); ImGui::SetNextWindowSize(ImVec2(650.0f, 450.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::SeparatorText("Backup"); if (g_lastWalletBackup > 0) ImGui::Text("Last GUI backup: %s", DateTimeStr(g_lastWalletBackup).c_str()); else ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Last GUI backup: never"); ImGui::TextDisabled("Data directory:"); ImGui::SameLine(); ImGui::TextWrapped("%s", GetAppDir().c_str()); ImGui::Spacing(); ImGui::Text("Backup file:"); ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##backup-path", g_backupPath, sizeof(g_backupPath)); if (PathIsInsideDataDir(g_backupPath)) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); ImGui::TextWrapped( "This path is inside the data directory. A backup kept there is lost " "with the thing it was protecting -- put it on another disk, or at " "least somewhere you would not delete along with the node's files."); ImGui::PopStyleColor(); } if (ImGui::Button("Use new default name", ImVec2(170.0f, 0.0f))) SetDefaultBackupPath(); ImGui::SameLine(); if (ImGui::Button("Copy data dir", ImVec2(120.0f, 0.0f))) ImGui::SetClipboardText(GetAppDir().c_str()); ImGui::Spacing(); if (ImGui::Button("Write Backup", ImVec2(150.0f, 0.0f))) { std::string path = g_backupPath; if (path.empty()) { g_backupStatus = "Choose a backup file first."; } else if (FileExists(path.c_str())) { g_backupStatus = "Refusing to overwrite an existing file. Use a new name."; } else { TopUpKeyPool(); if (BackupWallet(path)) { // BackupWallet records the time itself, so a backup taken // with /backupwallet counts the same as one taken here. CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); g_backupStatus = "Backup written: " + path; SetDefaultBackupPath(); } else { g_backupStatus = "Backup failed. See debug.log for the detailed error."; } } } if (!g_backupStatus.empty()) { ImVec4 col = g_backupStatus.find("Backup written") == 0 ? ImVec4(0.35f, 1.0f, 0.45f, 1.0f) : ImVec4(1.0f, 0.45f, 0.35f, 1.0f); ImGui::PushStyleColor(ImGuiCol_Text, col); ImGui::TextWrapped("%s", g_backupStatus.c_str()); ImGui::PopStyleColor(); } ImGui::Spacing(); ImGui::SeparatorText("Recovery Phrase"); if (HaveHDSeed()) ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "This wallet has a recovery phrase."); else ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "No recovery phrase. Only a file backup can rebuild this wallet."); if (g_recoveryAudit.fHaveSeed) { ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); ImGui::Text("BIP44 coin type: %u (SLIP-0044 BITFLASH)", g_recoveryAudit.nCoinType); } ImGui::Text("Phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); ImGui::Text("Not phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); ImGui::Text("Phrase-backed immature mining rewards: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); ImGui::Text("Not phrase-backed immature mining rewards: %s BTF", FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); if (!g_recoveryAudit.fDeriveComplete) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); ImGui::TextWrapped("%s", g_recoveryAudit.strDeriveError.c_str()); ImGui::PopStyleColor(); } if (g_recoveryAudit.nLegacyCredit > 0 || g_recoveryAudit.nLegacyImmatureCredit > 0) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); ImGui::TextWrapped( "Some coins are on keys the phrase does not reproduce. " "Keep a file backup of this wallet until that balance has been " "moved to a phrase-backed address."); ImGui::PopStyleColor(); } else if (HaveHDSeed() && g_recoveryAudit.nRecoverableCredit + g_recoveryAudit.nRecoverableImmatureCredit > 0) { ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "All known wallet balance is covered by the phrase."); } if (!HaveHDSeed()) { if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) { std::string strError; if (bitflash::BIP39GenerateMnemonic(g_pendingMnemonic, strError)) { g_phraseWrittenDown = false; g_phraseStatus.clear(); g_showCreatePhrase = true; } else { g_phraseStatus = "Could not generate a phrase: " + strError; } } ImGui::SameLine(); } if (ImGui::Button("Restore from phrase", ImVec2(190.0f, 0.0f))) g_showRestorePhrase = true; if (!g_phraseStatus.empty()) ImGui::TextWrapped("%s", g_phraseStatus.c_str()); ImGui::TextWrapped( "A phrase covers addresses created after it. Keys that already existed " "are not derived from it, so keep the file backups above as well."); ImGui::Spacing(); ImGui::SeparatorText("Key Pool"); int nPool = KeyPoolCount(); ImGui::Text("Pre-generated keys ready: %d / %d", nPool, KEYPOOL_SIZE); ImGui::ProgressBar(KEYPOOL_SIZE > 0 ? (float)nPool / (float)KEYPOOL_SIZE : 0.0f, ImVec2(-1.0f, 0.0f)); ImGui::TextWrapped( "A backup contains the keys that already exist. The key pool keeps " "future mining rewards and receive addresses inside the next backup " "window, but it is not a seed phrase. Back up again after heavy use."); ImGui::Spacing(); ImGui::Separator(); if (ImGui::Button("Close", ImVec2(90.0f, 0.0f))) g_showWalletSafety = false; } ImGui::End(); } // --------------------------------------------------------------------------- // About dialog // --------------------------------------------------------------------------- // What the node knows about itself, in the words it writes to the log. Same // text in both places on purpose: what a user pastes into an issue is then // exactly what a developer has already read a hundred times. static void DrawDiagnosticsDialog() { if (!g_showDiagnostics) return; ImGui::SetNextWindowSize(ImVec2(720.0f, 460.0f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Diagnostics", &g_showDiagnostics, ImGuiWindowFlags_NoCollapse)) { // Rebuilt at most once a second: it walks every peer and takes their // send locks, which is not something to do at frame rate. static std::string strReport; static int64 nLastBuilt = 0; if (GetTime() != nLastBuilt) { nLastBuilt = GetTime(); strReport = GetDiagnosticsText(); } if (ImGui::Button("Copy to clipboard", ImVec2(160.0f, 0.0f))) ImGui::SetClipboardText(strReport.c_str()); ImGui::SameLine(); ImGui::TextDisabled("The same report goes to debug.log every ten minutes."); ImGui::Separator(); ImGui::BeginChild("##diagtext", ImVec2(0.0f, 0.0f), false, ImGuiWindowFlags_HorizontalScrollbar); ImGui::TextUnformatted(strReport.c_str()); ImGui::EndChild(); } ImGui::End(); } static void OpenURL(const char* pszUrl) { #ifdef _WIN32 ShellExecuteA(NULL, "open", pszUrl, NULL, NULL, SW_SHOWNORMAL); #else std::string cmd = std::string("xdg-open '") + pszUrl + "' >/dev/null 2>&1 &"; int r = system(cmd.c_str()); (void)r; #endif } static void DrawAboutDialog() { if (!g_showAbout) return; ImGui::SetNextWindowSize(ImVec2(380.0f, 205.0f), ImGuiCond_Always); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("About Bitflash", &g_showAbout, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v" BITFLASH_VERSION_STRING); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, 1.0f)); ImGui::TextUnformatted("https://bitflash.network"); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) OpenURL("https://bitflash.network"); } ImGui::Spacing(); ImGui::TextWrapped( "CPU-only cryptocurrency. RandomX proof of work. " "Anonymous .btf addressing over Nostr. No premine."); ImGui::Spacing(); ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); ImGui::Spacing(); if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) g_showAbout = false; } 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 int g_restartArgc = 0; static char** g_restartArgv = NULL; #ifdef _WIN32 // Quote one argument so CommandLineToArgvW parses it back to exactly this // string: only backslashes that immediately precede a double quote (including // the closing one) are doubled, so ordinary Windows paths are not mangled. static std::string QuoteArgWin(const std::string& a) { if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos) return a; std::string r = "\""; size_t bs = 0; for (size_t i = 0; i < a.size(); i++) { if (a[i] == '\\') { bs++; continue; } if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; } r.append(bs, '\\'); bs = 0; r += a[i]; } r.append(bs * 2, '\\'); r += "\""; return r; } #endif // Relaunch Bitflash with the same arguments and datadir, then ask this instance // to exit -- but only once the relaunch has actually started, so a failure never // leaves the user with no node running. The new instance waits (-restartwait) // before it opens the network port and the block index, so this one is gone // first and they never fight over the same files. static void RestartApplication() { bool fLaunched = false; #ifdef _WIN32 char exePath[MAX_PATH]; if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0) return; std::string strCmd = QuoteArgWin(exePath); bool fHasDataDir = false; for (int i = 1; i < g_restartArgc; i++) { std::string a = g_restartArgv[i] ? g_restartArgv[i] : ""; if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0) fHasDataDir = true; strCmd += " " + QuoteArgWin(a); } if (!fHasDataDir) strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir()); strCmd += " -restartwait"; std::vector cmdBuf(strCmd.begin(), strCmd.end()); cmdBuf.push_back('\0'); STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); fLaunched = true; } #else pid_t pid = fork(); if (pid == 0) { // Child: wait for the parent to release the port and files, then re-exec. // /proc/self/exe is the reliable absolute path; argv[0] may be relative // or a bare name that execv() cannot resolve. sleep(2); char exePath[4096]; ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); if (len > 0) { exePath[len] = '\0'; execv(exePath, g_restartArgv); } if (g_restartArgv) execv(g_restartArgv[0], g_restartArgv); _exit(127); } fLaunched = (pid > 0); #endif if (fLaunched) { fShutdown = true; if (glfwGetCurrentContext()) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); } else g_wizardStatus = "Could not relaunch automatically -- please restart " "Bitflash yourself to switch to the SQLite wallet."; } 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( "The switch takes effect when Bitflash restarts. Until then it is " "still running on the old Berkeley DB wallet. Your wallet.dat is " "kept untouched as a fallback."); ImGui::Spacing(); if (ImGui::Button("Restart Now", ImVec2(140.0f, 0.0f))) RestartApplication(); ImGui::SameLine(); if (ImGui::Button("Later", ImVec2(120.0f, 0.0f))) g_showBackendWizard = false; ImGui::Spacing(); ImGui::TextDisabled( "If you keep mining before restarting, new coins go to the old " "wallet until you switch."); } 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 // --------------------------------------------------------------------------- int RunGUI(int argc, char* argv[]) { if (!glfwInit()) { printf("GUI ERROR: glfwInit() failed -- no display or GL available. Run headless with -nogui.\n"); return 1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif GLFWwindow* window = glfwCreateWindow(960, 640, "Bitflash", nullptr, nullptr); if (!window) { printf("GUI ERROR: could not create an OpenGL 3.3 window.\n"); printf(" Your GPU/driver may not support OpenGL 3.3 core -- common over Remote Desktop, in VMs, or on old GPUs.\n"); printf(" Fixes: update graphics drivers, use a software OpenGL (Mesa), or run headless with -nogui.\n"); glfwTerminate(); return 1; } glfwMakeContextCurrent(window); glfwSwapInterval(1); IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.IniFilename = nullptr; io.Fonts->AddFontFromMemoryCompressedTTF( RobotoMedium_compressed_data, RobotoMedium_compressed_size, 14.0f); ImGui::StyleColorsDark(); ImGuiStyle& st = ImGui::GetStyle(); st.WindowRounding = st.FrameRounding = st.PopupRounding = 4.0f; st.ItemSpacing = ImVec2(10.0f, 7.0f); st.FramePadding = ImVec2(8.0f, 5.0f); st.WindowPadding = ImVec2(12.0f, 12.0f); // Orange accent st.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f, .45f, .00f, 1.0f); st.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, .60f, .00f, 1.0f); st.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f, .45f, .00f, 1.0f); st.Colors[ImGuiCol_Button] = ImVec4(.25f, .25f, .25f, 1.0f); st.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f, .45f, .00f, 1.0f); st.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f, .30f, .00f, 1.0f); st.Colors[ImGuiCol_Header] = ImVec4(.80f, .45f, .00f, .40f); st.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f, .45f, .20f, .80f); st.Colors[ImGuiCol_Tab] = ImVec4(.20f, .20f, .20f, 1.0f); st.Colors[ImGuiCol_TabHovered] = ImVec4(.80f, .45f, .00f, 1.0f); st.Colors[ImGuiCol_TabActive] = ImVec4(.80f, .45f, .00f, 1.0f); st.Colors[ImGuiCol_SeparatorHovered]= ImVec4(.80f, .45f, .00f, 1.0f); st.Colors[ImGuiCol_FrameBg] = ImVec4(.14f, .14f, .14f, 1.0f); st.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f, .20f, .20f, 1.0f); st.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f, .18f, .18f, 1.0f); st.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f, .11f, .11f, 1.0f); st.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f, .14f, .14f, 1.0f); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); g_mineRadio = nMineMode; strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); // Kept for the wizard's "Restart Now": relaunch with the same command line. g_restartArgc = argc; g_restartArgv = argv; // 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) { glfwPollEvents(); if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; } frame++; ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // Draw order matters: status bar last so it is always on top. DrawMainWindow(); DrawSendDialog(); DrawOptionsDialog(); DrawWalletSafetyDialog(); DrawCreatePhraseDialog(); DrawRestorePhraseDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); DrawBackendWizardDialog(); DrawStatusBar(); ImGui::Render(); int w, h; glfwGetFramebufferSize(window, &w, &h); glViewport(0, 0, w, h); glClearColor(.08f, .08f, .08f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); } fShutdown = true; ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); return 0; }