Files
ObsidianDragon/src/ui/windows/export_all_keys_dialog.cpp
DanS df14533ad3 fix: Tier-1 UX robustness — backup/export integrity, verified installs, input validation
From the app-wide robustness audit (calibrated to the import-key dialog). These
are the safety-critical fixes; several could cost users funds:

- Backup/export false success (FUND LOSS): exportAllKeys pre-seeded a header, so
  a keyless result (the usual case when the wallet is encrypted+locked) still
  looked non-empty and backupWallet wrote a private-key-less file and reported
  "Backup saved". Now exportAllKeys returns an exported count; backupWallet and
  the export-all dialog refuse to write / report success on 0 keys, disclose the
  count, flag partial results as INCOMPLETE, and the backup dialog confirms
  before overwriting an existing file (+ trims the path).
- Bootstrap: fail CLOSED — refuse to install an unverified multi-GB archive when
  no checksum is published (was `return true`), mirroring the xmrig/daemon updaters.
- Restore-from-seed: require a valid BIP39 word count (12/15/18/21/24) with a live
  "should be 24 words — you have N" hint instead of accepting any non-empty text.
- Encryption passphrase: detect leading/trailing whitespace and BLOCK with a
  warning (not a silent trim, which would change the passphrase and lock the user
  out).
- Receive payment QR: emit the canonical `drgx:` scheme with a URL-encoded memo
  via a new shared util::buildPaymentUri (the request-payment dialog now routes
  through it too, so they can't diverge); the old "dragonx:" + raw memo was
  unparseable by the wallet's own scanner.

Also clamps the receive "Recent Received" list to the 4 most recent (companion to
the recent-lists commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:54:05 -05:00

264 lines
11 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "export_all_keys_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <fstream>
#include <ctime>
namespace dragonx {
namespace ui {
using json = nlohmann::json;
// Static state
static bool s_open = false;
static bool s_exporting = false;
static std::string s_status;
static std::string s_exported_keys;
static int s_total_addresses = 0;
static int s_exported_count = 0;
static bool s_include_z = true;
static bool s_include_t = true;
static char s_filename[256] = "";
void ExportAllKeysDialog::show()
{
s_open = true;
s_exporting = false;
s_status.clear();
s_exported_keys.clear();
s_total_addresses = 0;
s_exported_count = 0;
s_include_z = true;
s_include_t = true;
// Generate default filename with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
snprintf(s_filename, sizeof(s_filename), "dragonx_keys_%s.txt", timebuf);
}
bool ExportAllKeysDialog::isOpen()
{
return s_open;
}
void ExportAllKeysDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.export-all-keys");
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
material::OverlayDialogSpec ov;
ov.title = TR("export_keys_title"); ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = win.width; ov.cardBottomViewportRatio = 0.94f; // keep authored width
if (material::BeginOverlayDialog(ov)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("%s", TR("export_keys_danger"));
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (s_exporting) {
ImGui::BeginDisabled();
}
// Options
ImGui::Text("%s", TR("export_keys_options"));
ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z);
ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t);
ImGui::Spacing();
// Filename
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("file_save_location"));
if (s_exporting) {
ImGui::EndDisabled();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Export button
if (s_exporting) {
ImGui::BeginDisabled();
}
if (material::StyledButton(TR("export_keys_btn"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (!s_include_z && !s_include_t) {
Notifications::instance().warning("Select at least one address type");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon");
} else {
s_exporting = true;
s_exported_keys.clear();
s_exported_count = 0;
s_status = "Exporting keys...";
const auto& state = app->getWalletState();
// Count total addresses to export
s_total_addresses = 0;
if (s_include_z) s_total_addresses += static_cast<int>(state.z_addresses.size());
if (s_include_t) s_total_addresses += static_cast<int>(state.t_addresses.size());
if (s_total_addresses == 0) {
s_exporting = false;
s_status = "No addresses to export";
return;
}
// Collect addresses to export (copy for worker thread)
std::vector<std::string> z_addrs, t_addrs;
if (s_include_z) {
for (const auto& a : state.z_addresses) z_addrs.push_back(a.address);
}
if (s_include_t) {
for (const auto& a : state.t_addresses) t_addrs.push_back(a.address);
}
std::string filename(s_filename);
// Run all key exports on worker thread
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), z_addrs, t_addrs, filename]() -> rpc::RPCWorker::MainCb {
std::string keys;
int exported = 0;
int total = static_cast<int>(z_addrs.size() + t_addrs.size());
// Header
keys = "# DragonX Wallet - Private Keys Export\n";
keys += "# Generated: ";
std::time_t now = std::time(nullptr);
char timebuf[64];
std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S\n", std::localtime(&now));
keys += timebuf;
keys += "# KEEP THIS FILE SECURE!\n\n";
// Export Z-addresses
if (!z_addrs.empty()) {
keys += "# === Z-Addresses (Shielded) ===\n\n";
for (const auto& addr : z_addrs) {
try {
rpc::RPCClient::TraceScope trace("Settings / Export all keys");
auto result = rpc->call("z_exportkey", {addr});
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
keys += result.get<std::string>() + "\n\n";
exported++; // count only real successes (locked/failed keys don't count)
}
} catch (...) {}
}
}
// Export T-addresses
if (!t_addrs.empty()) {
keys += "# === T-Addresses (Transparent) ===\n\n";
for (const auto& addr : t_addrs) {
try {
rpc::RPCClient::TraceScope trace("Settings / Export all keys");
auto result = rpc->call("dumpprivkey", {addr});
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
keys += result.get<std::string>() + "\n\n";
exported++; // count only real successes (locked/failed keys don't count)
}
} catch (...) {}
}
}
// Only write a file if we actually exported keys — a keyless file would be a
// misleading "export" (the usual cause of 0 keys is an encrypted+locked wallet).
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + filename;
bool writeOk = false;
if (exported > 0) {
std::ofstream file(filepath);
if (file.is_open()) {
file << keys;
file.close();
writeOk = true;
}
}
return [exported, total, filepath, writeOk]() {
s_exported_count = exported;
s_exporting = false;
if (exported == 0) {
s_status = "No keys exported (0 of " + std::to_string(total) +
") — unlock the wallet (if encrypted) and try again.";
Notifications::instance().error("No keys could be exported — is the wallet unlocked?");
} else if (!writeOk) {
s_status = "Failed to write file";
Notifications::instance().error("Failed to save key file");
} else if (exported < total) {
s_status = "Exported " + std::to_string(exported) + " of " +
std::to_string(total) + " keys to: " + filepath +
" (INCOMPLETE — some addresses had no spending key or the wallet is locked)";
Notifications::instance().warning("Partial export: " + std::to_string(exported) +
" of " + std::to_string(total) + " keys");
} else {
s_status = "Exported to: " + filepath;
Notifications::instance().success(TR("export_keys_success"), 5.0f);
}
};
});
}
}
}
if (s_exporting) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled(TR("export_keys_progress"), s_exported_count, s_total_addresses);
}
ImGui::SameLine();
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
material::EndOverlayDialog();
}
}
} // namespace ui
} // namespace dragonx