Address a cluster of low-severity robustness nits where an async callback could be skipped (leaving a spinner spinning forever) or a synchronous action could be re-fired mid-flight: - app_network: add catch(...) fallbacks around the importPrivateKey and submitZSendMany worker lambdas so a non-std throw can't escape the worker and skip the main-thread callback (stuck "Importing…"/"Sending…"). - export_transactions: re-entrancy guard disabling Export while a write is in flight. - bootstrap_download: disable Cancel once clicked so the request is visibly acknowledged and can't be re-fired before the worker stops. - network_tab: give clear "already in list" feedback on a duplicate server (keep the inputs, clear only the stale invalid-URL error) and disable/relabel Refresh while a probe is in flight. - key_export: offer Retry from the error branch (falls back to Reveal) and guard against an empty/malformed address before dispatching. - receive_tab: only enter the "generating…" state when a dispatch is actually possible (guards a stuck spinner when disconnected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
171 lines
5.6 KiB
C++
171 lines
5.6 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "export_transactions_dialog.h"
|
|
#include "../../app.h"
|
|
#include "../../util/i18n.h"
|
|
#include "../../util/platform.h"
|
|
#include "../notifications.h"
|
|
#include "../schema/ui_schema.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "../theme.h"
|
|
#include "imgui.h"
|
|
|
|
#include <string>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
// Static state
|
|
static bool s_open = false;
|
|
static char s_filename[256] = "";
|
|
static std::string s_status;
|
|
// Re-entrancy guard: true while a CSV write is in flight (disables the
|
|
// Export button so a second synchronous write can't be kicked off).
|
|
static bool s_exporting = false;
|
|
|
|
// Helper to escape CSV field
|
|
static std::string escapeCSV(const std::string& field)
|
|
{
|
|
if (field.find(',') != std::string::npos ||
|
|
field.find('"') != std::string::npos ||
|
|
field.find('\n') != std::string::npos) {
|
|
// Escape quotes and wrap in quotes
|
|
std::string escaped;
|
|
escaped.reserve(field.size() + 4);
|
|
escaped += '"';
|
|
for (char c : field) {
|
|
if (c == '"') escaped += "\"\"";
|
|
else escaped += c;
|
|
}
|
|
escaped += '"';
|
|
return escaped;
|
|
}
|
|
return field;
|
|
}
|
|
|
|
void ExportTransactionsDialog::show()
|
|
{
|
|
s_open = true;
|
|
s_status.clear();
|
|
|
|
// 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_transactions_%s.csv", timebuf);
|
|
}
|
|
|
|
bool ExportTransactionsDialog::isOpen()
|
|
{
|
|
return s_open;
|
|
}
|
|
|
|
void ExportTransactionsDialog::render(App* app)
|
|
{
|
|
if (!s_open) return;
|
|
|
|
auto& S = schema::UI();
|
|
auto win = S.window("dialogs.export-transactions");
|
|
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
|
|
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
|
|
|
|
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) {
|
|
const auto& state = app->getWalletState();
|
|
|
|
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Filename
|
|
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("%s", TR("file_save_location"));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Export button (disabled while a write is already in flight)
|
|
ImGui::BeginDisabled(s_exporting);
|
|
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
|
if (state.transactions.empty()) {
|
|
Notifications::instance().warning(TR("export_tx_none"));
|
|
} else {
|
|
s_exporting = true;
|
|
std::string configDir = util::Platform::getConfigDir();
|
|
std::string filepath = configDir + "/" + s_filename;
|
|
|
|
std::ofstream file(filepath);
|
|
if (!file.is_open()) {
|
|
s_status = "Failed to create file";
|
|
Notifications::instance().error(TR("export_tx_file_fail"));
|
|
} else {
|
|
// Write CSV header
|
|
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
|
|
|
// Write transactions
|
|
for (const auto& tx : state.transactions) {
|
|
// Date
|
|
std::time_t t = static_cast<std::time_t>(tx.timestamp);
|
|
char datebuf[32];
|
|
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
|
file << datebuf << ",";
|
|
|
|
// Type
|
|
file << escapeCSV(tx.type) << ",";
|
|
|
|
// Amount
|
|
std::ostringstream amt;
|
|
amt << std::fixed << std::setprecision(8) << tx.amount;
|
|
file << amt.str() << ",";
|
|
|
|
// Address
|
|
file << escapeCSV(tx.address) << ",";
|
|
|
|
// TXID
|
|
file << escapeCSV(tx.txid) << ",";
|
|
|
|
// Confirmations
|
|
file << tx.confirmations << ",";
|
|
|
|
// Memo
|
|
file << escapeCSV(tx.memo) << "\n";
|
|
}
|
|
|
|
file.close();
|
|
|
|
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
|
" transactions to: " + filepath;
|
|
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
|
}
|
|
s_exporting = false;
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SameLine();
|
|
if (material::StyledButton("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
|