// 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_needRefresh = true; static char g_sendAddr[128] = {}; static char g_sendAmount[32] = {}; static std::string g_sendStatus; 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 void RefreshWallet() { 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("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); ImGui::EndMenu(); } if (ImGui::MenuItem("Options")) g_showOptions = 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, 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(500.0f, 335.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_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(); } 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(); } // --------------------------------------------------------------------------- // About dialog // --------------------------------------------------------------------------- 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(); 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; }