Files
ObsidianDragon/src/data/address_book.h
T
DanSandClaude Opus 5.5 62a5577491 fix(sweep): keep the UI sweep away from real wallet data
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>
2026-10-12 13:22:00 -05:00

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