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 "wallet/lite_wallet_controller.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/embedded_daemon.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.
|
||||
renderLiteFirstRunPrompt();
|
||||
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
|
||||
renderLiteUnlockPrompt();
|
||||
|
||||
if (show_import_key_) {
|
||||
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()
|
||||
{
|
||||
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "about");
|
||||
|
||||
Reference in New Issue
Block a user