feat(lite): send-time unlock prompt for locked encrypted wallets
When the user confirms a send on a locked encrypted lite wallet, show an unlock modal (passphrase -> unlockWallet) instead of letting the backend reject it with "Wallet is locked". After unlocking, the user re-confirms the send (the form is preserved). Balances remain viewable while locked; only spending needs unlock. - send_tab: the Confirm-and-send button routes to App::requestLiteUnlock() when getWalletState().isLocked(), else sends as before. - App::renderLiteUnlockPrompt(): centered modal, passphrase (Enter submits), Unlock/Cancel; the passphrase buffer is sodium-zeroed after every path. Full-node unaffected (gated on liteWallet()/isLocked()). Builds clean, launches clean, tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
47
src/app.cpp
47
src/app.cpp
@@ -13,6 +13,8 @@
|
|||||||
#include "config/settings.h"
|
#include "config/settings.h"
|
||||||
#include "wallet/lite_wallet_controller.h"
|
#include "wallet/lite_wallet_controller.h"
|
||||||
#include "wallet/lite_wallet_server_selection_adapter.h"
|
#include "wallet/lite_wallet_server_selection_adapter.h"
|
||||||
|
|
||||||
|
#include <sodium.h> // sodium_memzero for the lite unlock-passphrase buffer
|
||||||
#include "daemon/daemon_controller.h"
|
#include "daemon/daemon_controller.h"
|
||||||
#include "daemon/embedded_daemon.h"
|
#include "daemon/embedded_daemon.h"
|
||||||
#include "daemon/lifecycle_adapters.h"
|
#include "daemon/lifecycle_adapters.h"
|
||||||
@@ -1397,6 +1399,8 @@ void App::render()
|
|||||||
|
|
||||||
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
|
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
|
||||||
renderLiteFirstRunPrompt();
|
renderLiteFirstRunPrompt();
|
||||||
|
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
|
||||||
|
renderLiteUnlockPrompt();
|
||||||
|
|
||||||
if (show_import_key_) {
|
if (show_import_key_) {
|
||||||
renderImportKeyDialog();
|
renderImportKeyDialog();
|
||||||
@@ -1843,6 +1847,49 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::renderLiteUnlockPrompt()
|
||||||
|
{
|
||||||
|
if (!lite_wallet_) return;
|
||||||
|
static char pass[128] = "";
|
||||||
|
|
||||||
|
if (lite_unlock_prompt_ && !ImGui::IsPopupOpen("##LiteUnlock")) {
|
||||||
|
ImGui::OpenPopup("##LiteUnlock");
|
||||||
|
}
|
||||||
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f),
|
||||||
|
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||||
|
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
|
||||||
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||||
|
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||||
|
ImGui::TextUnformatted(TR("lite_unlock_title"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextUnformatted(TR("lite_unlock_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::SetNextItemWidth(280.0f);
|
||||||
|
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
|
||||||
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
|
ImGui::Spacing();
|
||||||
|
const float btnW = 130.0f;
|
||||||
|
bool doUnlock = ImGui::Button(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
|
||||||
|
if (doUnlock) {
|
||||||
|
const bool ok = lite_wallet_->unlockWallet(pass);
|
||||||
|
sodium_memzero(pass, sizeof(pass));
|
||||||
|
if (ok) ui::Notifications::instance().success(TR("lite_unlock_ok"), 5.0f);
|
||||||
|
else ui::Notifications::instance().error(TR("lite_unlock_failed"));
|
||||||
|
lite_unlock_prompt_ = false;
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
|
||||||
|
sodium_memzero(pass, sizeof(pass));
|
||||||
|
lite_unlock_prompt_ = false;
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::renderAboutDialog()
|
void App::renderAboutDialog()
|
||||||
{
|
{
|
||||||
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "about");
|
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "about");
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ public:
|
|||||||
config::Settings* settings() { return settings_.get(); }
|
config::Settings* settings() { return settings_.get(); }
|
||||||
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||||
|
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
|
||||||
|
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
|
||||||
// (Re)build the lite controller from current settings so a changed lite-server selection
|
// (Re)build the lite controller from current settings so a changed lite-server selection
|
||||||
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
||||||
void rebuildLiteWallet();
|
void rebuildLiteWallet();
|
||||||
@@ -427,6 +429,8 @@ private:
|
|||||||
bool lite_autoopen_done_ = false;
|
bool lite_autoopen_done_ = false;
|
||||||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||||
bool lite_firstrun_dismissed_ = false;
|
bool lite_firstrun_dismissed_ = false;
|
||||||
|
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||||||
|
bool lite_unlock_prompt_ = false;
|
||||||
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||||
util::AsyncTaskManager async_tasks_;
|
util::AsyncTaskManager async_tasks_;
|
||||||
@@ -651,6 +655,7 @@ private:
|
|||||||
void renderStatusBar();
|
void renderStatusBar();
|
||||||
void renderAboutDialog();
|
void renderAboutDialog();
|
||||||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||||
|
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||||
void renderImportKeyDialog();
|
void renderImportKeyDialog();
|
||||||
void renderExportKeyDialog();
|
void renderExportKeyDialog();
|
||||||
void renderBackupDialog();
|
void renderBackupDialog();
|
||||||
|
|||||||
@@ -771,6 +771,11 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
Type().text(TypeStyle::Body2, TR("sending"));
|
Type().text(TypeStyle::Body2, TR("sending"));
|
||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
||||||
|
// A locked encrypted lite wallet can't spend; prompt to unlock instead of sending
|
||||||
|
// (the backend would otherwise reject with "Wallet is locked").
|
||||||
|
if (app->liteWallet() && app->getWalletState().isLocked()) {
|
||||||
|
app->requestLiteUnlock();
|
||||||
|
} else {
|
||||||
s_sending = true;
|
s_sending = true;
|
||||||
s_send_start_time = ImGui::GetTime();
|
s_send_start_time = ImGui::GetTime();
|
||||||
s_tx_status = std::string(TR("sending")) + "...";
|
s_tx_status = std::string(TR("sending")) + "...";
|
||||||
@@ -804,6 +809,7 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
s_show_confirm = false;
|
s_show_confirm = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
|
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
|
||||||
|
|||||||
@@ -588,6 +588,12 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["lite_welcome_created"] = "Wallet created — back up your recovery phrase now in Settings → Backup & keys";
|
strings_["lite_welcome_created"] = "Wallet created — back up your recovery phrase now in Settings → Backup & keys";
|
||||||
strings_["lite_welcome_restore_hint"] = "Restore your wallet under Settings → Lite wallet request";
|
strings_["lite_welcome_restore_hint"] = "Restore your wallet under Settings → Lite wallet request";
|
||||||
strings_["lite_welcome_create_failed"] = "Could not create wallet";
|
strings_["lite_welcome_create_failed"] = "Could not create wallet";
|
||||||
|
// Lite send-time unlock prompt.
|
||||||
|
strings_["lite_unlock_title"] = "Unlock wallet";
|
||||||
|
strings_["lite_unlock_msg"] = "Enter your passphrase to unlock the wallet for spending.";
|
||||||
|
strings_["lite_unlock_btn"] = "Unlock";
|
||||||
|
strings_["lite_unlock_ok"] = "Wallet unlocked — you can send now";
|
||||||
|
strings_["lite_unlock_failed"] = "Unlock failed — wrong passphrase?";
|
||||||
|
|
||||||
// Send Tab
|
// Send Tab
|
||||||
strings_["pay_from"] = "Pay From";
|
strings_["pay_from"] = "Pay From";
|
||||||
|
|||||||
Reference in New Issue
Block a user