The full UI sweep (dev builds only) ran its demo surfaces against the
real profile. Its teardown deleted wallet-{cold,trading,mining,donations,
2023,payroll}.dat from the real datadir, it created zero-filled wallet
files and picker-demo/extra-wallets folders, its demo wallet-index upsert
re-minted the active wallet's scopeId (hiding "w:"-scoped contacts), the
Contacts tab could re-attach legacy-scoped contacts to such a throwaway
id in addressbook.json, and it persisted the demo active wallet file and
Mining pool worker.
- Preflight: a full sweep is refused (toast + log line) while a real
workflow is in flight: a pending seed_migration_* setting or open
migrate dialog, a wallet switch, an encryption/import workflow, an
encrypt/PIN/lock operation, the seed-backup dialog, or a bootstrap.
The DRAGONX_FULL_SWEEP hook then exits as for an empty catalog.
- Persistence latches: AddressBook (snapshot on the first sweepBegin,
save/load latched while a demo-contacts surface is shown), WalletIndex
and Settings (active wallet file, contacts view mode, pool URL/worker)
for the whole sweep. Settings::sweepEnd() is the last statement of
clearDemoWalletData and flushes once only if a save was suppressed; a
quit mid-sweep writes nothing to the profile (the 0700 sandbox, with
zero-filled demo files only, may stay in the temp dir).
- Scope ids: during a full sweep activeWalletScopeId() returns the
persisted id or "" and never mints one, so the Contacts tab cannot
move legacy-scoped contacts to a throwaway id. The only address-book
write a sweep can still cause is the one-time legacy re-attach to the
wallet's persisted id, which opening Contacts does anyway.
- Sandbox: demo wallet files and folders go to a fresh random 0700 dir
under the temp dir (exclusive create, a pre-existing path or symlink
is refused), removed at sweep end. The teardown that deleted those six
wallet files is gone.
- Cleanup: the migrate-to-seed state, wizard phase, demo chat
conversations and a fabricated demo chat identity are reset at sweep
end; the seed-backup surface always shows the demo phrase and a real
z_exportmnemonic result is dropped while the sweep runs.
- i18n: one new key (screenshot_sweep_refused) in i18n.cpp and the 8
language files.
Tests: the address book snapshots only on the first sweepBegin, keeps
addressbook.json byte-identical while latched and restores the real
book; the wallet index keeps wallets.json and the real scope id, also
with the background writer attached; Settings restores the latched
values and flushes once only when a save was suppressed; the sandbox
helper refuses a pre-existing path or symlink and removes only a
prefixed dir, never a symlink's target.
Release note: v2.0.x UI sweeps could delete wallet-{cold,trading,mining,
donations,2023,payroll}.dat from the data directory and re-scope
contacts; this cannot be undone automatically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.3 KiB
C++
102 lines
4.3 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace util { class CoalescingWriter; }
|
|
namespace data {
|
|
|
|
/**
|
|
* @brief Cached, file-keyed metadata for one wallet file.
|
|
*
|
|
* A wallet.dat on disk only reveals its size + mtime; balance and address count require the daemon
|
|
* to have loaded it. So those are cached here after each load and shown in the wallet-files list
|
|
* (P2) even when the wallet isn't the active one. walletIdentityHash bridges file -> per-wallet data
|
|
* (address book, tx history) so the right caches can be selected before/after a switch.
|
|
*/
|
|
struct WalletIndexEntry {
|
|
std::string fileName; // plain wallet filename in the datadir (e.g. "wallet.dat")
|
|
std::string displayName; // user-facing name (defaults to fileName)
|
|
std::string walletIdentityHash; // address-derived identity of the last load; "" = unknown
|
|
std::string scopeId; // stable per-wallet id ("w:"+hex) for scoping contacts etc.;
|
|
// generated once, never recomputed from the (mutable) address set
|
|
double cachedBalance = -1.0; // last-known total balance; < 0 = unknown (never opened)
|
|
long long cachedAddressCount = -1;// < 0 = unknown
|
|
long long lastOpenedEpoch = 0; // unix seconds of last open; 0 = never opened
|
|
long long sizeBytesAtLastOpen = 0;
|
|
bool syncedHere = false; // loaded in this datadir before -> catch-up on switch (no full rescan)
|
|
};
|
|
|
|
/**
|
|
* @brief Persistent wallet-files metadata index (wallets.json, in the config dir).
|
|
*
|
|
* Keyed by wallet file name. Populated after each load (P1b) and read by the wallet-files list (P2).
|
|
* Also remembers extra folders the user added to scan for wallet files. Not encrypted — it holds
|
|
* only file names + coarse metadata (no keys/addresses).
|
|
*/
|
|
class WalletIndex {
|
|
public:
|
|
bool load();
|
|
bool save(); // queued on the writer when one is attached (returns true once queued)
|
|
static std::string getDefaultPath();
|
|
|
|
/** @brief Route save() through a background writer (not owned; must outlive this). Null = synchronous. */
|
|
void setFileWriter(util::CoalescingWriter* writer) { writer_ = writer; }
|
|
|
|
const std::vector<WalletIndexEntry>& entries() const { return entries_; }
|
|
const std::vector<std::string>& extraFolders() const { return extra_folders_; }
|
|
|
|
/**
|
|
* @brief Insert or update the entry for e.fileName.
|
|
* @return true if a new entry was added or any field changed (so the caller can skip save()).
|
|
*/
|
|
bool upsert(const WalletIndexEntry& e);
|
|
|
|
/** @brief Find the entry for a wallet file, or nullptr. */
|
|
const WalletIndexEntry* find(const std::string& fileName) const;
|
|
|
|
/** @brief Add/remove an extra scan folder. Returns true if the set changed. */
|
|
bool addExtraFolder(const std::string& dir);
|
|
bool removeExtraFolder(const std::string& dir);
|
|
|
|
/**
|
|
* @brief UI-sweep ONLY: snapshot the entries and extra folders; until sweepEnd(), save() returns
|
|
* true without writing or queueing. The sweep's demo upserts drop the real entry's scopeId, and
|
|
* activeWalletScopeId() would then mint and save a new one, hiding the user's "w:"-scoped contacts.
|
|
*/
|
|
void sweepBegin() {
|
|
if (sweep_active_) return;
|
|
sweep_entries_ = entries_;
|
|
sweep_extra_folders_ = extra_folders_;
|
|
sweep_active_ = true;
|
|
}
|
|
/** @brief UI-sweep ONLY: restore the snapshot (no disk write). Idempotent. */
|
|
void sweepEnd() {
|
|
if (!sweep_active_) return;
|
|
entries_ = std::move(sweep_entries_);
|
|
extra_folders_ = std::move(sweep_extra_folders_);
|
|
sweep_entries_.clear();
|
|
sweep_extra_folders_.clear();
|
|
sweep_active_ = false;
|
|
}
|
|
bool sweepActive() const { return sweep_active_; }
|
|
|
|
private:
|
|
std::vector<WalletIndexEntry> entries_;
|
|
std::vector<std::string> extra_folders_;
|
|
std::string file_path_;
|
|
util::CoalescingWriter* writer_ = nullptr; // null -> synchronous writes
|
|
std::vector<WalletIndexEntry> sweep_entries_; // the real index while a UI sweep runs
|
|
std::vector<std::string> sweep_extra_folders_;
|
|
bool sweep_active_ = false;
|
|
};
|
|
|
|
} // namespace data
|
|
} // namespace dragonx
|