feat(bootstrap,validate): Retry keeps the mirror; Validate works offline

- Bootstrap: Retry now reuses the source the user actually started (was always
  the default), and a "Try mirror" button offers the alternate server on
  failure (s_lastUrl remembered on start, cleared on a fresh Confirm).
- Validate Address: classification + checksum are local, so the dialog no
  longer requires a node connection — offline/lite now validate and show the
  type, with ownership reported as "Unknown — node offline / not checkable in
  the lite wallet" instead of a dead "not connected" screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 13:05:40 -05:00
co-authored by Claude Opus 4.8
parent f3f0b32a48
commit 7cea9b7339
3 changed files with 29 additions and 15 deletions
+10 -1
View File
@@ -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;
};
+16 -14
View File
@@ -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.
+3
View File
@@ -688,6 +688,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.";
@@ -2723,6 +2724,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";