Compare commits
5
Commits
8fe20f6385
...
e9eeaf00c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9eeaf00c3 | ||
|
|
572b98cbcf | ||
|
|
bcfc324426 | ||
|
|
7cea9b7339 | ||
|
|
f3f0b32a48 |
+13
-37
@@ -3084,45 +3084,19 @@ void App::renderStatusBar()
|
||||
int blocksLeft = chainTip - state_.sync.blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
|
||||
// Calculate sync speed (blocks/sec) using a smoothed rolling average
|
||||
static int s_prev_blocks = 0;
|
||||
static double s_prev_time = 0.0;
|
||||
static double s_blocks_per_sec = 0.0;
|
||||
double now = ImGui::GetTime();
|
||||
if (s_prev_time == 0.0) {
|
||||
// First sample — seed baseline, don't compute rate yet
|
||||
s_prev_blocks = state_.sync.blocks;
|
||||
s_prev_time = now;
|
||||
} else if (state_.sync.blocks > s_prev_blocks) {
|
||||
double dt = now - s_prev_time;
|
||||
if (dt > 0.5) {
|
||||
double raw = (state_.sync.blocks - s_prev_blocks) / dt;
|
||||
// Exponential smoothing (alpha ~0.3 per update)
|
||||
s_blocks_per_sec = s_blocks_per_sec > 0.0
|
||||
? s_blocks_per_sec * 0.7 + raw * 0.3
|
||||
: raw;
|
||||
s_prev_blocks = state_.sync.blocks;
|
||||
s_prev_time = now;
|
||||
}
|
||||
} else if (now - s_prev_time > 10.0) {
|
||||
// No new blocks for 10s — decay the displayed rate
|
||||
s_blocks_per_sec *= 0.5;
|
||||
s_prev_time = now;
|
||||
}
|
||||
|
||||
if (s_blocks_per_sec > 0.1) {
|
||||
int eta_sec = (int)(blocksLeft / s_blocks_per_sec);
|
||||
// App-owned smoothed sync-rate estimator: self-resets after a rescan/restart height drop (and on
|
||||
// connect/disconnect via the pending flag), so the ETA never goes stale/absurd ("412h"); days-aware.
|
||||
if (sync_eta_reset_pending_.exchange(false)) sync_eta_.reset();
|
||||
sync_eta_.update(state_.sync.blocks, ImGui::GetTime());
|
||||
const ImVec4 syncCol = ImGui::ColorConvertU32ToFloat4(ui::material::Warning());
|
||||
const long long etaSec = sync_eta_.etaSeconds(blocksLeft);
|
||||
if (etaSec >= 0) {
|
||||
char eta[32];
|
||||
if (eta_sec >= 3600)
|
||||
snprintf(eta, sizeof(eta), "%dh %dm", eta_sec / 3600, (eta_sec % 3600) / 60);
|
||||
else if (eta_sec >= 60)
|
||||
snprintf(eta, sizeof(eta), "%dm %ds", eta_sec / 60, eta_sec % 60);
|
||||
else
|
||||
snprintf(eta, sizeof(eta), "%ds", eta_sec);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_eta"),
|
||||
state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec, eta);
|
||||
util::formatEta(etaSec, eta, sizeof(eta));
|
||||
ImGui::TextColored(syncCol, TR("sb_syncing_eta"),
|
||||
state_.sync.verification_progress * 100.0, blocksLeft, sync_eta_.rate(), eta);
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_basic"),
|
||||
ImGui::TextColored(syncCol, TR("sb_syncing_basic"),
|
||||
state_.sync.verification_progress * 100.0, blocksLeft);
|
||||
}
|
||||
} else if (state_.connected && !isLiteBuild()) {
|
||||
@@ -6337,6 +6311,7 @@ void App::rescanBlockchain()
|
||||
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
|
||||
// pre-restart daemon and see rescanning=false) can't be misread as instant completion.
|
||||
state_.sync.rescanning = true;
|
||||
sync_eta_reset_pending_.store(true, std::memory_order_relaxed); // rescan drops height → reset ETA
|
||||
rescan_confirmed_active_ = false;
|
||||
user_initiated_rescan_ = true; // user-triggered rescan → its completion should toast
|
||||
state_.sync.rescan_progress = 0.0f;
|
||||
@@ -8136,6 +8111,7 @@ std::string App::buildDiagnosticsReport()
|
||||
|
||||
void App::restartDaemon()
|
||||
{
|
||||
sync_eta_reset_pending_.store(true, std::memory_order_relaxed);
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "resources/provision_state.h"
|
||||
#include "util/background_sampler.h"
|
||||
#include "util/coalescing_writer.h"
|
||||
#include "util/sync_eta.h"
|
||||
#include "util/fetch_backoff.h"
|
||||
#include "util/idle_tracker.h"
|
||||
#include "util/pool_stats_service.h"
|
||||
@@ -1752,6 +1753,10 @@ private:
|
||||
std::atomic<bool> mining_toggle_in_progress_{false};
|
||||
// Mining error state (main-thread only — every writer is in a MainCb or render).
|
||||
std::string mining_last_error_;
|
||||
// Sync-ETA estimator (UI thread only). onConnected/onDisconnected may run off-thread, so they set
|
||||
// the atomic flag and the status bar consumes it before update().
|
||||
util::SyncRateEstimator sync_eta_;
|
||||
std::atomic<bool> sync_eta_reset_pending_{false};
|
||||
bool mining_error_av_ = false;
|
||||
void setMiningError(std::string msg, bool av = false) { mining_last_error_ = std::move(msg); mining_error_av_ = av; }
|
||||
|
||||
|
||||
@@ -741,6 +741,7 @@ void App::tryConnect()
|
||||
void App::onConnected()
|
||||
{
|
||||
maint_abort_.store(false); // maintenance calls queued before this connect have already failed (FIFO)
|
||||
sync_eta_reset_pending_.store(true, std::memory_order_relaxed); // fresh connect → recompute sync ETA
|
||||
++rpc_session_;
|
||||
if (consolidation_.abort()) ui::Notifications::instance().warning(TR("merge_run_aborted_toast"));
|
||||
state_.connected = true;
|
||||
@@ -854,6 +855,7 @@ void App::onConnected()
|
||||
|
||||
void App::onDisconnected(const std::string& reason)
|
||||
{
|
||||
sync_eta_reset_pending_.store(true, std::memory_order_relaxed);
|
||||
// Persist what the history snapshot is still missing while the rows, the address set (the cache identity),
|
||||
// the pending-send filter and the cache key are all live: state_.clear(), pending_opids_.clear() and the
|
||||
// session reset below empty them, which would turn a later flush into a silent no-op.
|
||||
|
||||
@@ -1548,9 +1548,13 @@ void RenderSettingsPage(App* app) {
|
||||
if (cardBtn(cx, cy, cw, "##exp_key", TR("settings_export_key"), ICON_MD_LOGOUT)) app->showExportKeyDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_key"));
|
||||
last = cy + bh; cy += bh + bgp;
|
||||
if (cardBtn(cx, cy, cw, "##exp_all", TR("settings_export_all"), ICON_MD_ARCHIVE)) ExportAllKeysDialog::show();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_all"));
|
||||
last = cy + bh; cy += bh + bgp;
|
||||
// Export-all only works over the full-node RPC; hide it in lite (which has its own key export
|
||||
// in the lite wallet section) rather than dead-ending on "Not connected to the daemon".
|
||||
if (fullNode) {
|
||||
if (cardBtn(cx, cy, cw, "##exp_all", TR("settings_export_all"), ICON_MD_ARCHIVE)) ExportAllKeysDialog::show();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_all"));
|
||||
last = cy + bh; cy += bh + bgp;
|
||||
}
|
||||
if (cardBtn(cx, cy, cw, "##exp_csv", TR("settings_export_csv"), ICON_MD_DESCRIPTION)) ExportTransactionsDialog::show();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_csv"));
|
||||
last = cy + bh;
|
||||
@@ -2727,7 +2731,10 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::TextUnformatted(TR("transaction_url"));
|
||||
ImGui::SameLine(urlRowX + urlLblW);
|
||||
ImGui::SetNextItemWidth(urlInputW);
|
||||
// These Explorer-tab controls persist on edit — they were the only ones still relying on the
|
||||
// About-only Save button, which a user on this tab never sees.
|
||||
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url"));
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
@@ -2737,6 +2744,7 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::SameLine(urlRowX + urlLblW);
|
||||
ImGui::SetNextItemWidth(urlInputW);
|
||||
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url"));
|
||||
|
||||
ImGui::PopFont();
|
||||
@@ -2754,10 +2762,12 @@ void RenderSettingsPage(App* app) {
|
||||
|
||||
// Checkboxes + Block Explorer button (button wraps to its own row when it won't fit).
|
||||
const float expRowRight = ImGui::GetCursorScreenPos().x + contentW;
|
||||
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
||||
if (ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
||||
if (ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
||||
const float expBtnW = ImGui::CalcTextSize(TR("block_explorer")).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
||||
|
||||
@@ -136,6 +136,18 @@ void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float v
|
||||
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
||||
ImGui::SameLine();
|
||||
|
||||
// Pending (unconfirmed) chip — so these layouts show it too, labeled, not just the Classic card.
|
||||
if (s_dispUnconfirmed > 1e-9) {
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pending_chip"), s_dispUnconfirmed);
|
||||
ImVec2 pp = ImGui::GetCursorScreenPos();
|
||||
dl->AddText(capFont, tickerFontSize, ImVec2(pp.x, tickerY), WithAlpha(Warning(), 230), buf);
|
||||
float pw = capFont->CalcTextSizeA(tickerFontSize, 10000, 0, buf).x;
|
||||
if (material::IsRectHovered(ImVec2(pp.x, tickerY), ImVec2(pp.x + pw, tickerY + tickerFontSize)))
|
||||
material::Tooltip("%s", TR("baltab_pending_tip"));
|
||||
ImGui::Dummy(ImVec2(pw + Layout::spacingLg(), lineH));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
// Total period-earnings (left of the USD) when the global earnings toggle is on.
|
||||
{
|
||||
std::string earnText; ImU32 earnCol;
|
||||
|
||||
@@ -525,9 +525,9 @@ static void RenderBalanceClassic(App* app)
|
||||
WithAlpha(Success(), 160), buf);
|
||||
}
|
||||
|
||||
// Unconfirmed badge (top-right corner)
|
||||
// Pending badge (top-right corner) — labeled so an unconfirmed "+X" doesn't read as a mystery.
|
||||
if (state.unconfirmed_balance > 0.0) {
|
||||
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pending_chip"), state.unconfirmed_balance);
|
||||
ImVec2 ts = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, buf);
|
||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
|
||||
@@ -540,6 +540,7 @@ static void RenderBalanceClassic(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(bMin.x + bp, bMin.y + bp * 0.5f),
|
||||
amberCol, buf);
|
||||
if (material::IsRectHovered(bMin, bMax)) material::Tooltip("%s", TR("baltab_pending_tip"));
|
||||
}
|
||||
|
||||
// Hover glow + click to Receive
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
s_state = State::Confirm;
|
||||
s_bootstrap.reset();
|
||||
s_errorMsg.clear();
|
||||
s_lastUrl.clear();
|
||||
s_wasDaemonRunning = false;
|
||||
}
|
||||
|
||||
@@ -314,9 +315,15 @@ private:
|
||||
|
||||
float btnW = 120.0f * dp;
|
||||
if (TactileButton(TR("retry"), ImVec2(btnW, 0))) {
|
||||
startDownload("");
|
||||
startDownload(s_lastUrl); // keep the user's source/mirror choice (was startDownload(""))
|
||||
}
|
||||
ImGui::SameLine();
|
||||
{
|
||||
const std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||
if (s_lastUrl != mirrorUrl && TactileButton(TR("bootstrap_try_mirror"), ImVec2(130.0f * dp, 0)))
|
||||
startDownload(mirrorUrl);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) {
|
||||
s_app->setBootstrapDownloading(false);
|
||||
s_open = false;
|
||||
@@ -336,6 +343,7 @@ private:
|
||||
s_bootstrap->start(dataDir);
|
||||
else
|
||||
s_bootstrap->start(dataDir, url);
|
||||
s_lastUrl = url; // remember what we started (set only after the guards passed)
|
||||
s_state = State::Downloading;
|
||||
s_errorMsg.clear();
|
||||
s_cancelling = false;
|
||||
@@ -347,6 +355,7 @@ private:
|
||||
static inline std::unique_ptr<util::Bootstrap> s_bootstrap;
|
||||
static inline bool s_wasDaemonRunning = false;
|
||||
static inline bool s_cancelling = false; // cancel requested; disables the Cancel button until the worker stops
|
||||
static inline std::string s_lastUrl; // last source started ("" = default); so Retry keeps the mirror choice
|
||||
static inline std::string s_errorMsg;
|
||||
};
|
||||
|
||||
|
||||
@@ -537,6 +537,7 @@ struct ConvSummary {
|
||||
std::int64_t lastTs = 0;
|
||||
int count = 0;
|
||||
bool hidden = false; // shown only while "Show hidden" is on
|
||||
bool pendingRequest = false; // unsolicited incoming contact request from a stranger: awaiting Accept/Block
|
||||
};
|
||||
|
||||
// Per-frame memoization of the conversation-list build and the open-thread message list (both otherwise
|
||||
@@ -808,9 +809,11 @@ void RenderChatTab(App* app)
|
||||
c.cid = cid;
|
||||
c.hidden = hidden;
|
||||
c.count = static_cast<int>(messages.size());
|
||||
bool sentAny = false;
|
||||
for (const auto& m : messages) { // pin to the EARLIEST (establishing) peer z-addr / key (B2 — the
|
||||
if (c.peerZaddr.empty() && !m.peer_zaddr.empty()) c.peerZaddr = m.peer_zaddr; // memo header rides
|
||||
if (c.peerPubKey.empty() && !m.peer_public_key_hex.empty()) c.peerPubKey = m.peer_public_key_hex; // outside the AEAD)
|
||||
if (m.direction == chat::ChatDirection::Outgoing) sentAny = true;
|
||||
}
|
||||
const auto& last = messages.back();
|
||||
c.lastBody = last.body;
|
||||
@@ -818,6 +821,12 @@ void RenderChatTab(App* app)
|
||||
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
||||
// Unsolicited: first message is an incoming contact request, we've never replied, and the peer
|
||||
// isn't a saved contact → show an Accept/Block card instead of treating it as a normal chat.
|
||||
const auto& first = messages.front();
|
||||
c.pendingRequest = (idx < 0) && !sentAny && !c.peerZaddr.empty() &&
|
||||
first.direction == chat::ChatDirection::Incoming &&
|
||||
first.kind == chat::ChatMessageKind::ContactRequest;
|
||||
s_convs.push_back(std::move(c));
|
||||
}
|
||||
std::sort(s_convs.begin(), s_convs.end(),
|
||||
@@ -1340,6 +1349,42 @@ void RenderChatTab(App* app)
|
||||
ImGui::SetCursorScreenPos(ImVec2(hp.x, hp.y + rowH + 4.0f * hdpi));
|
||||
ImGui::Separator();
|
||||
|
||||
// Stranger contact-request card: Accept (add to contacts) or Block (delete + block). Normal
|
||||
// ImGui flow, so ##ChatMessages below (sized from the remaining content height) shrinks to fit.
|
||||
if (sel->pendingRequest) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||
ImGui::TextUnformatted(TR("chat_request_title"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextWrapped("%s", TR("chat_request_body"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (ImGui::SmallButton(TR("chat_request_accept"))) {
|
||||
data::AddressBookEntry e;
|
||||
e.address = sel->peerZaddr;
|
||||
e.label = sel->peerName;
|
||||
book.addEntry(e); // book.revision() bumps → the request flag clears on the next rebuild
|
||||
Notifications::instance().info(TR("chat_request_accepted"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("chat_request_block"))) {
|
||||
const std::string cid = sel->cid, nm = sel->peerName; // copy before deleteConversation
|
||||
if (app->chatService().deleteConversation(cid, /*block=*/true)) {
|
||||
if (app->settings()) {
|
||||
app->settings()->setChatBlocked(cid, nm, true);
|
||||
app->settings()->setChatHidden(cid, false);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (s_selected_cid == cid) s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_blocked_toast"));
|
||||
} else {
|
||||
Notifications::instance().error(TR("chat_delete_failed"));
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
// Small message-list inset (the ##ChatThread WindowPadding above already holds content off
|
||||
// the glass edge; keep a little here so bubbles don't hug the scrollbar).
|
||||
|
||||
@@ -204,6 +204,25 @@ static double srcAmount() { return s_src == 0 ? s_t_amount : s_src == 1 ? s_z_am
|
||||
|
||||
static std::string fmtAmt(double v) { char b[48]; std::snprintf(b, sizeof(b), "%.4f", v); return b; }
|
||||
|
||||
// Shield-coinbase source helpers. "*" = all spendable mining (transparent) addresses; else one address.
|
||||
// Balances are DISPLAY totals (may include immature/non-coinbase), so the UI wording says "up to".
|
||||
static double miningBalance(const WalletState& st, const char* from) {
|
||||
if (std::strcmp(from, "*") == 0) {
|
||||
double sum = 0.0;
|
||||
for (const auto& a : st.t_addresses) if (a.isSpendable()) sum += a.balance;
|
||||
return sum;
|
||||
}
|
||||
const int i = addressIndexByString(st.t_addresses, from);
|
||||
return i >= 0 ? st.t_addresses[i].balance : 0.0;
|
||||
}
|
||||
static int miningAddrCount(const WalletState& st, const char* from) {
|
||||
if (std::strcmp(from, "*") == 0) {
|
||||
int n = 0; for (const auto& a : st.t_addresses) if (a.isSpendable() && a.balance > 0.0) ++n; return n;
|
||||
}
|
||||
const int i = addressIndexByString(st.t_addresses, from);
|
||||
return (i >= 0 && st.t_addresses[i].balance > 0.0) ? 1 : 0;
|
||||
}
|
||||
|
||||
// A caption-style line that wraps at the dialog edge (Type().textColored doesn't wrap).
|
||||
static void wrappedCaption(ImU32 color, const char* text)
|
||||
{
|
||||
@@ -567,8 +586,32 @@ void ShieldDialog::render(App* app)
|
||||
|
||||
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
|
||||
if (!isMerge && !opInFlight) {
|
||||
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
|
||||
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
||||
// Pick the mining (transparent) address to shield from a dropdown with balances, instead of a
|
||||
// free-text box with a "*" wildcard. "*" shields all of them.
|
||||
ImGui::TextUnformatted(TR("shield_from_address"));
|
||||
const bool allFrom = std::strcmp(s_from_address, "*") == 0;
|
||||
const std::string fromLbl = allFrom ? std::string(TR("shield_all_mining_addrs"))
|
||||
: (app->getAddressLabel(s_from_address).empty() ? shortAddr(s_from_address)
|
||||
: app->getAddressLabel(s_from_address));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::BeginCombo("##FromAddr", fromLbl.c_str())) {
|
||||
if (ImGui::Selectable(TR("shield_all_mining_addrs"), allFrom))
|
||||
strncpy(s_from_address, "*", sizeof(s_from_address));
|
||||
for (const auto& a : state.t_addresses) {
|
||||
if (!a.isSpendable()) continue;
|
||||
const std::string lbl = app->getAddressLabel(a.address);
|
||||
char item[192];
|
||||
std::snprintf(item, sizeof(item), "%s (%s DRGX)",
|
||||
lbl.empty() ? shortAddr(a.address).c_str() : lbl.c_str(),
|
||||
fmtAmt(a.balance).c_str());
|
||||
const bool sel = std::strcmp(s_from_address, a.address.c_str()) == 0;
|
||||
if (ImGui::Selectable(item, sel)) {
|
||||
strncpy(s_from_address, a.address.c_str(), sizeof(s_from_address) - 1);
|
||||
s_from_address[sizeof(s_from_address) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
@@ -833,9 +876,24 @@ void ShieldDialog::render(App* app)
|
||||
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
|
||||
ImGui::TextWrapped("%s", cb);
|
||||
ImGui::Spacing();
|
||||
} else if (s_confirm && !isMerge) {
|
||||
// Shield-coinbase preview: amount / address count / fee / destination (no amount/count was shown before).
|
||||
const bool allFrom = std::strcmp(s_from_address, "*") == 0;
|
||||
const std::string toLbl = app->getAddressLabel(s_to_address);
|
||||
char cb[360];
|
||||
std::snprintf(cb, sizeof(cb), TR("shield_confirm_fmt"),
|
||||
fmtAmt(miningBalance(state, s_from_address)).c_str(),
|
||||
allFrom ? miningAddrCount(state, s_from_address) : 1,
|
||||
s_utxo_limit,
|
||||
toLbl.empty() ? shortAddr(s_to_address).c_str() : toLbl.c_str(),
|
||||
fmtAmt(s_fee).c_str());
|
||||
ImGui::TextWrapped("%s", cb);
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
bool can_submit = haveDest && connected && !syncing;
|
||||
// Block a shield from an empty source or one with no funds (the free-text box allowed both).
|
||||
if (!isMerge && (s_from_address[0] == '\0' || miningBalance(state, s_from_address) <= 0.0)) can_submit = false;
|
||||
const bool scopeLoaded = s_scan.phase == services::ScopePhase::Loaded;
|
||||
if (isMerge && scopeLoaded && srcCount() == 0) can_submit = false;
|
||||
if (perAddress) can_submit = connected && !syncing && scopeLoaded && !selectedGroups().empty();
|
||||
|
||||
@@ -91,7 +91,8 @@ void ValidateAddressDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Validate button
|
||||
bool can_validate = strlen(s_address_input) > 0 && !s_validating && app->isConnected();
|
||||
// Classification + checksum are LOCAL, so allow validating offline (the is-mine check needs the node).
|
||||
bool can_validate = strlen(s_address_input) > 0 && !s_validating;
|
||||
|
||||
if (!can_validate) {
|
||||
ImGui::BeginDisabled();
|
||||
@@ -123,8 +124,9 @@ void ValidateAddressDialog::render(App* app)
|
||||
s_address_type.clear();
|
||||
s_validated = true;
|
||||
s_validating = false;
|
||||
} else if (app->isLiteBuild()) {
|
||||
// Lite has no node to ask: the checksum verdict is the answer, ownership is unknown (hidden).
|
||||
} else if (app->isLiteBuild() || !app->isConnected()) {
|
||||
// No node to ask (lite, or full-node offline): the checksum verdict is the answer; ownership
|
||||
// is unknown. The result block shows why.
|
||||
s_is_valid = true;
|
||||
s_is_mine = false;
|
||||
s_ownership_known = false;
|
||||
@@ -209,15 +211,17 @@ void ValidateAddressDialog::render(App* app)
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%s", s_address_type.c_str());
|
||||
|
||||
// Is mine? (not shown when no node was asked - lite)
|
||||
if (s_ownership_known) {
|
||||
ImGui::Text("%s", TR("validate_ownership"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_is_mine) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", TR("validate_not_mine"));
|
||||
}
|
||||
// Is mine? When no node was asked (lite / offline) say why ownership is unknown rather
|
||||
// than silently omitting it.
|
||||
ImGui::Text("%s", TR("validate_ownership"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (!s_ownership_known) {
|
||||
ImGui::TextDisabled("%s", TR(app->isLiteBuild() ? "validate_ownership_unknown_lite"
|
||||
: "validate_ownership_unknown_offline"));
|
||||
} else if (s_is_mine) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", TR("validate_not_mine"));
|
||||
}
|
||||
if (s_is_mining) { // valid, but not for payments
|
||||
ImGui::Spacing();
|
||||
@@ -227,8 +231,6 @@ void ValidateAddressDialog::render(App* app)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!app->isConnected()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected"));
|
||||
} else {
|
||||
// Pre-interaction placeholder: keeps the results area from reading as a large
|
||||
// dead gap between the Validate/Paste row and the Close button until a result exists.
|
||||
|
||||
+13
-1
@@ -232,6 +232,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_empty_hint"] = "No conversations yet. Messages you receive will appear here.";
|
||||
strings_["chat_you"] = "You";
|
||||
strings_["chat_contact_request"] = "contact request";
|
||||
strings_["chat_request_title"] = "New contact request";
|
||||
strings_["chat_request_body"] = "This person isn't in your contacts. Accept to add them, or block to delete this conversation and ignore them.";
|
||||
strings_["chat_request_accept"] = "Accept";
|
||||
strings_["chat_request_block"] = "Block";
|
||||
strings_["chat_request_accepted"] = "Contact added.";
|
||||
strings_["chat_send_failed"] = "not sent";
|
||||
strings_["chat_status_sending"] = "Sending… waiting for your wallet to broadcast it";
|
||||
strings_["chat_status_sent"] = "Sent to the network — it arrives after it confirms (about a minute)";
|
||||
@@ -688,6 +693,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["wiz_bootstrap_desc"] = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
|
||||
strings_["wiz_skip"] = "Skip";
|
||||
strings_["wiz_bootstrap_failed"] = "Bootstrap failed";
|
||||
strings_["bootstrap_try_mirror"] = "Try mirror";
|
||||
strings_["wiz_download_failed"] = "Download Failed";
|
||||
strings_["wiz_ext_daemon_running"] = "External daemon running";
|
||||
strings_["wiz_ext_daemon_warning"] = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
|
||||
@@ -1451,6 +1457,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["confirmations"] = "Confirmations";
|
||||
strings_["confirmed"] = "Confirmed";
|
||||
strings_["pending"] = "Pending";
|
||||
strings_["baltab_pending_chip"] = "Pending +%.4f";
|
||||
strings_["baltab_pending_tip"] = "Awaiting confirmations — not spendable yet; it moves to your available balance once confirmed.";
|
||||
strings_["sent"] = "sent";
|
||||
strings_["received"] = "received";
|
||||
strings_["mined"] = "mined";
|
||||
@@ -2618,7 +2626,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["shield_check_status"] = "Check Status";
|
||||
strings_["shield_completed"] = "Operation completed successfully!";
|
||||
strings_["shield_description"] = "Shield your mining rewards by sending coinbase outputs from transparent addresses to a shielded address. This improves privacy by hiding your mining income.";
|
||||
strings_["shield_from_address"] = "From Address:";
|
||||
strings_["shield_from_address"] = "From mining address:";
|
||||
strings_["shield_all_mining_addrs"] = "All mining addresses";
|
||||
strings_["shield_confirm_fmt"] = "Shield up to %s DRGX from %d mining address(es) (at most %d rewards per run) to %s? Network fee: %s DRGX.";
|
||||
strings_["shield_funds"] = "Shield Funds";
|
||||
strings_["shield_in_progress"] = "Operation in progress...";
|
||||
strings_["shield_max_utxos"] = "Max UTXOs per operation";
|
||||
@@ -2723,6 +2733,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["validate_invalid"] = "INVALID";
|
||||
strings_["validate_is_mine"] = "This wallet owns this address";
|
||||
strings_["validate_not_mine"] = "Not owned by this wallet";
|
||||
strings_["validate_ownership_unknown_offline"] = "Unknown — node offline";
|
||||
strings_["validate_ownership_unknown_lite"] = "Unknown — not checkable in the lite wallet";
|
||||
strings_["validate_ownership"] = "Ownership:";
|
||||
strings_["validate_results"] = "Results:";
|
||||
strings_["validate_results_placeholder"] = "Results will appear here";
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// A tiny, pure block-sync rate estimator for the status-bar / peers-card ETA. Pure (no ImGui, no App):
|
||||
// App owns one instance, feeds it the current height + a monotonic clock once per frame while syncing,
|
||||
// and resets it on connect / disconnect / rescan / restart so a height drop never produces a stale or
|
||||
// absurd ("412h") ETA.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
class SyncRateEstimator {
|
||||
public:
|
||||
void reset() { prevBlocks_ = -1; prevTime_ = 0.0; rate_ = 0.0; }
|
||||
|
||||
// Call once per frame while syncing. `now` is monotonic seconds (e.g. ImGui::GetTime()).
|
||||
void update(int blocks, double now) {
|
||||
// First sample, a rescan/restart height drop, or a clock reset → re-baseline, don't compute.
|
||||
if (prevBlocks_ < 0 || blocks < prevBlocks_ || now < prevTime_) {
|
||||
prevBlocks_ = blocks; prevTime_ = now; rate_ = 0.0; return;
|
||||
}
|
||||
const double dt = now - prevTime_;
|
||||
if (blocks > prevBlocks_) {
|
||||
if (dt > 0.5) {
|
||||
const double raw = (blocks - prevBlocks_) / dt;
|
||||
rate_ = rate_ > 0.0 ? rate_ * 0.7 + raw * 0.3 : raw; // smooth (same alpha as the old code)
|
||||
prevBlocks_ = blocks; prevTime_ = now;
|
||||
}
|
||||
} else if (dt > 10.0) { // stalled: decay and re-baseline the clock
|
||||
rate_ *= 0.5; prevTime_ = now;
|
||||
if (rate_ < 0.1) rate_ = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
double rate() const { return rate_; } // blocks/sec, 0 when unknown
|
||||
|
||||
// <= 0 when unknown. Capped at ~1 year so a tiny rate can't overflow.
|
||||
long long etaSeconds(int blocksLeft) const {
|
||||
if (rate_ <= 0.1 || blocksLeft <= 0) return -1;
|
||||
const double s = blocksLeft / rate_;
|
||||
return s > 3.15e7 ? 31500000LL : static_cast<long long>(s);
|
||||
}
|
||||
|
||||
private:
|
||||
int prevBlocks_ = -1; // -1 = unset (distinct from a legitimate height 0)
|
||||
double prevTime_ = 0.0;
|
||||
double rate_ = 0.0;
|
||||
};
|
||||
|
||||
// Human ETA with days, e.g. "3d 4h", "5h 12m", "7m 9s", "42s". English unit letters (as the old code).
|
||||
inline void formatEta(long long s, char* out, std::size_t n) {
|
||||
if (s >= 86400) std::snprintf(out, n, "%lldd %lldh", s / 86400, (s % 86400) / 3600);
|
||||
else if (s >= 3600) std::snprintf(out, n, "%lldh %lldm", s / 3600, (s % 3600) / 60);
|
||||
else if (s >= 60) std::snprintf(out, n, "%lldm %llds", s / 60, s % 60);
|
||||
else std::snprintf(out, n, "%llds", s);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user