feat(wallet): per-wallet data isolation foundation (P1)
First phase of multi-wallet support: keep each wallet's data separate so loading
a different wallet no longer shows the previous one's data, and lay the metadata
groundwork for the wallet-files list.
- Address book scoped per wallet: AddressBookEntry gains a "scope" ("global" or a
wallet-identity hash); the Contacts tab shows global + current-wallet contacts
(App::activeWalletIdentityHash) with a "show in every wallet" toggle + globe
badge. Legacy entries migrate to "global". Scope-aware de-dup allows the same
address across different wallets.
- Wallet metadata index (data/wallet_index -> wallets.json): file-keyed cache of
balance / address count / identity / size / last-opened / synced-here, since
those can't be read off a wallet.dat without loading it. Populated on connect +
address refresh (change-detecting upsert). Plus an active_wallet_file setting
for the -wallet=<name> switch coming in P2.
Unit-tested (testAddressBookScope, testWalletIndex): migration, visibility filter,
scope-aware de-dup, upsert change-detection, save/reload round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete co
|
||||
static char s_edit_label[128] = "";
|
||||
static char s_edit_address[512] = "";
|
||||
static char s_edit_notes[512] = "";
|
||||
static bool s_edit_global = false; // add/edit dialog: "visible in every wallet" toggle
|
||||
static char s_search[128] = "";
|
||||
|
||||
static void copyEditField(char* dest, size_t destSize, const std::string& source) {
|
||||
@@ -69,16 +70,22 @@ void RenderContactsTab(App* app)
|
||||
|
||||
auto& book = app->addressBook();
|
||||
|
||||
// The active wallet's identity hash scopes which contacts show + tags newly-added ones.
|
||||
// Empty pre-connect (addresses unknown) — then we don't filter, to avoid hiding contacts.
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
|
||||
auto clearEditFields = []() {
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
s_edit_notes[0] = '\0';
|
||||
s_edit_global = false; // new contacts default to the current wallet
|
||||
};
|
||||
|
||||
auto loadEditFields = [](const data::AddressBookEntry& entry) {
|
||||
copyEditField(s_edit_label, sizeof(s_edit_label), entry.label);
|
||||
copyEditField(s_edit_address, sizeof(s_edit_address), entry.address);
|
||||
copyEditField(s_edit_notes, sizeof(s_edit_notes), entry.notes);
|
||||
s_edit_global = entry.isGlobal();
|
||||
};
|
||||
|
||||
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size());
|
||||
@@ -149,6 +156,10 @@ void RenderContactsTab(App* app)
|
||||
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
|
||||
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Checkbox(TR("contact_global"), &s_edit_global);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_tt"));
|
||||
|
||||
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
|
||||
float totalActionsW = actionW * 2.0f + actionGap;
|
||||
material::BeginOverlayDialogFooter(totalActionsW);
|
||||
@@ -164,6 +175,9 @@ void RenderContactsTab(App* app)
|
||||
return s;
|
||||
};
|
||||
data::AddressBookEntry entry(trimAB(s_edit_label), trimAB(s_edit_address), s_edit_notes);
|
||||
// Global if the user asked, or as a safe fallback when we don't yet know the wallet
|
||||
// (pre-connect) — better a visible-everywhere contact than one orphaned to no wallet.
|
||||
entry.scope = (s_edit_global || activeHash.empty()) ? std::string("global") : activeHash;
|
||||
if (isEdit) {
|
||||
if (app->addressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
@@ -242,7 +256,12 @@ void RenderContactsTab(App* app)
|
||||
std::vector<size_t> visibleRows;
|
||||
visibleRows.reserve(book.size());
|
||||
for (size_t i = 0; i < book.size(); ++i) {
|
||||
if (matchesSearch(book.entries()[i], needle)) visibleRows.push_back(i);
|
||||
const auto& e = book.entries()[i];
|
||||
if (!matchesSearch(e, needle)) continue;
|
||||
// Scope filter: global contacts + this wallet's contacts. Pre-connect (activeHash empty)
|
||||
// we don't know the wallet yet, so show everything rather than hide contacts.
|
||||
if (!activeHash.empty() && !e.visibleInWallet(activeHash)) continue;
|
||||
visibleRows.push_back(i);
|
||||
}
|
||||
|
||||
// Address list — size the table to fill the tab, leaving room for the count footer.
|
||||
@@ -328,6 +347,13 @@ void RenderContactsTab(App* app)
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", entry.address.c_str());
|
||||
}
|
||||
// Globe badge marks a global contact (shown in every wallet).
|
||||
if (entry.isGlobal()) {
|
||||
ImGui::SameLine(0.0f, 8.0f);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()),
|
||||
"%s", ICON_MD_PUBLIC);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(entry.notes.c_str());
|
||||
|
||||
Reference in New Issue
Block a user