// 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 #include #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 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()); } static void SetDefaultBackupPath() { std::string path = GetAppDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; 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 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; } // 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) { gPoolServerRunning = true; _beginthread(ThreadRPCServer, 0, NULL); } 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 // --------------------------------------------------------------------------- static void DrawWalletSafetyDialog() { if (!g_showWalletSafety) return; LoadWalletSafetyState(); ImGui::SetNextWindowSize(ImVec2(650.0f, 390.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 (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)) { g_lastWalletBackup = GetTime(); CWalletDB().WriteSetting("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("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 DrawAboutDialog() { if (!g_showAbout) return; ImGui::SetNextWindowSize(ImVec2(380.0f, 170.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 v1.1.0"); 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(); } // --------------------------------------------------------------------------- // 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); 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(); DrawDiagnosticsDialog(); DrawAboutDialog(); 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; }