feat(lite): backup & keys UI — export seed/keys + import (Settings)
Add a "Backup & keys" section to the lite Settings page, shown only for an open wallet, wiring the M4 controller backup/import surface into the GUI: - "Show seed" / "Show private keys" -> exportSeed() / exportPrivateKeys(); the revealed secret is displayed read-only (TextWrapped, no extra copies) with Copy and "Hide & wipe" controls. - "Import key" (password input) -> importKey() (auto-detects WIF vs shielded); do_import_sk just records the key + saves (no synchronous rescan), so this is safe on the UI thread — history appears after the next sync. Secret hygiene: the revealed-backup buffer is sodium-wiped via secureWipeLiteSecret on hide, on a new export (overwrite), and if the wallet closes while revealed; each export also wipes the controller's result copy; the import input buffer is zeroed immediately after submission. Lite app + full-node variant build/link clean; controller methods already covered by testLiteWalletControllerM4; hygiene clean. GUI behavior itself isn't auto-verifiable here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,13 @@ struct SettingsPageState {
|
||||
bool lite_restore_overwrite = false;
|
||||
std::string lite_lifecycle_status;
|
||||
std::string lite_lifecycle_summary;
|
||||
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
|
||||
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
|
||||
// import. lite_import_key is the import input buffer (wiped right after submission).
|
||||
std::string lite_export_secret;
|
||||
std::string lite_export_label;
|
||||
char lite_import_key[512] = "";
|
||||
std::string lite_backup_status;
|
||||
bool mine_when_idle = false;
|
||||
int mine_idle_delay = 120;
|
||||
bool idle_thread_scaling = false;
|
||||
@@ -1724,6 +1731,89 @@ void RenderSettingsPage(App* app) {
|
||||
s_settingsState.lite_lifecycle_summary.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Backup & keys (open wallet only) ----------------------------------
|
||||
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
Type().text(TypeStyle::Body2, "Backup & keys");
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
auto r = app->liteWallet()->exportSeed();
|
||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||
if (r.ok) {
|
||||
s_settingsState.lite_export_secret = r.seedPhrase;
|
||||
s_settingsState.lite_export_label = "Seed phrase — write it down, never share it";
|
||||
s_settingsState.lite_backup_status.clear();
|
||||
} else {
|
||||
s_settingsState.lite_export_label.clear();
|
||||
s_settingsState.lite_backup_status = r.error;
|
||||
}
|
||||
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
auto r = app->liteWallet()->exportPrivateKeys();
|
||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||
if (r.ok) {
|
||||
s_settingsState.lite_export_secret = r.privateKeysJson;
|
||||
s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds";
|
||||
s_settingsState.lite_backup_status.clear();
|
||||
} else {
|
||||
s_settingsState.lite_export_label.clear();
|
||||
s_settingsState.lite_backup_status = r.error;
|
||||
}
|
||||
wallet::secureWipeLiteSecret(r.privateKeysJson);
|
||||
}
|
||||
|
||||
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
|
||||
if (!s_settingsState.lite_export_secret.empty()) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
s_settingsState.lite_export_label.c_str());
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_settingsState.lite_export_secret.c_str());
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||
s_settingsState.lite_export_label.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Import a spending/viewing key (history appears after the next sync).
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Import key");
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(liteInputW);
|
||||
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
||||
sizeof(s_settingsState.lite_import_key),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
|
||||
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
||||
s_settingsState.lite_backup_status =
|
||||
r.ok ? "Key imported — run a sync to scan its history" : r.error;
|
||||
}
|
||||
|
||||
if (!s_settingsState.lite_backup_status.empty()) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
s_settingsState.lite_backup_status.c_str());
|
||||
}
|
||||
} else if (!s_settingsState.lite_export_secret.empty()) {
|
||||
// Wallet closed while a backup was revealed — don't leave it in memory.
|
||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||
s_settingsState.lite_export_label.clear();
|
||||
}
|
||||
} else {
|
||||
|
||||
float rpcLblW = std::max(
|
||||
|
||||
Reference in New Issue
Block a user