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:
authorVictorio Nascimento <155357371+Vic-Nas@users.noreply.github.com>2026-07-27 16:45:28 -0400
committerGitHub <noreply@github.com>2026-07-27 17:45:28 -0300
commit6ca1a1d6799474f8ca977b93ab630af7ace879c2 (patch)
treea0312ebc10fc6a56742d0e5caa85c31eb6d3eaa7 /src/gui.cpp
parent56df9830d5a002ff9ce3fac47e497673f8159e39 (diff)
Add Linux GUI installer via Wine (#3)
* Add Linux GUI installer via Wine * Fix launcher creation and add BTF_RELAY env var support * Pool server, Linux GUI, unified binary, README update * Root Makefile: make linux / make windows, cleaner README * Remove headless.cpp * Fix XPM color count mismatch causing toolbar image errors on Linux * XPM icons: same code on Windows and Linux, no ifdefs around image loading * Fix GTK icon crash and Linux printf format * btfrv: 10s socket timeout on relay connects * Faster peer discovery: more dials per pass, shorter backoff and sleep * Parallel btf peer connections * Fix GetMyExternalIP - was disabled, now uses modern IP echo services * Fix tunnel disconnect on idle and restore external IP detection * Filter stale Nostr descriptors * Sort peers by freshness, fresh first * Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed * btf pool: self-replacing listeners, one per relay, no queuing between miners * Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client * Status bar: show external IP and .btf address * README: clean rewrite, concept-first, minimal * make linux produces AppImage * make linux produces AppImage * Pool dashboard: connect instructions, tabs, share bars * Perf fixes: GetMyExternalIP threaded, btf listener counted * Perf fixes: GetMyExternalIP threaded, btf listener counted * Fix pool payouts: defer until coinbase maturity * Pool: clean mode switching, IP warning * Pool: clean mode switching * Fix gPoolRunning: remove static * AppImage: use real icon from bitcoin.ico * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Windows build: use mingw32-make * Windows build: simpler make invocation * Add deps-windows target * Windows build fixes * Windows build fixes * Windows makefile: fix obj dir * Windows: output to repo root * Windows: bundle DLLs into zip * Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows * Fix %I64d that slipped back through rebase (main.h, net.h) * MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d * Fix gPoolRunning extern outside ifndef _WIN32 * Remove -lboost_system: boost is header-only in this codebase * Status bar: copy buttons for IP and .btf address * Windows: add app icon via windres, strip wx stuff from rc file * Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README * GUI: embed Roboto font at 16px for clear readability on all platforms * Windows: windres depends on obj dir target * Fix balance hidden off-screen: put on own line below address bar * Windows: inline mkdir in every rule, remove dir target that confused make * Windows: move all target to top so it's the default rule * Fix pool server not starting: always start when operator selected, re-sync dialog state on open * Write debug.log to data directory (AppData/Roaming/Bitflash on Windows) * Participant miner: use wallet address as username so payouts go to wallet * Add .gitattributes to lock LF line endings, prevent CRLF drift * Refactor pool mining to btf-only transport * Persist pool settings and startup wiring * Prune stale peers and expose pool queries * Fix build regressions and add CLI mode/arg parity * Fix rpc miner stats declaration order * Centralize pool runtime extern declarations * Separate operator/participant dashboards and restore peer stats * Add live telemetry strip and clearer per-mode runtime metrics * Reduce mining dashboard clutter and remove manual pool entry * Count only authorized pool miners * Seed initial pool job on startup * Use a real share target for pool work * Switch pool accounting to round-based shares * Fix nostr pool announcer stats call sites * Fix pool stats definition order * Improve pool worker status display * Add mutex include for participant status * Replace ImClamp in dashboard layout * Clarify participant work status * Log participant status transitions * Reduce peer discovery log noise * Add participant status badge * Track participant auth acknowledgements * Log and validate pool job delivery --------- Co-authored-by: Bitflash Pool Dev <pool@bitflash>
Diffstat (limited to 'src/gui.cpp')
-rw-r--r--src/gui.cpp766
1 files changed, 766 insertions, 0 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
new file mode 100644
index 0000000..6d81b9f
--- /dev/null
+++ b/src/gui.cpp
@@ -0,0 +1,766 @@
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 "font_roboto.h"
16
17// ---------------------------------------------------------------------------
18// Node API
19// ---------------------------------------------------------------------------
20extern int64 nTransactionFee;
21extern int fGenerateBitcoins;
22extern int nBestHeight;
23extern int nMineMode;
24extern std::string strParticipantPool;
25extern CAddress addrLocalHost;
26
27void MainFrameRepaint();
28string DateTimeStr(int64 nTime);
29bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew);
30int64 GetBalance();
31string PubKeyToAddress(const std::vector<unsigned char>& vchPubKey);
32bool AddressToHash160(const std::string& str, uint160& hash160Ret);
33void ThreadBitcoinMiner(void*);
34void ThreadRPCServer(void*);
35
36// ---------------------------------------------------------------------------
37// State
38// ---------------------------------------------------------------------------
39static std::string g_myAddress;
40static int64 g_balance = 0;
41static int64 g_lastWalletRefresh = 0;
42static bool g_showSend = false;
43static bool g_showOptions = false;
44static bool g_showAbout = false;
45static bool g_needRefresh = true;
46
47static char g_sendAddr[128] = {};
48static char g_sendAmount[32] = {};
49static std::string g_sendStatus;
50static char g_participantPool[256] = {};
51static char g_poolName[128] = {};
52static char g_poolFee[32] = {};
53static char g_poolDash[256] = {};
54static int g_mineRadio = 0;
55
56struct TxRow { std::string date, desc; int64 amount; int depth; };
57static std::vector<TxRow> g_txRows;
58
59// ---------------------------------------------------------------------------
60// Helpers
61// ---------------------------------------------------------------------------
62static std::string FmtMoney(int64 n)
63{
64 bool neg = n < 0; if (neg) n = -n;
65 char buf[64];
66 snprintf(buf, sizeof(buf), "%s%lld.%08lld",
67 neg?"-":"", (long long)(n/COIN), (long long)(n%COIN));
68 char* dot = strchr(buf, '.');
69 if (dot) { char* e = buf+strlen(buf)-1; while(e>dot+2&&*e=='0') *e--='\0'; }
70 return buf;
71}
72
73static std::string FmtAge(int64 createdAt)
74{
75 int64 age = GetTime() - createdAt;
76 if (age < 0)
77 age = 0;
78 if (age < 60)
79 return strprintf("%llds", (long long)age);
80 if (age < 3600)
81 return strprintf("%lldm", (long long)(age / 60));
82 return strprintf("%lldh", (long long)(age / 3600));
83}
84
85static ImVec4 ParticipantStatusColor(const std::string& status)
86{
87 if (status.find("failed") != std::string::npos ||
88 status.find("rejected") != std::string::npos ||
89 status.find("stopped") != std::string::npos)
90 return ImVec4(1.0f, 0.38f, 0.38f, 1.0f);
91 if (status.find("hashing") != std::string::npos ||
92 status.find("accepted") != std::string::npos ||
93 status.find("connected") != std::string::npos)
94 return ImVec4(0.35f, 1.0f, 0.45f, 1.0f);
95 if (status.find("asking") != std::string::npos ||
96 status.find("authoriz") != std::string::npos ||
97 status.find("subscrib") != std::string::npos)
98 return ImVec4(1.0f, 0.75f, 0.25f, 1.0f);
99 return ImVec4(0.55f, 0.80f, 1.0f, 1.0f);
100}
101
102static std::string PoolLabel(const BtfPoolAnnouncement& ann)
103{
104 std::ostringstream o;
105 o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%";
106 if (ann.connectedMiners > 0)
107 o << " - " << ann.connectedMiners << " miners";
108 return o.str();
109}
110
111static void RefreshWallet()
112{
113 std::vector<unsigned char> vchPubKey;
114 if (CWalletDB("r").ReadDefaultKey(vchPubKey))
115 g_myAddress = PubKeyToAddress(vchPubKey);
116 g_balance = GetBalance();
117
118 g_txRows.clear();
119 TRY_CRITICAL_BLOCK(cs_mapWallet)
120 {
121 std::vector<std::pair<int64,uint256>> vs;
122 for (auto& kv : mapWallet) vs.push_back({kv.second.GetTxTime(), kv.first});
123 std::sort(vs.rbegin(), vs.rend());
124 for (auto& sv : vs) {
125 auto mi = mapWallet.find(sv.second);
126 if (mi == mapWallet.end()) continue;
127 CWalletTx& wtx = mi->second;
128 int64 net = wtx.GetCredit() - wtx.GetDebit();
129 int depth = wtx.GetDepthInMainChain();
130 std::string desc;
131 if (wtx.IsCoinBase()) desc = depth<1 ? "Generated (unconfirmed)" : "Generated";
132 else if (wtx.GetDebit() > 0) desc = "Sent";
133 else desc = "Received";
134 g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth});
135 }
136 }
137 g_lastWalletRefresh = GetTime();
138}
139
140void MainFrameRepaint() { g_needRefresh = true; }
141
142string DateTimeStr(int64 nTime)
143{
144 time_t t = (time_t)nTime;
145 struct tm* p = localtime(&t);
146 if (!p) return "";
147 char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p);
148 return buf;
149}
150
151// ---------------------------------------------------------------------------
152// Main window
153// ---------------------------------------------------------------------------
154static void DrawMainWindow()
155{
156 ImGuiIO& io = ImGui::GetIO();
157 ImGui::SetNextWindowPos(ImVec2(0,0));
158 ImGui::SetNextWindowSize(io.DisplaySize);
159 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12,12));
160 ImGui::Begin("##main", nullptr,
161 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
162 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_MenuBar |
163 ImGuiWindowFlags_NoFocusOnAppearing);
164 ImGui::SetWindowFontScale(0.92f);
165 ImGui::PopStyleVar();
166
167 // ---- Menu bar ----
168 if (ImGui::BeginMenuBar()) {
169 if (ImGui::BeginMenu("File")) {
170 if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true);
171 ImGui::EndMenu();
172 }
173 if (ImGui::MenuItem("Options")) g_showOptions = true;
174 if (ImGui::MenuItem("About")) g_showAbout = true;
175 ImGui::EndMenuBar();
176 }
177
178 // ---- Address bar ----
179 ImGui::Spacing();
180 ImGui::TextDisabled("Address:");
181 ImGui::SameLine();
182 ImGui::SetNextItemWidth(-80);
183 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f,0.14f,0.14f,1));
184 ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1,
185 ImGuiInputTextFlags_ReadOnly);
186 ImGui::PopStyleColor();
187 ImGui::SameLine();
188 if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str());
189
190 // ---- Balance on its own line ----
191 ImGui::TextDisabled("Balance:");
192 ImGui::SameLine();
193 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f,1.0f,0.35f,1.0f));
194 ImGui::Text("%s BTF", FmtMoney(g_balance).c_str());
195 ImGui::PopStyleColor();
196
197 // ---- Toolbar ----
198 ImGui::Spacing();
199 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f,0.40f,0.15f,1));
200 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f,0.55f,0.20f,1));
201 if (ImGui::Button(" Send Coins ")) g_showSend = true;
202 ImGui::PopStyleColor(2);
203
204 // Mining status indicator
205 ImGui::SameLine(0, 20);
206 if (fGenerateBitcoins) {
207 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f,0.6f,0.0f,1.0f));
208 const char* modeLabel = nMineMode==MINE_OPERATOR ? "Mining (operator)" :
209 nMineMode==MINE_PARTICIPANT ? "Mining (pool)" : "Mining";
210 ImGui::Text("⬤ %s", modeLabel);
211 ImGui::PopStyleColor();
212 if (nMineMode == MINE_PARTICIPANT) {
213 std::string mineStatus = GetParticipantMiningStatus();
214 if (!mineStatus.empty()) {
215 ImGui::SameLine(0, 12);
216 ImGui::PushStyleColor(ImGuiCol_Text, ParticipantStatusColor(mineStatus));
217 ImGui::Text("[%s]", mineStatus.c_str());
218 ImGui::PopStyleColor();
219 }
220 }
221 } else {
222 ImGui::TextDisabled("○ Not mining");
223 }
224
225 ImGui::Spacing();
226 ImGui::Separator();
227 ImGui::Spacing();
228
229 // ---- Live telemetry strip ----
230 {
231 const char* modeName = nMineMode==MINE_OPERATOR ? "Operator" :
232 nMineMode==MINE_PARTICIPANT ? "Participant" : "Solo";
233 int64 age = g_lastWalletRefresh > 0 ? (GetTime() - g_lastWalletRefresh) : -1;
234 if (age < 0) age = 0;
235
236 ImGui::Text("Mode: %s", modeName);
237 ImGui::SameLine(0, 16);
238 ImGui::Text("Wallet refresh: %llds ago", (long long)age);
239
240 if (nMineMode == MINE_PARTICIPANT) {
241 uint64 sent = 0, accepted = 0;
242 double hashRate = 0.0;
243 std::string mineStatus = GetParticipantMiningStatus();
244 GetParticipantMiningStats(sent, accepted, hashRate);
245 if (sent > 0 || accepted > 0 || hashRate > 0.0) {
246 double acceptPct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0;
247 ImGui::TextDisabled("Your mining: sent %llu, accepted %llu (%.1f%%), %.2f H/s",
248 (unsigned long long)sent,
249 (unsigned long long)accepted,
250 acceptPct,
251 hashRate);
252 } else if (!mineStatus.empty()) {
253 ImGui::TextDisabled("Your mining: %s", mineStatus.c_str());
254 } else {
255 ImGui::TextDisabled("Your mining: asking pool for work");
256 }
257 } else if (nMineMode == MINE_OPERATOR) {
258 int miners = 0, blocksFound = 0;
259 uint64 roundShares = 0;
260 GetPoolOperatorStats(miners, blocksFound, roundShares);
261 if (miners > 0 || blocksFound > 0 || roundShares > 0)
262 ImGui::TextDisabled("Pool runtime: %d authorized miners, %llu round shares, %d blocks found", miners, (unsigned long long)roundShares, blocksFound);
263 else
264 ImGui::TextDisabled("Pool runtime: waiting for miners");
265 } else {
266 ImGui::TextDisabled("Solo runtime: local wallet mining telemetry");
267 }
268 ImGui::Spacing();
269 ImGui::Separator();
270 ImGui::Spacing();
271 }
272
273 // ---- Transaction list ----
274 float listHeight = io.DisplaySize.y * 0.26f;
275 if (listHeight < 190.0f) listHeight = 190.0f;
276 if (listHeight > 260.0f) listHeight = 260.0f;
277 if (ImGui::BeginTable("txlist", 4,
278 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
279 ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp,
280 ImVec2(0, listHeight)))
281 {
282 ImGui::TableSetupScrollFreeze(0, 1);
283 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110);
284 ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130);
285 ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch);
286 ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140);
287 ImGui::TableHeadersRow();
288
289 for (auto& row : g_txRows) {
290 ImGui::TableNextRow();
291 ImGui::TableSetColumnIndex(0);
292 if (row.depth < 1)
293 ImGui::TextColored(ImVec4(1,0.6f,0,1), "Unconfirmed");
294 else
295 ImGui::Text("%d blocks deep", row.depth);
296 ImGui::TableSetColumnIndex(1);
297 ImGui::TextDisabled("%s", row.date.c_str());
298 ImGui::TableSetColumnIndex(2);
299 ImGui::TextUnformatted(row.desc.c_str());
300 ImGui::TableSetColumnIndex(3);
301 if (row.amount >= 0)
302 ImGui::TextColored(ImVec4(0.35f,1,0.35f,1), "+%s BTF", FmtMoney(row.amount).c_str());
303 else
304 ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "%s BTF", FmtMoney(row.amount).c_str());
305 }
306 ImGui::EndTable();
307 }
308
309 std::vector<BtfPoolAnnouncement> pools;
310 BtfGetPoolAnnouncements(pools);
311
312 if (nMineMode == MINE_PARTICIPANT) {
313 ImGui::Spacing();
314 ImGui::SeparatorText("Participant Mining");
315 ImGui::Text("Selected pool:");
316 if (strParticipantPool.empty())
317 ImGui::TextDisabled("No pool selected yet");
318 else
319 ImGui::TextWrapped("%s", strParticipantPool.c_str());
320
321 const BtfPoolAnnouncement* selected = NULL;
322 for (size_t i = 0; i < pools.size(); i++)
323 if (pools[i].btfAddress == strParticipantPool) { selected = &pools[i]; break; }
324
325 if (selected) {
326 ImGui::Text("Pool name: %s", selected->poolName.c_str());
327 ImGui::Text("Fee: %.2f%%", selected->feePercent);
328 ImGui::Text("Live stats: %d miners, %d blocks", selected->connectedMiners, selected->blocksFound);
329 if (!selected->dashboardUrl.empty())
330 ImGui::TextWrapped("Dashboard: %s", selected->dashboardUrl.c_str());
331 } else if (!strParticipantPool.empty()) {
332 ImGui::TextDisabled("No fresh announcement found for this pool yet");
333 }
334
335 if (!pools.empty()) {
336 ImGui::Spacing();
337 ImGui::SeparatorText("Live Pools");
338 if (ImGui::BeginTable("livepools", 5,
339 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
340 {
341 ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch);
342 ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 70);
343 ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 160);
344 ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch);
345 ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 60);
346 ImGui::TableHeadersRow();
347 for (size_t i = 0; i < pools.size(); i++) {
348 const BtfPoolAnnouncement& ann = pools[i];
349 ImGui::TableNextRow();
350 ImGui::TableSetColumnIndex(0);
351 ImGui::TextUnformatted(ann.poolName.c_str());
352 ImGui::TextDisabled("%s", ann.btfAddress.c_str());
353 ImGui::TableSetColumnIndex(1);
354 ImGui::Text("%.2f%%", ann.feePercent);
355 ImGui::TableSetColumnIndex(2);
356 ImGui::Text("%d miners, %d blocks, %s old", ann.connectedMiners, ann.blocksFound, FmtAge(ann.createdAt).c_str());
357 ImGui::TableSetColumnIndex(3);
358 if (!ann.dashboardUrl.empty())
359 ImGui::TextWrapped("%s", ann.dashboardUrl.c_str());
360 else
361 ImGui::TextDisabled("-");
362 ImGui::TableSetColumnIndex(4);
363 if (ImGui::SmallButton((std::string("Use##") + ann.btfAddress).c_str())) {
364 strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1);
365 strParticipantPool = ann.btfAddress;
366 CWalletDB().WriteSetting("strParticipantPool", strParticipantPool);
367 g_needRefresh = true;
368 }
369 }
370 ImGui::EndTable();
371 }
372 }
373 }
374
375 if (nMineMode == MINE_OPERATOR) {
376 ImGui::Spacing();
377 ImGui::SeparatorText("Operator Overview");
378 ImGui::Text("Pool .btf address:");
379 std::string opAddr = BtfLocalAddress();
380 if (opAddr.empty())
381 ImGui::TextDisabled("Identity not ready yet");
382 else
383 ImGui::TextWrapped("%s", opAddr.c_str());
384
385 std::vector<PendingPayoutView> owed;
386 GetPendingPayouts(owed);
387 ImGui::Spacing();
388 ImGui::SeparatorText("Pending Pool Payouts (you owe miners)");
389 if (owed.empty()) {
390 ImGui::TextDisabled("No pending pool payouts");
391 } else if (ImGui::BeginTable("owedpayouts", 4,
392 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
393 {
394 ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 100);
395 ImGui::TableSetupColumn("Blocks Left", ImGuiTableColumnFlags_WidthFixed, 90);
396 ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 90);
397 ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch);
398 ImGui::TableHeadersRow();
399 for (const PendingPayoutView& row : owed) {
400 int remaining = std::max(0, row.matureAtHeight - nBestHeight);
401 ImGui::TableNextRow();
402 ImGui::TableSetColumnIndex(0);
403 ImGui::Text("%d", row.matureAtHeight);
404 ImGui::TableSetColumnIndex(1);
405 ImGui::Text("%d", remaining);
406 ImGui::TableSetColumnIndex(2);
407 ImGui::Text("%d", row.recipients);
408 ImGui::TableSetColumnIndex(3);
409 ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str());
410 }
411 ImGui::EndTable();
412 }
413
414 std::vector<PoolWorkerStatView> workers;
415 GetPoolWorkerStats(workers);
416 ImGui::Spacing();
417 ImGui::SeparatorText("Current Round Workers");
418 if (workers.empty()) {
419 ImGui::TextDisabled("No active workers in this round");
420 } else if (ImGui::BeginTable("workers", 4,
421 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
422 {
423 ImGui::TableSetupColumn("Payout Addr", ImGuiTableColumnFlags_WidthStretch);
424 ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch);
425 ImGui::TableSetupColumn("Round Shares", ImGuiTableColumnFlags_WidthFixed, 90);
426 ImGui::TableSetupColumn("Last Seen", ImGuiTableColumnFlags_WidthFixed, 100);
427 ImGui::TableHeadersRow();
428 for (const PoolWorkerStatView& row : workers) {
429 ImGui::TableNextRow();
430 ImGui::TableSetColumnIndex(0);
431 ImGui::TextUnformatted(row.address.empty() ? "unknown" : row.address.c_str());
432 ImGui::TableSetColumnIndex(1);
433 ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str());
434 ImGui::TableSetColumnIndex(2);
435 ImGui::Text("%llu", (unsigned long long)row.roundShares);
436 ImGui::TableSetColumnIndex(3);
437 ImGui::TextDisabled("%s", FmtAge(row.lastSeen).c_str());
438 }
439 ImGui::EndTable();
440 }
441 }
442
443 // ---- Status bar ----
444 {
445 ImGui::Spacing();
446 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,2));
447
448 int peers = (int)vNodes.size();
449 if (nMineMode == MINE_PARTICIPANT) {
450 uint64 sent = 0, accepted = 0;
451 double hashRate = 0.0;
452 std::string mineStatus = GetParticipantMiningStatus();
453 GetParticipantMiningStats(sent, accepted, hashRate);
454 if (sent > 0 || accepted > 0 || hashRate > 0.0)
455 ImGui::TextDisabled("Peers %d height %d shares sent %llu accepted %llu hash rate %.2f H/s",
456 peers,
457 nBestHeight,
458 (unsigned long long)sent,
459 (unsigned long long)accepted,
460 hashRate);
461 else if (!mineStatus.empty())
462 ImGui::TextDisabled("Peers %d height %d %s", peers, nBestHeight, mineStatus.c_str());
463 else
464 ImGui::TextDisabled("Peers %d height %d asking pool for work", peers, nBestHeight);
465 } else if (nMineMode == MINE_OPERATOR) {
466 int miners = 0, blocksFound = 0;
467 uint64 roundShares = 0;
468 GetPoolOperatorStats(miners, blocksFound, roundShares);
469 if (miners > 0 || blocksFound > 0 || roundShares > 0)
470 ImGui::TextDisabled("Peers %d height %d authorized miners %d round shares %llu blocks found %d",
471 peers, nBestHeight, miners, (unsigned long long)roundShares, blocksFound);
472 else
473 ImGui::TextDisabled("Peers %d height %d waiting for miners", peers, nBestHeight);
474 } else {
475 ImGui::TextDisabled("Peers %d height %d", peers, nBestHeight);
476 }
477 ImGui::PopStyleVar();
478 }
479
480 ImGui::SetWindowFontScale(1.0f);
481 ImGui::End();
482}
483
484// ---------------------------------------------------------------------------
485// Send dialog
486// ---------------------------------------------------------------------------
487static void DrawSendDialog()
488{
489 if (!g_showSend) return;
490 ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Always);
491 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f));
492 if (ImGui::Begin("Send Coins", &g_showSend,
493 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
494 {
495 ImGui::Text("Recipient address:");
496 ImGui::SetNextItemWidth(-1);
497 ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr));
498
499 ImGui::Text("Amount (BTF):");
500 ImGui::SetNextItemWidth(180);
501 ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount));
502
503 if (!g_sendStatus.empty()) {
504 ImGui::Spacing();
505 ImGui::TextColored(ImVec4(1,.4f,.4f,1), "%s", g_sendStatus.c_str());
506 }
507
508 ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing();
509 if (ImGui::Button("Send", ImVec2(90,0))) {
510 uint160 h; int64 nv = 0;
511 if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address.";
512 else if (!ParseMoney(g_sendAmount,nv)||nv<=0) g_sendStatus = "Invalid amount.";
513 else {
514 CScript s; s << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG;
515 CWalletTx wtx;
516 if (SendMoney(s, nv, wtx)) {
517 g_showSend=false; g_sendStatus="";
518 memset(g_sendAddr,0,sizeof(g_sendAddr));
519 memset(g_sendAmount,0,sizeof(g_sendAmount));
520 g_needRefresh = true;
521 } else g_sendStatus = "Transaction failed.";
522 }
523 }
524 ImGui::SameLine();
525 if (ImGui::Button("Cancel", ImVec2(90,0))) { g_showSend=false; g_sendStatus=""; }
526 }
527 ImGui::End();
528}
529
530// ---------------------------------------------------------------------------
531// Options dialog
532// ---------------------------------------------------------------------------
533static void DrawOptionsDialog()
534{
535 if (!g_showOptions) return;
536
537 // Always sync from current globals when dialog opens
538 static bool wasOpen = false;
539 if (!wasOpen) {
540 g_mineRadio = nMineMode;
541 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1);
542 strncpy(g_poolName, strPoolName.c_str(), sizeof(g_poolName)-1);
543 snprintf(g_poolFee, sizeof(g_poolFee), "%.2f", dPoolFeePercent);
544 strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1);
545 }
546 wasOpen = true;
547 ImGui::SetNextWindowSize(ImVec2(500, 310), ImGuiCond_Always);
548 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f));
549 if (ImGui::Begin("Options", &g_showOptions,
550 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
551 {
552 // Transaction fee
553 ImGui::SeparatorText("Transaction Fee");
554 static char feeStr[32] = {};
555 if (feeStr[0]=='\0') snprintf(feeStr,sizeof(feeStr),"%s",FmtMoney(nTransactionFee).c_str());
556 ImGui::SetNextItemWidth(180);
557 ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr));
558
559 // Mining
560 ImGui::SeparatorText("Mining Mode");
561 ImGui::RadioButton("Solo — rewards go to your wallet", &g_mineRadio, MINE_SOLO);
562 ImGui::RadioButton("Operator — run a pool for other miners", &g_mineRadio, MINE_OPERATOR);
563 ImGui::RadioButton("Participant — mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT);
564
565 if (g_mineRadio == MINE_OPERATOR) {
566 ImGui::Spacing();
567 ImGui::SeparatorText("Pool Announcement");
568 ImGui::Text("Pool name:");
569 ImGui::SetNextItemWidth(-1);
570 ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName));
571 ImGui::Text("Fee %%:");
572 ImGui::SetNextItemWidth(120);
573 ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee));
574 ImGui::Text("Dashboard URL:");
575 ImGui::SetNextItemWidth(-1);
576 ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash));
577 }
578
579 if (g_mineRadio == MINE_PARTICIPANT) {
580 ImGui::Spacing();
581 ImGui::SeparatorText("Pool Selection");
582 ImGui::TextDisabled("Pick from live discovered pools. Manual entry is disabled here.");
583
584 std::vector<BtfPoolAnnouncement> pools;
585 BtfGetPoolAnnouncements(pools);
586 if (!pools.empty()) {
587 std::string current = "Select a live pool";
588 for (size_t i = 0; i < pools.size(); i++) {
589 if (pools[i].btfAddress == g_participantPool) {
590 current = PoolLabel(pools[i]);
591 break;
592 }
593 }
594 if (ImGui::BeginCombo("Discovered pools", current.c_str())) {
595 for (size_t i = 0; i < pools.size(); i++) {
596 const BtfPoolAnnouncement& ann = pools[i];
597 std::string label = PoolLabel(ann);
598 bool selected = ann.btfAddress == g_participantPool;
599 if (ImGui::Selectable(label.c_str(), selected)) {
600 strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1);
601 }
602 }
603 ImGui::EndCombo();
604 }
605 } else {
606 ImGui::TextDisabled("No live pools discovered yet");
607 }
608 }
609
610 ImGui::Spacing();
611 const char* genLabel = fGenerateBitcoins ? "Stop Mining" : "Start Mining";
612 if (ImGui::Button(genLabel, ImVec2(130,0))) {
613 fGenerateBitcoins = fGenerateBitcoins ? 0 : 1;
614 if (fGenerateBitcoins) _beginthread(ThreadBitcoinMiner, 0, NULL);
615 CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins);
616 }
617
618 ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing();
619 if (ImGui::Button("OK", ImVec2(90,0))) {
620 int64 fee=0;
621 if (ParseMoney(feeStr,fee)) { nTransactionFee=fee; CWalletDB().WriteSetting("nTransactionFee",nTransactionFee); }
622
623 bool changed = (g_mineRadio != nMineMode);
624 nMineMode = g_mineRadio;
625 strParticipantPool = g_participantPool;
626 strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool";
627 strPoolDashboardUrl = g_poolDash;
628 dPoolFeePercent = atof(g_poolFee);
629 CWalletDB().WriteSetting("nMineMode", nMineMode);
630 CWalletDB().WriteSetting("strParticipantPool", strParticipantPool);
631 CWalletDB().WriteSetting("strPoolName", strPoolName);
632 CWalletDB().WriteSetting("strPoolDashboardUrl", strPoolDashboardUrl);
633 CWalletDB().WriteSetting("dPoolFeePercent", dPoolFeePercent);
634
635 if (nMineMode == MINE_OPERATOR) {
636 if (!gPoolServerRunning) {
637 gPoolServerRunning = true;
638 _beginthread(ThreadRPCServer, 0, NULL);
639 } else if (!gPoolRunning) {
640 gPoolRunning = true;
641 }
642 } else {
643 gPoolRunning = false;
644 }
645 g_showOptions = false;
646 }
647 ImGui::SameLine();
648 if (ImGui::Button("Cancel", ImVec2(90,0))) g_showOptions = false;
649 }
650 if (!g_showOptions) wasOpen = false;
651 ImGui::End();
652}
653
654// ---------------------------------------------------------------------------
655// About dialog
656// ---------------------------------------------------------------------------
657static void DrawAboutDialog()
658{
659 if (!g_showAbout) return;
660 ImGui::SetNextWindowSize(ImVec2(380, 170), ImGuiCond_Always);
661 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f));
662 if (ImGui::Begin("About Bitflash", &g_showAbout,
663 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
664 {
665 ImGui::TextColored(ImVec4(1,.6f,0,1), "Bitflash BTF v1.1.0");
666 ImGui::Spacing();
667 ImGui::TextWrapped("CPU-only cryptocurrency. RandomX proof of work. Anonymous .btf addressing over Nostr. No premine.");
668 ImGui::Spacing();
669 ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)");
670 ImGui::Spacing();
671 if (ImGui::Button("OK", ImVec2(90,0))) g_showAbout = false;
672 }
673 ImGui::End();
674}
675
676// ---------------------------------------------------------------------------
677// Entry point
678// ---------------------------------------------------------------------------
679int RunGUI(int argc, char* argv[])
680{
681 if (!glfwInit()) return 1;
682 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
683 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
684 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
685#ifdef __APPLE__
686 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
687#endif
688
689 GLFWwindow* window = glfwCreateWindow(960, 640, "Bitflash", nullptr, nullptr);
690 if (!window) { glfwTerminate(); return 1; }
691 glfwMakeContextCurrent(window);
692 glfwSwapInterval(1);
693
694 IMGUI_CHECKVERSION();
695 ImGui::CreateContext();
696 ImGuiIO& io = ImGui::GetIO();
697 io.IniFilename = nullptr;
698
699 // Load Roboto at 16px — clean, readable on all platforms
700 io.Fonts->AddFontFromMemoryCompressedTTF(
701 RobotoMedium_compressed_data, RobotoMedium_compressed_size, 16.0f);
702
703 ImGui::StyleColorsDark();
704 ImGuiStyle& s = ImGui::GetStyle();
705 s.WindowRounding = s.FrameRounding = s.PopupRounding = 4.0f;
706 s.ItemSpacing = ImVec2(10, 7);
707 s.FramePadding = ImVec2(8, 5);
708 s.WindowPadding = ImVec2(12, 12);
709 // Orange accent
710 s.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f,.45f,.00f,1);
711 s.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f,.60f,.00f,1);
712 s.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f,.45f,.00f,1);
713 s.Colors[ImGuiCol_Button] = ImVec4(.25f,.25f,.25f,1);
714 s.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f,.45f,.00f,1);
715 s.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f,.30f,.00f,1);
716 s.Colors[ImGuiCol_Header] = ImVec4(.80f,.45f,.00f,.40f);
717 s.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f,.45f,.20f,.80f);
718 s.Colors[ImGuiCol_Tab] = ImVec4(.20f,.20f,.20f,1);
719 s.Colors[ImGuiCol_TabHovered] = ImVec4(.80f,.45f,.00f,1);
720 s.Colors[ImGuiCol_TabActive] = ImVec4(.80f,.45f,.00f,1);
721 s.Colors[ImGuiCol_SeparatorHovered]=ImVec4(.80f,.45f,.00f,1);
722 s.Colors[ImGuiCol_FrameBg] = ImVec4(.14f,.14f,.14f,1);
723 s.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f,.20f,.20f,1);
724 s.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f,.18f,.18f,1);
725 s.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f,.11f,.11f,1);
726 s.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f,.14f,.14f,1);
727
728 ImGui_ImplGlfw_InitForOpenGL(window, true);
729 ImGui_ImplOpenGL3_Init("#version 330");
730
731 g_mineRadio = nMineMode;
732 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1);
733
734 int frame = 0;
735 while (!glfwWindowShouldClose(window) && !fShutdown)
736 {
737 glfwPollEvents();
738 if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; }
739 frame++;
740
741 ImGui_ImplOpenGL3_NewFrame();
742 ImGui_ImplGlfw_NewFrame();
743 ImGui::NewFrame();
744
745 DrawMainWindow();
746 DrawSendDialog();
747 DrawOptionsDialog();
748 DrawAboutDialog();
749
750 ImGui::Render();
751 int w, h; glfwGetFramebufferSize(window, &w, &h);
752 glViewport(0, 0, w, h);
753 glClearColor(.08f,.08f,.08f,1);
754 glClear(GL_COLOR_BUFFER_BIT);
755 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
756 glfwSwapBuffers(window);
757 }
758
759 fShutdown = true;
760 ImGui_ImplOpenGL3_Shutdown();
761 ImGui_ImplGlfw_Shutdown();
762 ImGui::DestroyContext();
763 glfwDestroyWindow(window);
764 glfwTerminate();
765 return 0;
766}