upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/gui.cpp
diff options
context:
space:
mode:
authorbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
committerbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
commit5ce650382352c1503f40abf08129ada69402af55 (patch)
tree6a18198901f1e2c4933f1ac53c9365eec512bf63 /src/gui.cpp
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'src/gui.cpp')
-rw-r--r--src/gui.cpp1502
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// ---------------------------------------------------------------------------
25extern int64 nTransactionFee;
26extern int fGenerateBitcoins;
27extern int nBestHeight;
28extern int nMineMode;
29extern std::string strParticipantPool;
30extern CAddress addrLocalHost;
31
32void MainFrameRepaint();
33int GetDiscoveredPeerCount();
34int GetPeerMedianHeight();
35string DateTimeStr(int64 nTime);
36bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew);
37int64 GetBalance();
38string PubKeyToAddress(const std::vector<unsigned char>& vchPubKey);
39bool AddressToHash160(const std::string& str, uint160& hash160Ret);
40void ThreadBitcoinMiner(void*);
41int StartMinerThreads();
42void ThreadRPCServer(void*);
43
44// ---------------------------------------------------------------------------
45// Layout constants
46// ---------------------------------------------------------------------------
47static const float STATUS_BAR_H = 22.0f;
48
49// ---------------------------------------------------------------------------
50// GUI state
51// ---------------------------------------------------------------------------
52static std::string g_myAddress;
53static int64 g_balance = 0;
54static int64 g_lastWalletRefresh = 0;
55static bool g_showSend = false;
56static bool g_showOptions = false;
57static bool g_showAbout = false;
58static bool g_showDiagnostics = false;
59static bool g_showWalletSafety = false;
60static bool g_needRefresh = true;
61
62static char g_sendAddr[128] = {};
63static char g_sendAmount[32] = {};
64static std::string g_sendStatus;
65static char g_backupPath[512] = {};
66static std::string g_backupStatus;
67static int64 g_lastWalletBackup = 0;
68static WalletRecoveryAudit g_recoveryAudit;
69static int64 g_recoveryAuditTime = 0;
70static bool g_walletSafetyLoaded = false;
71static char g_participantPool[256] = {};
72static char g_poolName[128] = {};
73static char g_poolFee[32] = {};
74static char g_poolDash[256] = {};
75static int g_mineRadio = 0;
76
77struct TxRow { std::string date, desc; int64 amount; int depth; };
78static std::vector<TxRow> g_txRows;
79
80// ---------------------------------------------------------------------------
81// Helpers
82// ---------------------------------------------------------------------------
83static 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
94static 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
103static 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
120static 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
130static 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.
142static 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.
155static 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
173static 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
180static void LoadWalletSafetyState()
181{
182 if (g_walletSafetyLoaded)
183 return;
184 CWalletDB("r").ReadSetting("nLastWalletBackup", g_lastWalletBackup);
185 SetDefaultBackupPath();
186 g_walletSafetyLoaded = true;
187}
188
189static 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
198static int KeyPoolCount()
199{
200 int n = 0;
201 CRITICAL_BLOCK(cs_keyPool)
202 n = (int)mapKeyPool.size();
203 return n;
204}
205
206static 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
281void 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// ---------------------------------------------------------------------------
294static 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// ---------------------------------------------------------------------------
379static 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// ---------------------------------------------------------------------------
731static 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// ---------------------------------------------------------------------------
788static 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// ---------------------------------------------------------------------------
945static bool g_showCreatePhrase = false;
946static bool g_showRestorePhrase = false;
947static std::string g_pendingMnemonic;
948static bool g_phraseWrittenDown = false;
949static std::string g_phraseStatus;
950
951static char g_restoreInput[600] = {};
952static std::string g_restoreStatus;
953static std::atomic<bool> g_restoreRunning(false);
954static std::atomic<int> g_restoreDerived(0);
955static std::atomic<int> g_restoreRecovered(0);
956static std::string g_restoreResult;
957static std::mutex g_restoreResultMutex;
958
959static void RestoreProgress(void*, int nDerived, int nRecovered)
960{
961 g_restoreDerived.store(nDerived);
962 g_restoreRecovered.store(nRecovered);
963}
964
965static 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
989static 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
1077static 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
1157static 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.
1340static 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
1372static 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// ---------------------------------------------------------------------------
1397int 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}