Commit Graph

654 Commits

Author SHA1 Message Date
818c29fca7 feat(market): settings gear, options modal & distinct portfolio styles
Add a settings-gear modal (portfolio layout / chart style / fetch prices) and persist the portfolio style. Redesign the three row styles into distinct purposes: Table (borderless grid + column header + % pill), Cards (glass card + muted label/shadow value + shielded/transparent split bar), Spotlight (heavy tile + enlarged direction-coloured hero value + delta chip). Dim empty groups. Add data::SumPortfolioSplit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:03 -05:00
0a43742897 feat(chat): side-by-side settings modal with live conversation preview
Widen the chat settings modal and split it into a live message preview (canned bubbles rendered with the real style/accent/density/font/emoji/timestamp code) beside the controls; size the Done button to its text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:03 -05:00
defe14d913 feat(contacts): avatar shapes, list scale & live settings preview
Add a settings-gear modal with a segmented avatar-shape picker (circle / rounded square / full-row left tab), a list-scale slider, and a live two-row preview. Smooth-scroll the address list and fix a latent wheel double-scroll on the outer scroll child. (settings.h also refreshes the portfolio-style doc comment.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:03 -05:00
8b55a90102 i18n: contacts/market settings strings + portfolio-style rename
Add contacts_shape_tab, the market settings-modal + column-header keys; rename the portfolio-style labels to Table / Cards / Spotlight (same keys, new values). Rebuild the CJK subset for the new glyphs. All 8 languages, additive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:02 -05:00
0fd3d214a4 fix(ui): clip & left-align overflowing SegmentedControl labels
material::SegmentedControl drew each label centred with no clip, so long non-English labels (Russian/German, etc.) overflowed their cell and overlapped neighbours. Left-align on overflow and clip per cell (mirroring the chat modal's segmented helper); the fits-case is visually unchanged. Benefits every caller (contacts, receive, wallets, market).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:02 -05:00
996e10ed02 fix(ui): stop forward-wheel double-scroll on peers & console tabs
The inner scroll child (##PeersList / ConsoleOutput) uses NoScrollWithMouse + ApplySmoothScroll; ImGui forwards its wheel up to the outer scroll container, so the wheel scrolled both. Add NoScrollWithMouse to the outer containers (safe: both fill the remaining height and never overflow), matching the contacts/explorer fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:02 -05:00
2c909d35ea feat(chat): message-list rework, customization surface, smooth scroll
Chat tab overhaul:
- Header/layout: single-row header (name + key-lock + compact click-to-copy address +
  right-aligned icon toolbar with a settings "notch"), composer moved below the message
  box (emoji toggle left of a bottom-flush input), tightened list-pane controls.
- Message list: per-day date separators (Today/Yesterday/date), time-only group headers,
  tight same-sender grouping with iMessage-style merged corners, per-run peer avatar,
  delivery status (clock -> check), hover-reveal per-message time. Message base ~18px.
- Customization: a settings "notch" gear opens a modal (also under Settings ->
  Chat & Contacts) with segmented controls for emoji style / bubble style / density /
  timestamps, sliders for poll rate + text size, a bubble-accent dropdown, Enter-to-send.
  Bubble style/accent/density/text-size/timestamps all applied live in the message loop.
- Settings: app-wide clock-format control lives in Settings -> General; chat keeps a
  per-tab override. Both chat panes now use the app's smooth wheel-scroll.
- i18n: new strings across all 8 languages; CJK subset rebuilt for the added glyphs.

Inline contact rename, hide/mute, 0-conf fast-scan and the export path are carried through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:45:19 -05:00
3b5db02e09 feat(app): font-atlas rebuild hook + clock/emoji sync
- requestFontRebuild() + preFrame handling so toggling color emoji swaps the atlas live;
  request a rebuild at startup when the saved setting wants color.
- preFrame syncs Typography's color-emoji flag and the util clock flag from settings.
- Chat 0-conf fast-scan cadence now reads the user's poll-rate setting (was a hardcoded 2.5s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:55 -05:00
8d8cd337cf feat(util): app-wide 24h/12h clock format
Add util::formatClockDateTime/formatClockTime driven by a process-wide flag (setClock12h,
synced from the time_format setting each frame). Switch the primary user-facing timestamp
displays to it — the transaction list, wallet-state tx + banned-peer times, the explorer
block time, and the block-info dialog — so one preference drives every clock.

Log files, export filenames/content, console line prefixes, the market chart axis, and the
worker-thread "last updated" string intentionally stay fixed 24h.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:44 -05:00
acde8c0833 feat(config): chat-customization + app-wide clock settings
Persisted (additive JSON, clamped on load) preferences backing the new chat settings
surface and the global clock:

- chat: emoji style (color default), poll rate, bubble style + accent, message density,
  text scale, per-tab timestamp override, Enter-to-send.
- app-wide time_format (0=24h, 1=12h) that the Chat tab falls back to and every other
  user-facing timestamp now follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:29 -05:00
a78b44246e build(freetype): color-emoji rendering via a cross-platform FreeType backend
The chat "Emoji style: Color" option renders the merged emoji in color (COLR/CPAL
Twemoji) instead of the monochrome NotoEmoji subset. This needs FreeType, which the
default stb_truetype rasterizer can't do for color glyphs.

- Vendor imgui_freetype (matches the bundled 1.92 ImFontLoader API) and embed a 1.4 MB
  COLRv0 Twemoji font (no libpng/harfbuzz needed).
- CMake gains an optional FreeType path: native Linux/macOS use the system FreeType via
  find_package; the mingw-w64 cross-compile has none, so build.sh --win-release now
  cross-builds a minimal static FreeType (scripts/build-freetype-mingw.sh) and passes it
  in. Absent FreeType => graceful monochrome fallback, so no build breaks.
- Typography selects the FreeType loader + the color font (LoadColor) when color emoji is
  on, else the stb loader + mono subset; toggling reloads the atlas. Both the DX11 and
  OpenGL backends already support the 1.92 RGBA dynamic atlas, so color glyphs render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:19 -05:00
63ed31d2ec feat(chat): polish thread header, emoji picker, and pane gutter
Tier A + B chat-tab visual pass:
- Widen the list<->thread gutter (1px -> 10px) for breathing room.
- Redesign the thread header: avatar + name on the left with a
  right-aligned frameless icon toolbar (add-contact/export/mute/hide,
  each with a tooltip); the name is clipped to the toolbar's left edge
  so a long contact label can't overrun the icons.
- Replace the "Copy Full Address" button with a click-to-copy shortened
  address (copies the full z-addr), preceded by a key-verify lock whose
  tooltip shows a comparable identity-key fingerprint.
- Show a "Waiting for reply" chip in the header when the peer's identity
  key isn't known yet.
- Emoji picker: frameless grid with tight cells (glyphs fill the cell)
  and leftover width spread into the column gaps so it stays edge-to-edge.
- Larger 30px composer emoji toggle that lights up while the picker is
  open; footer height + byte-counter centering adjusted to match.

New i18n keys (chat_copy_address_tip / chat_verify_key / chat_awaiting_key)
added additively to all 8 languages; CJK subset font rebuilt for the new
zh glyph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 09:14:44 -05:00
64b27db2ff feat(chat): emoji picker as an in-pane overlay with keyword search
Reworked the emoji picker from a floating popup (which overlapped the message
area) into an overlay that takes over the conversation-list pane while open:
a Cancel button + a keyword search box at the top, then a responsive grid below
that wraps to the pane width.

Each emoji now carries search keywords (grin/heart/fire/…) so the search box
filters the ~150-emoji set. The 🙂 composer button toggles the overlay;
selecting an emoji appends it to the composer (byte-cap respected) and the
overlay stays open for multiple picks.

+1 CJK glyph (絵) for the ja "Search emoji" string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:06:37 -05:00
03fe6e077c feat(chat): emoji picker + show-hidden toggle
- Emoji picker: a 🙂 button on the composer row opens a scrollable grid of ~150
  common single-codepoint emoji (all verified present in the bundled NotoEmoji
  subset); clicking appends to the composer, respecting the byte cap. ImGui does
  no shaping, so the set is single-codepoint only (ZWJ sequences / flags omitted).
- Show hidden: when there are hidden conversations, a "Show hidden (N)" toggle
  appears in the list; toggling it reveals them (dimmed) and the thread header's
  Hide button becomes Unhide. Off by default, and resets when nothing is hidden.

8-language strings; no new CJK glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 01:08:35 -05:00
125ffa8863 feat(chat): dedicated ~2.5s poll for the 0-conf fast-scan
The fast-scan was hung on the page's Transactions timer, which is 10–15s on most
pages (15s on Chat), so incoming messages still took up to ~15s to appear. Give it
its own ~2.5s accumulator (delta-time based, independent of the page cadence) so
messages land in a few seconds — network propagation then dominates, which is as
fast as 0-conf gets. Still gated behind the warmup/rescan block and self-gated in
fastScanChatMemos; the in-flight guard prevents overlapping RPCs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:58:42 -05:00
3465acc57b fix(chat): session-guard the fast-scan callback (stale-wallet ingest)
Review of 30bd0d9 found the 0-conf fast-scan MainCb was the one async chat
callback missing the chat_session_generation_ guard the broadcast + identity-fetch
callbacks use. worker_ survives a wallet switch/lock, so a fast-scan posted under
wallet A could drain after resetChatSession() and ingest A's metadata (or toast)
against wallet B's freshly-provisioned store.

- Capture scanGen at post time and drop the result if it changed by drain time.
- Clear chat_fast_scan_in_flight_ in resetChatSession() so a switch immediately
  re-enables the fast path; the stale callback returns WITHOUT clearing the flag so
  it can't clobber the new session's own in-flight scan (generation is bumped only
  in resetChatSession, which already reset the flag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:50:58 -05:00
30bd0d99ff feat(chat): 0-conf fast-scan so incoming messages show before a block
The receive harvest gates each z-address behind scannedAtTip — it only re-scans
when a new block advances the tip — so incoming chat waited ~1 confirmation even
though the daemon already exposes mempool notes (FindMySaplingNotes runs on
mempool txs; z_listreceivedbyaddress(addr,0) returns them).

Add App::fastScanChatMemos(): every transaction-refresh cycle, re-scan JUST the
chat reply address (where peers send) at minconf=0, extract chat metadata, and
ingest — so messages surface at mempool speed (a few seconds) instead of waiting
for a block. Full-node only (lite has its own harvest). An in-flight guard avoids
stacking RPCs; the store dedups on txid+position, so the confirmed harvest never
double-inserts.

Hidden conversations are deliberately skipped by the fast path — they don't get
the mempool speed-up and still come back through the normal confirmed harvest
(which un-hides on a new message). New non-muted messages toast off-tab as usual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:38:41 -05:00
29fbe46cce fix(chat): un-hide on lite receives + scope the contact picker per wallet
Review of fc414ee found two issues:

- HIGH: the lite variant harvests chat via ingestLiteChatMemos, which called
  ingest() without the newIncomingCids out-param — so the un-hide never ran and a
  hidden conversation stayed hidden PERMANENTLY on new messages (chat is default-ON
  and fully supported in Lite), breaking the "a new message brings it back"
  invariant. Wire the same un-hide + off-tab toast into the lite path.

- LOW: the new-conversation contact picker listed every z-address contact,
  ignoring the per-wallet scope the Contacts tab enforces — leaking another
  wallet's scoped contact into this wallet's picker. Apply the same scope test
  (global + legacy fail open; "w:" scopes match the active wallet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:36:14 -05:00
fc414eeed4 feat(chat): hide conversations + contact address picker for new conversation
Hide conversations:
- A "Hide" action in the thread header drops a conversation from the list. The
  messages stay in the seed-encrypted store (on-chain history can't be deleted);
  a new INCOMING message un-hides it (you can't un-receive), so nothing is lost.
- Hidden cids persist in settings (mirrors the mute list) and are skipped by both
  the conversation list and the unread badge.

New-conversation address picker:
- A "Choose from contacts" dropdown lists the address book's shielded (z-address)
  contacts and fills the recipient field on selection; manual paste still works.

8-language strings + CJK subset (+1 glyph 届).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:26:04 -05:00
fce873936e fix(chat): demo identity must not clobber a real one or be a shared constant
Root cause of the reported "my own messages come back as replies": seedChatDemoData
(the Settings "Seed demo chat" debug button) set the chat identity to a FIXED
secret ("obsidian-dragon-demo-chat") and, because maybeProvisionChatIdentity
no-ops once any identity exists (app_network.cpp:2771), that demo identity stuck
and overrode the wallet's real seed-derived one. Clicking it on two different-seed
wallets gave BOTH the same constant identity — so they were cryptographically the
same person, and a wallet's own outgoing memo (harvested) decrypted back as an
incoming message.

Two rules now:
- Only fabricate a demo identity when there is NO real one (never clobber a
  provisioned wallet identity).
- Derive it from a RANDOM per-run secret, so it can never be a constant shared
  across installs/wallets.

Complements 7351d8a (which removed the self-harvest path); together they close the
loopback both at the harvest and at the identity source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 22:41:43 -05:00
7351d8a06d fix(chat): stop our own sent messages appearing as incoming duplicates
Two causes of the "every message I send shows up again as a reply from the peer":

1. Harvest bug — the full-refresh path fed a SENT tx's outgoing memos (from
   z_viewtransaction outgoing outputs) into the chat metadata extractor, and
   ChatService::ingest marks everything Incoming. So each send was re-ingested as
   a phantom "from peer" message. The recent-refresh path already omitted this, so
   it was accidental. Drop the outgoing chat-harvest (keep the tx-history harvest);
   genuine incoming still comes from z_listreceivedbyaddress, and our sends are
   recorded by the local echo.

2. Own-identity ingest filter — a memo whose sender public key equals our own
   identity is by definition something we sent (only we hold our key); it must
   never be ingested as incoming. Skip those in ingest. This also collapses
   same-seed self-chat (running the SAME wallet in the full node and Lite makes
   them one chat identity, so sends land on an address we also own and loop back).

For a real two-party chat use two DIFFERENT wallets/seeds — same-seed wallets are
one identity and can't be distinct peers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:21:41 -05:00
76093fe82d fix(chat): address adversarial review of the send-path change
Four confirmed findings from the review of ef247c9:

1. Persistence regression — the deferred-persist echo (in-memory Sending, written
   only when the async callback resolved) meant a message broadcast on-chain but
   whose callback hadn't fired yet was LOST from history if the app quit/crashed
   in that window. Persist the echo immediately as Sending and UPSERT the final
   status on resolve (new ChatDatabase::upsert with ON CONFLICT DO UPDATE, since
   append is INSERT-OR-IGNORE). A stray persisted Sending still loads as Sent.

2. Fee ceiling — dragonxd REJECTS a 0-value tx whose fee exceeds the default
   miners fee (0.0001), and max(getDefaultFee(), 0.0001) can only raise it, so a
   default_fee > 0.0001 broke every chat send. Pin chat to exactly kChatMinFeeDrgx,
   dropping getDefaultFee() from this path (chat always moves 0 value).

3. Lifetime — the resolve callback had no generation guard, so a wallet lock (which
   doesn't disconnect) between submit and callback could resolve against a cleared
   store. Capture chat_session_generation_ and bail on mismatch (both the full-node
   callback and the lite optimistic resolve), matching the identity-fetch pattern.

4. Retry misdirect — Retry on a failed CONTACT REQUEST called sendChatMessage,
   which (no peer key yet) just showed "waiting for reply". Route it to
   sendContactRequestForCid() (refactored out of startChatConversation) so it
   re-sends the request into the SAME conversation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:42:59 -05:00
ef247c95ff feat(chat): fee floor, real delivery status, pay-from-funded, funds pre-check
Chat sends move 0 value, so the network fee is structurally load-bearing (it's
the only thing that forces a real shielded input; 0-value + 0-fee builds a
degenerate, unrelayable tx). Three gaps addressed:

1. Fee floor — broadcastChatMemos now uses max(getDefaultFee(), kChatMinFeeDrgx),
   so a 0 / too-low global default-fee setting can't silently break chat.

2. Real delivery status — the echo was marked Sent on SUBMIT regardless of the
   on-chain outcome (the z_sendmany callback was empty), so failures were
   invisible and the Retry affordance never fired for async failures. Add a third
   ChatDelivery::Sending state (appended so persisted 0=Sent stays valid); record
   the echo in-memory as Sending, and resolve it to Sent/Failed from the
   z_sendmany completion callback — persisting only the final status (so a restart
   never shows a stuck spinner; a stray persisted Sending loads as Sent). A subtle
   "sending…" label shows while in flight.

3. Pay-from-funded + pre-check — z_sendmany spends from one z-address, and the
   identity reply address may be unfunded while funds sit elsewhere. chatPayFromZaddr
   picks a spendable z-address that can cover the fee (preferring the identity
   address); the memo still advertises the identity address as reply-to, so paying
   from a different note is transport-transparent. If nothing can cover the fee, a
   clear "need a small shielded balance" toast replaces the cryptic failure.

Full node only for the callback path; lite resolves optimistically on queue.
8-language strings + CJK subset (+1 glyph 賄).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:27:43 -05:00
c291e8a587 fix(app): use #if not #ifdef for the per-variant instance lock
DRAGONX_LITE_BUILD is ALWAYS defined (0 for the full node, 1 for Lite) via
$<BOOL:...>, so the previous #ifdef was true for BOTH variants — the full node
took the Lite branch and grabbed the "obsidiandragonlite" lock, so launching Lite
next still collided ("ObsidianDragonLite already running"). Switch to
#if DRAGONX_LITE_BUILD (value check), matching how the rest of the codebase
guards this macro.

Verified: full-node binary now contains only "obsidiandragon"; preprocessor check
confirms LITE=1 selects "obsidiandragonlite". (wallet_capabilities.h's #ifndef is
a separate, correct default-definition idiom — not affected.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 19:08:41 -05:00
06e55d6394 feat(app): per-variant single-instance lock so full node + Lite can run together
The single-instance lock hardcoded the name "obsidiandragon" for both variants,
so launching ObsidianDragonLite while ObsidianDragon was running (or vice-versa)
was refused with "Another instance is already running". Nothing else actually
required them to be exclusive — DRAGONX_APP_NAME already gives each variant its
own config dir (settings / wallets index / address book / chat db), the full
node's daemon lives under ~/.hush/DRAGONX with no Lite counterpart, and the lock
guards no cross-instance IPC (the payment URI is handled locally).

Key the lock per variant (obsidiandragon / obsidiandragonlite) — the Windows
named mutex already derives from the same name, so it's fixed on both platforms.
Each variant still enforces a single instance of itself. Also make the
already-running message report the actual variant (DRAGONX_APP_NAME) and use
MessageBoxA so it can. mingw-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:42:10 -05:00
2072f70a60 fix(contacts): stable per-wallet scope so contacts don't vanish (address-hash drift)
Contacts were scoped by activeWalletIdentityHash() — a hash of the wallet's
ENTIRE address set. Creating a new receive address grows the set, changing the
hash, so every contact stamped with the old hash falls out of the scope filter
(contacts_tab.cpp:915) while still being counted — the "3 saved, 1 showing"
symptom, where only the one set to global (which bypasses the scope) survives.
It also hid scoped contacts on every startup before the daemon connected (hash
empty until addresses load).

Introduce a stable per-wallet scope id: WalletIndexEntry.scopeId ("w:"+random
hex), generated once and persisted in the wallet index (keyed by wallet file),
never recomputed from the mutable address set — so creating addresses, locking,
or disconnecting never changes it. App::activeWalletScopeId() establishes it on
first use. Contacts now scope + filter on this instead of the drifting hash. The
tx-history-cache identity (the hash's real purpose) is untouched.

Recovery for already-orphaned contacts:
- AddressBook::reattachLegacyScopes() re-attaches non-global, non-"w:" contacts
  to the active wallet's stable id; run once when there's a single known wallet
  (unambiguous attribution). Idempotent.
- The scope filter fails OPEN for legacy scopes (multi-wallet case where recovery
  can't attribute them) so no contact is ever hidden; stable "w:" scopes still
  match strictly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:27:23 -05:00
ee9ea15233 fix(rpc,ui): close the last secret residues found by the final-gate review
Two verified medium leaks in the B7 scrub chain:

- parseRpcResult parses the body into a local `response` tree and returns a COPY
  of response["result"] (operator[] yields an lvalue ref, so the by-value return
  copy-constructs). The local tree — holding its own heap copy of the secret — was
  then freed without zeroing, so callSecret/callSecretString still left one
  un-scrubbed copy. Add scrubJsonSecrets() (recursive string zero) and a
  scrubSource flag; the secret paths opt in, wiping the tree before it frees. The
  secret export chain is now fully covered: raw body → parse tree → result copy →
  caller-owned string.

- key_export_dialog cleared s_key with plain std::string::clear() on the Close
  button, the scrim/Esc dismiss path, and the QR cache (s_qr_cached) — leaving the
  displayed private/spending key in freed heap on the ordinary close paths. Route
  all three through wallet::secureWipeLiteSecret (zero-then-clear), matching
  show()/hide().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:41:24 -05:00
11de117331 feat(chat): render emoji in messages and user text (Q12)
Merge a monochrome Noto Emoji subset into the text fonts so chat messages, the
composer, contact names, and memos render emoji (😀 🎉🔥 👍 …) instead of
tofu.

- Enable IMGUI_USE_WCHAR32 (imconfig.h): emoji live above the BMP (U+1F300+), so
  16-bit ImWchar literally can't address them. This widens ImWchar build-wide;
  the only ImWchar uses in-tree are glyph-range arrays and one BMP private-use
  codepoint, so nothing else is affected. Tests + full build pass.
- Bundle res/fonts/NotoEmoji-Subset.ttf — the OFL monochrome Noto Emoji (color
  CBDT/COLR fonts can't be rasterized by ImGui's stb_truetype) pinned to wght=400
  and subset to the emoji planes (1411 glyphs, 747 KB). Reproducible via
  scripts/build_emoji_subset.py. Embedded via INCBIN like the CJK subset.
- Typography::loadFont merges it (MergeMode) only into the small text fonts
  (Body/Subtitle/Caption/Button) — not headers, which don't need 1400 emoji.
  The base font keeps precedence for U+2600–26FF, so text-style symbols stay.

Limits: ImGui does no shaping, so single-codepoint emoji render but ZWJ sequences
(family/profession) and regional-indicator flags won't compose; emoji are
monochrome (the OS emoji picker still inputs them fine, and the composer byte
counter already counts their 4-byte UTF-8 cost against the on-chain cap).

Verified headless: sizeof(ImWchar)==4 and every probed emoji is in-font and bakes
into the atlas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:28:50 -05:00
fac0245297 fix(chat,rpc): address adversarial review of the chat backlog
Four confirmed findings from the review pass:

SECURITY — B7 was incomplete:
- The single-key Export dialog (key_export_dialog) still used plain call() for
  z_exportkey/dumpprivkey/z_exportviewingkey — a live spending-key leak on the
  most common per-address export path, missed by the B7 commit.
- callSecret() zeros the raw body but the parsed json holds its OWN heap copy of
  the secret; several callers did .get<string>() on a temporary json and freed
  that copy un-wiped.
  Fix: add RPCClient::callSecretString() — returns the bare-string result with
  BOTH the raw body AND the json node zeroed, so callers can't forget. Route
  key_export_dialog (×2), exportPrivateKey, and export_all_keys (×2) through it;
  scrub the z_exportmnemonic json node in seed_wallet_creator (object result);
  also wipe the transient key copies, the displayed s_key on reset, and the
  aggregated export-all `keys` buffer.

CHAT:
- Jump-to-latest pill: SetCursorScreenPos moved the parent cursor and never
  restored it, so the composer footer rendered ~8px too high while scrolled up.
  Save + restore the cursor around the pill.
- New-message toast: gating on a chatUnreadCount() watermark delta could be
  swallowed when an outgoing echo (wall-clock) pushed the seen-watermark past a
  later reply's block time. ingest() now reports the cids it appended; the toast
  fires when any is a non-muted conversation — skew-proof, still mute-aware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:11:31 -05:00
9cb91415d9 i18n(chat): translate the new chat-backlog strings (8 languages)
Translate the 14 chat strings added by the backlog work (relative time, retry,
jump-to-latest, empty states, search, export, message-too-long, mute/unmute) into
de/es/fr/ja/ko/pt/ru/zh, added additively (no existing key overwritten).

Also fix a key collision the English pass introduced: the new empty-state
sub-hint reused "chat_empty_hint", which already meant the standalone list hint —
so English and the translations disagreed. Split it into a distinct
"chat_empty_start" for the "Start one with New conversation" sub-line, leaving
the original chat_empty_hint intact.

Rebuilt the CJK subset font: +3 glyphs (刚静音) for zh "刚刚"/"静音", 0 removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:53:48 -05:00
24c661f743 feat(chat): mute conversations (Q10)
Per-conversation mute toggle in the thread header. Muted conversations (tracked
by cid in settings, so it persists) are skipped by chatUnreadCount(), so they
neither raise the nav-item unread badge nor the new-message toast — the toast now
gates on a chatUnreadCount() delta across ingest, which already skips muted cids,
so mute is respected for free. "Block" (rejecting a peer's inbound memos) is a
larger ingest-filter change and is intentionally left out of this pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:48:57 -05:00
fecc9015fb feat(rpc): scrub the raw response body for secret-bearing calls (B7)
The secret exports (z_exportmnemonic / z_exportkey / dumpprivkey) already scrub
the parsed value at the call site, but the raw HTTP response string those RPCs
build — the curl write buffer, which holds the same secret in the clear — was
freed without zeroing. That's the "fuller fix belongs in the RPC layer" the
identity-fetch comment flagged.

Add RPCClient::callSecret(), a call() variant that sodium_memzeros the raw
response body after parsing (on success and on throw). NRVO makes the returned
string the very buffer curl wrote into, so one wipe covers it. Route every
secret-bearing export through it: chat identity (mnemonic + z_exportkey
fallback), Settings seed-phrase + single-key export, Export-all-keys, and the
migrate-to-seed isolated-node mnemonic export. Purely additive — the parsed
result is byte-identical, so no behavior change (safe for the fund-critical
migrate path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:45:41 -05:00
bce69362eb feat(chat): message bubbles, empty states, search, export, richer composer
Chat-tab backlog from the audit:

- V3/V7/Q7/Q9 — thread overhaul: direction-aligned rounded bubbles with
  sender/minute grouping and a grouped meta line; peer avatar in the header;
  hover shows the full timestamp; a right-aligned "not sent · Retry" affordance
  re-sends failed outgoing messages; a floating "Latest" pill appears when the
  thread is scrolled up.
- V4 — centered empty states (icon + title + hint) for the locked, no-conversations,
  and no-selection panes.
- V6 — faint sidebar tint on the conversation list + a tight single-line seam.
- Q5 — compact relative time ("now"/"5m"/"3h"/"2d"/"Mon DD") in the list preview.
- Q6 — multi-line composer (Enter sends, Ctrl+Enter newline) with a live byte
  counter against the on-chain body cap (= (512−len"utf8:")/2 − ABYTES = 236),
  Send disabled + counter reddened when over.
- Q8 — case-insensitive conversation search over name + last body (thread stays
  open even when filtered out); "no matches" hint.
- Q11 — export a decrypted conversation to a plaintext file in the config dir
  (restricted perms, plaintext-warning tooltip), toasting the path.

English strings added to i18n.cpp; per-language JSONs follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:41:00 -05:00
7f46d9e2d5 fix(chat): harden reply-target + scrub legacy secret (B2/B4)
B4: the legacy chat-identity fallback copied the z_exportkey spending key out of
the RPC response and let the temporary json destruct un-zeroed. Mirror the
mnemonic path — take our copy, then sodium_memzero the json's own buffer.

B2: the memo header's peer z-address / cid ride OUTSIDE the secretstream AEAD, so
trusting the newest message's header let a later message redirect our replies or
splice threads. Pin the reply target (and displayed peer) to the EARLIEST
(establishing) message instead of the latest, at both the send and display sites.
Because ChatStore returned filtered INSERTION order (a scan harvests txids in
set/hash order — not chronological), "earliest" wasn't reliable; ChatStore::
conversation now returns messages sorted by (timestamp, txid, payload_position),
which also fixes out-of-order thread rendering and the last-message preview.

A complete fix binds z+cid into the AEAD additional-data, but that's a coordinated
HushChat/SDXLite wire-format change; this pin hardens the reply target without it.
Adversarially verified; the store-ordering gap it surfaced is fixed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:07:12 -05:00
100ed01469 feat(chat): unread count + sidebar badge (Q1)
Track a per-conversation "last seen" watermark (message timestamp) on App:
- chatUnreadCount() sums incoming messages newer than each conversation's
  watermark; surfaced as SidebarStatus.chatUnreadCount → a badge on the Chat nav
  item (mirrors the History/Peers badges).
- Viewing a thread marks it seen (markChatConversationSeen while displayed).
- Baseline on load: existing stored messages are marked seen, so only messages
  that arrive while the app is open badge as unread.
- Wiped in resetChatSession so unread state never leaks across a wallet switch.
In-memory only (resets on app restart); persistence is a later refinement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:56:58 -05:00
055685a4c4 feat(chat): copy, add-to-contacts, and new-message toast (Q2/Q3/Q4)
- Q3: copy the peer z-address from the thread header (SmallButton), and right-click
  any message to copy its body.
- Q2: an "Add contact" action in the header when the peer isn't already known —
  one click saves them to the address book (rename later in Contacts).
- Q4: capture ChatService::ingest's new-message count (previously discarded) and
  fire an in-app toast when new encrypted chat arrives while the user isn't on the
  Chat tab (main-thread MainCb sites only).

i18n (EN + 8 languages, additive; no new CJK glyphs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:52:12 -05:00
46f93e380b feat(chat): house-style polish — modal, tactile rows + avatars, DPI
Audit B3/V1/V2/V5/B6 — bring the Chat tab up to the contacts_tab bar:
- New-Conversation modal rebuilt on the house BlurFloat OverlayDialog with
  LabeledInput fields and an accented TactileButton footer (Send disabled until
  both fields are set); wipes the sent plaintext. Replaces the raw ImGui popup and
  its hardcoded widths (V1).
- Conversation rows: leading letter-avatars (deterministic palette color + the
  peer's UTF-8 initial), Primary-tinted selected fill + border, OnSurface hover —
  matching contacts_tab's tactile card rows (V2). Stable PushID(cid) instead of the
  re-sorted loop index (B6). Taller rows.
- Send / New are accented TactileButtons with press feedback (V5).
- All hardcoded px (Send width, modal) go through Layout::dpiScale() so nothing
  clips at 150% (B3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:46:42 -05:00
6f6ad89c9f fix(chat): wipe typed plaintext on a wallet switch + per-thread draft
Audit B1/B5/B8:
- The chat composer / new-conversation buffers are file-static char[]; on a wallet
  switch or lock the plaintext a user typed for wallet A (a private message, or a
  recipient z-address) resurfaced verbatim in wallet B's composer and lingered
  unwiped in RAM. Add ui::ResetChatTab() (sodium_memzero the buffers + clear the
  selection ids) and call it from App::resetChatSession().
- Wipe the single composer draft when the active conversation changes, so text
  typed for one contact can't be sent to another (B5).
- Refresh the stale "read-only / Phase 3" docs — composing/sending is wired (B8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:40:19 -05:00
d9911a9bc9 feat(wallet): detect a corrupt target wallet on switch + offer -salvagewallet repair
When a switch fails, the failure modal now distinguishes a CORRUPT target wallet
from other failures and offers a one-click repair:
- The switch worker watermarks the node's captured console output before the
  start and, if the node dies in init, scans this start's output for a corruption
  signature ("Failed to rename … .bak" / "salvage failed" / "wallet.dat corrupt" /
  "Error loading wallet") → sets switch_wallet_corrupt_.
- The Failed modal then shows an accurate "this wallet appears corrupt" message
  (instead of the generic "Couldn't open that wallet") plus a "Try to repair
  (salvage)" button that retries the switch with the target node started under
  -salvagewallet (recovers readable keypairs; implies -rescan).
- EmbeddedDaemon::setSalvageOnNextStart (one-shot, precedence salvage > zap >
  rescan) + controller forwarder; switchToWallet gains a salvage arg.

Salvage operates only on the corrupt target (never the good wallet, no fund
movement). Adversarially verified: output isolation, one-shot lifecycle, state
handling, re-entrancy, no success-path regression.

i18n (EN + 8 languages) + 2 new CJK glyphs baked into the subset font.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:40:33 -05:00
faf77de9ce fix(console): stop the Clear button crashing the tab
render() computes visible_indices_ (indices into model_) once per frame at the
top, before the toolbar. The toolbar's Clear button called clear() → model_.clear(),
emptying model_ mid-frame (the "cleared" marker is only queued, drained next
frame). renderOutput() then indexed model_[visible_indices_[vi]] with the stale
indices → out-of-bounds → crash.

clear() now also drops visible_indices_ and the selection (both hold line indices
into model_), so this frame's renderOutput iterates zero lines and computeVisibleLines
rebuilds them next frame. The right-click "Clear console" menu item now routes
through clear() instead of a bare model_.clear() so it's covered too.

Adversarially verified: no other same-frame path indexes the emptied model
(fold-toggle, ingest, and selectAll are bounds-guarded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:09:04 -05:00
7f414b3dcf fix(wallet): reliable process detection + drop the harmful start-retry
Two issues the latest live log exposed:
- The switch's start-retry spawned a SECOND dragonxd while the first was still
  shutting down, so the two held wallet.dat against each other (BDB "Failed to
  rename wallet-savings.dat … Error"), and it then span on a stale "Daemon
  already running". Revert to a single start — the stopDaemonForWalletSwitch()
  wait already ensures the old process is gone, so a valid wallet opens cleanly
  and a bad one exits during init and reverts, without overlapping spawns.
- findProcessByName() used the non-suffixed PROCESSENTRY32/Process32First with an
  ANSI _stricmp; if UNICODE is defined those map to the wide variants, so the
  compare comparing garbage would NEVER match — silently making the process-gone
  wait a no-op. Rewritten with the explicit wide Toolhelp API + lstrcmpiW so it's
  correct either way (verified to compile under mingw with and without -DUNICODE).

Note: a corrupt wallet.dat (BDB recovery failing) still can't be opened by any
node — that's a data issue needing a clean reset, not a switch-flow bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:53:05 -05:00
994ddea6cd feat(wallet): richer detail in the wallet-switch progress modal
The switch modal now shows more than a bare phase line:
- The title carries the target wallet ("Switching wallet — savings") and a
  "from <previous>" caption for context (names prettified: wallet.dat → "Default
  wallet", wallet-<name>.dat → "<name>", in-place links → "External wallet").
- During the Reconnecting phase — the ~30-60s where the node loads the block
  index, verifies, and rescans — it surfaces the node's LIVE init stage
  (state_.warmup_status/description via the existing translateWarmup mapping:
  "Loading blockchain data…", "Verifying blockchain…", "Scanning for
  transactions…") instead of a static "Reconnecting…".
- An elapsed timer (m:ss) so the wait visibly progresses.

i18n: from/elapsed/default-wallet/external-wallet labels (EN + 8 languages,
additive; no new CJK glyphs). No switch-flow logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:11:53 -05:00
41e4e73e63 fix(wallet): wait for IPv6 port + retry start so a switch survives the DB-env race
Live logs showed the switch's new node starting on the correct wallet, reaching
"Verifying wallet…", then aborting with "Binding RPC on ::1 port 21769 failed" +
"Failed to rename wallet-savings.dat" — the OLD node's RPC port (on ::1/IPv6) and
Berkeley DB environment weren't fully released yet, so the wallet-verify DB
recovery couldn't rename the file. The app then reverted, and the connect loop
brought a node up on the DEFAULT wallet.

Two causes fixed:
- isPortInUse() only probed 127.0.0.1 (IPv4). The daemon also binds ::1 (IPv6),
  which lingers after IPv4 releases — so the readiness wait returned "free"
  prematurely. Now probe BOTH families (Windows: IPv4 + ::1 via in6addr_loopback;
  Linux: /proc/net/tcp + tcp6). mingw-verified.
- The datadir/DB-env can still be briefly held right after the old node exits, so
  the first start can abort. Retry the start (up to 6×, 2s backoff) with the
  CORRECT -wallet — active_wallet_file isn't reverted until we give up — resetting
  the crash count and re-arming -rescan each attempt, until one survives.

Also: the "Wallet switch failed" modal no longer repeats its title in the warning
header — it now shows the actual reason there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:45:20 -05:00
88091bd77c feat(wallet): live progress modal for wallet switching
The "Stop the running node?" confirm modal now stays open through the entire
switch — stop → wait-for-exit → start → reconnect — showing a live phase, and
auto-closes the moment the new node connects. This turns the up-to-a-minute
graceful-shutdown wait from an apparent freeze into visible progress.

- WalletSwitchPhase (Stopping/Starting/Reconnecting/Failed) + atomic phase and
  dialog-open flags; the worker advances the phase, onConnected closes the modal,
  and a failed switch shows the accurate reason with a Close button.
- Owned switches (no confirm) also show the progress modal directly.
- "Continue in background" escape hatch so a long rescan / a hung startup never
  traps the user (the switch keeps running; a toast reports the result).
- isWalletSwitchInProgress() keeps the frame loop redrawing so the phase text and
  spinner animate while otherwise idle.
- i18n (EN + 8 languages, additive) + a modal-switch-progress sweep surface.

State machine adversarially verified 6/6 (thread-safety, no stuck modal,
confirm→progress transition, owned/unowned, no phase leak, redraw scoping).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:56:53 -05:00
02839dc415 fix(wallet): wait for the old node to fully exit before restarting on switch
After the stop, the switch started the replacement dragonxd too early — while the
old (direct-connected, unowned) node was still doing a slow graceful shutdown
(~70s; its network threads block on peer TLS timeouts) and holding the DATADIR
LOCK. The replacement couldn't acquire the lock, failed repeatedly, and the
crash-wedge left it stuck "starting". Root cause: the readiness poll used
isRpcPortInUse(), which on Windows is a connect() probe that reads "free" the
moment the daemon stops ACCEPTING RPC — early in shutdown, long before the
process exits and releases the datadir.

- EmbeddedDaemon::isDaemonProcessRunning(): true while any dragonxd process is
  alive (Windows findProcessByName; Linux /proc/<pid>/comm scan; macOS port
  fallback) — reflects the PROCESS, not just RPC acceptance.
- stopDaemonForWalletSwitch: for an UNOWNED node (no handle), after the RPC stop
  wait until BOTH the port is free AND isDaemonProcessRunning() is false, bounded
  ~120s (or ~5s if the stop couldn't be sent). Owned nodes are unchanged
  (stopEmbeddedDaemon() blocks for exit via the handle).
- Switch notification reworded to set the up-to-a-minute expectation (60s toast).

daemon_restarting_ stays set across the wait so the connect loop can't spawn a
competing daemon. Adversarially verified 6/6; fixes the seed-adopt path too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:27:46 -05:00
17b70a1388 fix(wallet): route switch stop by process-handle ownership, not the external latch
Live logs showed the app usually just DIRECT-CONNECTS to an already-running
dragonxd (config found → connect; EmbeddedDaemon::start() never called). Two
consequences broke the switch: no process handle, and externalDaemonDetected()
stays false (it's only latched inside start()). So a direct-connected node was
treated as "owned" → stopEmbeddedDaemon() → an autoDetectConfig() temp RPC stop
that never reached the daemon → the node never stopped → the ~40s port poll timed
out → "the running node didn't release its connection in time" revert.

Gate on the real ownership signal — whether we hold a live process handle
(isEmbeddedDaemonRunning()) — instead of the unreliable externalDaemonDetected():
- stopDaemonForWalletSwitch: owned (we spawned it) → stopEmbeddedDaemon() with
  SIGTERM/SIGKILL; NOT owned (adopted or direct-connect, no handle) → RPC "stop"
  over the exact creds we're connected with (saved_config_), which is guaranteed
  to reach our node. Then the unchanged port-free poll.
- switchToWallet confirm gate: show "Stop the running node?" when connected to a
  node this session didn't spawn (state_.connected && !isEmbeddedDaemonRunning()).
- Fixes beginAdoptSeedWallet's direct-connect case identically (shared helper).

Adversarially verified (6/6): ownership signal, saved_config_ delivery incl.
cookie auth, foreign-daemon safety, confirm gate, seed-adopt, re-ownership/revert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:46:11 -05:00
42d59709f9 feat(wallet): confirm before stopping an adopted node to switch wallets
When switching wallets, the node the app would restart may be one this session
ADOPTED — a dragonxd already running at launch (left up by "keep node running",
or started by the user). Rather than stopping it silently (or the old dead-code
"stop any external dragonxd first" refusal), show a "Stop the running node?"
confirmation first; switching then stops it (RPC stop + wait for the port to
fully free) and relaunches on the selected wallet.

- switchToWallet(walletFile, stopDaemonConfirmed=false): when the node is adopted
  (externalDaemonDetected) and not yet confirmed, defer to the dialog and return.
- renderSwitchStopDaemonDialog(): BlurFloat overlay (house style) with a warning
  header; confirm re-enters switchToWallet(w, true); cancel/X aborts, node keeps
  running. Owned nodes (started this session) still restart silently.
- i18n (EN + 8 languages, additive) + CJK subset rebuild; modal-switch-stopnode
  sweep surface for visual review.

Gate/re-entrancy adversarially verified (no state leak; owned switches ungated;
prompt reappears on a reverted switch; dead-daemon-before-confirm handled).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:17:08 -05:00
30bc80b2f2 fix(seed): stop an adopted daemon before the migrate-to-seed swap
beginAdoptSeedWallet had the identical adopted-external-daemon bug the wallet
switch fix (3808b3e) fixed: it gated the wallet.dat swap on isEmbeddedDaemonRunning(),
which is process-handle-only and reads false immediately for an adopted daemon —
so the swap could run while a live daemon still held wallet.dat, and the restart
fast-failed on the held RPC port ("wallet swapped but daemon didn't restart").

Route the stop through stopDaemonForWalletSwitch() (RPC-stop the adopted daemon,
wait for the RPC port to actually free) and gate the swap on that port_free
signal instead. Clear the external latch before relaunch only when we actually
stopped it (port_free), so a still-running foreign process is never marked owned.
Owned daemons are unchanged in effect: stopEmbeddedDaemon() already blocks for
full process exit, so wallet.dat is closed before the swap.

The funded new wallet is never at risk (read-only copy source; its isolated
creator daemon was already stopped). Two rounds of adversarial review (fund/swap
safety + control-flow) cleared it. Still pending per policy: a live mainnet run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:00:21 -05:00
3808b3ee82 fix(wallet): let a switch restart an adopted (external) daemon
Switching to a named wallet failed every time when the app had connected to a
pre-existing dragonxd at startup: the daemon is flagged externalDaemonDetected,
so stopEmbeddedDaemon()'s policy is DisconnectOnly ("not ours to stop") and the
switch skips the stop entirely. The old daemon keeps the RPC port, and the
relaunch fast-fails on EmbeddedDaemon::start()'s isPortInUse check — misread as a
bad wallet, reverting after a ~40s hang on the process-handle-only run-wait
(which reads false immediately for an adopted daemon).

A switch legitimately needs to restart the node, so:
- App::stopDaemonForWalletSwitch(): for an adopted daemon, send a graceful RPC
  "stop" using the creds we actually connected with (saved_config_) — only our
  own daemon obeys it, so a foreign dragonxd is a safe no-op — then wait (bounded
  ~40s) for the RPC port to actually free (isRpcPortInUse, the same gate start()
  uses). Owned daemons take the normal stopEmbeddedDaemon() path. No PID/name kill
  is ever issued at an adopted daemon.
- switchToWallet: gate start() on the port actually freeing; if it doesn't,
  abort with a distinct switch_stop_failed_ reason instead of starting into a busy
  port. Clear the external latch before relaunch so the fresh process is owned.
- EmbeddedDaemon::clearExternalDaemonDetected() (+ controller forwarder).
- Accurate revert message: "the running node didn't release its connection in
  time" vs. the bad-wallet message.

Root-caused from live Windows logs; design + implementation adversarially
verified. Note: beginAdoptSeedWallet has the identical pattern and is left for a
separate fund-critical review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:20:50 -05:00
bb5124c1ae feat(wallets): show "HD wallet" instead of "?" when the seed flag is unreadable
When a row is probed as an HD wallet (has hdseed/hdchain records) but the
mnemonic flag can't be read — e.g. the rare tier-1 byte-scan fallback on an
unusual BDB variant or a >256 MB file — it now shows a neutral "HD wallet"
badge (ICON_MD_ACCOUNT_TREE) rather than a bare "?", which read as alarming.
"Unknown" (?) is reserved for a scan that couldn't even establish it's HD
(incomplete, no HD marker seen). Both still yield to the Lock badge when
encrypted. Seed/legacy/hd/unknown remain mutually exclusive.

Adds wallets_badge_hd / _hd_short strings (EN source + all 8 languages,
additive) and rebuilds the CJK subset font for the new glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:47:17 -05:00