fix: Tier-2 UX robustness — action guards, in-progress guards, input trimming
The app-wide "import-key pattern" fixes (missing guards / untrimmed input / no
re-entrancy protection), from the robustness audit:
- Import-key dialog: unify the type indicator and the RPC dispatch on one
classifier (classifyPrivateKey is now prefix-aware — an "SK..." shielded key no
longer misroutes to the transparent RPC), trim manual input (not just Paste),
and gate both the indicator and the Import button on a shared
isRecognizedPrivateKey() so unrecognized/empty input can't be submitted.
- Shield: guard the submit button on connected + !syncing (like Send), with a
disabled tooltip, instead of firing into a raw daemon error.
- Mining: disable the pool Mine button when the payout address is empty
("enter a payout address first"), and trim the pool URL/worker on persist.
- Console: track in-flight RPC commands (atomic counter + busy() override) so the
input is disabled while a command runs instead of piling up on the worker.
- Maintenance (rescan/repair/delete-chain/reinstall-daemon): re-entrancy guard —
bail with "already in progress" instead of launching a duplicate destructive op.
- Bootstrap dialog: re-attach to an in-flight download on reopen instead of
resetting state and orphaning the running worker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
src/app.cpp
61
src/app.cpp
@@ -2650,20 +2650,24 @@ void App::renderImportKeyDialog()
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
}
|
||||
|
||||
// Trimmed view for validation (importPrivateKey trims again before the RPC). The indicator and the
|
||||
// Import-button guard below both derive from the SAME shared classifier, so they can't disagree.
|
||||
std::string keyTrim(import_key_input_);
|
||||
while (!keyTrim.empty() && (keyTrim.front()==' '||keyTrim.front()=='\t'||keyTrim.front()=='\n'||keyTrim.front()=='\r')) keyTrim.erase(keyTrim.begin());
|
||||
while (!keyTrim.empty() && (keyTrim.back()==' '||keyTrim.back()=='\t'||keyTrim.back()=='\n'||keyTrim.back()=='\r')) keyTrim.pop_back();
|
||||
bool keyRecognized = services::WalletSecurityController::isRecognizedPrivateKey(keyTrim);
|
||||
bool keyIsZ = services::WalletSecurityController::classifyPrivateKey(keyTrim)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
|
||||
// Key validation indicator
|
||||
if (import_key_input_[0] != '\0') {
|
||||
std::string k(import_key_input_);
|
||||
bool isZKey = (k.substr(0, 20) == "secret-extended-key-") ||
|
||||
(k.length() >= 2 && k[0] == 'S' && k[1] == 'K');
|
||||
bool isTKey = (k.length() >= 51 && k.length() <= 52 &&
|
||||
(k[0] == '5' || k[0] == 'K' || k[0] == 'L' || k[0] == 'U'));
|
||||
if (isZKey || isTKey) {
|
||||
if (!keyTrim.empty()) {
|
||||
if (keyRecognized) {
|
||||
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s",
|
||||
isZKey ? "Shielded spending key" : "Transparent private key");
|
||||
keyIsZ ? "Shielded spending key" : "Transparent private key");
|
||||
} else {
|
||||
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP);
|
||||
@@ -2686,18 +2690,17 @@ void App::renderImportKeyDialog()
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::BeginDisabled(!keyRecognized); // only a recognized Z/T key can be imported
|
||||
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
std::string key(import_key_input_);
|
||||
if (!key.empty()) {
|
||||
importPrivateKey(key, [this](bool success, const std::string& msg) {
|
||||
import_success_ = success;
|
||||
import_status_ = msg;
|
||||
if (success) {
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
}
|
||||
});
|
||||
}
|
||||
importPrivateKey(std::string(import_key_input_), [this](bool success, const std::string& msg) {
|
||||
import_success_ = success;
|
||||
import_status_ = msg;
|
||||
if (success) {
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
}
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
show_import_key_ = false;
|
||||
@@ -3187,6 +3190,13 @@ void App::rescanBlockchain()
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
|
||||
// running would stomp each other — a second confirm must not launch a duplicate operation.
|
||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
|
||||
|
||||
@@ -3229,6 +3239,11 @@ void App::repairWallet()
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
|
||||
|
||||
@@ -3270,6 +3285,11 @@ void App::reinstallBundledDaemon()
|
||||
return;
|
||||
}
|
||||
|
||||
if (async_tasks_.isRunning("reinstall-daemon")) {
|
||||
ui::Notifications::instance().warning("The daemon reinstall is already in progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
|
||||
|
||||
@@ -3309,6 +3329,11 @@ void App::deleteBlockchainData()
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user