diff options
| author | bitflash-coin <bookofshamers@outlook.com> | 2026-08-04 06:34:32 -0700 |
|---|---|---|
| committer | bitflash-coin <bookofshamers@outlook.com> | 2026-08-04 06:34:32 -0700 |
| commit | 5ce650382352c1503f40abf08129ada69402af55 (patch) | |
| tree | 6a18198901f1e2c4933f1ac53c9365eec512bf63 /src/gui.cpp | |
| parent | f34113291da7e5e3dc7d2468d4f9c212c05c584b (diff) | |
public commit
Diffstat (limited to 'src/gui.cpp')
| -rw-r--r-- | src/gui.cpp | 1502 |
1 files changed, 1502 insertions, 0 deletions
diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 0000000..8f1d204 --- /dev/null +++ b/src/gui.cpp | |||
| @@ -0,0 +1,1502 @@ | |||
| 1 | // Bitflash GUI -- ImGui + GLFW + OpenGL3 | ||
| 2 | // Same source on Linux and Windows. No platform ifdefs. | ||
| 3 | |||
| 4 | #include "imgui/imgui.h" | ||
| 5 | #include "imgui/imgui_impl_glfw.h" | ||
| 6 | #include "imgui/imgui_impl_opengl3.h" | ||
| 7 | #include <GLFW/glfw3.h> | ||
| 8 | #include "headers_core.h" | ||
| 9 | #include <string> | ||
| 10 | #include <vector> | ||
| 11 | #include <ctime> | ||
| 12 | #include <cstdio> | ||
| 13 | #include <sstream> | ||
| 14 | #include <iomanip> | ||
| 15 | #include <atomic> | ||
| 16 | #include <mutex> | ||
| 17 | #include <thread> | ||
| 18 | #include "bip32.h" | ||
| 19 | #include "walletcmd.h" | ||
| 20 | #include "font_roboto.h" | ||
| 21 | |||
| 22 | // --------------------------------------------------------------------------- | ||
| 23 | // Node API | ||
| 24 | // --------------------------------------------------------------------------- | ||
| 25 | extern int64 nTransactionFee; | ||
| 26 | extern int fGenerateBitcoins; | ||
| 27 | extern int nBestHeight; | ||
| 28 | extern int nMineMode; | ||
| 29 | extern std::string strParticipantPool; | ||
| 30 | extern CAddress addrLocalHost; | ||
| 31 | |||
| 32 | void MainFrameRepaint(); | ||
| 33 | int GetDiscoveredPeerCount(); | ||
| 34 | int GetPeerMedianHeight(); | ||
| 35 | string DateTimeStr(int64 nTime); | ||
| 36 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); | ||
| 37 | int64 GetBalance(); | ||
| 38 | string PubKeyToAddress(const std::vector<unsigned char>& vchPubKey); | ||
| 39 | bool AddressToHash160(const std::string& str, uint160& hash160Ret); | ||
| 40 | void ThreadBitcoinMiner(void*); | ||
| 41 | int StartMinerThreads(); | ||
| 42 | void ThreadRPCServer(void*); | ||
| 43 | |||
| 44 | // --------------------------------------------------------------------------- | ||
| 45 | // Layout constants | ||
| 46 | // --------------------------------------------------------------------------- | ||
| 47 | static const float STATUS_BAR_H = 22.0f; | ||
| 48 | |||
| 49 | // --------------------------------------------------------------------------- | ||
| 50 | // GUI state | ||
| 51 | // --------------------------------------------------------------------------- | ||
| 52 | static std::string g_myAddress; | ||
| 53 | static int64 g_balance = 0; | ||
| 54 | static int64 g_lastWalletRefresh = 0; | ||
| 55 | static bool g_showSend = false; | ||
| 56 | static bool g_showOptions = false; | ||
| 57 | static bool g_showAbout = false; | ||
| 58 | static bool g_showDiagnostics = false; | ||
| 59 | static bool g_showWalletSafety = false; | ||
| 60 | static bool g_needRefresh = true; | ||
| 61 | |||
| 62 | static char g_sendAddr[128] = {}; | ||
| 63 | static char g_sendAmount[32] = {}; | ||
| 64 | static std::string g_sendStatus; | ||
| 65 | static char g_backupPath[512] = {}; | ||
| 66 | static std::string g_backupStatus; | ||
| 67 | static int64 g_lastWalletBackup = 0; | ||
| 68 | static WalletRecoveryAudit g_recoveryAudit; | ||
| 69 | static int64 g_recoveryAuditTime = 0; | ||
| 70 | static bool g_walletSafetyLoaded = false; | ||
| 71 | static char g_participantPool[256] = {}; | ||
| 72 | static char g_poolName[128] = {}; | ||
| 73 | static char g_poolFee[32] = {}; | ||
| 74 | static char g_poolDash[256] = {}; | ||
| 75 | static int g_mineRadio = 0; | ||
| 76 | |||
| 77 | struct TxRow { std::string date, desc; int64 amount; int depth; }; | ||
| 78 | static std::vector<TxRow> g_txRows; | ||
| 79 | |||
| 80 | // --------------------------------------------------------------------------- | ||
| 81 | // Helpers | ||
| 82 | // --------------------------------------------------------------------------- | ||
| 83 | static std::string FmtMoney(int64 n) | ||
| 84 | { | ||
| 85 | bool neg = n < 0; if (neg) n = -n; | ||
| 86 | char buf[64]; | ||
| 87 | snprintf(buf, sizeof(buf), "%s%lld.%08lld", | ||
| 88 | neg?"-":"", (long long)(n/COIN), (long long)(n%COIN)); | ||
| 89 | char* dot = strchr(buf, '.'); | ||
| 90 | if (dot) { char* e = buf+strlen(buf)-1; while(e>dot+2&&*e=='0') *e--='\0'; } | ||
| 91 | return buf; | ||
| 92 | } | ||
| 93 | |||
| 94 | static std::string FmtAge(int64 createdAt) | ||
| 95 | { | ||
| 96 | int64 age = GetTime() - createdAt; | ||
| 97 | if (age < 0) age = 0; | ||
| 98 | if (age < 60) return strprintf("%llds", (long long)age); | ||
| 99 | if (age < 3600) return strprintf("%lldm", (long long)(age / 60)); | ||
| 100 | return strprintf("%lldh", (long long)(age / 3600)); | ||
| 101 | } | ||
| 102 | |||
| 103 | static ImVec4 StatusColor(const std::string& s) | ||
| 104 | { | ||
| 105 | if (s.find("failed") != std::string::npos || | ||
| 106 | s.find("rejected") != std::string::npos || | ||
| 107 | s.find("stopped") != std::string::npos) | ||
| 108 | return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); // red | ||
| 109 | if (s.find("hashing") != std::string::npos || | ||
| 110 | s.find("accepted") != std::string::npos || | ||
| 111 | s.find("connected")!= std::string::npos) | ||
| 112 | return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); // green | ||
| 113 | if (s.find("asking") != std::string::npos || | ||
| 114 | s.find("authoriz") != std::string::npos || | ||
| 115 | s.find("subscrib") != std::string::npos) | ||
| 116 | return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); // amber | ||
| 117 | return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); // blue-grey | ||
| 118 | } | ||
| 119 | |||
| 120 | static std::string PoolLabel(const BtfPoolAnnouncement& ann) | ||
| 121 | { | ||
| 122 | std::ostringstream o; | ||
| 123 | o << ann.poolName << " - " | ||
| 124 | << std::fixed << std::setprecision(2) << ann.feePercent << "%"; | ||
| 125 | if (ann.connectedMiners > 0) | ||
| 126 | o << " - " << ann.connectedMiners << " miners"; | ||
| 127 | return o.str(); | ||
| 128 | } | ||
| 129 | |||
| 130 | static std::string BackupTimeSuffix() | ||
| 131 | { | ||
| 132 | return strprintf("%lld", (long long)GetTime()); | ||
| 133 | } | ||
| 134 | |||
| 135 | // Where to put a backup by default. | ||
| 136 | // | ||
| 137 | // Not the data directory. A copy that lives in the folder it is meant to | ||
| 138 | // outlive is not a backup: same disk, same directory the README tells people to | ||
| 139 | // copy wholesale, and the same directory they are eventually going to delete -- | ||
| 140 | // which is the moment the backup was for. The home directory is not off-site | ||
| 141 | // either, but it survives the one accident this file exists to survive. | ||
| 142 | static std::string DefaultBackupDir() | ||
| 143 | { | ||
| 144 | #ifdef _WIN32 | ||
| 145 | const char* pszHome = getenv("USERPROFILE"); | ||
| 146 | #else | ||
| 147 | const char* pszHome = getenv("HOME"); | ||
| 148 | #endif | ||
| 149 | if (pszHome && *pszHome) | ||
| 150 | return std::string(pszHome); | ||
| 151 | return GetAppDir(); // nothing better available; the warning below still fires | ||
| 152 | } | ||
| 153 | |||
| 154 | // True when a path sits inside the data directory, so the dialog can say so. | ||
| 155 | static bool PathIsInsideDataDir(const std::string& strPath) | ||
| 156 | { | ||
| 157 | std::string strDir = GetAppDir(); | ||
| 158 | if (strPath.size() < strDir.size()) | ||
| 159 | return false; | ||
| 160 | std::string a = strPath.substr(0, strDir.size()); | ||
| 161 | // Windows paths are case-insensitive and mix separators; compare loosely. | ||
| 162 | for (size_t i = 0; i < a.size(); i++) | ||
| 163 | { | ||
| 164 | char c1 = a[i], c2 = strDir[i]; | ||
| 165 | if (c1 == '\\') c1 = '/'; | ||
| 166 | if (c2 == '\\') c2 = '/'; | ||
| 167 | if (tolower((unsigned char)c1) != tolower((unsigned char)c2)) | ||
| 168 | return false; | ||
| 169 | } | ||
| 170 | return true; | ||
| 171 | } | ||
| 172 | |||
| 173 | static void SetDefaultBackupPath() | ||
| 174 | { | ||
| 175 | std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; | ||
| 176 | strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); | ||
| 177 | g_backupPath[sizeof(g_backupPath)-1] = '\0'; | ||
| 178 | } | ||
| 179 | |||
| 180 | static void LoadWalletSafetyState() | ||
| 181 | { | ||
| 182 | if (g_walletSafetyLoaded) | ||
| 183 | return; | ||
| 184 | CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); | ||
| 185 | SetDefaultBackupPath(); | ||
| 186 | g_walletSafetyLoaded = true; | ||
| 187 | } | ||
| 188 | |||
| 189 | static void RefreshRecoveryAudit(bool fForce=false) | ||
| 190 | { | ||
| 191 | int64 nNow = GetTime(); | ||
| 192 | if (!fForce && g_recoveryAuditTime != 0 && nNow - g_recoveryAuditTime < 5) | ||
| 193 | return; | ||
| 194 | g_recoveryAudit = GetWalletRecoveryAudit(); | ||
| 195 | g_recoveryAuditTime = nNow; | ||
| 196 | } | ||
| 197 | |||
| 198 | static int KeyPoolCount() | ||
| 199 | { | ||
| 200 | int n = 0; | ||
| 201 | CRITICAL_BLOCK(cs_keyPool) | ||
| 202 | n = (int)mapKeyPool.size(); | ||
| 203 | return n; | ||
| 204 | } | ||
| 205 | |||
| 206 | static void RefreshWallet() | ||
| 207 | { | ||
| 208 | LoadWalletSafetyState(); | ||
| 209 | std::vector<unsigned char> vchPubKey; | ||
| 210 | if (CWalletDB("r").ReadDefaultKey(vchPubKey)) | ||
| 211 | g_myAddress = PubKeyToAddress(vchPubKey); | ||
| 212 | g_balance = GetBalance(); | ||
| 213 | |||
| 214 | g_txRows.clear(); | ||
| 215 | TRY_CRITICAL_BLOCK(cs_mapWallet) | ||
| 216 | { | ||
| 217 | // Sort: pending first, then confirmed by depth ascending (least deep = | ||
| 218 | // most recent), then orphaned coinbases at the very bottom. | ||
| 219 | // | ||
| 220 | // Orphans have to be pushed down explicitly. They are depth 0, exactly | ||
| 221 | // like a genuinely pending transaction, so sorting on depth alone put | ||
| 222 | // every orphan the node had ever produced at the top of the list -- and | ||
| 223 | // they never age out, because depth 0 is where they stay forever. A | ||
| 224 | // miner with a week of ordinary orphaned races opened the wallet and | ||
| 225 | // saw nothing but failures stacked above every block it had actually | ||
| 226 | // won. The balance was right; the list was telling a different story. | ||
| 227 | // | ||
| 228 | // We can't sort on nTimeReceived because a resync re-stamps every | ||
| 229 | // transaction with the same time. Depth is always correct. | ||
| 230 | // | ||
| 231 | // 0 = pending, 1 = confirmed, 2 = orphaned. | ||
| 232 | std::vector<std::pair<std::pair<int,int>,uint256>> vs; | ||
| 233 | for (auto& kv : mapWallet) { | ||
| 234 | int d = kv.second.GetDepthInMainChain(); | ||
| 235 | bool orphaned = kv.second.IsCoinBase() && kv.second.hashBlock != 0 && d == 0; | ||
| 236 | int rank = orphaned ? 2 : (d == 0 ? 0 : 1); | ||
| 237 | vs.push_back({{rank, d}, kv.second.GetHash()}); | ||
| 238 | } | ||
| 239 | std::sort(vs.begin(), vs.end(), | ||
| 240 | [](const std::pair<std::pair<int,int>,uint256>& a, | ||
| 241 | const std::pair<std::pair<int,int>,uint256>& b) { | ||
| 242 | return a.first < b.first; | ||
| 243 | }); | ||
| 244 | for (auto& sv : vs) { | ||
| 245 | auto mi = mapWallet.find(sv.second); | ||
| 246 | if (mi == mapWallet.end()) continue; | ||
| 247 | CWalletTx& wtx = mi->second; | ||
| 248 | int depth = wtx.GetDepthInMainChain(); | ||
| 249 | int toMature = wtx.IsCoinBase() ? wtx.GetBlocksToMaturity() : 0; | ||
| 250 | // An orphaned coinbase has hashBlock set but depth=0 (block not in main chain). | ||
| 251 | // A genuinely unconfirmed tx has hashBlock==0. | ||
| 252 | bool isOrphaned = wtx.IsCoinBase() && wtx.hashBlock != 0 && depth == 0; | ||
| 253 | int64 net; | ||
| 254 | std::string desc; | ||
| 255 | if (wtx.IsCoinBase()) { | ||
| 256 | if (isOrphaned) { | ||
| 257 | net = 0; | ||
| 258 | desc = "Generated (orphaned -- block not in main chain)"; | ||
| 259 | } else if (toMature > 0) { | ||
| 260 | net = wtx.GetValueOut(); | ||
| 261 | char buf[64]; | ||
| 262 | snprintf(buf, sizeof(buf), "Generated (matures in %d blocks)", toMature); | ||
| 263 | desc = buf; | ||
| 264 | } else { | ||
| 265 | net = wtx.GetCredit() - wtx.GetDebit(); | ||
| 266 | desc = depth < 1 ? "Generated (unconfirmed)" : "Generated"; | ||
| 267 | } | ||
| 268 | } else if (wtx.GetDebit() > 0) { | ||
| 269 | net = wtx.GetCredit() - wtx.GetDebit(); | ||
| 270 | desc = "Sent"; | ||
| 271 | } else { | ||
| 272 | net = wtx.GetCredit() - wtx.GetDebit(); | ||
| 273 | desc = "Received"; | ||
| 274 | } | ||
| 275 | g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); | ||
| 276 | } | ||
| 277 | } | ||
| 278 | g_lastWalletRefresh = GetTime(); | ||
| 279 | } | ||
| 280 | |||
| 281 | void MainFrameRepaint() | ||
| 282 | { | ||
| 283 | g_needRefresh = true; | ||
| 284 | g_recoveryAuditTime = 0; | ||
| 285 | } | ||
| 286 | |||
| 287 | // DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is | ||
| 288 | // needed by builds that have no GUI at all. | ||
| 289 | |||
| 290 | // --------------------------------------------------------------------------- | ||
| 291 | // Status bar -- separate fullscreen-width overlay pinned to bottom edge. | ||
| 292 | // Drawn last so it always sits on top of everything else. | ||
| 293 | // --------------------------------------------------------------------------- | ||
| 294 | static void DrawStatusBar() | ||
| 295 | { | ||
| 296 | ImGuiIO& io = ImGui::GetIO(); | ||
| 297 | ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y - STATUS_BAR_H)); | ||
| 298 | ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, STATUS_BAR_H)); | ||
| 299 | ImGui::SetNextWindowBgAlpha(1.0f); | ||
| 300 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 3.0f)); | ||
| 301 | ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); | ||
| 302 | ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); | ||
| 303 | ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.13f, 0.13f, 0.13f, 1.0f)); | ||
| 304 | ImGui::Begin("##statusbar", nullptr, | ||
| 305 | ImGuiWindowFlags_NoTitleBar | | ||
| 306 | ImGuiWindowFlags_NoResize | | ||
| 307 | ImGuiWindowFlags_NoMove | | ||
| 308 | ImGuiWindowFlags_NoScrollbar | | ||
| 309 | ImGuiWindowFlags_NoScrollWithMouse| | ||
| 310 | ImGuiWindowFlags_NoSavedSettings | | ||
| 311 | ImGuiWindowFlags_NoBringToFrontOnFocus); | ||
| 312 | ImGui::PopStyleVar(3); | ||
| 313 | ImGui::PopStyleColor(); | ||
| 314 | |||
| 315 | int connected = (int)vNodes.size(); | ||
| 316 | int discovered = GetDiscoveredPeerCount(); | ||
| 317 | |||
| 318 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 319 | uint64 sent = 0, accepted = 0; | ||
| 320 | double hashRate = 0.0; | ||
| 321 | std::string st = GetParticipantMiningStatus(); | ||
| 322 | GetParticipantMiningStats(sent, accepted, hashRate); | ||
| 323 | if (sent > 0 || accepted > 0 || hashRate > 0.0) | ||
| 324 | ImGui::TextDisabled( | ||
| 325 | "Connected peers: %d Detected peers: %d Height: %d" | ||
| 326 | " Shares: %llu sent / %llu accepted %.2f H/s", | ||
| 327 | connected, discovered, nBestHeight, | ||
| 328 | (unsigned long long)sent, (unsigned long long)accepted, hashRate); | ||
| 329 | else if (!st.empty()) | ||
| 330 | ImGui::TextDisabled( | ||
| 331 | "Connected peers: %d Detected peers: %d Height: %d %s", | ||
| 332 | connected, discovered, nBestHeight, st.c_str()); | ||
| 333 | else | ||
| 334 | ImGui::TextDisabled( | ||
| 335 | "Connected peers: %d Detected peers: %d Height: %d", | ||
| 336 | connected, discovered, nBestHeight); | ||
| 337 | } else if (nMineMode == MINE_OPERATOR) { | ||
| 338 | int miners = 0, blocksFound = 0; | ||
| 339 | uint64 roundShares = 0; | ||
| 340 | double totalHashRate = 0.0; | ||
| 341 | GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); | ||
| 342 | ImGui::TextDisabled( | ||
| 343 | "Connected peers: %d Detected peers: %d Height: %d" | ||
| 344 | " Miners: %d Round shares: %llu Blocks found: %d %.2f H/s", | ||
| 345 | connected, discovered, nBestHeight, | ||
| 346 | miners, (unsigned long long)roundShares, blocksFound, totalHashRate); | ||
| 347 | } else { | ||
| 348 | ImGui::TextDisabled( | ||
| 349 | "Connected peers: %d Detected peers: %d Height: %d", | ||
| 350 | connected, discovered, nBestHeight); | ||
| 351 | } | ||
| 352 | |||
| 353 | // A node that has stopped receiving looks exactly like one with nothing to | ||
| 354 | // do. Now that peers announce their height, say the difference out loud | ||
| 355 | // instead of leaving the user to guess from a number that stopped moving. | ||
| 356 | int peerHeight = GetPeerMedianHeight(); | ||
| 357 | if (peerHeight >= 0 && nBestHeight < peerHeight - 1) | ||
| 358 | ImGui::TextColored(ImVec4(0.95f, 0.65f, 0.20f, 1.0f), | ||
| 359 | "Behind the network by %d block(s) -- peers report height %d", | ||
| 360 | peerHeight - nBestHeight, peerHeight); | ||
| 361 | |||
| 362 | ImGui::End(); | ||
| 363 | } | ||
| 364 | |||
| 365 | // --------------------------------------------------------------------------- | ||
| 366 | // Main window | ||
| 367 | // | ||
| 368 | // Layout: | ||
| 369 | // [menu bar] -- ImGui built-in menu | ||
| 370 | // [header strip] -- address, balance, send button, mine status | ||
| 371 | // [separator] | ||
| 372 | // [telemetry strip] -- mode-specific live stats (never scrolls) | ||
| 373 | // [separator] | ||
| 374 | // [scrollable content child] -- transactions + mode panels | ||
| 375 | // | ||
| 376 | // The scrollable child ends at the top of the status bar so content never | ||
| 377 | // hides behind it. The child is explicitly sized to fill the remaining space. | ||
| 378 | // --------------------------------------------------------------------------- | ||
| 379 | static void DrawMainWindow() | ||
| 380 | { | ||
| 381 | ImGuiIO& io = ImGui::GetIO(); | ||
| 382 | |||
| 383 | // Full display height minus the status bar at the bottom. | ||
| 384 | const float winH = io.DisplaySize.y - STATUS_BAR_H; | ||
| 385 | |||
| 386 | ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); | ||
| 387 | ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, winH)); | ||
| 388 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 12.0f)); | ||
| 389 | ImGui::Begin("##main", nullptr, | ||
| 390 | ImGuiWindowFlags_NoTitleBar | | ||
| 391 | ImGuiWindowFlags_NoResize | | ||
| 392 | ImGuiWindowFlags_NoMove | | ||
| 393 | ImGuiWindowFlags_NoScrollbar | | ||
| 394 | ImGuiWindowFlags_NoScrollWithMouse | | ||
| 395 | ImGuiWindowFlags_MenuBar | | ||
| 396 | ImGuiWindowFlags_NoBringToFrontOnFocus); | ||
| 397 | ImGui::SetWindowFontScale(0.92f); | ||
| 398 | ImGui::PopStyleVar(); | ||
| 399 | |||
| 400 | // ---- Menu bar ---- | ||
| 401 | if (ImGui::BeginMenuBar()) { | ||
| 402 | if (ImGui::BeginMenu("File")) { | ||
| 403 | if (ImGui::MenuItem("Backup Wallet")) g_showWalletSafety = true; | ||
| 404 | if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); | ||
| 405 | ImGui::EndMenu(); | ||
| 406 | } | ||
| 407 | if (ImGui::MenuItem("Options")) g_showOptions = true; | ||
| 408 | if (ImGui::MenuItem("Wallet Safety")) g_showWalletSafety = true; | ||
| 409 | if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; | ||
| 410 | if (ImGui::MenuItem("About")) g_showAbout = true; | ||
| 411 | ImGui::EndMenuBar(); | ||
| 412 | } | ||
| 413 | |||
| 414 | // ---- Header strip (never scrolls) ---- | ||
| 415 | ImGui::Spacing(); | ||
| 416 | ImGui::TextDisabled("Address:"); | ||
| 417 | ImGui::SameLine(); | ||
| 418 | ImGui::SetNextItemWidth(-80.0f); | ||
| 419 | ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 1.0f)); | ||
| 420 | ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, | ||
| 421 | ImGuiInputTextFlags_ReadOnly); | ||
| 422 | ImGui::PopStyleColor(); | ||
| 423 | ImGui::SameLine(); | ||
| 424 | if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); | ||
| 425 | |||
| 426 | ImGui::TextDisabled("Balance:"); | ||
| 427 | ImGui::SameLine(); | ||
| 428 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f, 1.0f, 0.35f, 1.0f)); | ||
| 429 | ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); | ||
| 430 | ImGui::PopStyleColor(); | ||
| 431 | |||
| 432 | ImGui::Spacing(); | ||
| 433 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.40f, 0.15f, 1.0f)); | ||
| 434 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.55f, 0.20f, 1.0f)); | ||
| 435 | if (ImGui::Button(" Send Coins ")) g_showSend = true; | ||
| 436 | ImGui::PopStyleColor(2); | ||
| 437 | |||
| 438 | ImGui::SameLine(0.0f, 8.0f); | ||
| 439 | if (ImGui::Button(" Backup Wallet ")) g_showWalletSafety = true; | ||
| 440 | |||
| 441 | ImGui::SameLine(0.0f, 20.0f); | ||
| 442 | if (fGenerateBitcoins) { | ||
| 443 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); | ||
| 444 | const char* modeLabel = | ||
| 445 | nMineMode == MINE_OPERATOR ? "Mining (operator)" : | ||
| 446 | nMineMode == MINE_PARTICIPANT ? "Mining (pool)" : | ||
| 447 | nMineMode == MINE_RELAY ? "Relay" : "Mining"; | ||
| 448 | ImGui::Text("* %s", modeLabel); | ||
| 449 | ImGui::PopStyleColor(); | ||
| 450 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 451 | std::string st = GetParticipantMiningStatus(); | ||
| 452 | if (!st.empty()) { | ||
| 453 | ImGui::SameLine(0.0f, 12.0f); | ||
| 454 | ImGui::PushStyleColor(ImGuiCol_Text, StatusColor(st)); | ||
| 455 | ImGui::Text("[%s]", st.c_str()); | ||
| 456 | ImGui::PopStyleColor(); | ||
| 457 | } | ||
| 458 | } | ||
| 459 | } else { | ||
| 460 | ImGui::TextDisabled("Not mining"); | ||
| 461 | } | ||
| 462 | |||
| 463 | ImGui::Spacing(); | ||
| 464 | ImGui::Separator(); | ||
| 465 | |||
| 466 | // ---- Telemetry strip (never scrolls) ---- | ||
| 467 | { | ||
| 468 | ImGui::Spacing(); | ||
| 469 | const char* modeName = | ||
| 470 | nMineMode == MINE_OPERATOR ? "Operator" : | ||
| 471 | nMineMode == MINE_PARTICIPANT ? "Participant" : | ||
| 472 | nMineMode == MINE_RELAY ? "Relay" : "Solo"; | ||
| 473 | int64 age = g_lastWalletRefresh > 0 ? GetTime() - g_lastWalletRefresh : 0; | ||
| 474 | |||
| 475 | ImGui::Text("Mode: %s", modeName); | ||
| 476 | ImGui::SameLine(0.0f, 16.0f); | ||
| 477 | ImGui::TextDisabled("Wallet refreshed %llds ago", (long long)age); | ||
| 478 | |||
| 479 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 480 | uint64 sent = 0, accepted = 0; | ||
| 481 | double hashRate = 0.0; | ||
| 482 | std::string st = GetParticipantMiningStatus(); | ||
| 483 | GetParticipantMiningStats(sent, accepted, hashRate); | ||
| 484 | if (sent > 0 || accepted > 0 || hashRate > 0.0) { | ||
| 485 | double pct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; | ||
| 486 | ImGui::TextDisabled( | ||
| 487 | "Sent %llu shares, accepted %llu (%.1f%%), %.2f H/s", | ||
| 488 | (unsigned long long)sent, (unsigned long long)accepted, pct, hashRate); | ||
| 489 | } else if (!st.empty()) { | ||
| 490 | ImGui::TextDisabled("Status: %s", st.c_str()); | ||
| 491 | } else { | ||
| 492 | ImGui::TextDisabled("Waiting for pool work..."); | ||
| 493 | } | ||
| 494 | } else if (nMineMode == MINE_OPERATOR) { | ||
| 495 | int miners = 0, blocksFound = 0; | ||
| 496 | uint64 roundShares = 0; | ||
| 497 | double totalHashRate = 0.0; | ||
| 498 | GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); | ||
| 499 | if (miners > 0 || blocksFound > 0 || roundShares > 0) | ||
| 500 | ImGui::TextDisabled( | ||
| 501 | "Authorized miners: %d Round shares: %llu Blocks found: %d %.2f H/s", | ||
| 502 | miners, (unsigned long long)roundShares, blocksFound, totalHashRate); | ||
| 503 | else | ||
| 504 | ImGui::TextDisabled("Pool server running, waiting for miners"); | ||
| 505 | } else if (nMineMode == MINE_RELAY) { | ||
| 506 | ImGui::TextDisabled("Relay mode: syncing, not mining"); | ||
| 507 | } else { | ||
| 508 | ImGui::TextDisabled("Solo mining"); | ||
| 509 | } | ||
| 510 | ImGui::Spacing(); | ||
| 511 | ImGui::Separator(); | ||
| 512 | ImGui::Spacing(); | ||
| 513 | } | ||
| 514 | |||
| 515 | // ---- Scrollable content child ---- | ||
| 516 | // Fills all remaining height in the window (cursor Y to bottom of window). | ||
| 517 | // Content that overflows scrolls; nothing leaks past the window edge. | ||
| 518 | // Size the scrollable child to fill exactly the remaining window height. | ||
| 519 | // GetContentRegionAvail() already accounts for padding/borders. | ||
| 520 | const float availH = ImGui::GetContentRegionAvail().y; | ||
| 521 | ImGui::BeginChild("##scroll", ImVec2(0.0f, availH), false, 0); | ||
| 522 | |||
| 523 | // ---- Transaction list ---- | ||
| 524 | float txH = io.DisplaySize.y * 0.26f; | ||
| 525 | if (txH < 180.0f) txH = 180.0f; | ||
| 526 | if (txH > 260.0f) txH = 260.0f; | ||
| 527 | |||
| 528 | if (ImGui::BeginTable("txlist", 4, | ||
| 529 | ImGuiTableFlags_Borders | | ||
| 530 | ImGuiTableFlags_RowBg | | ||
| 531 | ImGuiTableFlags_ScrollY | | ||
| 532 | ImGuiTableFlags_SizingStretchProp, | ||
| 533 | ImVec2(0.0f, txH))) | ||
| 534 | { | ||
| 535 | ImGui::TableSetupScrollFreeze(0, 1); | ||
| 536 | ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110.0f); | ||
| 537 | ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130.0f); | ||
| 538 | ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); | ||
| 539 | ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140.0f); | ||
| 540 | ImGui::TableHeadersRow(); | ||
| 541 | |||
| 542 | for (auto& row : g_txRows) { | ||
| 543 | ImGui::TableNextRow(); | ||
| 544 | ImGui::TableSetColumnIndex(0); | ||
| 545 | bool isImmatureCoinbase = (row.desc.find("matures in") != std::string::npos); | ||
| 546 | bool isOrphaned = (row.desc.find("orphaned") != std::string::npos); | ||
| 547 | if (isOrphaned) | ||
| 548 | ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Orphaned"); | ||
| 549 | else if (row.depth < 1) | ||
| 550 | ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Unconfirmed"); | ||
| 551 | else if (isImmatureCoinbase) | ||
| 552 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Immature"); | ||
| 553 | else if (row.depth < 6) | ||
| 554 | ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%d confirm%s", | ||
| 555 | row.depth, row.depth == 1 ? "" : "s"); | ||
| 556 | else | ||
| 557 | ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Confirmed"); | ||
| 558 | ImGui::TableSetColumnIndex(1); | ||
| 559 | ImGui::TextDisabled("%s", row.date.c_str()); | ||
| 560 | ImGui::TableSetColumnIndex(2); | ||
| 561 | ImGui::TextUnformatted(row.desc.c_str()); | ||
| 562 | ImGui::TableSetColumnIndex(3); | ||
| 563 | if (row.amount >= 0) | ||
| 564 | ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f), | ||
| 565 | "+%s BTF", FmtMoney(row.amount).c_str()); | ||
| 566 | else | ||
| 567 | ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), | ||
| 568 | "%s BTF", FmtMoney(row.amount).c_str()); | ||
| 569 | } | ||
| 570 | ImGui::EndTable(); | ||
| 571 | } | ||
| 572 | |||
| 573 | // ---- Mode panels ---- | ||
| 574 | std::vector<BtfPoolAnnouncement> pools; | ||
| 575 | BtfGetPoolAnnouncements(pools); | ||
| 576 | |||
| 577 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 578 | ImGui::Spacing(); | ||
| 579 | ImGui::SeparatorText("Participant Mining"); | ||
| 580 | |||
| 581 | ImGui::TextDisabled("Selected pool:"); | ||
| 582 | ImGui::SameLine(); | ||
| 583 | if (strParticipantPool.empty()) | ||
| 584 | ImGui::TextDisabled("none"); | ||
| 585 | else | ||
| 586 | ImGui::TextWrapped("%s", strParticipantPool.c_str()); | ||
| 587 | |||
| 588 | const BtfPoolAnnouncement* sel = nullptr; | ||
| 589 | for (size_t i = 0; i < pools.size(); i++) | ||
| 590 | if (pools[i].btfAddress == strParticipantPool) { sel = &pools[i]; break; } | ||
| 591 | |||
| 592 | if (sel) { | ||
| 593 | ImGui::Text("Pool: %s Fee: %.2f%% Miners: %d Blocks: %d %.2f H/s", | ||
| 594 | sel->poolName.c_str(), sel->feePercent, | ||
| 595 | sel->connectedMiners, sel->blocksFound, sel->hashRate); | ||
| 596 | if (!sel->dashboardUrl.empty()) | ||
| 597 | ImGui::TextDisabled("Dashboard: %s", sel->dashboardUrl.c_str()); | ||
| 598 | } else if (!strParticipantPool.empty()) { | ||
| 599 | ImGui::TextDisabled("No live announcement for this pool yet."); | ||
| 600 | } | ||
| 601 | |||
| 602 | if (!pools.empty()) { | ||
| 603 | ImGui::Spacing(); | ||
| 604 | ImGui::SeparatorText("Live Pools"); | ||
| 605 | if (ImGui::BeginTable("livepools", 5, | ||
| 606 | ImGuiTableFlags_Borders | | ||
| 607 | ImGuiTableFlags_RowBg | | ||
| 608 | ImGuiTableFlags_SizingStretchProp)) | ||
| 609 | { | ||
| 610 | ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); | ||
| 611 | ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 60.0f); | ||
| 612 | ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 150.0f); | ||
| 613 | ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); | ||
| 614 | ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 50.0f); | ||
| 615 | ImGui::TableHeadersRow(); | ||
| 616 | for (size_t i = 0; i < pools.size(); i++) { | ||
| 617 | const BtfPoolAnnouncement& ann = pools[i]; | ||
| 618 | ImGui::TableNextRow(); | ||
| 619 | ImGui::TableSetColumnIndex(0); | ||
| 620 | ImGui::TextUnformatted(ann.poolName.c_str()); | ||
| 621 | ImGui::TextDisabled("%s", ann.btfAddress.c_str()); | ||
| 622 | ImGui::TableSetColumnIndex(1); | ||
| 623 | ImGui::Text("%.2f%%", ann.feePercent); | ||
| 624 | ImGui::TableSetColumnIndex(2); | ||
| 625 | ImGui::Text("%d miners %d blocks %.2f H/s %s", | ||
| 626 | ann.connectedMiners, ann.blocksFound, ann.hashRate, | ||
| 627 | FmtAge(ann.createdAt).c_str()); | ||
| 628 | ImGui::TableSetColumnIndex(3); | ||
| 629 | if (!ann.dashboardUrl.empty()) | ||
| 630 | ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); | ||
| 631 | else | ||
| 632 | ImGui::TextDisabled("-"); | ||
| 633 | ImGui::TableSetColumnIndex(4); | ||
| 634 | std::string btnId = "Use##" + ann.btfAddress; | ||
| 635 | if (ImGui::SmallButton(btnId.c_str())) { | ||
| 636 | strncpy(g_participantPool, ann.btfAddress.c_str(), | ||
| 637 | sizeof(g_participantPool)-1); | ||
| 638 | strParticipantPool = ann.btfAddress; | ||
| 639 | CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); | ||
| 640 | g_needRefresh = true; | ||
| 641 | } | ||
| 642 | } | ||
| 643 | ImGui::EndTable(); | ||
| 644 | } | ||
| 645 | } | ||
| 646 | } | ||
| 647 | |||
| 648 | if (nMineMode == MINE_OPERATOR) { | ||
| 649 | ImGui::Spacing(); | ||
| 650 | ImGui::SeparatorText("Operator Overview"); | ||
| 651 | |||
| 652 | std::string opAddr = BtfLocalAddress(); | ||
| 653 | ImGui::TextDisabled("Pool .btf address:"); | ||
| 654 | ImGui::SameLine(); | ||
| 655 | if (opAddr.empty()) | ||
| 656 | ImGui::TextDisabled("identity not ready"); | ||
| 657 | else | ||
| 658 | ImGui::TextWrapped("%s", opAddr.c_str()); | ||
| 659 | |||
| 660 | // Pending payouts | ||
| 661 | std::vector<PendingPayoutView> owed; | ||
| 662 | GetPendingPayouts(owed); | ||
| 663 | ImGui::Spacing(); | ||
| 664 | ImGui::SeparatorText("Pending Payouts (owed to miners)"); | ||
| 665 | if (owed.empty()) { | ||
| 666 | ImGui::TextDisabled("No pending payouts"); | ||
| 667 | } else if (ImGui::BeginTable("owedpayouts", 4, | ||
| 668 | ImGuiTableFlags_Borders | | ||
| 669 | ImGuiTableFlags_RowBg | | ||
| 670 | ImGuiTableFlags_SizingStretchProp)) | ||
| 671 | { | ||
| 672 | ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 90.0f); | ||
| 673 | ImGui::TableSetupColumn("Blocks left", ImGuiTableColumnFlags_WidthFixed, 80.0f); | ||
| 674 | ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 80.0f); | ||
| 675 | ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); | ||
| 676 | ImGui::TableHeadersRow(); | ||
| 677 | for (const PendingPayoutView& row : owed) { | ||
| 678 | int left = std::max(0, row.matureAtHeight - nBestHeight); | ||
| 679 | ImGui::TableNextRow(); | ||
| 680 | ImGui::TableSetColumnIndex(0); ImGui::Text("%d", row.matureAtHeight); | ||
| 681 | ImGui::TableSetColumnIndex(1); ImGui::Text("%d", left); | ||
| 682 | ImGui::TableSetColumnIndex(2); ImGui::Text("%d", row.recipients); | ||
| 683 | ImGui::TableSetColumnIndex(3); ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); | ||
| 684 | } | ||
| 685 | ImGui::EndTable(); | ||
| 686 | } | ||
| 687 | |||
| 688 | // Workers | ||
| 689 | std::vector<PoolWorkerStatView> workers; | ||
| 690 | GetPoolWorkerStats(workers); | ||
| 691 | ImGui::Spacing(); | ||
| 692 | ImGui::SeparatorText("Current Round Workers"); | ||
| 693 | if (workers.empty()) { | ||
| 694 | ImGui::TextDisabled("No active workers this round"); | ||
| 695 | } else if (ImGui::BeginTable("workers", 5, | ||
| 696 | ImGuiTableFlags_Borders | | ||
| 697 | ImGuiTableFlags_RowBg | | ||
| 698 | ImGuiTableFlags_SizingStretchProp)) | ||
| 699 | { | ||
| 700 | ImGui::TableSetupColumn("Payout Address", ImGuiTableColumnFlags_WidthStretch); | ||
| 701 | ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); | ||
| 702 | ImGui::TableSetupColumn("Hashrate", ImGuiTableColumnFlags_WidthFixed, 90.0f); | ||
| 703 | ImGui::TableSetupColumn("Round shares", ImGuiTableColumnFlags_WidthFixed, 90.0f); | ||
| 704 | ImGui::TableSetupColumn("Last seen", ImGuiTableColumnFlags_WidthFixed, 90.0f); | ||
| 705 | ImGui::TableHeadersRow(); | ||
| 706 | for (const PoolWorkerStatView& row : workers) { | ||
| 707 | ImGui::TableNextRow(); | ||
| 708 | ImGui::TableSetColumnIndex(0); | ||
| 709 | ImGui::TextUnformatted(row.address.empty() ? "unknown" : row.address.c_str()); | ||
| 710 | ImGui::TableSetColumnIndex(1); | ||
| 711 | ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); | ||
| 712 | ImGui::TableSetColumnIndex(2); | ||
| 713 | ImGui::Text("%.2f H/s", row.hashRate); | ||
| 714 | ImGui::TableSetColumnIndex(3); | ||
| 715 | ImGui::Text("%llu", (unsigned long long)row.roundShares); | ||
| 716 | ImGui::TableSetColumnIndex(4); | ||
| 717 | ImGui::TextDisabled("%s ago", FmtAge(row.lastSeen).c_str()); | ||
| 718 | } | ||
| 719 | ImGui::EndTable(); | ||
| 720 | } | ||
| 721 | } | ||
| 722 | |||
| 723 | ImGui::EndChild(); // ##scroll | ||
| 724 | ImGui::SetWindowFontScale(1.0f); | ||
| 725 | ImGui::End(); // ##main | ||
| 726 | } | ||
| 727 | |||
| 728 | // --------------------------------------------------------------------------- | ||
| 729 | // Send dialog | ||
| 730 | // --------------------------------------------------------------------------- | ||
| 731 | static void DrawSendDialog() | ||
| 732 | { | ||
| 733 | if (!g_showSend) return; | ||
| 734 | ImGui::SetNextWindowSize(ImVec2(500.0f, 180.0f), ImGuiCond_Always); | ||
| 735 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 736 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 737 | if (ImGui::Begin("Send Coins", &g_showSend, | ||
| 738 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 739 | { | ||
| 740 | ImGui::Text("Recipient address:"); | ||
| 741 | ImGui::SetNextItemWidth(-1.0f); | ||
| 742 | ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); | ||
| 743 | |||
| 744 | ImGui::Text("Amount (BTF):"); | ||
| 745 | ImGui::SetNextItemWidth(180.0f); | ||
| 746 | ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); | ||
| 747 | |||
| 748 | if (!g_sendStatus.empty()) { | ||
| 749 | ImGui::Spacing(); | ||
| 750 | ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), | ||
| 751 | "%s", g_sendStatus.c_str()); | ||
| 752 | } | ||
| 753 | |||
| 754 | ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); | ||
| 755 | if (ImGui::Button("Send", ImVec2(90.0f, 0.0f))) { | ||
| 756 | uint160 h; int64 nv = 0; | ||
| 757 | if (!AddressToHash160(g_sendAddr, h)) | ||
| 758 | g_sendStatus = "Invalid address."; | ||
| 759 | else if (!ParseMoney(g_sendAmount, nv) || nv <= 0) | ||
| 760 | g_sendStatus = "Invalid amount."; | ||
| 761 | else { | ||
| 762 | CScript sc; | ||
| 763 | sc << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; | ||
| 764 | CWalletTx wtx; | ||
| 765 | if (SendMoney(sc, nv, wtx)) { | ||
| 766 | g_showSend = false; | ||
| 767 | g_sendStatus = ""; | ||
| 768 | memset(g_sendAddr, 0, sizeof(g_sendAddr)); | ||
| 769 | memset(g_sendAmount, 0, sizeof(g_sendAmount)); | ||
| 770 | g_needRefresh = true; | ||
| 771 | } else { | ||
| 772 | g_sendStatus = "Transaction failed."; | ||
| 773 | } | ||
| 774 | } | ||
| 775 | } | ||
| 776 | ImGui::SameLine(); | ||
| 777 | if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) { | ||
| 778 | g_showSend = false; | ||
| 779 | g_sendStatus = ""; | ||
| 780 | } | ||
| 781 | } | ||
| 782 | ImGui::End(); | ||
| 783 | } | ||
| 784 | |||
| 785 | // --------------------------------------------------------------------------- | ||
| 786 | // Options dialog | ||
| 787 | // --------------------------------------------------------------------------- | ||
| 788 | static void DrawOptionsDialog() | ||
| 789 | { | ||
| 790 | if (!g_showOptions) return; | ||
| 791 | |||
| 792 | static bool wasOpen = false; | ||
| 793 | if (!wasOpen) { | ||
| 794 | g_mineRadio = nMineMode; | ||
| 795 | strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); | ||
| 796 | strncpy(g_poolName, strPoolName.c_str(), sizeof(g_poolName)-1); | ||
| 797 | snprintf(g_poolFee, sizeof(g_poolFee), "%.2f", dPoolFeePercent); | ||
| 798 | strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); | ||
| 799 | } | ||
| 800 | wasOpen = true; | ||
| 801 | |||
| 802 | ImGui::SetNextWindowSize(ImVec2(540.0f, 385.0f), ImGuiCond_Always); | ||
| 803 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 804 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 805 | if (ImGui::Begin("Options", &g_showOptions, | ||
| 806 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 807 | { | ||
| 808 | // Transaction fee | ||
| 809 | ImGui::SeparatorText("Transaction Fee"); | ||
| 810 | static char feeStr[32] = {}; | ||
| 811 | if (feeStr[0] == '\0') | ||
| 812 | snprintf(feeStr, sizeof(feeStr), "%s", FmtMoney(nTransactionFee).c_str()); | ||
| 813 | ImGui::SetNextItemWidth(180.0f); | ||
| 814 | ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); | ||
| 815 | |||
| 816 | // Mining mode | ||
| 817 | ImGui::SeparatorText("Mining Mode"); | ||
| 818 | ImGui::RadioButton("Relay -- just run the node, don't mine", &g_mineRadio, MINE_RELAY); | ||
| 819 | ImGui::RadioButton("Solo -- rewards go to your wallet", &g_mineRadio, MINE_SOLO); | ||
| 820 | ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); | ||
| 821 | ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); | ||
| 822 | |||
| 823 | if (g_mineRadio != MINE_RELAY && g_lastWalletBackup == 0) { | ||
| 824 | ImGui::Spacing(); | ||
| 825 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 826 | 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); | ||
| 827 | ImGui::PopStyleColor(); | ||
| 828 | if (ImGui::SmallButton("Backup now")) g_showWalletSafety = true; | ||
| 829 | } | ||
| 830 | |||
| 831 | if (g_mineRadio == MINE_OPERATOR) { | ||
| 832 | ImGui::Spacing(); | ||
| 833 | ImGui::SeparatorText("Pool Announcement"); | ||
| 834 | ImGui::Text("Pool name:"); | ||
| 835 | ImGui::SetNextItemWidth(-1.0f); | ||
| 836 | ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); | ||
| 837 | ImGui::Text("Fee %%:"); | ||
| 838 | ImGui::SetNextItemWidth(120.0f); | ||
| 839 | ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); | ||
| 840 | ImGui::Text("Dashboard URL:"); | ||
| 841 | ImGui::SetNextItemWidth(-1.0f); | ||
| 842 | ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); | ||
| 843 | } | ||
| 844 | |||
| 845 | if (g_mineRadio == MINE_PARTICIPANT) { | ||
| 846 | ImGui::Spacing(); | ||
| 847 | ImGui::SeparatorText("Pool Selection"); | ||
| 848 | ImGui::TextDisabled("Pick from live discovered pools."); | ||
| 849 | |||
| 850 | std::vector<BtfPoolAnnouncement> pools; | ||
| 851 | BtfGetPoolAnnouncements(pools); | ||
| 852 | if (!pools.empty()) { | ||
| 853 | std::string current = "Select a live pool"; | ||
| 854 | for (size_t i = 0; i < pools.size(); i++) { | ||
| 855 | if (pools[i].btfAddress == g_participantPool) { | ||
| 856 | current = PoolLabel(pools[i]); | ||
| 857 | break; | ||
| 858 | } | ||
| 859 | } | ||
| 860 | if (ImGui::BeginCombo("Discovered pools##combo", current.c_str())) { | ||
| 861 | for (size_t i = 0; i < pools.size(); i++) { | ||
| 862 | const BtfPoolAnnouncement& ann = pools[i]; | ||
| 863 | bool selected = (ann.btfAddress == g_participantPool); | ||
| 864 | if (ImGui::Selectable(PoolLabel(ann).c_str(), selected)) | ||
| 865 | strncpy(g_participantPool, ann.btfAddress.c_str(), | ||
| 866 | sizeof(g_participantPool)-1); | ||
| 867 | } | ||
| 868 | ImGui::EndCombo(); | ||
| 869 | } | ||
| 870 | } else { | ||
| 871 | ImGui::TextDisabled("No live pools discovered yet."); | ||
| 872 | } | ||
| 873 | } | ||
| 874 | |||
| 875 | ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); | ||
| 876 | if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) { | ||
| 877 | int64 fee = 0; | ||
| 878 | if (ParseMoney(feeStr, fee)) { | ||
| 879 | nTransactionFee = fee; | ||
| 880 | CWalletDB().WriteSetting("nTransactionFee", nTransactionFee); | ||
| 881 | } | ||
| 882 | |||
| 883 | bool poolChanged = | ||
| 884 | nMineMode != g_mineRadio || | ||
| 885 | strPoolName != (g_poolName[0] ? g_poolName : "Bitflash Pool") || | ||
| 886 | strPoolDashboardUrl != g_poolDash || | ||
| 887 | dPoolFeePercent != atof(g_poolFee); | ||
| 888 | |||
| 889 | nMineMode = g_mineRadio; | ||
| 890 | strParticipantPool = g_participantPool; | ||
| 891 | strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; | ||
| 892 | strPoolDashboardUrl = g_poolDash; | ||
| 893 | dPoolFeePercent = atof(g_poolFee); | ||
| 894 | |||
| 895 | if (nMineMode == MINE_OPERATOR && poolChanged) gAnnounceNow = true; | ||
| 896 | |||
| 897 | if (nMineMode == MINE_RELAY) { | ||
| 898 | fGenerateBitcoins = 0; | ||
| 899 | } else { | ||
| 900 | fGenerateBitcoins = 1; | ||
| 901 | if (!vfThreadRunning[3]) StartMinerThreads(); | ||
| 902 | } | ||
| 903 | |||
| 904 | // Remember the choice. Without this the node came back from every | ||
| 905 | // restart as a plain relay, and a machine that had been mining for | ||
| 906 | // days looked identical to one that had never been asked to. | ||
| 907 | CWalletDB().WriteSetting("nMineMode", nMineMode); | ||
| 908 | CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); | ||
| 909 | CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); | ||
| 910 | |||
| 911 | if (nMineMode == MINE_OPERATOR) { | ||
| 912 | if (!gPoolServerRunning) { | ||
| 913 | gPoolServerRunning = true; | ||
| 914 | _beginthread(ThreadRPCServer, 0, NULL); | ||
| 915 | } else if (!gPoolRunning) { | ||
| 916 | gPoolRunning = true; | ||
| 917 | } | ||
| 918 | } else { | ||
| 919 | gPoolRunning = false; | ||
| 920 | } | ||
| 921 | g_showOptions = false; | ||
| 922 | } | ||
| 923 | ImGui::SameLine(); | ||
| 924 | if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) g_showOptions = false; | ||
| 925 | } | ||
| 926 | if (!g_showOptions) wasOpen = false; | ||
| 927 | ImGui::End(); | ||
| 928 | } | ||
| 929 | |||
| 930 | // --------------------------------------------------------------------------- | ||
| 931 | // Wallet safety dialog | ||
| 932 | // --------------------------------------------------------------------------- | ||
| 933 | // --------------------------------------------------------------------------- | ||
| 934 | // Recovery phrase | ||
| 935 | // | ||
| 936 | // The phrase is shown once, and the seed is installed only after the user says | ||
| 937 | // they have written it down. Generating it and storing it in the same breath | ||
| 938 | // would hand somebody a wallet whose only backup is twelve words they may have | ||
| 939 | // closed the window on -- which is worse than no phrase at all, because they | ||
| 940 | // would believe they had one. | ||
| 941 | // | ||
| 942 | // Nothing here writes the words to disk or to debug.log. They live in one | ||
| 943 | // std::string that is cleared the moment the dialog closes. | ||
| 944 | // --------------------------------------------------------------------------- | ||
| 945 | static bool g_showCreatePhrase = false; | ||
| 946 | static bool g_showRestorePhrase = false; | ||
| 947 | static std::string g_pendingMnemonic; | ||
| 948 | static bool g_phraseWrittenDown = false; | ||
| 949 | static std::string g_phraseStatus; | ||
| 950 | |||
| 951 | static char g_restoreInput[600] = {}; | ||
| 952 | static std::string g_restoreStatus; | ||
| 953 | static std::atomic<bool> g_restoreRunning(false); | ||
| 954 | static std::atomic<int> g_restoreDerived(0); | ||
| 955 | static std::atomic<int> g_restoreRecovered(0); | ||
| 956 | static std::string g_restoreResult; | ||
| 957 | static std::mutex g_restoreResultMutex; | ||
| 958 | |||
| 959 | static void RestoreProgress(void*, int nDerived, int nRecovered) | ||
| 960 | { | ||
| 961 | g_restoreDerived.store(nDerived); | ||
| 962 | g_restoreRecovered.store(nRecovered); | ||
| 963 | } | ||
| 964 | |||
| 965 | static void RestoreThread(std::string strPhrase) | ||
| 966 | { | ||
| 967 | std::string strError; | ||
| 968 | int nRecovered = 0, nDerived = 0; | ||
| 969 | bool fOk = RestoreFromPhrase(strPhrase, 0, RestoreProgress, NULL, | ||
| 970 | strError, nRecovered, nDerived); | ||
| 971 | { | ||
| 972 | std::lock_guard<std::mutex> lock(g_restoreResultMutex); | ||
| 973 | if (!fOk) | ||
| 974 | g_restoreResult = "Nothing was changed. " + strError; | ||
| 975 | else if (nRecovered > 0) | ||
| 976 | g_restoreResult = strprintf( | ||
| 977 | "Restored %d transaction(s) across %d derived addresses. " | ||
| 978 | "Restart the node to see the balance.", nRecovered, nDerived); | ||
| 979 | else | ||
| 980 | g_restoreResult = strprintf( | ||
| 981 | "No transactions found for the first %d addresses of that phrase. " | ||
| 982 | "Check the words and their order.", nDerived); | ||
| 983 | } | ||
| 984 | g_restoreRunning.store(false); | ||
| 985 | g_recoveryAuditTime = 0; | ||
| 986 | g_needRefresh = true; | ||
| 987 | } | ||
| 988 | |||
| 989 | static void DrawCreatePhraseDialog() | ||
| 990 | { | ||
| 991 | if (!g_showCreatePhrase) return; | ||
| 992 | |||
| 993 | ImGui::SetNextWindowSize(ImVec2(640.0f, 340.0f), ImGuiCond_Always); | ||
| 994 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 995 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 996 | if (ImGui::Begin("Recovery Phrase", &g_showCreatePhrase, | ||
| 997 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 998 | { | ||
| 999 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), | ||
| 1000 | "Write these twelve words down, in order, on paper."); | ||
| 1001 | ImGui::TextWrapped( | ||
| 1002 | "They are the only thing that can rebuild this wallet if the file is lost. " | ||
| 1003 | "Anyone who reads them can spend your coins. They are not stored anywhere, " | ||
| 1004 | "and this is the only time they will be shown."); | ||
| 1005 | ImGui::Spacing(); | ||
| 1006 | |||
| 1007 | ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.10f, 1.0f)); | ||
| 1008 | ImGui::BeginChild("##phrase", ImVec2(0.0f, 70.0f), true); | ||
| 1009 | ImGui::PushTextWrapPos(0.0f); | ||
| 1010 | ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "%s", g_pendingMnemonic.c_str()); | ||
| 1011 | ImGui::PopTextWrapPos(); | ||
| 1012 | ImGui::EndChild(); | ||
| 1013 | ImGui::PopStyleColor(); | ||
| 1014 | |||
| 1015 | ImGui::Spacing(); | ||
| 1016 | ImGui::TextWrapped( | ||
| 1017 | "Keys already in this wallet are NOT covered by the phrase -- they existed " | ||
| 1018 | "before it did. Keep your file backups as well."); | ||
| 1019 | ImGui::Spacing(); | ||
| 1020 | |||
| 1021 | ImGui::Checkbox("I have written these words down", &g_phraseWrittenDown); | ||
| 1022 | ImGui::Spacing(); | ||
| 1023 | |||
| 1024 | // Read the condition once, before the button that changes it. | ||
| 1025 | // | ||
| 1026 | // Pressing Finish clears g_phraseWrittenDown, so testing it again after | ||
| 1027 | // the click asked BeginDisabled and EndDisabled two different questions: | ||
| 1028 | // the pair was opened while the checkbox was ticked and closed after the | ||
| 1029 | // handler had unticked it. ImGui asserts on the unmatched EndDisabled -- | ||
| 1030 | // on the success path, so every phrase created from the window hit it. | ||
| 1031 | const bool fFinishDisabled = !g_phraseWrittenDown; | ||
| 1032 | if (fFinishDisabled) | ||
| 1033 | ImGui::BeginDisabled(); | ||
| 1034 | if (ImGui::Button("Finish", ImVec2(120.0f, 0.0f))) | ||
| 1035 | { | ||
| 1036 | std::string strError; | ||
| 1037 | if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) | ||
| 1038 | { | ||
| 1039 | TopUpKeyPool(); | ||
| 1040 | g_recoveryAuditTime = 0; | ||
| 1041 | g_phraseStatus = "Recovery phrase created. New addresses come from it."; | ||
| 1042 | } | ||
| 1043 | else | ||
| 1044 | { | ||
| 1045 | g_phraseStatus = "Could not install the seed: " + strError; | ||
| 1046 | } | ||
| 1047 | g_pendingMnemonic.clear(); | ||
| 1048 | g_phraseWrittenDown = false; | ||
| 1049 | g_showCreatePhrase = false; | ||
| 1050 | g_needRefresh = true; | ||
| 1051 | } | ||
| 1052 | if (fFinishDisabled) | ||
| 1053 | ImGui::EndDisabled(); | ||
| 1054 | |||
| 1055 | ImGui::SameLine(); | ||
| 1056 | if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) | ||
| 1057 | { | ||
| 1058 | // Nothing was installed, so there is nothing to undo. The words go | ||
| 1059 | // away with the string. | ||
| 1060 | g_pendingMnemonic.clear(); | ||
| 1061 | g_phraseWrittenDown = false; | ||
| 1062 | g_showCreatePhrase = false; | ||
| 1063 | g_phraseStatus = "Cancelled. No recovery phrase was created."; | ||
| 1064 | } | ||
| 1065 | } | ||
| 1066 | ImGui::End(); | ||
| 1067 | |||
| 1068 | // Closing with the window X is the same as cancelling. | ||
| 1069 | if (!g_showCreatePhrase && !g_pendingMnemonic.empty()) | ||
| 1070 | { | ||
| 1071 | g_pendingMnemonic.clear(); | ||
| 1072 | g_phraseWrittenDown = false; | ||
| 1073 | g_phraseStatus = "Cancelled. No recovery phrase was created."; | ||
| 1074 | } | ||
| 1075 | } | ||
| 1076 | |||
| 1077 | static void DrawRestorePhraseDialog() | ||
| 1078 | { | ||
| 1079 | if (!g_showRestorePhrase) return; | ||
| 1080 | |||
| 1081 | ImGui::SetNextWindowSize(ImVec2(640.0f, 330.0f), ImGuiCond_Always); | ||
| 1082 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 1083 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 1084 | if (ImGui::Begin("Restore From Phrase", &g_showRestorePhrase, | ||
| 1085 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 1086 | { | ||
| 1087 | ImGui::TextWrapped( | ||
| 1088 | "Type the twelve words, in order, separated by spaces. The node will " | ||
| 1089 | "derive the addresses they describe and search the chain for their coins."); | ||
| 1090 | ImGui::Spacing(); | ||
| 1091 | |||
| 1092 | if (HaveHDSeed()) | ||
| 1093 | { | ||
| 1094 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 1095 | ImGui::TextWrapped( | ||
| 1096 | "This wallet already has a recovery phrase. Restoring replaces it: " | ||
| 1097 | "coins on addresses from the current phrase would no longer come back " | ||
| 1098 | "from the words you wrote down for it. Back up wallet.dat first."); | ||
| 1099 | ImGui::PopStyleColor(); | ||
| 1100 | ImGui::Spacing(); | ||
| 1101 | } | ||
| 1102 | |||
| 1103 | ImGui::SetNextItemWidth(-1.0f); | ||
| 1104 | ImGui::InputTextMultiline("##phrase-in", g_restoreInput, sizeof(g_restoreInput), | ||
| 1105 | ImVec2(0.0f, 60.0f)); | ||
| 1106 | |||
| 1107 | ImGui::Spacing(); | ||
| 1108 | bool fBusy = g_restoreRunning.load(); | ||
| 1109 | if (fBusy) | ||
| 1110 | ImGui::BeginDisabled(); | ||
| 1111 | if (ImGui::Button("Restore", ImVec2(140.0f, 0.0f))) | ||
| 1112 | { | ||
| 1113 | std::string strWhy; | ||
| 1114 | if (!CanScanWalletTransactions(strWhy)) | ||
| 1115 | { | ||
| 1116 | std::lock_guard<std::mutex> lock(g_restoreResultMutex); | ||
| 1117 | g_restoreResult = strWhy; | ||
| 1118 | } | ||
| 1119 | else | ||
| 1120 | { | ||
| 1121 | { | ||
| 1122 | std::lock_guard<std::mutex> lock(g_restoreResultMutex); | ||
| 1123 | g_restoreResult.clear(); | ||
| 1124 | } | ||
| 1125 | g_restoreDerived.store(0); | ||
| 1126 | g_restoreRecovered.store(0); | ||
| 1127 | g_restoreRunning.store(true); | ||
| 1128 | // On its own thread: a restore walks the whole chain once per | ||
| 1129 | // batch and would otherwise freeze the window for as long as it | ||
| 1130 | // takes. | ||
| 1131 | std::thread(RestoreThread, std::string(g_restoreInput)).detach(); | ||
| 1132 | } | ||
| 1133 | } | ||
| 1134 | if (fBusy) | ||
| 1135 | ImGui::EndDisabled(); | ||
| 1136 | |||
| 1137 | ImGui::SameLine(); | ||
| 1138 | if (ImGui::Button("Close", ImVec2(100.0f, 0.0f)) && !fBusy) | ||
| 1139 | g_showRestorePhrase = false; | ||
| 1140 | |||
| 1141 | ImGui::Spacing(); | ||
| 1142 | if (fBusy) | ||
| 1143 | { | ||
| 1144 | ImGui::Text("Searching: %d addresses checked, %d transaction(s) so far", | ||
| 1145 | g_restoreDerived.load(), g_restoreRecovered.load()); | ||
| 1146 | } | ||
| 1147 | else | ||
| 1148 | { | ||
| 1149 | std::lock_guard<std::mutex> lock(g_restoreResultMutex); | ||
| 1150 | if (!g_restoreResult.empty()) | ||
| 1151 | ImGui::TextWrapped("%s", g_restoreResult.c_str()); | ||
| 1152 | } | ||
| 1153 | } | ||
| 1154 | ImGui::End(); | ||
| 1155 | } | ||
| 1156 | |||
| 1157 | static void DrawWalletSafetyDialog() | ||
| 1158 | { | ||
| 1159 | if (!g_showWalletSafety) return; | ||
| 1160 | LoadWalletSafetyState(); | ||
| 1161 | RefreshRecoveryAudit(); | ||
| 1162 | |||
| 1163 | ImGui::SetNextWindowSize(ImVec2(650.0f, 450.0f), ImGuiCond_Always); | ||
| 1164 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 1165 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 1166 | if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, | ||
| 1167 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 1168 | { | ||
| 1169 | ImGui::SeparatorText("Backup"); | ||
| 1170 | if (g_lastWalletBackup > 0) | ||
| 1171 | ImGui::Text("Last GUI backup: %s", DateTimeStr(g_lastWalletBackup).c_str()); | ||
| 1172 | else | ||
| 1173 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), | ||
| 1174 | "Last GUI backup: never"); | ||
| 1175 | |||
| 1176 | ImGui::TextDisabled("Data directory:"); | ||
| 1177 | ImGui::SameLine(); | ||
| 1178 | ImGui::TextWrapped("%s", GetAppDir().c_str()); | ||
| 1179 | |||
| 1180 | ImGui::Spacing(); | ||
| 1181 | ImGui::Text("Backup file:"); | ||
| 1182 | ImGui::SetNextItemWidth(-1.0f); | ||
| 1183 | ImGui::InputText("##backup-path", g_backupPath, sizeof(g_backupPath)); | ||
| 1184 | |||
| 1185 | if (PathIsInsideDataDir(g_backupPath)) | ||
| 1186 | { | ||
| 1187 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 1188 | ImGui::TextWrapped( | ||
| 1189 | "This path is inside the data directory. A backup kept there is lost " | ||
| 1190 | "with the thing it was protecting -- put it on another disk, or at " | ||
| 1191 | "least somewhere you would not delete along with the node's files."); | ||
| 1192 | ImGui::PopStyleColor(); | ||
| 1193 | } | ||
| 1194 | |||
| 1195 | if (ImGui::Button("Use new default name", ImVec2(170.0f, 0.0f))) | ||
| 1196 | SetDefaultBackupPath(); | ||
| 1197 | ImGui::SameLine(); | ||
| 1198 | if (ImGui::Button("Copy data dir", ImVec2(120.0f, 0.0f))) | ||
| 1199 | ImGui::SetClipboardText(GetAppDir().c_str()); | ||
| 1200 | |||
| 1201 | ImGui::Spacing(); | ||
| 1202 | if (ImGui::Button("Write Backup", ImVec2(150.0f, 0.0f))) | ||
| 1203 | { | ||
| 1204 | std::string path = g_backupPath; | ||
| 1205 | if (path.empty()) | ||
| 1206 | { | ||
| 1207 | g_backupStatus = "Choose a backup file first."; | ||
| 1208 | } | ||
| 1209 | else if (FileExists(path.c_str())) | ||
| 1210 | { | ||
| 1211 | g_backupStatus = "Refusing to overwrite an existing file. Use a new name."; | ||
| 1212 | } | ||
| 1213 | else | ||
| 1214 | { | ||
| 1215 | TopUpKeyPool(); | ||
| 1216 | if (BackupWallet(path)) | ||
| 1217 | { | ||
| 1218 | // BackupWallet records the time itself, so a backup taken | ||
| 1219 | // with /backupwallet counts the same as one taken here. | ||
| 1220 | CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup); | ||
| 1221 | g_backupStatus = "Backup written: " + path; | ||
| 1222 | SetDefaultBackupPath(); | ||
| 1223 | } | ||
| 1224 | else | ||
| 1225 | { | ||
| 1226 | g_backupStatus = "Backup failed. See debug.log for the detailed error."; | ||
| 1227 | } | ||
| 1228 | } | ||
| 1229 | } | ||
| 1230 | |||
| 1231 | if (!g_backupStatus.empty()) | ||
| 1232 | { | ||
| 1233 | ImVec4 col = g_backupStatus.find("Backup written") == 0 | ||
| 1234 | ? ImVec4(0.35f, 1.0f, 0.45f, 1.0f) | ||
| 1235 | : ImVec4(1.0f, 0.45f, 0.35f, 1.0f); | ||
| 1236 | ImGui::PushStyleColor(ImGuiCol_Text, col); | ||
| 1237 | ImGui::TextWrapped("%s", g_backupStatus.c_str()); | ||
| 1238 | ImGui::PopStyleColor(); | ||
| 1239 | } | ||
| 1240 | |||
| 1241 | ImGui::Spacing(); | ||
| 1242 | ImGui::SeparatorText("Recovery Phrase"); | ||
| 1243 | if (HaveHDSeed()) | ||
| 1244 | ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), | ||
| 1245 | "This wallet has a recovery phrase."); | ||
| 1246 | else | ||
| 1247 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), | ||
| 1248 | "No recovery phrase. Only a file backup can rebuild this wallet."); | ||
| 1249 | |||
| 1250 | if (g_recoveryAudit.fHaveSeed) | ||
| 1251 | { | ||
| 1252 | ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); | ||
| 1253 | ImGui::Text("BIP44 coin type: %u (provisional BITFLASH)", g_recoveryAudit.nCoinType); | ||
| 1254 | } | ||
| 1255 | ImGui::Text("Phrase-backed spendable balance: %s BTF", | ||
| 1256 | FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); | ||
| 1257 | ImGui::Text("Wallet.dat-only spendable balance: %s BTF", | ||
| 1258 | FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); | ||
| 1259 | ImGui::Text("Phrase-backed immature mining rewards: %s BTF", | ||
| 1260 | FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); | ||
| 1261 | ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", | ||
| 1262 | FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); | ||
| 1263 | if (!g_recoveryAudit.fDeriveComplete) | ||
| 1264 | { | ||
| 1265 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 1266 | ImGui::TextWrapped("%s", g_recoveryAudit.strDeriveError.c_str()); | ||
| 1267 | ImGui::PopStyleColor(); | ||
| 1268 | } | ||
| 1269 | if (g_recoveryAudit.nLegacyCredit > 0 || | ||
| 1270 | g_recoveryAudit.nLegacyImmatureCredit > 0) | ||
| 1271 | { | ||
| 1272 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 1273 | ImGui::TextWrapped( | ||
| 1274 | "Some coins are on keys the phrase does not reproduce. " | ||
| 1275 | "Keep wallet.dat backups until that balance has been moved to a " | ||
| 1276 | "phrase-backed address."); | ||
| 1277 | ImGui::PopStyleColor(); | ||
| 1278 | } | ||
| 1279 | else if (HaveHDSeed() && | ||
| 1280 | g_recoveryAudit.nRecoverableCredit + | ||
| 1281 | g_recoveryAudit.nRecoverableImmatureCredit > 0) | ||
| 1282 | { | ||
| 1283 | ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), | ||
| 1284 | "All known wallet balance is covered by the phrase."); | ||
| 1285 | } | ||
| 1286 | |||
| 1287 | if (!HaveHDSeed()) | ||
| 1288 | { | ||
| 1289 | if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) | ||
| 1290 | { | ||
| 1291 | std::string strError; | ||
| 1292 | if (bitflash::BIP39GenerateMnemonic(g_pendingMnemonic, strError)) | ||
| 1293 | { | ||
| 1294 | g_phraseWrittenDown = false; | ||
| 1295 | g_phraseStatus.clear(); | ||
| 1296 | g_showCreatePhrase = true; | ||
| 1297 | } | ||
| 1298 | else | ||
| 1299 | { | ||
| 1300 | g_phraseStatus = "Could not generate a phrase: " + strError; | ||
| 1301 | } | ||
| 1302 | } | ||
| 1303 | ImGui::SameLine(); | ||
| 1304 | } | ||
| 1305 | if (ImGui::Button("Restore from phrase", ImVec2(190.0f, 0.0f))) | ||
| 1306 | g_showRestorePhrase = true; | ||
| 1307 | |||
| 1308 | if (!g_phraseStatus.empty()) | ||
| 1309 | ImGui::TextWrapped("%s", g_phraseStatus.c_str()); | ||
| 1310 | |||
| 1311 | ImGui::TextWrapped( | ||
| 1312 | "A phrase covers addresses created after it. Keys that already existed " | ||
| 1313 | "are not derived from it, so keep the file backups above as well."); | ||
| 1314 | |||
| 1315 | ImGui::Spacing(); | ||
| 1316 | ImGui::SeparatorText("Key Pool"); | ||
| 1317 | int nPool = KeyPoolCount(); | ||
| 1318 | ImGui::Text("Pre-generated keys ready: %d / %d", nPool, KEYPOOL_SIZE); | ||
| 1319 | ImGui::ProgressBar(KEYPOOL_SIZE > 0 ? (float)nPool / (float)KEYPOOL_SIZE : 0.0f, | ||
| 1320 | ImVec2(-1.0f, 0.0f)); | ||
| 1321 | ImGui::TextWrapped( | ||
| 1322 | "A backup contains the keys that already exist. The key pool keeps " | ||
| 1323 | "future mining rewards and receive addresses inside the next backup " | ||
| 1324 | "window, but it is not a seed phrase. Back up again after heavy use."); | ||
| 1325 | |||
| 1326 | ImGui::Spacing(); | ||
| 1327 | ImGui::Separator(); | ||
| 1328 | if (ImGui::Button("Close", ImVec2(90.0f, 0.0f))) | ||
| 1329 | g_showWalletSafety = false; | ||
| 1330 | } | ||
| 1331 | ImGui::End(); | ||
| 1332 | } | ||
| 1333 | |||
| 1334 | // --------------------------------------------------------------------------- | ||
| 1335 | // About dialog | ||
| 1336 | // --------------------------------------------------------------------------- | ||
| 1337 | // What the node knows about itself, in the words it writes to the log. Same | ||
| 1338 | // text in both places on purpose: what a user pastes into an issue is then | ||
| 1339 | // exactly what a developer has already read a hundred times. | ||
| 1340 | static void DrawDiagnosticsDialog() | ||
| 1341 | { | ||
| 1342 | if (!g_showDiagnostics) return; | ||
| 1343 | ImGui::SetNextWindowSize(ImVec2(720.0f, 460.0f), ImGuiCond_FirstUseEver); | ||
| 1344 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 1345 | ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); | ||
| 1346 | if (ImGui::Begin("Diagnostics", &g_showDiagnostics, ImGuiWindowFlags_NoCollapse)) | ||
| 1347 | { | ||
| 1348 | // Rebuilt at most once a second: it walks every peer and takes their | ||
| 1349 | // send locks, which is not something to do at frame rate. | ||
| 1350 | static std::string strReport; | ||
| 1351 | static int64 nLastBuilt = 0; | ||
| 1352 | if (GetTime() != nLastBuilt) | ||
| 1353 | { | ||
| 1354 | nLastBuilt = GetTime(); | ||
| 1355 | strReport = GetDiagnosticsText(); | ||
| 1356 | } | ||
| 1357 | |||
| 1358 | if (ImGui::Button("Copy to clipboard", ImVec2(160.0f, 0.0f))) | ||
| 1359 | ImGui::SetClipboardText(strReport.c_str()); | ||
| 1360 | ImGui::SameLine(); | ||
| 1361 | ImGui::TextDisabled("The same report goes to debug.log every ten minutes."); | ||
| 1362 | ImGui::Separator(); | ||
| 1363 | |||
| 1364 | ImGui::BeginChild("##diagtext", ImVec2(0.0f, 0.0f), false, | ||
| 1365 | ImGuiWindowFlags_HorizontalScrollbar); | ||
| 1366 | ImGui::TextUnformatted(strReport.c_str()); | ||
| 1367 | ImGui::EndChild(); | ||
| 1368 | } | ||
| 1369 | ImGui::End(); | ||
| 1370 | } | ||
| 1371 | |||
| 1372 | static void DrawAboutDialog() | ||
| 1373 | { | ||
| 1374 | if (!g_showAbout) return; | ||
| 1375 | ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); | ||
| 1376 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 1377 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | ||
| 1378 | if (ImGui::Begin("About Bitflash", &g_showAbout, | ||
| 1379 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 1380 | { | ||
| 1381 | ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); | ||
| 1382 | ImGui::Spacing(); | ||
| 1383 | ImGui::TextWrapped( | ||
| 1384 | "CPU-only cryptocurrency. RandomX proof of work. " | ||
| 1385 | "Anonymous .btf addressing over Nostr. No premine."); | ||
| 1386 | ImGui::Spacing(); | ||
| 1387 | ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); | ||
| 1388 | ImGui::Spacing(); | ||
| 1389 | if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) g_showAbout = false; | ||
| 1390 | } | ||
| 1391 | ImGui::End(); | ||
| 1392 | } | ||
| 1393 | |||
| 1394 | // --------------------------------------------------------------------------- | ||
| 1395 | // Entry point | ||
| 1396 | // --------------------------------------------------------------------------- | ||
| 1397 | int RunGUI(int argc, char* argv[]) | ||
| 1398 | { | ||
| 1399 | if (!glfwInit()) { | ||
| 1400 | printf("GUI ERROR: glfwInit() failed -- no display or GL available. Run headless with -nogui.\n"); | ||
| 1401 | return 1; | ||
| 1402 | } | ||
| 1403 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | ||
| 1404 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); | ||
| 1405 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | ||
| 1406 | #ifdef __APPLE__ | ||
| 1407 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | ||
| 1408 | #endif | ||
| 1409 | |||
| 1410 | GLFWwindow* window = glfwCreateWindow(960, 640, "Bitflash", nullptr, nullptr); | ||
| 1411 | if (!window) { | ||
| 1412 | printf("GUI ERROR: could not create an OpenGL 3.3 window.\n"); | ||
| 1413 | printf(" Your GPU/driver may not support OpenGL 3.3 core -- common over Remote Desktop, in VMs, or on old GPUs.\n"); | ||
| 1414 | printf(" Fixes: update graphics drivers, use a software OpenGL (Mesa), or run headless with -nogui.\n"); | ||
| 1415 | glfwTerminate(); | ||
| 1416 | return 1; | ||
| 1417 | } | ||
| 1418 | glfwMakeContextCurrent(window); | ||
| 1419 | glfwSwapInterval(1); | ||
| 1420 | |||
| 1421 | IMGUI_CHECKVERSION(); | ||
| 1422 | ImGui::CreateContext(); | ||
| 1423 | ImGuiIO& io = ImGui::GetIO(); | ||
| 1424 | io.IniFilename = nullptr; | ||
| 1425 | |||
| 1426 | io.Fonts->AddFontFromMemoryCompressedTTF( | ||
| 1427 | RobotoMedium_compressed_data, RobotoMedium_compressed_size, 14.0f); | ||
| 1428 | |||
| 1429 | ImGui::StyleColorsDark(); | ||
| 1430 | ImGuiStyle& st = ImGui::GetStyle(); | ||
| 1431 | st.WindowRounding = st.FrameRounding = st.PopupRounding = 4.0f; | ||
| 1432 | st.ItemSpacing = ImVec2(10.0f, 7.0f); | ||
| 1433 | st.FramePadding = ImVec2(8.0f, 5.0f); | ||
| 1434 | st.WindowPadding = ImVec2(12.0f, 12.0f); | ||
| 1435 | // Orange accent | ||
| 1436 | st.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f, .45f, .00f, 1.0f); | ||
| 1437 | st.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, .60f, .00f, 1.0f); | ||
| 1438 | st.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f, .45f, .00f, 1.0f); | ||
| 1439 | st.Colors[ImGuiCol_Button] = ImVec4(.25f, .25f, .25f, 1.0f); | ||
| 1440 | st.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f, .45f, .00f, 1.0f); | ||
| 1441 | st.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f, .30f, .00f, 1.0f); | ||
| 1442 | st.Colors[ImGuiCol_Header] = ImVec4(.80f, .45f, .00f, .40f); | ||
| 1443 | st.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f, .45f, .20f, .80f); | ||
| 1444 | st.Colors[ImGuiCol_Tab] = ImVec4(.20f, .20f, .20f, 1.0f); | ||
| 1445 | st.Colors[ImGuiCol_TabHovered] = ImVec4(.80f, .45f, .00f, 1.0f); | ||
| 1446 | st.Colors[ImGuiCol_TabActive] = ImVec4(.80f, .45f, .00f, 1.0f); | ||
| 1447 | st.Colors[ImGuiCol_SeparatorHovered]= ImVec4(.80f, .45f, .00f, 1.0f); | ||
| 1448 | st.Colors[ImGuiCol_FrameBg] = ImVec4(.14f, .14f, .14f, 1.0f); | ||
| 1449 | st.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f, .20f, .20f, 1.0f); | ||
| 1450 | st.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f, .18f, .18f, 1.0f); | ||
| 1451 | st.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f, .11f, .11f, 1.0f); | ||
| 1452 | st.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f, .14f, .14f, 1.0f); | ||
| 1453 | |||
| 1454 | ImGui_ImplGlfw_InitForOpenGL(window, true); | ||
| 1455 | ImGui_ImplOpenGL3_Init("#version 330"); | ||
| 1456 | |||
| 1457 | g_mineRadio = nMineMode; | ||
| 1458 | strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); | ||
| 1459 | |||
| 1460 | int frame = 0; | ||
| 1461 | while (!glfwWindowShouldClose(window) && !fShutdown) | ||
| 1462 | { | ||
| 1463 | glfwPollEvents(); | ||
| 1464 | if (g_needRefresh || (frame % 120 == 0)) { | ||
| 1465 | RefreshWallet(); | ||
| 1466 | g_needRefresh = false; | ||
| 1467 | } | ||
| 1468 | frame++; | ||
| 1469 | |||
| 1470 | ImGui_ImplOpenGL3_NewFrame(); | ||
| 1471 | ImGui_ImplGlfw_NewFrame(); | ||
| 1472 | ImGui::NewFrame(); | ||
| 1473 | |||
| 1474 | // Draw order matters: status bar last so it is always on top. | ||
| 1475 | DrawMainWindow(); | ||
| 1476 | DrawSendDialog(); | ||
| 1477 | DrawOptionsDialog(); | ||
| 1478 | DrawWalletSafetyDialog(); | ||
| 1479 | DrawCreatePhraseDialog(); | ||
| 1480 | DrawRestorePhraseDialog(); | ||
| 1481 | DrawDiagnosticsDialog(); | ||
| 1482 | DrawAboutDialog(); | ||
| 1483 | DrawStatusBar(); | ||
| 1484 | |||
| 1485 | ImGui::Render(); | ||
| 1486 | int w, h; | ||
| 1487 | glfwGetFramebufferSize(window, &w, &h); | ||
| 1488 | glViewport(0, 0, w, h); | ||
| 1489 | glClearColor(.08f, .08f, .08f, 1.0f); | ||
| 1490 | glClear(GL_COLOR_BUFFER_BIT); | ||
| 1491 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); | ||
| 1492 | glfwSwapBuffers(window); | ||
| 1493 | } | ||
| 1494 | |||
| 1495 | fShutdown = true; | ||
| 1496 | ImGui_ImplOpenGL3_Shutdown(); | ||
| 1497 | ImGui_ImplGlfw_Shutdown(); | ||
| 1498 | ImGui::DestroyContext(); | ||
| 1499 | glfwDestroyWindow(window); | ||
| 1500 | glfwTerminate(); | ||
| 1501 | return 0; | ||
| 1502 | } | ||