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 <btf01@proton.me>2026-07-28 12:55:04 -0300
committerGitHub <noreply@github.com>2026-07-28 12:55:04 -0300
commit663784eae9b04ec71e3e1eebf445910e5a8add42 (patch)
tree1482b0778a11c1b46cfbd476a5d91e12ef31204b /src/gui.cpp
parent1d0314c0d220231afc56193ba66b70234fa24092 (diff)
Merge PR #6 (large refactor), preserving main's fixes (#9)
* 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 * Stop persisting mining mode/pool settings across restarts LoadWallet() runs after CLI flags are parsed in main_gui.cpp, and was restoring nMineMode/strParticipantPool/strPoolName/strPoolDashboardUrl/ dPoolFeePercent/fGenerateBitcoins from wallet.dat every time, silently overwriting whatever was just set on the command line or in the current Options session -- no error, no log line. This is why pool name/address could show correctly in the UI but stay stale in the logs (UI held the in-memory value just set; logs eventually reflected whatever LoadWallet clobbered it back to), and why mode changed behavior across restarts. Mode/pool config is now decided fresh every launch: CLI flags if given, otherwise the compiled-in default. Nothing is written back to wallet.dat for these settings anymore, so there's nothing left to go stale or fight over load order. * Add explicit Relay mode as the new startup default 4th mining mode: node runs/syncs, never mines, no pool server -- an explicit option instead of an implicit side-effect of 'Solo + never click Start Mining'. Now the compiled-in default (was Solo). - main.h: MINE_RELAY = 0, renumbered other modes (nothing compares nMineMode against raw integers elsewhere, verified). - main.cpp: default nMineMode = MINE_RELAY; BitcoinMiner() early-returns for Relay, mirroring the existing Participant-mode guard. - gui.cpp: added Relay radio button, status label, and telemetry-strip text. Start/Stop Mining button is hidden for Relay (meaningless -- it never mines by definition); OK forces fGenerateBitcoins off if you switch into Relay while previously mining. * Announce pool changes immediately instead of waiting up to 60s ThreadBtfPoolAnnouncer re-read strPoolName/dPoolFeePercent/etc live, but only once per iteration of its 60s sleep loop, so a pool name/fee change in Options could take up to a minute to reach other nodes. - nostr.h/.cpp: add gAnnounceNow flag; the announcer's sleep loop now wakes early and publishes immediately when it's set, then clears it. - gui.cpp: Options OK handler sets gAnnounceNow when operator pool settings actually changed (name/dashboard/fee/mode), so a saved change is announced right away instead of on the next tick. * Give each pool miner a distinct extranonce2 so jobs no longer overlap mining.notify previously broadcast one identical 80-byte header (baked from a single server-side extraNonce) to every connected miner, so any two miners on the same job searched the exact same nNonce space instead of disjoint ranges. Each Miner connection is now assigned a unique extranonce2 on connect. BuildMinerBlock() takes the shared job template and appends that miner's extranonce2 to the coinbase scriptSig, recomputing the merkle root, so every miner gets its own header variant of the same job. mining.submit validates and, on a full block, assembles the block using that same miner-specific variant (via m->extranonce2), not the shared template. No client-side protocol change needed: main.cpp's participant miner already just takes whatever header bytes mining.notify hands it and submits (jobId, nonce) back, so it works unmodified against per-miner headers. Still simplified vs standard Stratum (no coinb1/coinb2/merkle-branch wire split, no mining.extranonce.subscribe reply payload) since this protocol was already a custom simplification pre-existing this fix. Syntax-checked with g++ -fsyntax-only (full link needs secp256k1/ RandomX/GLFW not present in this sandbox). * Fix debug.log never being written on the real Windows build printf-to-debug.log routing was gated on: #if defined(_WINDOWS) || !defined(_WIN32) _WINDOWS is only ever defined by the legacy, unused MSVC makefile.vc. The actual Windows build (makefile.mingw) never defines it, and _WIN32 IS defined there (mingw builtin), so on real Windows this condition was always false || false = false. printf() fell through to plain libc printf (stdout only), so debug.log was never created, and output was only visible if the process happened to be run attached to a console. OutputDebugStringF() was already platform-safe internally (always writes debug.log; only echoes to console when not on Windows), so the outer condition was pure dead weight. Made the routing unconditional. * Reduce base UI font size one more step (16px -> 14px) * Remove Start/Stop Mining button; mining state follows selected mode The button let a user configure a pool then click a separate toggle, creating states where the button's label/action didn't match what the selected mode actually needed. OK now starts/stops the miner thread itself based on nMineMode: Relay clears fGenerateBitcoins, the other three modes set it and spawn ThreadBitcoinMiner if it isn't already running (guarded by vfThreadRunning[3]). * Fix participant miner stuck waiting for notify after buffered multi-line recv Root cause of the operator/participant pairing successfully but no work ever flowing: PoolParticipantMiner()'s receive loop only pulled one line out of the socket per select() wakeup. The server sends the authorize reply, mining.set_difficulty, and mining.notify back-to-back on the same connection, and these often land in a single TCP segment — one recv() inside StratumRecvLine() buffers all three lines into 'buf', but only the first was extracted and processed. The remaining lines (frequently mining.notify itself) sat in 'buf' indefinitely, because select() only reports readability of the OS socket, not whether our own buffer still holds unprocessed complete lines — and the server had nothing further to send once the initial burst was over, so select() never fired again. Fix: drain any complete line already sitting in 'buf' first; only fall back to select()+recv() when 'buf' has no complete line left. * Fix mining.submit rejecting every share due to wrong param-count check Server required params.size() >= 4 for mining.submit, but the client only ever sends 3: [username, jobId, nonceHex] (indices 0-2, matching what the handler actually reads: p[1]=jobId, p[2]=nonceHex). Every submission was bounced with 'bad params' before its nonce was ever checked against the job, producing an endless share-sent/share-rejected loop with no other visible cause. Likely a leftover from the standard 5-field Stratum mining.submit shape, which this simplified 3-field protocol never matched. Threshold corrected to size() >= 3. * Fix mining.submit rejecting legitimate shares that weren't also winning blocks Pool share difficulty is intentionally ~256x easier than full network difficulty (MakeShareTarget() shifts the block target left 8 bits), so miners are expected to submit many shares per actual block found. But the handler replied to the miner based on ProcessBlock()/CheckBlock() success — validation against the much harder network target — rather than on whether the share met pool difficulty. Every legitimate share that wasn't also a winning block (the vast majority, by design) failed CheckBlock()'s nBits check and got reported to the miner as rejected. Now: reply(true) as soon as a submission clears pool-share difficulty. Only if it ALSO clears full network difficulty (job.target, not job.shareTarget) does it get submitted as a block — with no second reply sent, since the share was already acknowledged. * Fix dead-peer dial priority: freshness sort was undone by full shuffle ConnectDiscoveredBtfPeers sorted candidates newest-first, then immediately shuffled the whole list, making the freshness sort a no-op — every pass was fully random. Meanwhile g_btfPeerFails was tracked but never used to prioritize, so a peer that had failed 20 times in a row got equal footing with an untried or previously-successful peer; the only effect was backoff, capped at 15 minutes, so dead descriptors cycled back into the dial pool constantly. - Track successful connections (g_btfPeerEverConnected) and use it, plus fail history, to order candidates in tiers: known-good, then untried, then known-bad (worst offenders last). Freshness order is preserved within each tier instead of being discarded. - Raise the backoff cap for chronic failures (6+ in a row) from 15 minutes to 6 hours, so descriptors that are clearly dead stop consuming dial budget every pass while still getting retried eventually. * Batch-resolve .btf descriptors to stop hammering public relays during dial passes Root cause of 'stuck at 1 peer' despite good peer discovery: each parallel dial thread in ConnectDiscoveredBtfPeers called ConnectNodeBtf, which itself walked all configured relays (relay.damus.io, nos.lol, relay.nostr.band, relay.snort.social) to resolve that one peer's descriptor. With up to BTF_DIALS_PER_PASS (12) threads dialing at once, a single pass could fire up to 12 * nNostrRelays near-simultaneous fresh connections at the same handful of public relays -- easily enough to trip rate limits or get handshakes refused (exactly what the logs showed: 'handshake refused by relay.damus.io', repeated connect failures to relay.nostr.band / relay.snort.social). Peers that were genuinely reachable looked dead simply because their descriptor couldn't be fetched under that load, and then got penalized as known-bad by the dial-priority tiering from the previous fix. - Add BtfResolveMany: resolves every still-unresolved candidate's descriptor in a single Nostr REQ per relay (multiple authors in one filter), instead of one relay connection per candidate. A 12-candidate pass now costs at most a few relay round-trips total instead of dozens. - Add ConnectNodeBtfResolved / ConnectNodeBtfTail (net.cpp): given an already-resolved descriptor, dial straight to the peer's rendezvous node with zero further relay contact. ConnectDiscoveredBtfPeers now batch- resolves all candidates up front, then fans out tunnel-only connect threads in parallel -- no thundering herd against the relays. - Candidates that fail to resolve are recorded as a failure immediately (same backoff/fail-count bookkeeping as before) instead of spawning a thread with nothing to dial. * Stop reconnecting to relays constantly: pool one persistent connection per relay Real root cause of the ongoing 'handshake refused' / 'connect failed' spam and peer count stuck at 0-1: every relay-touching operation (startup self- test, per-pass seeding, batch descriptor resolve, pool announce, pool query) opened its own brand-new CWebSocket every single time it ran. Two independent threads (ThreadNostrSeed and ThreadBtfPoolAnnouncer) each reconnect from scratch on their own ~60s cycle, so a node could fire well over a dozen fresh TCP+TLS+WS handshakes at the same 4 public relays inside any given minute. Public relays -- relay.damus.io especially -- rate-limit new connections per source IP fairly aggressively, so this node was effectively DoS-ing itself: peers were genuinely reachable, but their descriptors couldn't be fetched because the relay had already started refusing this node's own repeated reconnects. - Add a small per-relay connection pool (g_relayPool) and an RAII CRelayLease that reuses a relay's live socket across calls instead of reconnecting every time, only reconnecting when a socket actually drops or was never established. - Route every relay call site through it: the startup self-test, the self-test loop, SeedFromRelay, BtfResolve, BtfResolveMany, BtfPublishPoolAnnouncement, BtfQueryPoolAnnouncement. - Only log 'connected to <relay>' on an actual fresh (re)connect, not on every pass that's just reusing an already-live socket, so the log reflects what's really happening instead of implying constant churn. This cuts fresh relay connections from several per minute down to essentially one per relay for the life of the process (until a relay actually drops the connection), which should make the rate-limit-driven refusals go away and let descriptor resolution -- and therefore dialing -- actually succeed at the rate the peer list implies it should. * Fix wallet crash on rejected send; remove legacy IP-based peer discovery - SendMoney() no longer throws when a committed tx is rejected by AcceptTransaction() (e.g. raced input already spent). It now unwinds the wallet state via RollbackTransactionSpent() and returns false, which the GUI already handles as 'Transaction failed.' - Removed the dead IP-based peer discovery/connect path now that discovery is entirely Nostr/.btf: CAddrDB/addr.dat, mapAddresses, AddAddress, ConnectNode(CAddress), FindNode(CAddress), ConnectSocket (SOCKS4 proxy helper), addrProxy, ThreadOpenConnections/2 class-C dialer and its thread spawn, the addr/getaddr P2P handlers, and the dead vAddrToSend/setAddrKnown fields on CNode. - Deleted irc.cpp/irc.h (already-disabled legacy seed code) and scrubbed references from headers_core.h and all four makefiles. * Raise peer connection target and add dial diversity; fix IPv4-only relay connect - BTF_TARGET_CONN: 8 -> 24. With ~100 known .btf peers, average degree needs only to exceed ln(N)~=4.6 for the network to stay connected with overwhelming probability (Erdos-Renyi); 24 is a healthy margin. - ConnectDiscoveredBtfPeers now splits each pass's dial budget ~70/30 between the existing reliability-tiered order and a random sample from the full fresh peer pool. Pure reliability bias means every node converges on the same 'known good' hub set, which correlates peer sets across the network -- a relay outage then drops the same edges everywhere at once instead of independent ones. The random slice keeps a floor of uncorrelated edges. - CWebSocket::Connect (relay websocket) now resolves with AF_UNSPEC and tries every returned address (IPv6 or IPv4) in turn instead of hard-failing on AF_INET-only resolution, which was one plausible cause of the relay.nostr.band:443 connect failures (CDN-fronted relays increasingly rate-limit/filter the v4 path). * Remove accidentally committed build binary; ignore src/bitflash * Fix pool participants getting stuck after the first job JobWatcherFn only rebuilt/broadcast a new Stratum job when a new block arrived on this node or no job existed yet. Once a job was built after startup, gHaveJob stayed true forever unless *this* node found the block, so with no new block (slow interval, or this node just not seeing one) every connected miner sat hashing the exact same job -- stale timestamp, stale mempool snapshot -- indefinitely. From the miner's side that's indistinguishable from being stuck: it worked at connect (gets the one-time job sent on authorize) and then nothing ever arrives again. Added a 60s periodic refresh so the job (and therefore mining.notify) gets rebroadcast on a timer too, same as a normal stratum pool, not only on new-block/no-job events. * Diversify Nostr discovery relay list Went from 4 relays to 7, and reduced Cloudflare concentration: previously 2 of 4 (damus.io, snort.social) sat behind the same CDN, so a Cloudflare- side issue could take out half the list even though they're nominally independent relays. Kept damus.io (most established) as the one CF-fronted entry and filled the rest with different hosts (nos.lol, nostr.band on Hetzner, nostr21.com, offchain.pub, nostr.mom, primal.net) so peer discovery doesn't have a single shared upstream as a point of failure. 7 relays is within the commonly recommended 5-10 range for redundancy. * Fix data race in RebuildJob: chain-state reads weren't holding cs_main pindexBest, nBestHeight, GetNextWorkRequired() and GetMedianTimePast() were all read without cs_main. A block being accepted concurrently by the P2P thread could interleave with this: pindexPrev captured from the old best block, but nBestHeight already bumped to the new one by the time job.height is computed, or pindexBest reassigned mid-read. That mismatch would make QueuePayouts() schedule a payout for the wrong maturity height for whatever block gets found against that job. Wrapped the whole chain-state snapshot in one CRITICAL_BLOCK(cs_main), consistent with how the rest of the codebase touches this state. * add operator fee * Add categorized, burst-collapsing logging for pool/worker/payout flow debug.log was growing unboundedly because every printf() call wrote unconditionally, so loops that log once per item (discovered peers, connect attempts) produced a near-identical line per address with no cap. Add LogPrint(category, ...): a drop-in printf replacement that tags each call with a subsystem (pool/worker/payout/net/nostr) and collapses repeated calls from the same call site into one line per 3s window plus a '(xN similar in 3s)' summary, so log growth is bounded by elapsed time instead of event count. Wire it through the whole pool -> work -> payout pipeline (rpc.cpp operator side, main.cpp participant/worker side, net.cpp/nostr.cpp peer & relay discovery), and along the way fix several previously *silent* failure points that produced exactly the reported symptom (worker stuck asking for work, nothing logged): - Relay listener registration failing: no log at all, so a pool with every relay down looked identical to a healthy idle pool. - No relays configured: silent infinite 5s retry loop. - Initial job seed failing at pool startup: silent. - JobWatcher: added a stuck-job watchdog that, after 15s with no job available, logs a specific diagnosis (no chain tip yet vs. RebuildJob() failing) instead of retrying forever in silence. - QueuePayouts(): a block found with zero recorded shares queued no payout and logged nothing. - FlushMaturePayouts(): an unparseable payout address was skipped with no log. - Participant miner: the 'waiting for pool notify' stall was only ever shown as a transient GUI status string, never written to debug.log. Now logs on a 15s cadence while stalled, and after 120s proactively drops the tunnel so the existing reconnect-in-10s loop gets a fresh attempt instead of hanging indefinitely. - Participant miner: share rejections were only a transient GUI status string; now logged with the pool's rejection reason. All edited files verified with g++ -fsyntax-only (gnu++14). * Fix two bugs causing worker stuck + log bloat 1. Worker stuck asking for work (main.cpp): The participant miner checked msg["result"].is_boolean() to detect the mining.subscribe acknowledgement (id==1), but the pool sends an array ([[["mining.notify",sid],sid,4]]) as per Stratum spec, not a boolean. is_boolean() always returned false, so gotSubscribeAck was never set true. This didn't directly block authorize (id==2 path is independent), but it did mean the status never advanced through "subscribed", leaving the progress logging misleading. More importantly, the same wrong assumption meant any pool that also gated job dispatch on a prior subscribe confirmation would leave the worker stuck forever with no diagnosis. Fix: accept any non-null result for id==1 (the array is the session/extranonce info; its contents don't matter for our use case). 2. Log bloat from ProcessMessages (main.cpp): ProcessMessages() used a bare printf() which fired unconditionally for every P2P message received -- the bulk of debug.log was this one line repeating thousands of times. Convert to LogPrint("net", ...) so it is gated behind fDebug (off by default) and burst-collapsed by the dedup introduced in the previous commit. * Fix log bloat, em-dashes, dead code, status bar, Connected/Detected peers Log bloat: - Gate all per-message/per-block/per-loop printf in main.cpp, net.h, net.cpp behind LogAcceptsCategory("net") -- only visible with -debug. This includes: received:/sending: hex dumps, got inventory, askfor, getblocks, getdata, received block, AddToWallet, RelayWalletTransactions, ProcessMessage FAILED, mining loop N-transactions line, retarget block, socket recv/send errors. AddToBlockIndex new best and ProcessBlock ACCEPTED intentionally left always-on (one line per block). - PoolParticipantMiner: status -> now LogPrint(worker) not printf. Em-dashes: - Replace all UTF-8 em-dashes (0xE2 0x80 0x94) with ASCII '--' in every source file (rpc.cpp, gui.cpp, main.cpp, main_gui.cpp, net.cpp, nostr.cpp, util.cpp, btfrv.cpp, db.cpp, db.h, main.h, headers.h, headers_core.h, util.h). 40 occurrences across 14 files. Dead code: - Remove unused gJobSeq atomic from rpc.cpp (written, never read). - Fix minerReward: was computed but payout loop used blockReward directly (operator fee was advertised but never deducted). Now payout correctly uses minerReward = blockReward * (1 - feeFrac). Status bar / peers: - Add GetDiscoveredPeerCount() to nostr.cpp/nostr.h (reads g_btfPeers size under cs_btfPeers). - Replace scrollable inline status text with DrawStatusBar(): a separate ImGui overlay window pinned to bottom of screen (NoScrollbar, NoBringToDisplayFront). Labels changed from 'Peers N' to 'Connected peers: N Detected peers: N'. * Rewrite gui.cpp: proper fixed status bar, scrollable content child Layout architecture: - DrawMainWindow() covers the full display width, height minus STATUS_BAR_H (22px). NoScrollbar + NoScrollWithMouse on the outer window so the chrome (menu, address/balance strip, telemetry strip, separators) never scrolls. - A BeginChild("##scroll") fills the remaining height after the fixed chrome, sized to GetContentRegionAvail().y -- which is already the space between the cursor and the bottom of the outer window. All mode-specific content (transaction table, participant pool list, operator workers/payouts) lives inside this child. Content that overflows gets a scrollbar; nothing bleeds past the outer window edge. - DrawStatusBar() is a separate overlay window pinned to the bottom 22px, drawn last so it is always on top. NoBringToDisplayFront is NOT used here -- drawing last is sufficient and avoids interaction issues with dialogs. - Status bar background is fully opaque (not alpha-blended) so it never shows content behind it. Labels and content: - 'Peers N' replaced with 'Connected peers: N Detected peers: N' using GetDiscoveredPeerCount(). - Mining indicator: '* Mode' instead of the 'filled circle' Unicode glyph that renders as boxes on some Linux font configs. - FmtAge used consistently; 'blocks deep' shortened to 'N deep' in tx list. - Options dialog: em-dashes removed from RadioButton labels (ASCII '--'). - Removed usedY dead variable (GetCursorPosY() result was unused). * Fix blocking bugs, dead code, pool/participant flow correctness Bugs fixed: StratumRecvLine blocking (main.cpp): The participant miner's recv loop called recv() with no timeout after select() said the socket was readable. If the server sent a partial line (split across two TCP segments), the second recv() would block indefinitely waiting for the rest -- the classic 'stuck asking for work' hang even though the pool was alive and had sent data. Fix: single non-blocking recv() pass (MSG_DONTWAIT on Linux, FIONREAD+recv on Windows). If no complete line is in the buffer yet, return false; the outer select() loop retries when more data arrives. BroadcastJob holding mutex during send() (rpc.cpp): BroadcastJob() held gMinersMutex across all SendLine() calls. send() can block if a slow miner's TCP receive buffer is full. With the mutex held, MinerThreadFn could not acquire it to remove a disconnecting miner, causing a deadlock-adjacent stall: broadcast blocked on slow miner, disconnect blocked on broadcast, new jobs not delivered. Fix: snapshot {fd, authorised, address, extranonce2} under the mutex, release it, then send to each fd outside the lock. Dead code removed (rpc.cpp): - gRpcUser, gRpcPassword: defined, never read or set by anything. - dHashesPerSec, nHPSTimerStart: leftover Bitcoin 0.1 stubs, unused. - IsInitialBlockDownload(): stub that returned pindexBest==NULL, never called anywhere in this codebase. - struct Share: declared with three fields, never instantiated. - Duplicate comment block before payout section. Other cleanup (main.cpp): - STUCK_NO_JOB_TIMEOUT_SECS moved from inside the loop body to file scope (was 'static const' inside a loop -- correct but confusing). - pblock->print() in solo miner gated behind LogAcceptsCategory("net") -- was unconditionally dumping a full block to stdout on every solve. - ERROR in BitcoinMiner printf converted to LogPrint("net"). * Pool/participant: fix recv blocking, BroadcastJob mutex, listener spawn, escaped em-dash main.cpp -- StratumRecvLine rewrite: The previous version called recv() in a while loop -- if select() said the socket was readable but only a partial line had arrived, the second recv() call would block indefinitely waiting for the rest of the line. This was the root cause of the recurring stuck-worker bug. Rewrite: single non-blocking recv (MSG_DONTWAIT on Linux, FIONREAD+recv on Windows). If no complete newline is in the buffer yet, return false immediately; the outer 50ms select() loop retries when more data arrives. This means the hash loop always stays responsive regardless of network fragmentation. rpc.cpp -- BroadcastJob mutex held across send(): BroadcastJob held gMinersMutex for the entire duration of sending jobs to all connected miners. send() can block if a slow miner's TCP buffer is full. With the mutex held, MinerThreadFn could not acquire it to clean up a disconnecting miner, stalling the whole broadcast. Fix: snapshot {fd, authorised, address, extranonce2} under the mutex, release it, then send to each fd without holding any lock. rpc.cpp -- LaunchThread failure not handled in BtfStratumAcceptOneFn: The replacement listener thread was launched via LaunchThread() which wraps _beginthread() but discarded the return value. If _beginthread fails (thread limit, OOM), the listener count stayed at 1 with no thread behind it -- that relay would get no new listener until the 30s outer loop in BtfStratumAcceptFn fired. Fix: call _beginthread() directly, check the return value, decrement and log on failure. rpc.cpp -- escaped em-dash in log string: BtfStratumAcceptFn had a literal \xe2\x80\x94 in a LogPrint format string that survived the em-dash sweep (it was source-encoded, not a UTF-8 byte in the file). Replaced with '--'. * Rewrite pool server: single event loop, no per-miner threads Replace the old architecture (one thread per connected miner + separate JobWatcher thread + BtfStratumAcceptFn thread with listener count tracking) with a clean two-layer design: Accept layer (one thread per relay, AcceptOneFn): Each relay gets exactly one persistent thread. It loops: calls blocking RvServiceRegister (waits for a miner to knock), then BtfServiceWrap (echan handshake), then pushes the finished plain socket to a small lock-protected queue, then loops immediately to accept the next miner. No listener count tracking. No replacement thread spawning. No races around spawn failure. The relay always has exactly one pending accept. Event loop (PoolEventLoop, runs on the ThreadRPCServer thread): Single thread owns all connected miner sockets. Each tick: 1. Drain the accept queue -- pick up any newly-connected miners. 2. Check for a new block -- rebuild job and broadcast if so. 3. No-job watchdog -- log and retry if job is missing too long. 4. Periodic job refresh -- restamp nTime every 60s. 5. select() over all miner fds with 50ms timeout. 6. For each readable fd: non-blocking RecvLine, drain all complete lines, dispatch each to HandleLine, collect dead fds. 7. Refresh GUI stats snapshot every 2s. Because the event loop is the only writer to gMiners, no mutex is needed on the miner list. The only mutexes remaining are: - gStatsMutex: guards the GUI-visible snapshot (StatsSnapshot, gWorkerStats, gPayoutViews) written by the event loop and read by GUI threads via GetPoolOperatorStats/GetPoolWorkerStats/GetPendingPayouts. - gReadyMutex: guards the accept queue between AcceptOneFn threads and the event loop. Other improvements in this rewrite: - gPendingPayouts is event-loop-only (no mutex needed). - Round share tracking lives in the event loop (roundShareCount map + roundShareTotal counter) -- zeroed on block found, no per-miner stat mutex needed mid-round. - QueuePayouts takes the share snapshot as a parameter instead of reaching into gMinerStats under a separate mutex. - BroadcastJob no longer snapshots miners to avoid a mutex -- it runs directly in the event loop with exclusive access to gMiners. - AcceptOneFn backs off 30s on relay registration failure instead of returning -- one relay going down doesn't kill the accept thread. - No goto. No per-thread dynamic allocation for listener contexts on each accepted connection. * Thorough logging and correctness fixes across the full pool/participant flow Every failure point in the operator->miner->payout chain now logs exactly what broke, with enough context to diagnose without a debugger. Operator side (rpc.cpp): RebuildJob: logs 'no chain tip (pindexBest is null)' on failure instead of silently returning false. HandleLine: - JSON parse errors logged with the raw line (first 80 chars) - subscribe: logs fd and session id - authorize: logs address, worker, and whether the address is valid - submit: logs stale-job case with submitted vs current job id; logs high-hash with actual hash and target values; logs valid shares with nonce and running session count; logs BLOCK CANDIDATE separately from BLOCK ACCEPTED/REJECTED so a ProcessBlock failure is unambiguous - unknown method logged (reveals miner software quirks) - reply() logs send failures with method name QueuePayouts: logs total payout amount, miner count, fee applied, and how many miners were below dust threshold. FlushMaturePayouts: logs 'insufficient funds or wallet locked?' on SendMoney failure instead of just 'FAILED'. BroadcastJob: logs job id and miner count on every broadcast. Event loop: logs when each new miner socket enters the loop; logs disconnect with session and round share counts. No-job watchdog: RebuildJob retried every 5s (not every 50ms tick), eliminating cs_main hammering when there is no chain tip. AcceptLoopFn: re-polls BtfAllRelays every 60s so relays discovered after startup are picked up; logs each new relay thread spawned. AcceptOneFn: logs relay name on start/stop; logs BtfServiceWrap (echan handshake) failure separately from RvServiceRegister failure. RecvLine: disconnected out-param distinguishes 'socket closed' from 'no complete line yet', preventing incorrect disconnection on partial recv. Participant side (main.cpp): StratumRecvLine: same disconnected out-param fix as RecvLine in rpc.cpp. Previously a false return could mean either 'disconnect' or 'no data yet', causing spurious reconnects when a partial line arrived. PoolParticipantMiner rewritten with step-by-step logging: Step 1 resolve: logs specific failure (no descriptor vs bad format) Step 2 tunnel: logs relay and failure reason Step 3 address: warns explicitly if payout address cannot be determined Step 4 subscribe/authorize: logs each send and each response Step 5 RandomX: logs VM mode Step 6 main loop: - malformed mining.notify (wrong param count or short header/target) logged with sizes instead of silently ignored - authorize rejected: logs reason, does not disconnect (some pools reject bad addresses but still send work) - share accepted/rejected: logs share id and rejection reason - unknown server method logged - pool disconnect logged with handshake state at that moment (gotSubscribeAck, gotAuthorizeAck, haveJob) so you know exactly how far the connection got - nonce space exhausted logged (was silent before) - on exit: logs total hashes, shares sent, shares accepted * Fix build error: ImGuiWindowFlags_NoBringToDisplayFront -> NoBringToFrontOnFocus NoBringToDisplayFront does not exist in this version of ImGui (v1.89.x). The correct flag is ImGuiWindowFlags_NoBringToFrontOnFocus (bit 13). Used in two places: ##statusbar and ##main windows. Also remove dead variable usedY (GetCursorPosY() result assigned but never read) and replace the confusing 'ImGuiWindowFlags_NoScrollbar * 0' expression (evaluates to 0 but looks like a mistake) with a plain 0 in the BeginChild call. * Remove duplicate status logging from SetParticipantMiningStatus Every transition in PoolParticipantMiner already has a direct LogPrint with full context (addresses, job ids, nonces, error reasons). The LogPrint inside SetParticipantMiningStatus was firing on top of those, producing redundant 'PoolParticipantMiner: status -> ...' lines after every meaningful log entry. SetParticipantMiningStatus now just updates the GUI string silently. * Implement vardiff: stable share rate instead of fixed difficulty Previously the pool set shareTarget = blockTarget >> 8 for every miner forever. This meant at low network difficulty a fast miner found shares every few nonces (dozens per second), while at high difficulty a slow miner might go hours without one -- both are bad for fair payout accounting and pool stability. Pool (rpc.cpp): Per-miner difficulty that adjusts to target one share every 15 seconds. After every 5 shares, check actual seconds/share vs target. If the ratio is off by more than 50% (VARDIFF_RETARGET_RATIO), recompute: new_difficulty = current_difficulty / (actual_secs_per_share / target) i.e. shares coming too fast -> difficulty goes up; too slow -> down. Only apply if the change exceeds 10% to avoid jitter. Difficulty is bounded to [0.001, 1e12]. Each miner gets their own shareTarget derived from their difficulty via ShareTargetFromDifficulty(), which maps Stratum difficulty to a uint256 target using the Bitcoin difficulty-1 base (0x1d00ffff compact). New difficulty is sent to the miner via mining.set_difficulty. Participant (main.cpp): Apply the received difficulty value from mining.set_difficulty. Previously the handler was a no-op ('acknowledge only'); now it parses the difficulty double and logs the change. The share target used for comparison comes from the pool's mining.notify p[3] field, which the pool updates after each vardiff adjustment -- this is the standard approach and matches how cpuminer, xmrig etc. handle it. With vardiff, share rate is roughly constant regardless of miner speed or network difficulty. Payouts are proportional to shares submitted, so each miner's payout accurately reflects their CPU contribution. * Fix share target mismatch: pool rejected shares it should have accepted Root cause: the pool put job.shareTarget (from blockTarget<<8, a fixed per-job value) into mining.notify p[3], but checked m->shareTarget (from ShareTargetFromDifficulty, the vardiff value) on submit. With difficulty=1.0, ShareTargetFromDifficulty gives a much HARDER target than blockTarget<<8, so shares that the participant thought were valid (passed the easier notify target) were rejected by the pool (failed the harder difficulty-1 target). Fix: MakeNotifyParams now takes the per-miner shareTarget as an explicit parameter. SendJob passes m->shareTarget. Both sides -- the participant reading p[3] and HandleLine checking m->shareTarget on submit -- now use exactly the same value. Single source of truth. Cleanup: - Remove shareTarget field from StratumJob (was only set, never read after the MakeNotifyParams call was fixed). - Remove MakeShareTarget() function (was only used to populate that now-dead field). - Participant set_difficulty log: 'initial' vs 'updated' instead of 'unchanged' (first connect sends 1.0 which matches the default 1.0, so it was misleadingly logged as 'unchanged'). * Fix double broadcast, correct block reward calculation, improve payout log Double broadcast (event loop vs HandleLine): When a pool miner finds a block, HandleLine calls RebuildJob() + BroadcastJob(true) immediately. The event loop then detects nBestHeight changed (step 2) and was calling RebuildJob() + BroadcastJob(true) again -- miners received duplicate job broadcasts. Fix: step 2 now checks gHaveJob before rebuilding. If HandleLine already built and broadcast a job for this height (gHaveJob=true), step 2 just logs 'job already broadcast by submit handler' and skips. If the block came from an external peer (not our miner), gHaveJob is false and step 2 handles it as before. Block reward calculation in QueuePayouts: Was calling CBlock().GetBlockValue(0) on an empty block object. GetBlockValue uses nBestHeight globally, which by the time QueuePayouts runs has already been incremented to the height AFTER the found block. Fix: compute subsidy directly as '50 COIN >> (blockHeight / 210000)' using the actual found height. At current scale this makes no difference (same subsidy for thousands of blocks) but is now correct by construction. Payout log now shows: - Full block subsidy - Miner share after fee - Operator fee amount in BTF (not just percentage) - Number of recipients, maturity height, dust-filtered count Example: '[payout] block 2096: subsidy 50.00 BTF, miners get 49.00 (fee 2.00% = 1.00 to operator), 3 recipients, matures height 2196' * Fix 6 bugs found in full flow re-read Bug 1 -- Double broadcast (re-applied fix that was lost): When HandleLine accepted a block it called RebuildJob()+BroadcastJob(true). The event loop then detected nBestHeight changed and did it again. Miners received two back-to-back mining.notify clean=true, wasting their work. Fix: step 2 of event loop checks gHaveJob before rebuilding. If HandleLine already built a fresh job (gHaveJob=true), step 2 just logs and skips. Bug 3 -- Disconnect log showed global totals, not session totals: gParticipantSharesSent/Accepted accumulate since node start, not since the last connect. Added sessionSharesSent/sessionSharesAccepted local counters that reset each time PoolParticipantMiner() is called. Bug 4 -- Dead function ParticipantShareTargetFromDiff (main.cpp): Defined but never called anywhere. Removed. Bug 5 -- Invalid-address miner shares silently burned: If a miner authorized with an invalid address, their share of the block reward was neither paid out nor logged -- it just vanished. QueuePayouts now validates addresses up front, logs the forfeited amount explicitly ('X BTF forfeited (bad address)'), and excludes those entries so the payout log is accurate. Bug 6 -- SendMoney called from event loop (stalls miner I/O): FlushMaturePayouts called SendMoney which takes cs_main. If the P2P thread held cs_main (processing a block), the event loop would stall for seconds, causing all connected miners to time out waiting for responses. Fix: FlushMaturePayouts now only moves matured entries to gPayoutExecQueue (fast, no locks except a brief mutex). A new PayoutThreadFn thread drains the queue and calls SendMoney independently, so the event loop is never blocked by wallet I/O. Bug 7 -- Dead goto + dead HandleLine return-false path: HandleLine always returns true; the goto next_miner path was never reached. Removed goto and simplified the select drain loop. * Fix share-target scale mismatch and dead pool hashrate reporting - ShareTargetFromDifficulty() based Stratum difficulty 1 on Bitcoin's SHA256 diff-1 constant (0x1d00ffff, ~2^32 hashes/share) instead of this chain's actual PoW floor (bnProofOfWorkLimit = ~uint256(0)>>12, ~2^12 hashes/share). That's a ~2^20 (~1,048,576x) mismatch: at the supposedly-easy difficulty of 1.0, workers were being asked for shares roughly a million times harder than the chain's own easiest target. Vardiff never gets a chance to correct this because it only retargets after real shares come in, and none were coming in. Fixed by basing the diff-1 reference on bnProofOfWorkLimit instead. - Pool hashrate was never computed anywhere despite the data (per-miner difficulty + share timing) being available: ThreadBtfPoolAnnouncer in nostr.cpp hardcoded ann.hashRate = 0.0, PoolWorkerStatView had no hashrate field, and the GUI workers table had no column for it. Added an EWMA hashrate estimate per miner (updated on each accepted share from expected-hashes-at-difficulty / time-since-last-share), threaded it through GetPoolOperatorStats/GetPoolWorkerStats, the pool announcement payload, and the GUI (new Hashrate column + totals in the telemetry line). Verified: full clean build via src/Makefile (secp256k1 + RandomX deps built from source, g++ 13, all four touched files compile and link). * Remove stale wxWidgets-era build files and orphaned assets These predate the ImGui/GLFW migration (83af065) and are dead weight: - src/makefile.linux, build-linux.sh: superseded by src/Makefile + root Makefile (make linux). The old ones reference obj/uibase.o and obj/ui.o, which don't exist -- they never worked post-migration. - src/makefile.vc: MSVC build for the old wx UI (obj\ui.obj, obj\uibase.obj, obj\irc.obj) -- irc.cpp doesn't even exist anymore, and nothing invokes this makefile. Windows builds go through makefile.mingw via the root Makefile's target. - src/ui.rc, src/uiproject.fbp: wxFormBuilder project/resource files for the old wx UI. The current Windows resource file is bitflash.rc. - src/rc/{addressbook,send}*.bmp, check.ico, bitcoin_bc_original.ico: icons used only by the old wx UI. bitcoin.ico (used by bitflash.rc) is kept. Verified nothing includes uibase.h/ui.h/irc.h anywhere, and did a full clean rebuild afterward (secp256k1 + RandomX from source, src/Makefile) to confirm removal doesn't break the real build. * Mine with RandomX fast (2GB dataset) mode instead of light mode forever RandomXInitDataset() was fully implemented and ready to switch mining into fast mode, but nothing ever called it -- not in PoolParticipantMiner() (pool worker), not in the solo BitcoinMiner() loop. RandomXCreateMinerVM() falls back to light mode whenever the dataset isn't built, so every miner, participant or solo, has always been hashing in light/cache-only mode. Light mode exists for cheap single-hash verification (which is why the pool's own share verification correctly stays on it) -- it recomputes dataset items on the fly for every hash, which is dramatically slower than fast mode's direct dataset lookups. Combined with the share-target scale bug fixed in e87a905, this compounds into exactly the symptom reported: a participant sitting there hashing for many minutes per share instead of the vardiff-targeted ~15s, even after the target fix landed. Fix: build the ~2GB dataset once (multi-threaded, sized to hardware_concurrency) before mining starts, in both the participant and solo paths, so RandomXCreateMinerVM() actually gets to use fast mode. Verified: full clean rebuild via src/Makefile. * Add vardiff stall watchdog: force difficulty down on zero shares, not just share count ShareTargetFromDifficulty already fixed the diff-1 scale mismatch (chain floor vs Bitcoin's SHA256 diff-1), so difficulty 1 correctly means ~4096 expected hashes on this chain, not ~2^32. That part was already correct. But the normal vardiff retarget in HandleLine only runs after VARDIFF_SHARES_PER_CHECK (5) shares arrive. A miner that never finds even one share -- e.g. stuck in RandomX light/cache mode instead of the fast 2GB dataset, or just slower hardware than the target assumes -- never reaches that check, so it can sit at its initial/current difficulty indefinitely with vardiff having no samples to act on. Add a time-based fallback in PoolEventLoop: if a miner's vardiff window has been open for VARDIFF_STALL_SECS (90s, 6x the 15s target) with zero shares, force difficulty down by VARDIFF_STALL_CUT (4x) regardless of share count, and re-notify via mining.set_difficulty. Once a miner is already at VARDIFF_MIN, log it plainly as a hashrate/connectivity issue instead of a difficulty issue, so it's not confused with the same-looking but different problem this was actually fixed for before. * Fix PrintException rethrow killing the whole node on any transient exception The uploaded participant log shows the real bug behind the mining stalls: the node process itself was dying and restarting from scratch every minute or two ('UNKNOWN EXCEPTION ... in ThreadMessageHandler()' / 'ThreadSocketHandler()'), wiping the RandomX dataset/cache and losing all progress toward a share -- not a difficulty problem. Every background thread (ThreadMessageHandler, ThreadSocketHandler, ThreadBtfConnect, the Nostr helpers) wraps its real work in a 'try { Foo2() } catch(...) { log, sleep 5s, loop again }' self-healing retry pattern specifically so one bad exception doesn't take the whole node down. But PrintException(), called from inside every one of those catch blocks, ended with 'if (!fShutdown) throw;' -- re-throwing the just- caught exception during ordinary operation. Nothing above the retry loop catches it, so it escapes the thread and std::terminate()s the entire process on the very first transient error (a flaky relay connect, a malformed message, whatever). The comment's intent (suppress noise while already shutting down) was inverted from what the surrounding retry loops actually need. Removed the rethrow. Threads now log the exception and let their own retry loop handle it, as designed. * Push a fresh job immediately after any difficulty change, not just on next broadcast mining.set_difficulty alone doesn't change what the worker actually hashes against -- the participant only reads its share target from mining.notify's params[3] (see main.cpp's mining.notify handler); the set_difficulty message just updates a locally-logged currentDifficulty value. So both the normal vardiff retarget (in HandleLine, on 5 shares) and the new stall watchdog were updating m->shareTarget and telling the worker about the new difficulty, but the worker kept hashing against its old target until the next natural job broadcast -- a new block, or the 60s trickle refresh. That could delay any difficulty correction by up to a minute for no reason. Call SendJob(m, false) right after each set_difficulty send so the updated shareTarget reaches the worker in the very next tick instead of waiting on the next unrelated broadcast. * Build RandomX dataset before stratum handshake, not after Worker was sending mining.subscribe/authorize, then blocking for the duration of the synchronous ~2GB RandomX dataset build, and only then reading the socket. The pool's replies (subscribe ack, authorize ack, set_difficulty, first job) sat unread in the OS receive buffer for that entire window -- nothing was lost, but with the build taking anywhere from seconds to a couple minutes depending on core count, the worker looked and logged as permanently stuck on 'waiting for pool response' when it just wasn't listening yet. Move the dataset build ahead of the subscribe/authorize sends so the socket is actively being read the moment the pool replies. Also add missing RandomXDestroyMinerVM cleanup on the two early-return paths that now happen after VM creation. * Reset round shares on externally-found blocks, not just our own Round shares were only cleared in HandleLine() after our own miner's block got accepted. When the chain height advanced because someone else found the block, the main loop rebuilt the job but left roundShareCount/roundShareTotal/Miner::roundShares untouched, so they kept accumulating across rounds instead of resetting. * Show pool hashrate in participant view; show immature coinbase value with maturity countdown * Persist pending payouts to disk -- survive operator restarts * Fix log escapes, stall timeout 90->45s, save-before-exec ordering, Immature tx status * Sort transactions by nTimeReceived not chain median time -- new txs always on top * Sort txs by chain depth not nTimeReceived -- survives resync, unconfirmed always first * Fix orphan block false-positive payout; meaningful tx status instead of raw depth * Show Orphaned status in red for orphaned coinbases; distinguish from genuinely unconfirmed --------- Co-authored-by: Vic-Nas <nasci.victorio@gmail.com> Co-authored-by: Bitflash Pool Dev <pool@bitflash> Co-authored-by: Victorio Nascimento <155357371+Vic-Nas@users.noreply.github.com> Co-authored-by: Claude <claude@anthropic.com>
Diffstat (limited to 'src/gui.cpp')
-rw-r--r--src/gui.cpp701
1 files changed, 424 insertions, 277 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
index 2210296..a1a0698 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -1,4 +1,4 @@
1// Bitflash GUI — ImGui + GLFW + OpenGL3 1// Bitflash GUI -- ImGui + GLFW + OpenGL3
2// Same source on Linux and Windows. No platform ifdefs. 2// Same source on Linux and Windows. No platform ifdefs.
3 3
4#include "imgui/imgui.h" 4#include "imgui/imgui.h"
@@ -25,6 +25,7 @@ extern std::string strParticipantPool;
25extern CAddress addrLocalHost; 25extern CAddress addrLocalHost;
26 26
27void MainFrameRepaint(); 27void MainFrameRepaint();
28int GetDiscoveredPeerCount();
28string DateTimeStr(int64 nTime); 29string DateTimeStr(int64 nTime);
29bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); 30bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew);
30int64 GetBalance(); 31int64 GetBalance();
@@ -34,15 +35,20 @@ void ThreadBitcoinMiner(void*);
34void ThreadRPCServer(void*); 35void ThreadRPCServer(void*);
35 36
36// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
37// State 38// Layout constants
39// ---------------------------------------------------------------------------
40static const float STATUS_BAR_H = 22.0f;
41
42// ---------------------------------------------------------------------------
43// GUI state
38// --------------------------------------------------------------------------- 44// ---------------------------------------------------------------------------
39static std::string g_myAddress; 45static std::string g_myAddress;
40static int64 g_balance = 0; 46static int64 g_balance = 0;
41static int64 g_lastWalletRefresh = 0; 47static int64 g_lastWalletRefresh = 0;
42static bool g_showSend = false; 48static bool g_showSend = false;
43static bool g_showOptions = false; 49static bool g_showOptions = false;
44static bool g_showAbout = false; 50static bool g_showAbout = false;
45static bool g_needRefresh = true; 51static bool g_needRefresh = true;
46 52
47static char g_sendAddr[128] = {}; 53static char g_sendAddr[128] = {};
48static char g_sendAmount[32] = {}; 54static char g_sendAmount[32] = {};
@@ -73,36 +79,34 @@ static std::string FmtMoney(int64 n)
73static std::string FmtAge(int64 createdAt) 79static std::string FmtAge(int64 createdAt)
74{ 80{
75 int64 age = GetTime() - createdAt; 81 int64 age = GetTime() - createdAt;
76 if (age < 0) 82 if (age < 0) age = 0;
77 age = 0; 83 if (age < 60) return strprintf("%llds", (long long)age);
78 if (age < 60) 84 if (age < 3600) return strprintf("%lldm", (long long)(age / 60));
79 return strprintf("%llds", (long long)age); 85 return strprintf("%lldh", (long long)(age / 3600));
80 if (age < 3600)
81 return strprintf("%lldm", (long long)(age / 60));
82 return strprintf("%lldh", (long long)(age / 3600));
83} 86}
84 87
85static ImVec4 ParticipantStatusColor(const std::string& status) 88static ImVec4 StatusColor(const std::string& s)
86{ 89{
87 if (status.find("failed") != std::string::npos || 90 if (s.find("failed") != std::string::npos ||
88 status.find("rejected") != std::string::npos || 91 s.find("rejected") != std::string::npos ||
89 status.find("stopped") != std::string::npos) 92 s.find("stopped") != std::string::npos)
90 return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); 93 return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); // red
91 if (status.find("hashing") != std::string::npos || 94 if (s.find("hashing") != std::string::npos ||
92 status.find("accepted") != std::string::npos || 95 s.find("accepted") != std::string::npos ||
93 status.find("connected") != std::string::npos) 96 s.find("connected")!= std::string::npos)
94 return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); 97 return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); // green
95 if (status.find("asking") != std::string::npos || 98 if (s.find("asking") != std::string::npos ||
96 status.find("authoriz") != std::string::npos || 99 s.find("authoriz") != std::string::npos ||
97 status.find("subscrib") != std::string::npos) 100 s.find("subscrib") != std::string::npos)
98 return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); 101 return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); // amber
99 return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); 102 return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); // blue-grey
100} 103}
101 104
102static std::string PoolLabel(const BtfPoolAnnouncement& ann) 105static std::string PoolLabel(const BtfPoolAnnouncement& ann)
103{ 106{
104 std::ostringstream o; 107 std::ostringstream o;
105 o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%"; 108 o << ann.poolName << " - "
109 << std::fixed << std::setprecision(2) << ann.feePercent << "%";
106 if (ann.connectedMiners > 0) 110 if (ann.connectedMiners > 0)
107 o << " - " << ann.connectedMiners << " miners"; 111 o << " - " << ann.connectedMiners << " miners";
108 return o.str(); 112 return o.str();
@@ -118,19 +122,47 @@ static void RefreshWallet()
118 g_txRows.clear(); 122 g_txRows.clear();
119 TRY_CRITICAL_BLOCK(cs_mapWallet) 123 TRY_CRITICAL_BLOCK(cs_mapWallet)
120 { 124 {
121 std::vector<std::pair<int64,uint256>> vs; 125 // Sort: unconfirmed first (depth 0), then by depth ascending (least
122 for (auto& kv : mapWallet) vs.push_back({kv.second.GetTxTime(), kv.first}); 126 // deep = most recent). We can't use nTimeReceived because a node
123 std::sort(vs.rbegin(), vs.rend()); 127 // resync re-stamps all txs with the same time. Depth is always correct.
128 std::vector<std::pair<int,uint256>> vs;
129 for (auto& kv : mapWallet)
130 vs.push_back({kv.second.GetDepthInMainChain(), kv.second.GetHash()});
131 std::sort(vs.begin(), vs.end(),
132 [](const std::pair<int,uint256>& a, const std::pair<int,uint256>& b) {
133 return a.first < b.first; // 0 (unconfirmed) first, then shallowest
134 });
124 for (auto& sv : vs) { 135 for (auto& sv : vs) {
125 auto mi = mapWallet.find(sv.second); 136 auto mi = mapWallet.find(sv.second);
126 if (mi == mapWallet.end()) continue; 137 if (mi == mapWallet.end()) continue;
127 CWalletTx& wtx = mi->second; 138 CWalletTx& wtx = mi->second;
128 int64 net = wtx.GetCredit() - wtx.GetDebit(); 139 int depth = wtx.GetDepthInMainChain();
129 int depth = wtx.GetDepthInMainChain(); 140 int toMature = wtx.IsCoinBase() ? wtx.GetBlocksToMaturity() : 0;
141 // An orphaned coinbase has hashBlock set but depth=0 (block not in main chain).
142 // A genuinely unconfirmed tx has hashBlock==0.
143 bool isOrphaned = wtx.IsCoinBase() && wtx.hashBlock != 0 && depth == 0;
144 int64 net;
130 std::string desc; 145 std::string desc;
131 if (wtx.IsCoinBase()) desc = depth<1 ? "Generated (unconfirmed)" : "Generated"; 146 if (wtx.IsCoinBase()) {
132 else if (wtx.GetDebit() > 0) desc = "Sent"; 147 if (isOrphaned) {
133 else desc = "Received"; 148 net = 0;
149 desc = "Generated (orphaned -- block not in main chain)";
150 } else if (toMature > 0) {
151 net = wtx.GetValueOut();
152 char buf[64];
153 snprintf(buf, sizeof(buf), "Generated (matures in %d blocks)", toMature);
154 desc = buf;
155 } else {
156 net = wtx.GetCredit() - wtx.GetDebit();
157 desc = depth < 1 ? "Generated (unconfirmed)" : "Generated";
158 }
159 } else if (wtx.GetDebit() > 0) {
160 net = wtx.GetCredit() - wtx.GetDebit();
161 desc = "Sent";
162 } else {
163 net = wtx.GetCredit() - wtx.GetDebit();
164 desc = "Received";
165 }
134 g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); 166 g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth});
135 } 167 }
136 } 168 }
@@ -149,18 +181,103 @@ string DateTimeStr(int64 nTime)
149} 181}
150 182
151// --------------------------------------------------------------------------- 183// ---------------------------------------------------------------------------
184// Status bar -- separate fullscreen-width overlay pinned to bottom edge.
185// Drawn last so it always sits on top of everything else.
186// ---------------------------------------------------------------------------
187static void DrawStatusBar()
188{
189 ImGuiIO& io = ImGui::GetIO();
190 ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y - STATUS_BAR_H));
191 ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, STATUS_BAR_H));
192 ImGui::SetNextWindowBgAlpha(1.0f);
193 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 3.0f));
194 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
195 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
196 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.13f, 0.13f, 0.13f, 1.0f));
197 ImGui::Begin("##statusbar", nullptr,
198 ImGuiWindowFlags_NoTitleBar |
199 ImGuiWindowFlags_NoResize |
200 ImGuiWindowFlags_NoMove |
201 ImGuiWindowFlags_NoScrollbar |
202 ImGuiWindowFlags_NoScrollWithMouse|
203 ImGuiWindowFlags_NoSavedSettings |
204 ImGuiWindowFlags_NoBringToFrontOnFocus);
205 ImGui::PopStyleVar(3);
206 ImGui::PopStyleColor();
207
208 int connected = (int)vNodes.size();
209 int discovered = GetDiscoveredPeerCount();
210
211 if (nMineMode == MINE_PARTICIPANT) {
212 uint64 sent = 0, accepted = 0;
213 double hashRate = 0.0;
214 std::string st = GetParticipantMiningStatus();
215 GetParticipantMiningStats(sent, accepted, hashRate);
216 if (sent > 0 || accepted > 0 || hashRate > 0.0)
217 ImGui::TextDisabled(
218 "Connected peers: %d Detected peers: %d Height: %d"
219 " Shares: %llu sent / %llu accepted %.2f H/s",
220 connected, discovered, nBestHeight,
221 (unsigned long long)sent, (unsigned long long)accepted, hashRate);
222 else if (!st.empty())
223 ImGui::TextDisabled(
224 "Connected peers: %d Detected peers: %d Height: %d %s",
225 connected, discovered, nBestHeight, st.c_str());
226 else
227 ImGui::TextDisabled(
228 "Connected peers: %d Detected peers: %d Height: %d",
229 connected, discovered, nBestHeight);
230 } else if (nMineMode == MINE_OPERATOR) {
231 int miners = 0, blocksFound = 0;
232 uint64 roundShares = 0;
233 double totalHashRate = 0.0;
234 GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate);
235 ImGui::TextDisabled(
236 "Connected peers: %d Detected peers: %d Height: %d"
237 " Miners: %d Round shares: %llu Blocks found: %d %.2f H/s",
238 connected, discovered, nBestHeight,
239 miners, (unsigned long long)roundShares, blocksFound, totalHashRate);
240 } else {
241 ImGui::TextDisabled(
242 "Connected peers: %d Detected peers: %d Height: %d",
243 connected, discovered, nBestHeight);
244 }
245
246 ImGui::End();
247}
248
249// ---------------------------------------------------------------------------
152// Main window 250// Main window
251//
252// Layout:
253// [menu bar] -- ImGui built-in menu
254// [header strip] -- address, balance, send button, mine status
255// [separator]
256// [telemetry strip] -- mode-specific live stats (never scrolls)
257// [separator]
258// [scrollable content child] -- transactions + mode panels
259//
260// The scrollable child ends at the top of the status bar so content never
261// hides behind it. The child is explicitly sized to fill the remaining space.
153// --------------------------------------------------------------------------- 262// ---------------------------------------------------------------------------
154static void DrawMainWindow() 263static void DrawMainWindow()
155{ 264{
156 ImGuiIO& io = ImGui::GetIO(); 265 ImGuiIO& io = ImGui::GetIO();
157 ImGui::SetNextWindowPos(ImVec2(0,0)); 266
158 ImGui::SetNextWindowSize(io.DisplaySize); 267 // Full display height minus the status bar at the bottom.
159 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12,12)); 268 const float winH = io.DisplaySize.y - STATUS_BAR_H;
269
270 ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f));
271 ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, winH));
272 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 12.0f));
160 ImGui::Begin("##main", nullptr, 273 ImGui::Begin("##main", nullptr,
161 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | 274 ImGuiWindowFlags_NoTitleBar |
162 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_MenuBar | 275 ImGuiWindowFlags_NoResize |
163 ImGuiWindowFlags_NoFocusOnAppearing); 276 ImGuiWindowFlags_NoMove |
277 ImGuiWindowFlags_NoScrollbar |
278 ImGuiWindowFlags_NoScrollWithMouse |
279 ImGuiWindowFlags_MenuBar |
280 ImGuiWindowFlags_NoBringToFrontOnFocus);
164 ImGui::SetWindowFontScale(0.92f); 281 ImGui::SetWindowFontScale(0.92f);
165 ImGui::PopStyleVar(); 282 ImGui::PopStyleVar();
166 283
@@ -170,179 +287,209 @@ static void DrawMainWindow()
170 if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); 287 if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true);
171 ImGui::EndMenu(); 288 ImGui::EndMenu();
172 } 289 }
173 if (ImGui::MenuItem("Options")) g_showOptions = true; 290 if (ImGui::MenuItem("Options")) g_showOptions = true;
174 if (ImGui::MenuItem("About")) g_showAbout = true; 291 if (ImGui::MenuItem("About")) g_showAbout = true;
175 ImGui::EndMenuBar(); 292 ImGui::EndMenuBar();
176 } 293 }
177 294
178 // ---- Address bar ---- 295 // ---- Header strip (never scrolls) ----
179 ImGui::Spacing(); 296 ImGui::Spacing();
180 ImGui::TextDisabled("Address:"); 297 ImGui::TextDisabled("Address:");
181 ImGui::SameLine(); 298 ImGui::SameLine();
182 ImGui::SetNextItemWidth(-80); 299 ImGui::SetNextItemWidth(-80.0f);
183 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f,0.14f,0.14f,1)); 300 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 1.0f));
184 ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, 301 ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1,
185 ImGuiInputTextFlags_ReadOnly); 302 ImGuiInputTextFlags_ReadOnly);
186 ImGui::PopStyleColor(); 303 ImGui::PopStyleColor();
187 ImGui::SameLine(); 304 ImGui::SameLine();
188 if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); 305 if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str());
189 306
190 // ---- Balance on its own line ----
191 ImGui::TextDisabled("Balance:"); 307 ImGui::TextDisabled("Balance:");
192 ImGui::SameLine(); 308 ImGui::SameLine();
193 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f,1.0f,0.35f,1.0f)); 309 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f, 1.0f, 0.35f, 1.0f));
194 ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); 310 ImGui::Text("%s BTF", FmtMoney(g_balance).c_str());
195 ImGui::PopStyleColor(); 311 ImGui::PopStyleColor();
196 312
197 // ---- Toolbar ----
198 ImGui::Spacing(); 313 ImGui::Spacing();
199 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f,0.40f,0.15f,1)); 314 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.40f, 0.15f, 1.0f));
200 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f,0.55f,0.20f,1)); 315 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.55f, 0.20f, 1.0f));
201 if (ImGui::Button(" Send Coins ")) g_showSend = true; 316 if (ImGui::Button(" Send Coins ")) g_showSend = true;
202 ImGui::PopStyleColor(2); 317 ImGui::PopStyleColor(2);
203 318
204 // Mining status indicator 319 ImGui::SameLine(0.0f, 20.0f);
205 ImGui::SameLine(0, 20);
206 if (fGenerateBitcoins) { 320 if (fGenerateBitcoins) {
207 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f,0.6f,0.0f,1.0f)); 321 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
208 const char* modeLabel = nMineMode==MINE_OPERATOR ? "Mining (operator)" : 322 const char* modeLabel =
209 nMineMode==MINE_PARTICIPANT ? "Mining (pool)" : "Mining"; 323 nMineMode == MINE_OPERATOR ? "Mining (operator)" :
210 ImGui::Text("⬤ %s", modeLabel); 324 nMineMode == MINE_PARTICIPANT ? "Mining (pool)" :
325 nMineMode == MINE_RELAY ? "Relay" : "Mining";
326 ImGui::Text("* %s", modeLabel);
211 ImGui::PopStyleColor(); 327 ImGui::PopStyleColor();
212 if (nMineMode == MINE_PARTICIPANT) { 328 if (nMineMode == MINE_PARTICIPANT) {
213 std::string mineStatus = GetParticipantMiningStatus(); 329 std::string st = GetParticipantMiningStatus();
214 if (!mineStatus.empty()) { 330 if (!st.empty()) {
215 ImGui::SameLine(0, 12); 331 ImGui::SameLine(0.0f, 12.0f);
216 ImGui::PushStyleColor(ImGuiCol_Text, ParticipantStatusColor(mineStatus)); 332 ImGui::PushStyleColor(ImGuiCol_Text, StatusColor(st));
217 ImGui::Text("[%s]", mineStatus.c_str()); 333 ImGui::Text("[%s]", st.c_str());
218 ImGui::PopStyleColor(); 334 ImGui::PopStyleColor();
219 } 335 }
220 } 336 }
221 } else { 337 } else {
222 ImGui::TextDisabled("○ Not mining"); 338 ImGui::TextDisabled("Not mining");
223 } 339 }
224 340
225 ImGui::Spacing(); 341 ImGui::Spacing();
226 ImGui::Separator(); 342 ImGui::Separator();
227 ImGui::Spacing();
228 343
229 // ---- Live telemetry strip ---- 344 // ---- Telemetry strip (never scrolls) ----
230 { 345 {
231 const char* modeName = nMineMode==MINE_OPERATOR ? "Operator" : 346 ImGui::Spacing();
232 nMineMode==MINE_PARTICIPANT ? "Participant" : "Solo"; 347 const char* modeName =
233 int64 age = g_lastWalletRefresh > 0 ? (GetTime() - g_lastWalletRefresh) : -1; 348 nMineMode == MINE_OPERATOR ? "Operator" :
234 if (age < 0) age = 0; 349 nMineMode == MINE_PARTICIPANT ? "Participant" :
350 nMineMode == MINE_RELAY ? "Relay" : "Solo";
351 int64 age = g_lastWalletRefresh > 0 ? GetTime() - g_lastWalletRefresh : 0;
235 352
236 ImGui::Text("Mode: %s", modeName); 353 ImGui::Text("Mode: %s", modeName);
237 ImGui::SameLine(0, 16); 354 ImGui::SameLine(0.0f, 16.0f);
238 ImGui::Text("Wallet refresh: %llds ago", (long long)age); 355 ImGui::TextDisabled("Wallet refreshed %llds ago", (long long)age);
239 356
240 if (nMineMode == MINE_PARTICIPANT) { 357 if (nMineMode == MINE_PARTICIPANT) {
241 uint64 sent = 0, accepted = 0; 358 uint64 sent = 0, accepted = 0;
242 double hashRate = 0.0; 359 double hashRate = 0.0;
243 std::string mineStatus = GetParticipantMiningStatus(); 360 std::string st = GetParticipantMiningStatus();
244 GetParticipantMiningStats(sent, accepted, hashRate); 361 GetParticipantMiningStats(sent, accepted, hashRate);
245 if (sent > 0 || accepted > 0 || hashRate > 0.0) { 362 if (sent > 0 || accepted > 0 || hashRate > 0.0) {
246 double acceptPct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; 363 double pct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0;
247 ImGui::TextDisabled("Your mining: sent %llu, accepted %llu (%.1f%%), %.2f H/s", 364 ImGui::TextDisabled(
248 (unsigned long long)sent, 365 "Sent %llu shares, accepted %llu (%.1f%%), %.2f H/s",
249 (unsigned long long)accepted, 366 (unsigned long long)sent, (unsigned long long)accepted, pct, hashRate);
250 acceptPct, 367 } else if (!st.empty()) {
251 hashRate); 368 ImGui::TextDisabled("Status: %s", st.c_str());
252 } else if (!mineStatus.empty()) {
253 ImGui::TextDisabled("Your mining: %s", mineStatus.c_str());
254 } else { 369 } else {
255 ImGui::TextDisabled("Your mining: asking pool for work"); 370 ImGui::TextDisabled("Waiting for pool work...");
256 } 371 }
257 } else if (nMineMode == MINE_OPERATOR) { 372 } else if (nMineMode == MINE_OPERATOR) {
258 int miners = 0, blocksFound = 0; 373 int miners = 0, blocksFound = 0;
259 uint64 roundShares = 0; 374 uint64 roundShares = 0;
260 GetPoolOperatorStats(miners, blocksFound, roundShares); 375 double totalHashRate = 0.0;
376 GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate);
261 if (miners > 0 || blocksFound > 0 || roundShares > 0) 377 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); 378 ImGui::TextDisabled(
379 "Authorized miners: %d Round shares: %llu Blocks found: %d %.2f H/s",
380 miners, (unsigned long long)roundShares, blocksFound, totalHashRate);
263 else 381 else
264 ImGui::TextDisabled("Pool runtime: waiting for miners"); 382 ImGui::TextDisabled("Pool server running, waiting for miners");
383 } else if (nMineMode == MINE_RELAY) {
384 ImGui::TextDisabled("Relay mode: syncing, not mining");
265 } else { 385 } else {
266 ImGui::TextDisabled("Solo runtime: local wallet mining telemetry"); 386 ImGui::TextDisabled("Solo mining");
267 } 387 }
268 ImGui::Spacing(); 388 ImGui::Spacing();
269 ImGui::Separator(); 389 ImGui::Separator();
270 ImGui::Spacing(); 390 ImGui::Spacing();
271 } 391 }
272 392
393 // ---- Scrollable content child ----
394 // Fills all remaining height in the window (cursor Y to bottom of window).
395 // Content that overflows scrolls; nothing leaks past the window edge.
396 // Size the scrollable child to fill exactly the remaining window height.
397 // GetContentRegionAvail() already accounts for padding/borders.
398 const float availH = ImGui::GetContentRegionAvail().y;
399 ImGui::BeginChild("##scroll", ImVec2(0.0f, availH), false, 0);
400
273 // ---- Transaction list ---- 401 // ---- Transaction list ----
274 float listHeight = io.DisplaySize.y * 0.26f; 402 float txH = io.DisplaySize.y * 0.26f;
275 if (listHeight < 190.0f) listHeight = 190.0f; 403 if (txH < 180.0f) txH = 180.0f;
276 if (listHeight > 260.0f) listHeight = 260.0f; 404 if (txH > 260.0f) txH = 260.0f;
405
277 if (ImGui::BeginTable("txlist", 4, 406 if (ImGui::BeginTable("txlist", 4,
278 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | 407 ImGuiTableFlags_Borders |
279 ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, 408 ImGuiTableFlags_RowBg |
280 ImVec2(0, listHeight))) 409 ImGuiTableFlags_ScrollY |
410 ImGuiTableFlags_SizingStretchProp,
411 ImVec2(0.0f, txH)))
281 { 412 {
282 ImGui::TableSetupScrollFreeze(0, 1); 413 ImGui::TableSetupScrollFreeze(0, 1);
283 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110); 414 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110.0f);
284 ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130); 415 ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130.0f);
285 ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); 416 ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch);
286 ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140); 417 ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140.0f);
287 ImGui::TableHeadersRow(); 418 ImGui::TableHeadersRow();
288 419
289 for (auto& row : g_txRows) { 420 for (auto& row : g_txRows) {
290 ImGui::TableNextRow(); 421 ImGui::TableNextRow();
291 ImGui::TableSetColumnIndex(0); 422 ImGui::TableSetColumnIndex(0);
292 if (row.depth < 1) 423 bool isImmatureCoinbase = (row.desc.find("matures in") != std::string::npos);
293 ImGui::TextColored(ImVec4(1,0.6f,0,1), "Unconfirmed"); 424 bool isOrphaned = (row.desc.find("orphaned") != std::string::npos);
425 if (isOrphaned)
426 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Orphaned");
427 else if (row.depth < 1)
428 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Unconfirmed");
429 else if (isImmatureCoinbase)
430 ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Immature");
431 else if (row.depth < 6)
432 ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%d confirm%s",
433 row.depth, row.depth == 1 ? "" : "s");
294 else 434 else
295 ImGui::Text("%d blocks deep", row.depth); 435 ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Confirmed");
296 ImGui::TableSetColumnIndex(1); 436 ImGui::TableSetColumnIndex(1);
297 ImGui::TextDisabled("%s", row.date.c_str()); 437 ImGui::TextDisabled("%s", row.date.c_str());
298 ImGui::TableSetColumnIndex(2); 438 ImGui::TableSetColumnIndex(2);
299 ImGui::TextUnformatted(row.desc.c_str()); 439 ImGui::TextUnformatted(row.desc.c_str());
300 ImGui::TableSetColumnIndex(3); 440 ImGui::TableSetColumnIndex(3);
301 if (row.amount >= 0) 441 if (row.amount >= 0)
302 ImGui::TextColored(ImVec4(0.35f,1,0.35f,1), "+%s BTF", FmtMoney(row.amount).c_str()); 442 ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f),
443 "+%s BTF", FmtMoney(row.amount).c_str());
303 else 444 else
304 ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "%s BTF", FmtMoney(row.amount).c_str()); 445 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
446 "%s BTF", FmtMoney(row.amount).c_str());
305 } 447 }
306 ImGui::EndTable(); 448 ImGui::EndTable();
307 } 449 }
308 450
451 // ---- Mode panels ----
309 std::vector<BtfPoolAnnouncement> pools; 452 std::vector<BtfPoolAnnouncement> pools;
310 BtfGetPoolAnnouncements(pools); 453 BtfGetPoolAnnouncements(pools);
311 454
312 if (nMineMode == MINE_PARTICIPANT) { 455 if (nMineMode == MINE_PARTICIPANT) {
313 ImGui::Spacing(); 456 ImGui::Spacing();
314 ImGui::SeparatorText("Participant Mining"); 457 ImGui::SeparatorText("Participant Mining");
315 ImGui::Text("Selected pool:"); 458
459 ImGui::TextDisabled("Selected pool:");
460 ImGui::SameLine();
316 if (strParticipantPool.empty()) 461 if (strParticipantPool.empty())
317 ImGui::TextDisabled("No pool selected yet"); 462 ImGui::TextDisabled("none");
318 else 463 else
319 ImGui::TextWrapped("%s", strParticipantPool.c_str()); 464 ImGui::TextWrapped("%s", strParticipantPool.c_str());
320 465
321 const BtfPoolAnnouncement* selected = NULL; 466 const BtfPoolAnnouncement* sel = nullptr;
322 for (size_t i = 0; i < pools.size(); i++) 467 for (size_t i = 0; i < pools.size(); i++)
323 if (pools[i].btfAddress == strParticipantPool) { selected = &pools[i]; break; } 468 if (pools[i].btfAddress == strParticipantPool) { sel = &pools[i]; break; }
324 469
325 if (selected) { 470 if (sel) {
326 ImGui::Text("Pool name: %s", selected->poolName.c_str()); 471 ImGui::Text("Pool: %s Fee: %.2f%% Miners: %d Blocks: %d %.2f H/s",
327 ImGui::Text("Fee: %.2f%%", selected->feePercent); 472 sel->poolName.c_str(), sel->feePercent,
328 ImGui::Text("Live stats: %d miners, %d blocks", selected->connectedMiners, selected->blocksFound); 473 sel->connectedMiners, sel->blocksFound, sel->hashRate);
329 if (!selected->dashboardUrl.empty()) 474 if (!sel->dashboardUrl.empty())
330 ImGui::TextWrapped("Dashboard: %s", selected->dashboardUrl.c_str()); 475 ImGui::TextDisabled("Dashboard: %s", sel->dashboardUrl.c_str());
331 } else if (!strParticipantPool.empty()) { 476 } else if (!strParticipantPool.empty()) {
332 ImGui::TextDisabled("No fresh announcement found for this pool yet"); 477 ImGui::TextDisabled("No live announcement for this pool yet.");
333 } 478 }
334 479
335 if (!pools.empty()) { 480 if (!pools.empty()) {
336 ImGui::Spacing(); 481 ImGui::Spacing();
337 ImGui::SeparatorText("Live Pools"); 482 ImGui::SeparatorText("Live Pools");
338 if (ImGui::BeginTable("livepools", 5, 483 if (ImGui::BeginTable("livepools", 5,
339 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) 484 ImGuiTableFlags_Borders |
485 ImGuiTableFlags_RowBg |
486 ImGuiTableFlags_SizingStretchProp))
340 { 487 {
341 ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); 488 ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch);
342 ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 70); 489 ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 60.0f);
343 ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 160); 490 ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 150.0f);
344 ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); 491 ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch);
345 ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 60); 492 ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 50.0f);
346 ImGui::TableHeadersRow(); 493 ImGui::TableHeadersRow();
347 for (size_t i = 0; i < pools.size(); i++) { 494 for (size_t i = 0; i < pools.size(); i++) {
348 const BtfPoolAnnouncement& ann = pools[i]; 495 const BtfPoolAnnouncement& ann = pools[i];
@@ -353,15 +500,19 @@ static void DrawMainWindow()
353 ImGui::TableSetColumnIndex(1); 500 ImGui::TableSetColumnIndex(1);
354 ImGui::Text("%.2f%%", ann.feePercent); 501 ImGui::Text("%.2f%%", ann.feePercent);
355 ImGui::TableSetColumnIndex(2); 502 ImGui::TableSetColumnIndex(2);
356 ImGui::Text("%d miners, %d blocks, %s old", ann.connectedMiners, ann.blocksFound, FmtAge(ann.createdAt).c_str()); 503 ImGui::Text("%d miners %d blocks %.2f H/s %s",
504 ann.connectedMiners, ann.blocksFound, ann.hashRate,
505 FmtAge(ann.createdAt).c_str());
357 ImGui::TableSetColumnIndex(3); 506 ImGui::TableSetColumnIndex(3);
358 if (!ann.dashboardUrl.empty()) 507 if (!ann.dashboardUrl.empty())
359 ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); 508 ImGui::TextWrapped("%s", ann.dashboardUrl.c_str());
360 else 509 else
361 ImGui::TextDisabled("-"); 510 ImGui::TextDisabled("-");
362 ImGui::TableSetColumnIndex(4); 511 ImGui::TableSetColumnIndex(4);
363 if (ImGui::SmallButton((std::string("Use##") + ann.btfAddress).c_str())) { 512 std::string btnId = "Use##" + ann.btfAddress;
364 strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); 513 if (ImGui::SmallButton(btnId.c_str())) {
514 strncpy(g_participantPool, ann.btfAddress.c_str(),
515 sizeof(g_participantPool)-1);
365 strParticipantPool = ann.btfAddress; 516 strParticipantPool = ann.btfAddress;
366 CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); 517 CWalletDB().WriteSetting("strParticipantPool", strParticipantPool);
367 g_needRefresh = true; 518 g_needRefresh = true;
@@ -375,55 +526,60 @@ static void DrawMainWindow()
375 if (nMineMode == MINE_OPERATOR) { 526 if (nMineMode == MINE_OPERATOR) {
376 ImGui::Spacing(); 527 ImGui::Spacing();
377 ImGui::SeparatorText("Operator Overview"); 528 ImGui::SeparatorText("Operator Overview");
378 ImGui::Text("Pool .btf address:"); 529
379 std::string opAddr = BtfLocalAddress(); 530 std::string opAddr = BtfLocalAddress();
531 ImGui::TextDisabled("Pool .btf address:");
532 ImGui::SameLine();
380 if (opAddr.empty()) 533 if (opAddr.empty())
381 ImGui::TextDisabled("Identity not ready yet"); 534 ImGui::TextDisabled("identity not ready");
382 else 535 else
383 ImGui::TextWrapped("%s", opAddr.c_str()); 536 ImGui::TextWrapped("%s", opAddr.c_str());
384 537
538 // Pending payouts
385 std::vector<PendingPayoutView> owed; 539 std::vector<PendingPayoutView> owed;
386 GetPendingPayouts(owed); 540 GetPendingPayouts(owed);
387 ImGui::Spacing(); 541 ImGui::Spacing();
388 ImGui::SeparatorText("Pending Pool Payouts (you owe miners)"); 542 ImGui::SeparatorText("Pending Payouts (owed to miners)");
389 if (owed.empty()) { 543 if (owed.empty()) {
390 ImGui::TextDisabled("No pending pool payouts"); 544 ImGui::TextDisabled("No pending payouts");
391 } else if (ImGui::BeginTable("owedpayouts", 4, 545 } else if (ImGui::BeginTable("owedpayouts", 4,
392 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) 546 ImGuiTableFlags_Borders |
547 ImGuiTableFlags_RowBg |
548 ImGuiTableFlags_SizingStretchProp))
393 { 549 {
394 ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 100); 550 ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 90.0f);
395 ImGui::TableSetupColumn("Blocks Left", ImGuiTableColumnFlags_WidthFixed, 90); 551 ImGui::TableSetupColumn("Blocks left", ImGuiTableColumnFlags_WidthFixed, 80.0f);
396 ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 90); 552 ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 80.0f);
397 ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); 553 ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch);
398 ImGui::TableHeadersRow(); 554 ImGui::TableHeadersRow();
399 for (const PendingPayoutView& row : owed) { 555 for (const PendingPayoutView& row : owed) {
400 int remaining = std::max(0, row.matureAtHeight - nBestHeight); 556 int left = std::max(0, row.matureAtHeight - nBestHeight);
401 ImGui::TableNextRow(); 557 ImGui::TableNextRow();
402 ImGui::TableSetColumnIndex(0); 558 ImGui::TableSetColumnIndex(0); ImGui::Text("%d", row.matureAtHeight);
403 ImGui::Text("%d", row.matureAtHeight); 559 ImGui::TableSetColumnIndex(1); ImGui::Text("%d", left);
404 ImGui::TableSetColumnIndex(1); 560 ImGui::TableSetColumnIndex(2); ImGui::Text("%d", row.recipients);
405 ImGui::Text("%d", remaining); 561 ImGui::TableSetColumnIndex(3); ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str());
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 } 562 }
411 ImGui::EndTable(); 563 ImGui::EndTable();
412 } 564 }
413 565
566 // Workers
414 std::vector<PoolWorkerStatView> workers; 567 std::vector<PoolWorkerStatView> workers;
415 GetPoolWorkerStats(workers); 568 GetPoolWorkerStats(workers);
416 ImGui::Spacing(); 569 ImGui::Spacing();
417 ImGui::SeparatorText("Current Round Workers"); 570 ImGui::SeparatorText("Current Round Workers");
418 if (workers.empty()) { 571 if (workers.empty()) {
419 ImGui::TextDisabled("No active workers in this round"); 572 ImGui::TextDisabled("No active workers this round");
420 } else if (ImGui::BeginTable("workers", 4, 573 } else if (ImGui::BeginTable("workers", 5,
421 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) 574 ImGuiTableFlags_Borders |
575 ImGuiTableFlags_RowBg |
576 ImGuiTableFlags_SizingStretchProp))
422 { 577 {
423 ImGui::TableSetupColumn("Payout Addr", ImGuiTableColumnFlags_WidthStretch); 578 ImGui::TableSetupColumn("Payout Address", ImGuiTableColumnFlags_WidthStretch);
424 ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); 579 ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch);
425 ImGui::TableSetupColumn("Round Shares", ImGuiTableColumnFlags_WidthFixed, 90); 580 ImGui::TableSetupColumn("Hashrate", ImGuiTableColumnFlags_WidthFixed, 90.0f);
426 ImGui::TableSetupColumn("Last Seen", ImGuiTableColumnFlags_WidthFixed, 100); 581 ImGui::TableSetupColumn("Round shares", ImGuiTableColumnFlags_WidthFixed, 90.0f);
582 ImGui::TableSetupColumn("Last seen", ImGuiTableColumnFlags_WidthFixed, 90.0f);
427 ImGui::TableHeadersRow(); 583 ImGui::TableHeadersRow();
428 for (const PoolWorkerStatView& row : workers) { 584 for (const PoolWorkerStatView& row : workers) {
429 ImGui::TableNextRow(); 585 ImGui::TableNextRow();
@@ -432,53 +588,19 @@ static void DrawMainWindow()
432 ImGui::TableSetColumnIndex(1); 588 ImGui::TableSetColumnIndex(1);
433 ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); 589 ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str());
434 ImGui::TableSetColumnIndex(2); 590 ImGui::TableSetColumnIndex(2);
435 ImGui::Text("%llu", (unsigned long long)row.roundShares); 591 ImGui::Text("%.2f H/s", row.hashRate);
436 ImGui::TableSetColumnIndex(3); 592 ImGui::TableSetColumnIndex(3);
437 ImGui::TextDisabled("%s", FmtAge(row.lastSeen).c_str()); 593 ImGui::Text("%llu", (unsigned long long)row.roundShares);
594 ImGui::TableSetColumnIndex(4);
595 ImGui::TextDisabled("%s ago", FmtAge(row.lastSeen).c_str());
438 } 596 }
439 ImGui::EndTable(); 597 ImGui::EndTable();
440 } 598 }
441 } 599 }
442 600
443 // ---- Status bar ---- 601 ImGui::EndChild(); // ##scroll
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); 602 ImGui::SetWindowFontScale(1.0f);
481 ImGui::End(); 603 ImGui::End(); // ##main
482} 604}
483 605
484// --------------------------------------------------------------------------- 606// ---------------------------------------------------------------------------
@@ -487,42 +609,53 @@ static void DrawMainWindow()
487static void DrawSendDialog() 609static void DrawSendDialog()
488{ 610{
489 if (!g_showSend) return; 611 if (!g_showSend) return;
490 ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Always); 612 ImGui::SetNextWindowSize(ImVec2(500.0f, 180.0f), ImGuiCond_Always);
491 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); 613 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
614 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
492 if (ImGui::Begin("Send Coins", &g_showSend, 615 if (ImGui::Begin("Send Coins", &g_showSend,
493 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) 616 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
494 { 617 {
495 ImGui::Text("Recipient address:"); 618 ImGui::Text("Recipient address:");
496 ImGui::SetNextItemWidth(-1); 619 ImGui::SetNextItemWidth(-1.0f);
497 ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); 620 ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr));
498 621
499 ImGui::Text("Amount (BTF):"); 622 ImGui::Text("Amount (BTF):");
500 ImGui::SetNextItemWidth(180); 623 ImGui::SetNextItemWidth(180.0f);
501 ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); 624 ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount));
502 625
503 if (!g_sendStatus.empty()) { 626 if (!g_sendStatus.empty()) {
504 ImGui::Spacing(); 627 ImGui::Spacing();
505 ImGui::TextColored(ImVec4(1,.4f,.4f,1), "%s", g_sendStatus.c_str()); 628 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
629 "%s", g_sendStatus.c_str());
506 } 630 }
507 631
508 ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); 632 ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing();
509 if (ImGui::Button("Send", ImVec2(90,0))) { 633 if (ImGui::Button("Send", ImVec2(90.0f, 0.0f))) {
510 uint160 h; int64 nv = 0; 634 uint160 h; int64 nv = 0;
511 if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address."; 635 if (!AddressToHash160(g_sendAddr, h))
512 else if (!ParseMoney(g_sendAmount,nv)||nv<=0) g_sendStatus = "Invalid amount."; 636 g_sendStatus = "Invalid address.";
637 else if (!ParseMoney(g_sendAmount, nv) || nv <= 0)
638 g_sendStatus = "Invalid amount.";
513 else { 639 else {
514 CScript s; s << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; 640 CScript sc;
641 sc << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG;
515 CWalletTx wtx; 642 CWalletTx wtx;
516 if (SendMoney(s, nv, wtx)) { 643 if (SendMoney(sc, nv, wtx)) {
517 g_showSend=false; g_sendStatus=""; 644 g_showSend = false;
518 memset(g_sendAddr,0,sizeof(g_sendAddr)); 645 g_sendStatus = "";
519 memset(g_sendAmount,0,sizeof(g_sendAmount)); 646 memset(g_sendAddr, 0, sizeof(g_sendAddr));
647 memset(g_sendAmount, 0, sizeof(g_sendAmount));
520 g_needRefresh = true; 648 g_needRefresh = true;
521 } else g_sendStatus = "Transaction failed."; 649 } else {
650 g_sendStatus = "Transaction failed.";
651 }
522 } 652 }
523 } 653 }
524 ImGui::SameLine(); 654 ImGui::SameLine();
525 if (ImGui::Button("Cancel", ImVec2(90,0))) { g_showSend=false; g_sendStatus=""; } 655 if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) {
656 g_showSend = false;
657 g_sendStatus = "";
658 }
526 } 659 }
527 ImGui::End(); 660 ImGui::End();
528} 661}
@@ -534,7 +667,6 @@ static void DrawOptionsDialog()
534{ 667{
535 if (!g_showOptions) return; 668 if (!g_showOptions) return;
536 669
537 // Always sync from current globals when dialog opens
538 static bool wasOpen = false; 670 static bool wasOpen = false;
539 if (!wasOpen) { 671 if (!wasOpen) {
540 g_mineRadio = nMineMode; 672 g_mineRadio = nMineMode;
@@ -544,42 +676,46 @@ static void DrawOptionsDialog()
544 strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); 676 strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1);
545 } 677 }
546 wasOpen = true; 678 wasOpen = true;
547 ImGui::SetNextWindowSize(ImVec2(500, 310), ImGuiCond_Always); 679
548 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); 680 ImGui::SetNextWindowSize(ImVec2(500.0f, 335.0f), ImGuiCond_Always);
681 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
682 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
549 if (ImGui::Begin("Options", &g_showOptions, 683 if (ImGui::Begin("Options", &g_showOptions,
550 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) 684 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
551 { 685 {
552 // Transaction fee 686 // Transaction fee
553 ImGui::SeparatorText("Transaction Fee"); 687 ImGui::SeparatorText("Transaction Fee");
554 static char feeStr[32] = {}; 688 static char feeStr[32] = {};
555 if (feeStr[0]=='\0') snprintf(feeStr,sizeof(feeStr),"%s",FmtMoney(nTransactionFee).c_str()); 689 if (feeStr[0] == '\0')
556 ImGui::SetNextItemWidth(180); 690 snprintf(feeStr, sizeof(feeStr), "%s", FmtMoney(nTransactionFee).c_str());
691 ImGui::SetNextItemWidth(180.0f);
557 ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); 692 ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr));
558 693
559 // Mining 694 // Mining mode
560 ImGui::SeparatorText("Mining Mode"); 695 ImGui::SeparatorText("Mining Mode");
561 ImGui::RadioButton("Solo — rewards go to your wallet", &g_mineRadio, MINE_SOLO); 696 ImGui::RadioButton("Relay -- just run the node, don't mine", &g_mineRadio, MINE_RELAY);
562 ImGui::RadioButton("Operator — run a pool for other miners", &g_mineRadio, MINE_OPERATOR); 697 ImGui::RadioButton("Solo -- rewards go to your wallet", &g_mineRadio, MINE_SOLO);
563 ImGui::RadioButton("Participant — mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); 698 ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR);
699 ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT);
564 700
565 if (g_mineRadio == MINE_OPERATOR) { 701 if (g_mineRadio == MINE_OPERATOR) {
566 ImGui::Spacing(); 702 ImGui::Spacing();
567 ImGui::SeparatorText("Pool Announcement"); 703 ImGui::SeparatorText("Pool Announcement");
568 ImGui::Text("Pool name:"); 704 ImGui::Text("Pool name:");
569 ImGui::SetNextItemWidth(-1); 705 ImGui::SetNextItemWidth(-1.0f);
570 ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); 706 ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName));
571 ImGui::Text("Fee %%:"); 707 ImGui::Text("Fee %%:");
572 ImGui::SetNextItemWidth(120); 708 ImGui::SetNextItemWidth(120.0f);
573 ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); 709 ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee));
574 ImGui::Text("Dashboard URL:"); 710 ImGui::Text("Dashboard URL:");
575 ImGui::SetNextItemWidth(-1); 711 ImGui::SetNextItemWidth(-1.0f);
576 ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); 712 ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash));
577 } 713 }
578 714
579 if (g_mineRadio == MINE_PARTICIPANT) { 715 if (g_mineRadio == MINE_PARTICIPANT) {
580 ImGui::Spacing(); 716 ImGui::Spacing();
581 ImGui::SeparatorText("Pool Selection"); 717 ImGui::SeparatorText("Pool Selection");
582 ImGui::TextDisabled("Pick from live discovered pools. Manual entry is disabled here."); 718 ImGui::TextDisabled("Pick from live discovered pools.");
583 719
584 std::vector<BtfPoolAnnouncement> pools; 720 std::vector<BtfPoolAnnouncement> pools;
585 BtfGetPoolAnnouncements(pools); 721 BtfGetPoolAnnouncements(pools);
@@ -591,46 +727,49 @@ static void DrawOptionsDialog()
591 break; 727 break;
592 } 728 }
593 } 729 }
594 if (ImGui::BeginCombo("Discovered pools", current.c_str())) { 730 if (ImGui::BeginCombo("Discovered pools##combo", current.c_str())) {
595 for (size_t i = 0; i < pools.size(); i++) { 731 for (size_t i = 0; i < pools.size(); i++) {
596 const BtfPoolAnnouncement& ann = pools[i]; 732 const BtfPoolAnnouncement& ann = pools[i];
597 std::string label = PoolLabel(ann); 733 bool selected = (ann.btfAddress == g_participantPool);
598 bool selected = ann.btfAddress == g_participantPool; 734 if (ImGui::Selectable(PoolLabel(ann).c_str(), selected))
599 if (ImGui::Selectable(label.c_str(), selected)) { 735 strncpy(g_participantPool, ann.btfAddress.c_str(),
600 strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); 736 sizeof(g_participantPool)-1);
601 }
602 } 737 }
603 ImGui::EndCombo(); 738 ImGui::EndCombo();
604 } 739 }
605 } else { 740 } else {
606 ImGui::TextDisabled("No live pools discovered yet"); 741 ImGui::TextDisabled("No live pools discovered yet.");
607 } 742 }
608 } 743 }
609 744
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(); 745 ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing();
619 if (ImGui::Button("OK", ImVec2(90,0))) { 746 if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) {
620 int64 fee=0; 747 int64 fee = 0;
621 if (ParseMoney(feeStr,fee)) { nTransactionFee=fee; CWalletDB().WriteSetting("nTransactionFee",nTransactionFee); } 748 if (ParseMoney(feeStr, fee)) {
622 749 nTransactionFee = fee;
623 bool changed = (g_mineRadio != nMineMode); 750 CWalletDB().WriteSetting("nTransactionFee", nTransactionFee);
624 nMineMode = g_mineRadio; 751 }
625 strParticipantPool = g_participantPool; 752
626 strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; 753 bool poolChanged =
627 strPoolDashboardUrl = g_poolDash; 754 nMineMode != g_mineRadio ||
628 dPoolFeePercent = atof(g_poolFee); 755 strPoolName != (g_poolName[0] ? g_poolName : "Bitflash Pool") ||
629 CWalletDB().WriteSetting("nMineMode", nMineMode); 756 strPoolDashboardUrl != g_poolDash ||
630 CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); 757 dPoolFeePercent != atof(g_poolFee);
631 CWalletDB().WriteSetting("strPoolName", strPoolName); 758
632 CWalletDB().WriteSetting("strPoolDashboardUrl", strPoolDashboardUrl); 759 nMineMode = g_mineRadio;
633 CWalletDB().WriteSetting("dPoolFeePercent", dPoolFeePercent); 760 strParticipantPool = g_participantPool;
761 strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool";
762 strPoolDashboardUrl = g_poolDash;
763 dPoolFeePercent = atof(g_poolFee);
764
765 if (nMineMode == MINE_OPERATOR && poolChanged) gAnnounceNow = true;
766
767 if (nMineMode == MINE_RELAY) {
768 fGenerateBitcoins = 0;
769 } else {
770 fGenerateBitcoins = 1;
771 if (!vfThreadRunning[3]) _beginthread(ThreadBitcoinMiner, 0, NULL);
772 }
634 773
635 if (nMineMode == MINE_OPERATOR) { 774 if (nMineMode == MINE_OPERATOR) {
636 if (!gPoolServerRunning) { 775 if (!gPoolServerRunning) {
@@ -645,7 +784,7 @@ static void DrawOptionsDialog()
645 g_showOptions = false; 784 g_showOptions = false;
646 } 785 }
647 ImGui::SameLine(); 786 ImGui::SameLine();
648 if (ImGui::Button("Cancel", ImVec2(90,0))) g_showOptions = false; 787 if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) g_showOptions = false;
649 } 788 }
650 if (!g_showOptions) wasOpen = false; 789 if (!g_showOptions) wasOpen = false;
651 ImGui::End(); 790 ImGui::End();
@@ -657,18 +796,21 @@ static void DrawOptionsDialog()
657static void DrawAboutDialog() 796static void DrawAboutDialog()
658{ 797{
659 if (!g_showAbout) return; 798 if (!g_showAbout) return;
660 ImGui::SetNextWindowSize(ImVec2(380, 170), ImGuiCond_Always); 799 ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always);
661 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); 800 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
801 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
662 if (ImGui::Begin("About Bitflash", &g_showAbout, 802 if (ImGui::Begin("About Bitflash", &g_showAbout,
663 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) 803 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
664 { 804 {
665 ImGui::TextColored(ImVec4(1,.6f,0,1), "Bitflash BTF v1.1.0"); 805 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0");
666 ImGui::Spacing(); 806 ImGui::Spacing();
667 ImGui::TextWrapped("CPU-only cryptocurrency. RandomX proof of work. Anonymous .btf addressing over Nostr. No premine."); 807 ImGui::TextWrapped(
808 "CPU-only cryptocurrency. RandomX proof of work. "
809 "Anonymous .btf addressing over Nostr. No premine.");
668 ImGui::Spacing(); 810 ImGui::Spacing();
669 ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); 811 ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)");
670 ImGui::Spacing(); 812 ImGui::Spacing();
671 if (ImGui::Button("OK", ImVec2(90,0))) g_showAbout = false; 813 if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) g_showAbout = false;
672 } 814 }
673 ImGui::End(); 815 ImGui::End();
674} 816}
@@ -705,34 +847,33 @@ int RunGUI(int argc, char* argv[])
705 ImGuiIO& io = ImGui::GetIO(); 847 ImGuiIO& io = ImGui::GetIO();
706 io.IniFilename = nullptr; 848 io.IniFilename = nullptr;
707 849
708 // Load Roboto at 16px — clean, readable on all platforms
709 io.Fonts->AddFontFromMemoryCompressedTTF( 850 io.Fonts->AddFontFromMemoryCompressedTTF(
710 RobotoMedium_compressed_data, RobotoMedium_compressed_size, 16.0f); 851 RobotoMedium_compressed_data, RobotoMedium_compressed_size, 14.0f);
711 852
712 ImGui::StyleColorsDark(); 853 ImGui::StyleColorsDark();
713 ImGuiStyle& s = ImGui::GetStyle(); 854 ImGuiStyle& st = ImGui::GetStyle();
714 s.WindowRounding = s.FrameRounding = s.PopupRounding = 4.0f; 855 st.WindowRounding = st.FrameRounding = st.PopupRounding = 4.0f;
715 s.ItemSpacing = ImVec2(10, 7); 856 st.ItemSpacing = ImVec2(10.0f, 7.0f);
716 s.FramePadding = ImVec2(8, 5); 857 st.FramePadding = ImVec2(8.0f, 5.0f);
717 s.WindowPadding = ImVec2(12, 12); 858 st.WindowPadding = ImVec2(12.0f, 12.0f);
718 // Orange accent 859 // Orange accent
719 s.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f,.45f,.00f,1); 860 st.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f, .45f, .00f, 1.0f);
720 s.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f,.60f,.00f,1); 861 st.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, .60f, .00f, 1.0f);
721 s.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f,.45f,.00f,1); 862 st.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f, .45f, .00f, 1.0f);
722 s.Colors[ImGuiCol_Button] = ImVec4(.25f,.25f,.25f,1); 863 st.Colors[ImGuiCol_Button] = ImVec4(.25f, .25f, .25f, 1.0f);
723 s.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f,.45f,.00f,1); 864 st.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f, .45f, .00f, 1.0f);
724 s.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f,.30f,.00f,1); 865 st.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f, .30f, .00f, 1.0f);
725 s.Colors[ImGuiCol_Header] = ImVec4(.80f,.45f,.00f,.40f); 866 st.Colors[ImGuiCol_Header] = ImVec4(.80f, .45f, .00f, .40f);
726 s.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f,.45f,.20f,.80f); 867 st.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f, .45f, .20f, .80f);
727 s.Colors[ImGuiCol_Tab] = ImVec4(.20f,.20f,.20f,1); 868 st.Colors[ImGuiCol_Tab] = ImVec4(.20f, .20f, .20f, 1.0f);
728 s.Colors[ImGuiCol_TabHovered] = ImVec4(.80f,.45f,.00f,1); 869 st.Colors[ImGuiCol_TabHovered] = ImVec4(.80f, .45f, .00f, 1.0f);
729 s.Colors[ImGuiCol_TabActive] = ImVec4(.80f,.45f,.00f,1); 870 st.Colors[ImGuiCol_TabActive] = ImVec4(.80f, .45f, .00f, 1.0f);
730 s.Colors[ImGuiCol_SeparatorHovered]=ImVec4(.80f,.45f,.00f,1); 871 st.Colors[ImGuiCol_SeparatorHovered]= ImVec4(.80f, .45f, .00f, 1.0f);
731 s.Colors[ImGuiCol_FrameBg] = ImVec4(.14f,.14f,.14f,1); 872 st.Colors[ImGuiCol_FrameBg] = ImVec4(.14f, .14f, .14f, 1.0f);
732 s.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f,.20f,.20f,1); 873 st.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f, .20f, .20f, 1.0f);
733 s.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f,.18f,.18f,1); 874 st.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f, .18f, .18f, 1.0f);
734 s.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f,.11f,.11f,1); 875 st.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f, .11f, .11f, 1.0f);
735 s.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f,.14f,.14f,1); 876 st.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f, .14f, .14f, 1.0f);
736 877
737 ImGui_ImplGlfw_InitForOpenGL(window, true); 878 ImGui_ImplGlfw_InitForOpenGL(window, true);
738 ImGui_ImplOpenGL3_Init("#version 330"); 879 ImGui_ImplOpenGL3_Init("#version 330");
@@ -744,22 +885,28 @@ int RunGUI(int argc, char* argv[])
744 while (!glfwWindowShouldClose(window) && !fShutdown) 885 while (!glfwWindowShouldClose(window) && !fShutdown)
745 { 886 {
746 glfwPollEvents(); 887 glfwPollEvents();
747 if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; } 888 if (g_needRefresh || (frame % 120 == 0)) {
889 RefreshWallet();
890 g_needRefresh = false;
891 }
748 frame++; 892 frame++;
749 893
750 ImGui_ImplOpenGL3_NewFrame(); 894 ImGui_ImplOpenGL3_NewFrame();
751 ImGui_ImplGlfw_NewFrame(); 895 ImGui_ImplGlfw_NewFrame();
752 ImGui::NewFrame(); 896 ImGui::NewFrame();
753 897
898 // Draw order matters: status bar last so it is always on top.
754 DrawMainWindow(); 899 DrawMainWindow();
755 DrawSendDialog(); 900 DrawSendDialog();
756 DrawOptionsDialog(); 901 DrawOptionsDialog();
757 DrawAboutDialog(); 902 DrawAboutDialog();
903 DrawStatusBar();
758 904
759 ImGui::Render(); 905 ImGui::Render();
760 int w, h; glfwGetFramebufferSize(window, &w, &h); 906 int w, h;
907 glfwGetFramebufferSize(window, &w, &h);
761 glViewport(0, 0, w, h); 908 glViewport(0, 0, w, h);
762 glClearColor(.08f,.08f,.08f,1); 909 glClearColor(.08f, .08f, .08f, 1.0f);
763 glClear(GL_COLOR_BUFFER_BIT); 910 glClear(GL_COLOR_BUFFER_BIT);
764 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 911 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
765 glfwSwapBuffers(window); 912 glfwSwapBuffers(window);