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>
168 lines
5.6 KiB
C++
168 lines
5.6 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace data {
|
|
|
|
/**
|
|
* @brief A single address book entry
|
|
*/
|
|
struct AddressBookEntry {
|
|
std::string label;
|
|
std::string address;
|
|
std::string notes;
|
|
// Per-wallet visibility scope: "global" (shown in every wallet) or a wallet-identity hash
|
|
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
|
|
// entries — written before multi-wallet scoping — keep showing everywhere.
|
|
std::string scope = "global";
|
|
// Contact avatar: "" = default type badge (Z/T), "icon:<name>" = a Material wallet-icon,
|
|
// "img:<path>" = a custom image (copied into <config>/contact-avatars/).
|
|
std::string avatar;
|
|
|
|
AddressBookEntry() = default;
|
|
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",
|
|
const std::string& s = "global")
|
|
: label(l), address(a), notes(n), scope(s) {}
|
|
|
|
bool isGlobal() const { return scope.empty() || scope == "global"; }
|
|
// Visible under the wallet whose identity hash is walletHash (or globally).
|
|
bool visibleInWallet(const std::string& walletHash) const {
|
|
return isGlobal() || scope == walletHash;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief Address book manager
|
|
*
|
|
* Stores labeled addresses for easy access when sending.
|
|
*/
|
|
class AddressBook {
|
|
public:
|
|
AddressBook();
|
|
~AddressBook();
|
|
|
|
/**
|
|
* @brief Load address book from disk
|
|
* @return true if loaded successfully
|
|
*/
|
|
bool load();
|
|
|
|
/**
|
|
* @brief Save address book to disk
|
|
* @return true if saved successfully
|
|
*/
|
|
bool save();
|
|
|
|
/**
|
|
* @brief Get default address book file path
|
|
*/
|
|
static std::string getDefaultPath();
|
|
|
|
/**
|
|
* @brief Add a new entry
|
|
* @param entry The entry to add
|
|
* @return true if added (false if duplicate address)
|
|
*/
|
|
bool addEntry(const AddressBookEntry& entry);
|
|
|
|
/**
|
|
* @brief Update an existing entry
|
|
* @param index Index of entry to update
|
|
* @param entry New entry data
|
|
* @return true if updated
|
|
*/
|
|
bool updateEntry(size_t index, const AddressBookEntry& entry);
|
|
|
|
/**
|
|
* @brief Remove an entry
|
|
* @param index Index of entry to remove
|
|
* @return true if removed
|
|
*/
|
|
bool removeEntry(size_t index);
|
|
|
|
/**
|
|
* @brief Re-attach contacts stuck on a legacy (drifting address-hash) scope to a stable wallet id.
|
|
* Rewrites every non-global entry whose scope is NOT already a stable "w:"-prefixed id to
|
|
* `scopeId`, and saves once if anything changed. Recovery for contacts orphaned when the
|
|
* old address-set-hash scope shifted (e.g. after creating a new address).
|
|
* @return number of entries re-scoped.
|
|
*/
|
|
int reattachLegacyScopes(const std::string& scopeId);
|
|
|
|
/**
|
|
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
|
* @param address Address to search for
|
|
* @return Index or -1 if not found
|
|
*/
|
|
int findByAddress(const std::string& address) const;
|
|
|
|
/**
|
|
* @brief Is there an existing entry with this address that would be visible ALONGSIDE a new
|
|
* entry of the given scope? (Same wallet, or either side global.) Used to reject
|
|
* duplicates within a wallet while still allowing the same address in different wallets.
|
|
* @param excludeIndex Storage index to skip (for edit), or -1.
|
|
*/
|
|
bool hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
|
int excludeIndex = -1) const;
|
|
|
|
/**
|
|
* @brief Get all entries
|
|
*/
|
|
const std::vector<AddressBookEntry>& entries() const { return entries_; }
|
|
|
|
/**
|
|
* @brief Get entry count
|
|
*/
|
|
size_t size() const { return entries_.size(); }
|
|
|
|
/**
|
|
* @brief Monotonic counter bumped on every content change (add/update/remove/load/sweep). Consumers
|
|
* (e.g. the chat conversation-list memo) key their caches off this so an IN-PLACE edit — a rename or
|
|
* address change that keeps size() constant — still invalidates them.
|
|
*/
|
|
std::uint64_t revision() const { return revision_; }
|
|
|
|
/**
|
|
* @brief UI-sweep ONLY: show `demo` instead of the real book. The first call snapshots the real
|
|
* entries; later calls (the sweep re-runs its setup every frame) only swap the demo list. While
|
|
* active, save() and load() return true without touching disk or the snapshot, so nothing (an
|
|
* add/update/remove, reattachLegacyScopes) can persist demo contacts. Do not use outside the sweep.
|
|
*/
|
|
void sweepBegin(std::vector<AddressBookEntry> demo) {
|
|
if (!sweep_active_) { sweep_backup_ = std::move(entries_); sweep_active_ = true; }
|
|
entries_ = std::move(demo);
|
|
++revision_;
|
|
}
|
|
/** @brief UI-sweep ONLY: put the real book back (no disk write). Idempotent. */
|
|
void sweepEnd() {
|
|
if (!sweep_active_) return;
|
|
entries_ = std::move(sweep_backup_);
|
|
sweep_backup_.clear();
|
|
sweep_active_ = false;
|
|
++revision_;
|
|
}
|
|
bool sweepActive() const { return sweep_active_; }
|
|
|
|
/**
|
|
* @brief Check if empty
|
|
*/
|
|
bool empty() const { return entries_.empty(); }
|
|
|
|
private:
|
|
std::vector<AddressBookEntry> entries_;
|
|
std::uint64_t revision_ = 0;
|
|
std::string file_path_;
|
|
std::vector<AddressBookEntry> sweep_backup_; // the real book while a UI sweep shows demo contacts
|
|
bool sweep_active_ = false;
|
|
};
|
|
|
|
} // namespace data
|
|
} // namespace dragonx
|