Commit Graph
370 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 e24ca015d1 feat(ui): implement UI/UX audit — i18n, HiDPI, theme, destructive-action & overflow fixes
Implements all 26 confirmed UI/UX audit findings plus the 4 dashboard/timeline
tile labels. Verified across full-node + Lite + Windows builds (ctest green) and
an adversarial diff review (one market-tab delete regression caught + fixed).

i18n coverage:
- Balance hero/quick-actions/toasts + dashboard & timeline tiles (Total Balance,
  Shielded, Transparent, Quick Send, Quick Receive, Click to open, Market)
- Send: Review Send / Cancel / Paste / view-only tooltip / memo byte counter
- Settings: Lite lifecycle errors, plaintext-RPC security warning, 8 toasts
- Mining pool-payout tooltip
- Resolve daemon_update_title double-assignment collision (new daemon_update_prompt_title)
- 23 new keys translated into all 8 locales; CJK subset font rebuilt

HiDPI: DPI-scale the Send amount bar, mining thread-tile clamp bounds, receive
loading skeleton, and sidebar notification-badge insets.

Light theme: theme-aware material::SurfaceOverlay() for balance bar tracks and
row-hover highlights; contacts active-pill foreground uses OnPrimary().

Destructive actions: arm/confirm for portfolio-group delete, saved pool/worker
remove, and avatar-image delete.

Interaction/overflow: route balance star/eye buttons and the console fold-toggle
through popup-safe guards; clip peer addr/subver; truncate the balance custom
label; measure Settings tool-button widths; mining stepper disabled-state
feedback; +/- stepper buttons restyled to match the thread tiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 17:54:15 -05:00
DanSandClaude Opus 4.8 0de44569b7 feat(mining): thread-count stepper, adaptive tiles, dropdown click-through fix, xmrig version state
Mining-tab UI improvements:
- Thread selector: a centered [-] N [+] stepper (top-aligned in the header) to pick an exact
  thread count — number centered, -/+ step by one (clamped to [1, cores]), still typeable
  (commits on Enter so it doesn't restart the miner mid-typing).
- Thread tiles now render at an adaptive step (1/2/4/8 by core count) plus 1 and the max, so a
  high-core CPU (e.g. a 192-thread EPYC) shows ~25 tiles instead of one-per-thread and no longer
  overflows the card. Unchanged for <=24-core machines.
- Fix: clicking the X (or a row) in an open saved-pools / payout-address dropdown no longer bleeds
  through to the thread tiles / Mine button — the custom drawlist hit-tests now gate on
  IsPopupOpen(AnyPopup).
- xmrig update button: shows "xmrig releases" when installed >= latest (numeric version compare),
  else "Update <latest>"; the "Current: <ver>" text and the button are subtle green when up to date
  and subtle orange when an update is available (neutral when either version is unknown).

New i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; the stepper layout confirmed via the UI sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 15:39:43 -05:00
DanSandClaude Opus 4.8 06afbee4f8 fix(mining): remediate mining-tab audit (22 findings) — crash-safety, async control, validation, math
Fixes all 22 confirmed findings from the mining-tab audit (10 Medium, 12 Low; 0 Critical/High),
adversarially reviewed (6 follow-ups found + fixed, incl. the review-caught idle-auto-start bypass
and a wrong benchmark-restore condition).

Crash-safety & lifecycle:
- M-04: join a stale/finished monitor thread in XmrigManager::start() and ~XmrigManager so an xmrig
  crash-then-restart (or quit) no longer std::terminate()s the wallet.
- L-03/L-10: surface an unexpected miner exit once and clear the stale running flag.

UI never blocks (M-03/L-06/L-08/L-09/L-13): pool start/stop now run on a dedicated serialized FIFO
mining-control thread (joined before teardown), so the ~13 call sites don't block the render thread on
stop()'s SIGTERM->SIGKILL->join; the spawn result marshals back to the UI.

Miner-process / pool trust boundary:
- M-01: validate the payout address (util::isValidRecipientAddress) at EVERY start path — the UI gate
  AND App::startPoolMining() (idle auto-start / thread scaling) — so a stale/wrong-chain address can't
  silently lose rewards.
- M-09: SSRF guard skips the background pool-stats GET for loopback/private/link-local/single-label hosts.
- M-02/L-02: cap the pool-stats + xmrig-API HTTP response bodies.
- L-01: write the xmrig config 0600 at creation (POSIX open with mode) — no world/group-readable window.
- M-10: reject shell-metacharacter binary paths before the version popen (excluding '()' so Program Files
  (x86) still works).

Solo mining: M-06/M-08 clamp thread count to [1, cores] at the setgenerate/xmrig boundary; M-07 notify +
don't lie on stop failure.

Correctness: L-05 block-time constant 75->150s (chainparams); M-05 discloses pool-mode "Est. Daily" as a
rough solo-equivalent; L-04/L-11/L-12 benchmark lifecycle (cancel on nav-away / mode-switch with restore,
skip rebalance mid-benchmark); L-07 honor cancel mid-extract in both the xmrig and daemon updaters.

Two new i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 14:53:46 -05:00
DanSandClaude Opus 4.8 6ee81a5abe fix: security-audit remediation (15 findings), empty-wallet warning, and send/chat/console/shutdown UX
Security audit remediation (15 confirmed findings from the codebase audit):
- H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths
  (RAII guard) and purge stale obsidiandecryptexport* files at startup.
- M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker
  passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported
  key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and
  the first-run wizard "Skip" buffers.
- M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon
  getters (dedicated error_mutex_; DaemonController::lastError now by value),
  route xmrig last_error_ writes through a locked setter, and wrap
  shutdown_status_/wizard_stop_status_ in a locking GuardedStatus
  (wizard_stopping_external_ -> std::atomic).
- M-02: persist after a console send/shield/import in the lite backend.
- L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress.
- L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules.
- L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs.
- I-01: extract updater archives from the already-verified in-memory buffer
  (no disk re-read TOCTOU).

Feature: warn once (full-node) when the active wallet loads empty while a sibling
wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to
the recovery/Restore flow; a funded sibling .dat routes to the wallet manager.
Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false
positives on warm reconnect / spent-down wallets.

UX fixes:
- send: show the TOTAL balance (with a spendable "available" note) in the source
  dropdown and keep pending-change addresses visible.
- chat: insert emoji at the cursor position; restrict new-chat recipients to
  shielded (z) addresses.
- console: optional auto-focus of the command input on tab open (off by default).
- shutdown: when "stop external daemon" is on, keep the shutdown screen up until
  the external node actually exits, showing live status.

Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 13:18:49 -05:00
DanSandClaude Opus 4.8 ea26c0cbbb fix(balance): stop the displayed balance cratering during a pending shielded send
Sending a small amount from an address holding a large balance made the displayed
balance collapse to ~0 until the tx confirmed. A shielded spend consumes the whole
source note; the change returns as a 0-confirmation note, and every balance query
used the default minconf=1 — so the spent note dropped out and the change wasn't
counted yet.

Split every balance into two views:

- DISPLAY (balance / privateBalance / transparentBalance / totalBalance) — now
  queried at minconf=0, so it INCLUDES the user's own pending change and no longer
  craters. This is what the Overview, balance tab, market portfolio and receive
  tab show. unconfirmedBalance is now populated (= total - spendable).
- SPENDABLE (new spendableBalance / spendable*Balance) — confirmed (minconf>=1),
  what z_sendmany (run at minconf=1) can actually spend. The Send form's available/
  Max/validation, the from-address selection, the drag-to-transfer dialog cap, the
  chat pay-from and the auto-shield gate all size off these, so they never offer
  0-conf change the daemon would reject.

Implementation: a single z_listunspent(0)/listunspent(0), partitioned per-note by
"confirmations">=1; z_gettotalbalance called at minconf 0 (display) and 1
(spendable); the z_getbalance fallback queries both. applyPendingSendDelta (the
optimistic post-send debit) now touches ONLY the spendable fields — debiting the
display too would re-crater it on top of the honest minconf=0 RPC. Lite mirrors
spendableBalance = balance (its per-address balance is already confirmed) so lite
sends aren't zeroed. The confirmed-only gates (seed-migration/sweep z_gettotalbalance,
sweep z_getbalance(addr,1), z_sendmany's minconf arg) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 16:40:14 -05:00
DanSandClaude Opus 4.8 001e85ac1a fix(ui): prevent text/button cutoff and scale hand-drawn geometry at HiDPI
Findings from a UI-cutoff audit — each is a spot where an in-tree helper
(truncateMiddle / TruncateToWidth / measured button width / the *dpiScale/*hs
factors) was bypassed:

- notifications: the toast-pill height/padding/icon-gap were raw logical px while
  the icon/text drawn inside are DPI-baked, so they clipped the pill at HiDPI.
  Scale the geometry by dpiScale (not the already-scaled glyph metrics).
- settings: in the two-column NODE & SECURITY layout the data-directory path could
  overrun into the Daemon-binary column (shared draw list, no clip rect between
  them). Middle-ellipsize it to the column width; the full path stays in the
  tooltip + click-to-open + copy.
- send: the "Confirm & Send" button width came straight from the schema and was
  never measured against the label, clipping the Russian translation on the
  pre-broadcast dialog. Size to max(schema width, measured label + padding).
- balance: the recent-tx address-column offset missed the `* hs` DPI factor its
  sibling (amount-right-margin) uses, overlapping the type label at HiDPI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 22:11:07 -05:00
DanSandClaude Opus 4.8 f88304fed2 fix(wallets): stop a legacy wallet showing as a seed-phrase wallet when linked
The wallets list badged the active/linked row from the runtime seed status
(activeWalletSeedBadge → wallet_seed_status_, reset only on disconnect) in
preference to the offline on-disk probe. Two issues let a genuinely legacy
wallet render as "seed phrase":

- The offline probe's budget-fallback branch dropped the fMnemonicSeed flag:
  res.mnemonic was set only in the (parsed && complete) branch. The probe shares
  a 768 MB budget across all wallet files, so a large wallet (e.g. a 194 MB one)
  probed after the budget is spent falls into the fallback, loses its seed/legacy
  classification (mnemonic → 0), and the row defers to the runtime badge.
- With mnemonic == 0, the code used the runtime badge, which can still carry a
  HasMnemonic from a previously-active mnemonic wallet — mislabelling the legacy
  wallet.

Fix:
- Carry the definitive positives (fMnemonicSeed/hdSeed/mkey) from a cap-truncated
  btree walk — a found marker is authoritative even when the scan didn't finish.
- Make the on-disk fMnemonicSeed read take precedence: it's the SAME flag the
  daemon's IsMnemonicSeed()/z_exportmnemonic consult, so a definitive read wins;
  the runtime badge is used only when the probe genuinely couldn't decide, and
  never overrides a definitive on-disk classification.

Verified the wallet in question is truly legacy (fMnemonicSeed=false on disk,
matching the daemon's CHDChain serialization + IsMnemonicSeed). The flag reader
(hdChainMnemonicFlag) is already unit-tested; suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 15:29:23 -05:00
DanSandClaude Opus 4.8 c3e81a5fa6 fix(send): accept P2SH/multisig recipients in the send + URI address gates
Same defect class as the import-key fix: a hardcoded prefix/length pre-filter
layered over the checksum validators rejected valid addresses before the daemon
saw them. The send-screen recipient gate required a[0]=='R', and the payment-URI
parser accepted only 'R'/'t' with rigid length bands — so every valid P2SH /
multisig address (DragonX SCRIPT_ADDRESS=85 → 'b…') was silently refused, leaving
the Send button disabled with no usable recipient.

Centralize recipient recognition in util/address_validation:
- isTransparentAddress: Base58Check with a 21-byte version+hash160 payload —
  covers P2PKH ('R…', v60) AND P2SH ('b…', v85) on every network, rejects WIF
  keys / typos by real checksum.
- isShieldedAddress: Bech32 + a Sapling payment-address HRP (zs / ztestsapling /
  zregtestsapling), distinguishing a payment address from a viewing key.
- isValidRecipientAddress: either of the above.

send_tab's two validity helpers (the single choke point for all 5 call sites) and
the payment-URI format check now route through these. The URI parser now
checksum-validates the recipient (fail-fast on transcription errors) rather than
being prefix/length-only.

Tests use real checksummed vectors (P2PKH/P2SH/shielded, WIF- and typo-rejection);
testPaymentUri updated off its old fake fixed-char addresses. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 00:12:17 -05:00
DanSandClaude Opus 4.8 c7c3440a7b feat(diagnostics): refresh-staleness badge on the Total Balance card (W6-2)
When the wallet is connected but the balance has quietly stopped refreshing — a busy
daemon can fail z_gettotalbalance without dropping the whole connection (only *both*
core RPCs failing 3x triggers a disconnect) — the old number sits on screen while the
node-status banner stays hidden. The Total Balance card now shows a small pill on its
status line ("Updated 2m ago", amber, escalating to red past 3 min) so the stale value
isn't silently trusted; hovering explains it and points at the node connection.

No refresh-path changes: WalletState::last_balance_update is already stamped only on a
successful fetch (network_refresh_service.cpp), so the badge reads it and computes age
against the same std::time clock via util::formatTimeAgoShort. The decision is a pure,
unit-tested helper (ui/staleness_badge.h::evaluateStalenessBadge, 45s/180s thresholds)
gated on connected so it never contradicts the banner.

Closes P2 (5/5). Build-clean; ctest 1/1 (adds testStalenessBadge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:48:56 -05:00
DanSandClaude Opus 4.8 da0e9f5915 fix(console): redact secret-bearing commands from the console echo and history (W7-1)
The RPC console echoed and stored typed commands verbatim, so `walletpassphrase
<secret>`, `z_importkey <key>`, `encryptwallet <pass>` etc. left the secret in the
visible log AND the 100-entry recall history (copyable). Adds a pure, unit-testable
RedactConsoleCommand()/ConsoleCommandCarriesSecret() (allowlist of 13 secret-bearing
first-tokens) in console_tab_helpers; submitConsoleCommand() now echoes and stores
`> walletpassphrase ****` while still executing the real command unredacted. Bare
secret commands and non-secret commands pass through unchanged.

Output-secret commands (dumpprivkey / z_exportkey / z_exportmnemonic) — whose secret is
in the RESULT — are a separate redaction concern, tracked as a follow-up.

First fix in the wallet-hardening P0-A cluster (see docs/wallet-hardening.md). New
testConsoleSecretRedaction (11 assertions); ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 13:57:52 -05:00
DanSandClaude Opus 4.8 45b652f514 feat(mining): live pool fee, saved/custom pool rows, and payout-address fix
Several related mining-tab pool improvements:

- Report the default pool fee correctly: pool.dragonx.is is 1%, not 0%.
  The registry constant was hardcoded to 0. It now also fetches the live
  poolFee from the pool's /api/stats alongside hashrate (no extra
  request), so the displayed fee self-corrects and falls back to the
  compile-time value only when the fetch hasn't landed.

- Show fractional fees: new FormatFeePercent trims trailing zeros so
  whole fees read "1%" and fractional ones keep their decimals ("1.5%").

- Surface saved + custom pools in the pool list card: the list is now
  the union of the official pools, the user's saved favorites, and the
  currently-mined pool (effectivePools), each a selectable, endpoint-
  deduped row. Previously the card only showed the hardcoded knownPools().

- Fix the xmrig "user" field: the "Payout Address" field now drives the
  pool login rewards are credited to (resolveMiningUserAddress), instead
  of being written only to "pass" while "user" was auto-derived from the
  wallet's own first z-address -- which silently ignored a configured
  payout address and could route rewards to the wrong address.

Unit tests cover parsePoolFee, FormatFeePercent, effectivePools, and
resolveMiningUserAddress; full app + ObsidianDragonTests build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:48:05 -05:00
DanSandClaude Fable 5 02554d523d fix(ui): align contacts table-view footer with cards/list views
In table view the table is inset into its glass panel by tVpad and its
outer_size is listH-2*tVpad, so the post-table cursor ended tVpad (~10px) above
where the cards/list views leave it, pulling the "N address saved" footer up.
Land the cursor at the glass-panel bottom (tpMin.y + listH) so the footer lines
up across all three view styles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:45:14 -05:00
DanSandClaude Fable 5 c53b7f771e fix(ui): overview/mining polish + default-pool cleanup
- balance: inset the address-row favorite (star) button by the card's inner
  padding so it mirrors the left margin instead of hugging the card edge.
- mining: remove pool.dragonx.cc from the built-in default pools (pool.dragonx.is
  is now the sole default); update the pool-registry test accordingly.
- mining: middle-truncate saved pool-URL and payout-address dropdown rows (new
  shared material::TruncateToWidth helper) so a full z-address no longer runs
  under the trailing delete (X) button.
- mining: fix the thread-grid cells overflowing the card at >100% display
  scaling — the reserved Mine-button width used a raw clamp that didn't scale;
  scale it by dp so cols is estimated correctly (no-op at 100%).

Full-node build + test suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:01:51 -05:00
DanSandClaude Fable 5 bcee4bfe72 fix(security): apply audit remediations to dev (ported + dev-only)
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.

Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
  before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
  metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
  cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
  Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
  startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
  userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
  success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
  CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
  verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
  (F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)

Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
  for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
  heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
  failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
  writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).

Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
  wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
  residual) — inherent to an echoing console; would need output redaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:08:24 -05:00
DanSandClaude Opus 4.8 7cf0bb8bc7 feat(settings): add hover tooltips to the settings controls that lacked them
A per-card audit found 38 interactive controls with no tooltip. Add them, with
new tt_* i18n keys (English source; per-language JSONs fall back to English
until translated):
- Chat & Contacts (8): emoji/bubble style, accent, density, text size, poll
  rate, timestamps, enter-sends — the segmented/beginRow helpers added none.
- Node & Security (26): the whole lite wallet lifecycle / backup-keys /
  encryption panel, plus the full-node RPC-details toggle and daemon Refresh.
- Debug Options (4): the screenshot-sweep / full-UI-sweep / open-folder /
  seed-demo-chat buttons.

Theme, Wallet, Explorer, About were already fully covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 21:31:15 -05:00
DanSandClaude Opus 4.8 541a9e1fd5 feat(settings): split Chat & Contacts into Appearance | Messaging columns
Break up the tall, left-heavy Chat & Contacts card internally: Appearance on
the left, Messaging on the right when the card is wide (fills the width, ~halves
the height). Same Indent technique as the Node & Security card; the row helpers
retarget their leftX/rowW per column so controls right-align within each. The
narrow chat-settings modal stays single-column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 21:16:37 -05:00
DanSandClaude Opus 4.8 3a2be661ea feat(chat): custom DragonX emoji via the :drgx: shortcode
Add a DragonX custom emoji to chat. The emoji picker gains a DragonX tile (first
cell) that inserts the text ":drgx:"; message bodies containing it render the mark
inline via layoutChatBodyRich, which flows text words + the emoji image with word
wrap (plain bodies keep the tighter layoutChatBody path, so normal messages are
unaffected). Other clients simply show the literal ":drgx:" text — a portable
encoding with graceful degradation.

The emoji is the DragonX SVG rasterized once at fixed brand colors (crimson body,
white detail) via LoadTextureFromSvg — theme-independent so it looks identical for
sender and receiver — exposed as App::getDrgxEmojiTexture(). Emoji insertion is
factored into one insertToken() helper (space-prepend + on-chain byte cap), shared
by the DragonX tile and the Unicode emojis. Drag-to-select text is skipped on
":drgx:" messages (their inline layout doesn't match the plain-text hit-test
geometry); right-click "copy" still copies the whole message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:42:30 -05:00
DanSandClaude Opus 4.8 267d839d5b feat(chat): add a dragon-themed emoji set to the picker
Adds 🐉 🐲 🐍 🦎 🐊 🐾 🥚 🪺 🏰 ⚔ 🛡 🗡 to the emoji picker, with DRGX-flavoured
keywords (the dragons are searchable by "dragon"/"drgx"). All are single-codepoint
emoji verified present in both the bundled monochrome NotoEmoji subset and the
color Twemoji font, so no font rebuild is needed and they render on other clients
(standard Unicode, UTF-8 in the memo, within the 236-byte cap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 12:45:49 -05:00
DanSandClaude Opus 4.8 987d9b93c7 feat(history): badge chat transactions + add a Chat filter
Transactions that carried a chat message (sent or received, messages + contact
requests) now show a "Message" badge in the History tab, and a new "Chat" option
in the type-filter combo shows only those transactions.

Detection reuses what the chat store already tracks: ChatStore gains an inline
chatTxids() returning the set of on-chain txids that carried a chat message; the
tab builds it once per frame (cheap — O(chat messages)) and tests txid membership
for the badge and the filter. Empty when chat is disabled or the wallet has no
chat identity. Both variants populate the chat store before this tab renders, so
the same code serves full-node and lite with no separate handling.

The "Message" badge reuses the stacked top-pill slot (chat txs are send/receive,
not the autoshield "shield" type, so it and the "Shielded" badge are mutually
exclusive; chat takes precedence) in a distinct Secondary() colour. Guards the
summary-card accent idx_map so the new filter value (4) can't index it OOB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:43:55 -05:00
DanSandClaude Opus 4.8 4c43f2e082 fix(chat): draw the jump-to-latest pill on top of the messages
The pill was drawn on the parent window's draw list after EndChild, but ImGui
renders a child window ON TOP of its parent — so the message text covered it.
Draw it instead on the thread child's own draw list, after the message loop
(later draw commands render on top), with a PushClipRect so the child's content
padding / scrollbar doesn't clip it. Clickability is unchanged (hand-drawn +
IsMouseHoveringRect / IsMouseClicked test the cursor directly), and it stays
pinned to the visible bottom-right corner via absolute screen coordinates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:23:23 -05:00
DanSandClaude Opus 4.8 57fa6470b7 feat(chat): thread UX — auto-scroll, selectable text, clickable Latest, emoji polish
Five message-thread improvements:
- Auto-scroll like the Console tab: new messages pin to the bottom only while
  the reader is already there; a wheel-up detaches (re-armed on return to the
  bottom, after a cooldown). A direct mouse-in-rect test (not IsWindowHovered,
  which is false while the composer holds focus) matches ApplySmoothScroll so
  scrolling up to read history while typing still detaches.
- The "Latest" jump pill is now hand-drawn (rect + IsMouseHoveringRect /
  IsMouseClicked) instead of ImGui::Button: it overlaps the thread child which
  owns mouse-hover there, so a real widget on the parent never got the click.
  Clicking it re-arms auto-scroll; its geometry is shared with the deselect
  guard so a pill click no longer drops an active selection.
- Sending a message closes the emoji picker (it takes over the conversation
  list) so the list reappears, and clears its search filter.
- Inserting an emoji prepends a space when the draft doesn't already end in
  whitespace (still respecting the on-chain byte cap).
- Message text is selectable by click-drag (chatBodyLines / chatBodyHitTest
  mirror layoutChatBody's wrap so highlight + hit-test align with the drawn
  text); while a range is active a copy button shows at the bubble's top on the
  opposite side and copies the selected substring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 23:04:26 -05:00
DanSandClaude Opus 4.8 b0a4333cdf feat(chat): pre-split note buffer for rapid sends + status + console logging
Each chat message is a shielded tx that spends a verified note, so a burst of
messages hit "insufficient verified funds" once the single note is spent. Add a
per-frame note-buffer coordinator (both variants) that keeps ~10 verified
spendable notes: it serializes sends through the one broadcast channel, counts
verified notes by block depth (lite) or a rate-limited z_listunspent scan
(full node), self-splits in the background to refill toward the target, and
queues overflow to drain as change matures — with honest Sent/Failed status
instead of the prior optimistic "Sent". Guardrails: single split in flight with
a watchdog, cooldown, and session-generation guards so a wallet switch can't
drain another wallet's queue. Surface a "Chat buffer: N/10 ready" indicator in
the status bar while the Chat tab is active, and route chat diagnostics through
the console on both variants (the full-node console now drains the shared ring).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:00:00 -05:00
DanSandClaude Opus 4.8 e08121af2b feat(chat): redesign message composer — radial byte gauge, autogrow, wrap
Replace the plain "N/236" byte counter with a radial wheel drawn inside the
input, right-aligned, filling green→amber→red as the message approaches the
236-byte memo cap. Vertically center the text with left padding (FramePadding),
hard-cap typing at the cap (no silent overflow), add Shift+Enter for a newline
(via an always-callback, since ImGui 1.92's Enter shortcut needs exact mods)
while plain Enter still sends, animate the input growing taller as lines are
added, and word-wrap the display instead of overflowing left. Give a hard
newline a touch more space above than a soft wrap in the message bubble.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:59:37 -05:00
DanSandClaude Opus 4.8 38f2aa2f8f fix(ui): frost the chat + contacts panes with acrylic blur + padding
The chat and contacts panes drew flat ImGui fills (ChildBg / default
WindowBg / NoBackground), so they never sampled the acrylic blur and
showed the raw backdrop texture. Route them through DrawGlassPanel --
the pattern the peers_tab sibling and every other tab already use:

- chat: frost the conversation-list + thread panes and the composer
  input box, and add inner padding so content doesn't hug the glass edges
- contacts: frost the card/list container AND the table view, padding both

Padding needed ImGuiChildFlags_AlwaysUseWindowPadding -- a borderless
child silently ignores WindowPadding otherwise (documented gotcha in
daemon_download_dialog.h). BeginTable can't take that flag, so the table
view is inset within its glass panel instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 19:32:11 -05:00
DanSandClaude Opus 4.8 5c570613c8 feat(console): lite backend command-reference modal
Give the lite console the parity analog of the full-node RPC command
reference: the same searchable two-pane modal (browse/search, detail
pane, examples, Insert / Insert & run, destructive confirm), driven by
the lite backend's own command set instead of daemon RPC.

- ConsoleCommandExecutor::commandReference() returns the category table
  (full node = consoleCommandCategories(); lite = new
  liteConsoleCommandCategories() -- 25 backend verbs in 5 categories).
  The shared renderCommandsPopup reads the table from the executor.
- The Commands button now shows for both variants (gated on
  commandReference()!=nullptr); title/tooltip/arg-quoting branch on
  hasRpcReference() (clarified to mean "speaks JSON-RPC / full node").
- Lite args are bare tokens (the backend takes one unsplit arg string
  and does not strip quotes), so the param-builder's JSON string
  auto-quoting is disabled for lite -- Insert & run emits runnable bare
  commands. send uses the JSON-array form (its positional form is
  unreachable via the single-arg transport); new uses zs/R; import
  takes just the key (the backend hardcodes birthday=0).

Adds 7 i18n keys across all 8 languages (additive). Full-node behavior
is unchanged. Adversarially reviewed (data vs the backend registry,
plumbing/regression, i18n) with all findings fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 12:27:51 -05:00
DanSandClaude Opus 4.8 567732206e fix(console): localize the lite console + drop full-node-only wording
The lite console tab lagged the full-node one on i18n: its toolbar status,
status lines, help, and error/warning strings were hard-coded English or
worded for a full node (a "daemon" the lite wallet doesn't have).

- toolbarStatus()/statusLines(): route through TR() (reusing the existing
  lite_net_* keys where they already map)
- printHelp(): enumerate the 25 pass-through backend verbs for discoverability,
  since the C++ tab intercepts `help` before the backend's own HelpCommand runs
- gate the destructive 'stop' confirmation to the full node (hasRpcReference);
  lite has no node, so it lets 'stop' fall through to the backend instead of
  showing a phantom "shut down the node" warning behind a dead gate
- word the not-connected error per variant (daemon vs "no wallet open")
- TR() the "(no output)" command-result fallback

Adds 7 i18n keys across all 8 languages (additive); rebuilds the CJK subset
for the one new glyph (U+C5D4 for the Korean "backend"). Build + ctest +
source-hygiene green; changes adversarially verified (no logic/i18n defects).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 20:10:17 -05:00
DanSandClaude Opus 4.8 0dcc09fc5f fix(console): DPI-scale the toolbar status-dot offsets
The status indicator's left inset (+2) and dot-to-text reservation (+6) were raw pixels while the dot radius is DPI-scaled, so the dot shifted a couple native px at HiDPI. Multiply both by Layout::dpiScale() per the project's hand-drawn-geometry rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 18:03:11 -05:00
DanSandClaude Opus 4.8 7d47c6e14e fix(console): balance colour-toggle push/pop so it stops drawing a red error rect
The accent + text-colour toggle buttons pushed/popped ImGuiCol_Text guarded by a flag the TactileButton flips in between, so a click left the colour stack unbalanced for that frame — and ImGui's error recovery drew a red rectangle around the console window (imgui.cpp:11727). Capture the flag into a local before the button and guard both the push and the pop with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:39:42 -05:00
DanSandClaude Opus 4.8 973bc0d338 feat(market): trend sparkline in all portfolio styles + inline chart/style pickers
Render the portfolio trend sparkline in every row style, each with a layout that features it:
- Table: a dedicated header-labelled TREND column (48dp rows), aligned across rows.
- Cards: a left-aligned info stack (icon + label / value+delta chip / DRGX) with a full-height sparkline filling the rest of the width (content-driven, so short groups give the chart more room).
- Spotlight: a big-value-over-a-full-width-chart-band hero tile (92dp).
Default sparkline interval is now MONTH (a real curve from the daily series, not the young minute buffer). Removed the per-row container accent line (redundant with the icon/sparkline colour) and the per-group shielded/transparent bar (private-by-default makes it near-always 100%, and dropped its now-dead SumPortfolioSplit helper). DPI-scale the sparkline stroke. Adds the "Trend" column string across 9 languages.

Relocated the Market controls out of a removed settings modal + gear: the chart line/candle picker now sits top-right of the chart (left of the trade button, candle-capable ranges only), and the Table/Cards/Spotlight picker sits left of Manage. Dropped the market fetch-prices toggle (still in the general Settings page).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:39:42 -05:00
DanSandClaude Opus 4.8 e4c9b98ca2 feat(market): trend sparkline in all portfolio styles + fix candlestick availability
Two coupled Market-tab changes (same file):
- Sparkline in every style: render the per-entry trend sparkline in Table (a reserved trend column, aligned under the column header, reserved only when at least one group opts in so the dense grid isn't padded otherwise) and Cards (a filled strip on the right, above the full-width Z/T bar), not just Spotlight — so the per-entry 'show sparkline' setting is honored in all three views. DPI-scale the sparkline stroke (1.2f/1.4f * dp), from the sparkline audit's L1.
- Fix the candlestick toggle intermittently missing: the Market tab never triggered the per-exchange OHLC fetch on its own (only a pair-chip / refresh click did), so candlesticks were unavailable on a fresh tab open. Call refreshExchangeChart() every frame (self-throttled + in-flight-guarded — its own doc comment already says 'safe to call every frame'), which also fixes the new pair's candles never loading if you switch pairs mid-fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 00:56:06 -05:00
DanSandClaude Opus 4.8 1c38f68781 fix(market): HiDPI-scale hand-drawn geometry + dedup row/column helpers
Market-tab audit follow-up (all 9 confirmed findings):
- Scale the unscaled corner radii on the Cards/Spotlight row rects, the address-preview glass panel, the chart Y-axis labels, and both chart hover tooltips by dpiScale() — they mismatched the coincident already-scaled rects on HiDPI (the CLAUDE.md hand-drawn-geometry rule).
- Share the portfolio row-height/gap formula (pfRowHeight/pfRowGapFor) and the TABLE column widths (kPfValColW/kPfDrgxColW) between the draw loop and the scroll-region height budget so they can't drift and clip.
- Derive the portfolio total from the shielded/transparent split (one address scan instead of two in Cards).
- Build the address-picker selected-set once (unordered_set) instead of a linear PortfolioEntryContains per sort-compare and per row.
No correctness/security/threading defects were found in the audit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:00:02 -05:00
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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