A wallet reached via two different path strings — e.g. a symlinked mount
(/mnt/usb) and its real target (/media/usb) — used to hash to two different
in-place link names and list as two rows, so the same file looked like two
wallets with two datadir links.
Give WalletRow a canonPath (fs::canonical, symlinks resolved, computed once at
scan time so the every-frame render/active-marking path stays cheap) and key
both the link name and the row de-dup on it. Now the same physical wallet maps
to one identity regardless of the path used to reach it: one link name (honors
the "distinct per-target name" invariant), and scan() lists it once — datadir
first, so a datadir wallet wins over the same file seen via an external path.
The link also points straight at the resolved real file instead of through the
symlinked path. Genuinely distinct wallets keep distinct identities (verified
with a standalone symlinked-mount harness).
Closes the last deferred finding from the open-in-place adversarial review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Unix a symlink is the better datadir link than a hard link: it needs no
privilege, spans volumes (a wallet on a USB / other partition just works,
where a hard link can't), and is visibly a pointer rather than an
indistinguishable second copy. Windows keeps hard-link-first (a symlink there
needs admin / Developer Mode), falling back to a symlink.
Symlink-first made two latent issues in the pre-clear reachable, fixed here
after an adversarial review:
- Dangling prior symlink: fs::exists follows the link (false when the source
moved), so it was skipped and create_symlink then tripped on the occupied
path. Catch it with fs::is_symlink (lstat) and replace it.
- Stale reserved-name occupant: the old "hard_link_count == 1 → reuse" path
could load the WRONG wallet — a count-1 file at our name is not a valid link
to the source (a live hard link has count >= 2), it's a stale orphan (source
deleted, or a copy left by a datadir migrated across filesystems). Gate reuse
on fs::equivalent(link, src) (same inode) instead, and reclaim the name only
when the occupant is provably redundant (another hard link still holds the
data); otherwise refuse rather than risk destroying something unique. So
hard_link_count is no longer trusted to prove identity — only data-safety.
Also guard against a stale row (source moved/deleted between the scan and the
click): !exists(src) refreshes the list instead of linking a ghost the daemon
would silently swap for wallet.dat. Verified with a standalone harness across
fresh / re-open / valid-hardlink-reuse / stale-orphan / redundant-occupant /
vanished-source cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the "Import" action — which copied an out-of-datadir wallet into the
datadir and cluttered the list with hard-to-tell-apart duplicates — with an
in-place "Open" that links the real file into the datadir under a stable
per-target name (wallet-ip-<FNV8>.dat) and switches to it. The daemon only
loads a bare filename from its own datadir, so a link is the minimal bridge:
hard link first (no privileges, same volume — covers non-admin Windows),
symlink fallback (cross-volume), then an error. Never a copy (that would fork
the wallet) and never a delete of a real file.
Also:
- show each external wallet's originating sub-directory (…/Backups/2021)
- add a per-row "open folder location" button
- widen the modal (780 → 860) for the extra button
- guard the daemon against a dangling link (external file moved / USB
unplugged) by falling back to wallet.dat rather than creating an empty one
Per-target link names (not one shared name) make switching between two
external wallets a real -wallet switch, so the rescan/cache index tracks each
correctly. Drops the now-dead wallets_import* i18n keys and back-fills the new
open/folder strings across all 8 languages + rebuilds the CJK subset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepend the containing folder (last 1-2 path components, e.g. "…/Backups/2021")
to an external wallet's metadata line, so several same-named wallet.dat files
surfaced by the recursive scan are distinguishable at a glance (the info-icon
tooltip still shows the full path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the rounded-rect StyledButton for material::IconButton (square size →
bgRounding = radius, so a circle) with the arrow glyph mathematically centered;
carries its own hover fill + asc/desc tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the sort dropdown with a material::SegmentedControl — Created / Addresses
/ Txs / Size — matching the Receive tab's address-type toggle, with the asc/desc
arrow beside it. Shorten the four sort labels to fit the segments (updated across
all eight languages + the back-fill script). The direction arrow is positioned
explicitly on the segment's baseline (the control is draw-list based and doesn't
advance the layout cursor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make "is the active wallet" the primary sort key so the current wallet always
stays at the top regardless of the chosen sort/direction; the remaining wallets
sort by the selected key underneath it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse the earliest keymeta nCreateTime out of the wallet.dat btree (the daemon's
"wallet birthday") and surface it in each row's metadata line ("created Aug 2025").
Add a sort control above the list — Date created / Address count / Transaction
count / Wallet size, with an ascending/descending toggle (descending default:
newest / most / largest first). Sorting reorders a display-index array
(s_order) rather than s_rows, so the async, index-aligned probe batch is
untouched; sort keys read a single frame-consistent snapshot of the probe
results. Address count prefers the authoritative cached index value, else the
btree key count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The async probe fills badges/counts a frame after the dialog opens, which the
single-frame screenshot sweep captured before it landed (badges/counts missing).
Run the probe synchronously when App::isScreenshotSweeping(), so the captured
modal-wallets frame is populated; interactive use keeps the detached-thread path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the shared ApplySmoothScroll helper (with NoScrollWithMouse) for the
##walletList child, matching the Settings page and the other scroll areas, so
the wallet list eases on wheel input instead of jumping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parseWalletBtree() walks the wallet.dat Berkeley DB btree directly (no libdb, no
daemon): validate the metapage magic, follow the "main" sub-database (its pgno is
stored big-endian in the master map), traverse internal/leaf pages, and tally
records by their length-prefixed type name — transparent + shielded spendable
keys, address-book, and tx count, plus exact encryption/seed flags. Every offset
is bounds-checked, pages are deduped at push time (stack stays O(npages)), and a
visited-set + page/key caps make it safe on a corrupt/adversarial imported file;
files using DB_CHKSUM/DB_ENCRYPT (which shift the page layout) are rejected so the
byte-scan fallback runs instead.
The async probe now uses this as the primary path (exact badges + counts), falling
back to the byte-scan only when the btree can't be fully parsed. The wallets list
shows "N keys · M txs" for probed rows (labeled "keys", not "addresses", since the
count includes change keys the daemon's address list omits). Validated read-only
against real wallets (counts cross-checked; enc/seed match the byte-scan) and a
hand-built minimal btree fixture in the unit suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the wallet.dat status badges as a right-aligned vertical stack of
"label icon" rows ("Seed phrase 🌱" over "Encrypted 🔒", "Legacy", or "Unknown"),
each with a hover tooltip; name/metadata reserve the stack width and truncate
before it.
Move the wallet.dat probing off the UI thread: scan() now builds the row list
synchronously (filename/size only) and hands the file reads to a detached
background thread whose results land in a shared, index-aligned batch that
render() reads under a mutex. A re-scan supersedes the old batch (cancel + swap);
the thread touches only its own heap batch (never s_rows/statics), so it's safe
across re-scans and shutdown. The dialog opens instantly even with a large
active wallet; badges fill in over the next few frames.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Search user-added wallet folders RECURSIVELY (bounded: depth/hit/visit caps,
skip_permission_denied, no symlink-follow, node-junk + node-subtree pruning);
the datadir stays top-level. r.dir is each file's real parent so Open/import
resolve subdir wallets. Auto-suffix the import destination on a name collision.
Add util/wallet_file_probe.h: read encryption/seed/shielded flags straight off
a wallet.dat WITHOUT loading it — validate the Berkeley DB btree magic, then a
bounded streaming byte-scan for the length-prefixed record markers the daemon
writes (mkey -> encrypted; hdseed/hdchain -> seed/HD; zkey/sapzkey -> shielded).
Reads no key material. The Wallets dialog shows Encrypted / Seed / Legacy badges,
plus an Unknown badge when a large file couldn't be fully scanned (so absence of
a lock never falsely reads as unencrypted). Unit-tested + validated read-only
against real wallets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Scan another folder" now anchors the picker at getDragonXDataDir() (where the
datadir wallets live) instead of home, so the user starts from a familiar spot
and navigates up/out to find wallets elsewhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wallets dialog hand-computed a content-sized card height. With many wallets
on a small / HiDPI (150%) window the computed height could exceed the viewport,
and the framework clips a too-tall card — footer and all.
Cap the card at 0.86 of the viewport height. When capped, the wallet list flexes
to the space left above the create/scan/footer controls and scrolls internally,
so those controls stay pinned and visible; uncapped (the common case, few
wallets) nothing changes and there's no spurious scrollbar. Add a
modal-wallets-many sweep surface (8 wallets, self-cleaning teardown) to exercise
the capped-scroll path. Verified at 100% + 150%.
This is the one real fixed-height clip risk from the modal audit; the other 44
modals use auto-height (grow-to-fit, capped at a viewport ratio) and were
confirmed non-clipping — e.g. the 24-word seed-backup grid renders fully at 150%
in a 720px window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wallets dialog refinements:
- More space between wallet cards (cardGap: spacingSm -> spacingMd).
- "Scan another folder" is now always visible as a full-width button (no
expandable toggle) that opens the new picker.
- The Create-wallet row and the scan button span the full list width, so their
right edges align with the wallet cards / Open|Import buttons.
New in-app folder picker (folder_picker.h): a Material-styled modal replacing
the raw path input. Browse the filesystem (Up / Home / click a sub-folder),
see the *.dat wallet files present + a count, and pick a folder to scan. The
picker takes over the modal surface while open (the overlay framework doesn't
nest); it remembers the last browsed location across opens. Fixed-height card
(no auto-height feedback), UTF-8-aware truncation, full-node icons confirmed
in the embedded MaterialIcons range. Added a modal-folder-picker sweep surface;
verified at 100% + 150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues on the card list: it showed a scrollbar (and clipped the last card)
with only a few wallets, and the cards felt cramped.
- Scrollbar: the per-card advance added a trailing gap after EVERY card (incl.
the last) and ImGui inserted its default ItemSpacing between the Dummy items,
so the content ran ~1 gap taller than the child's height budget -> overflow ->
scrollbar + clipped card. Zero ItemSpacing inside the list child, register each
card's footprint with a Dummy, and gap only BETWEEN cards (no trailing gap), so
the content matches the budget exactly. Sized the list to fit up to 7 cards
before it scrolls.
- Roominess: bigger name (subtitle1), more vertical padding (spacingMd), and a
touch more space after the icon.
- Removed the full-card InvisibleButton (hover is IsMouseHoveringRect again): its
overlap with the action button tripped ImGui's ConfigDebugHighlightIdConflicts,
drawing a red debug border around the list at 150% DPI.
Verified via the headless sweep at 100% and font_scale 1.5 — no scrollbar, no red
border, cards fully visible with breathing room.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the wallet table with a stack of Material-style cards inside a scrolling
child. Each wallet is a rounded card: a leading wallet icon, the filename as the
primary line, and a muted metadata subtitle (size · N addresses · balance DRGX ·
last opened). The action sits on the right — Open / Import buttons, or a green
"Active" chip for the current wallet. The current wallet's card gets a green tint,
border, and icon; external files show an info icon + tooltip; cards highlight on
hover. The dialog stays content-sized + fixed-height (computed from the wallet
count, up to 6 before it scrolls), so the monitor-drag stability is unaffected.
Adversarially reviewed (two-lens workflow + synthesis): the reviewers' DPI
unit-mixing and broken-scroll findings were false positives (LegacySize is the
DPI-scaled atlas size, and BeginChild takes physical px while the framework's
cardHeight takes logical) — the only real issue was byte-wise truncation that
could split a multibyte UTF-8 char, now fixed to pop whole code points. Verified
via the headless sweep at 100%, 150%, and light skin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Act on the design review of the Wallets modal (all six points):
1. Content-sized card — size the card to the wallet count (up to 8 rows, then the
list scrolls) instead of a fixed 620px, killing the huge empty void a short
list floated in. Computed from the KNOWN row count, so it stays a fixed-height
dialog (no auto-height feedback — that's what slid content off-screen on a
monitor move).
2. Framed wallet list — BordersOuter + PadOuterX so the list reads as a contained
panel with room to grow, not loose rows on the backdrop.
3. Right-aligned numeric columns (Size / Addresses / Balance) and their headers,
via a custom header row + a right-aligned cell helper, so the numbers line up.
4. Consistent action column — the active wallet's row gets a subtle green tint and
an "Active" chip (badge), distinct from the Open/Import buttons.
5. De-cluttered bottom — "Create wallet" stays the primary action; the folder-scan
is tucked behind a "+ Scan another folder…" toggle.
6. Polish — the external-file marker is now a real info icon + tooltip (was
ICON_MD_FOLDER rendering as a "?" tofu, since that glyph isn't in the subset);
Refresh is an icon-only button, de-emphasized next to Close.
New i18n keys (wallets_add_folder_toggle, wallets_external_tt) + 8 langs; CJK
subset rebuilt. Verified via the headless sweep at 100% and font_scale 1.5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups to the modal float work:
- Console RPC command reference (renderCommandsPopup) had the SAME auto-height
feedback bug as Wallets: BlurFloat with no cardHeight, and its fill-height
command list read GetContentRegionAvail() inside the AutoResizeY child ->
self-referential, sliding content off-screen after a monitor move. Give it a
FIXED viewport-relative cardHeight (fills ~84% of the viewport). Also removed
its duplicate body H6 heading (the framework h6 title already shows it) —
missed earlier because it's spec-form, not the positional overload.
- Wallets footer clipping: after the fixed-height conversion the content child
clips overflow (NoScrollbar), and the hardcoded 130*dp reserve below the table
was too small for the create/add-folder rows + separator + footer (~160*dp),
so the buttons clipped — worse at 150%. Compute the reserve from real widget
metrics (GetFrameHeightWithSpacing + caption height) so it holds at any DPI.
Added a modal-console-commands sweep surface (debug-gated ConsoleTab setter) so
the popup is captured. Verified both at 100% and font_scale 1.5 (150%): footers
on-screen, single headings, lists fill, nothing clipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Wallets modal used the auto-height BeginOverlayDialog overload, whose content
child is AutoResizeY. It sized its table with `tableH = GetContentRegionAvail().y
- 130*dp` INSIDE that child — but on an AutoResizeY child GetContentRegionAvail().y
derives from the child's Size.y, carried over from the previous frame's content
height. That makes tableH self-referential: a fixed point on one monitor, but a
monitor move jumps dpiScale() (the 130*dp reserve + table column/row sizes switch
to new units while the child's Size.y is still old units), knocking it off
equilibrium. With only a lower clamp and no upper bound, the excursion runs
unbounded and pushes the create/folder/footer rows off the bottom.
Give the dialog a FIXED cardHeight (620) via the spec form — the pattern Manage
Portfolio already uses. The content child is then fixed-height (not AutoResizeY),
so GetContentRegionAvail().y is a stable, DPI-current "space left in the card",
tableH stops feeding back, and the fixed-height path already clamps the card to
the viewport (min(cardHeight*dp, vp_size.y-32)). Root-caused via a multi-agent
audit against the ImGui internals.
Verified via the headless sweep at 100% and font_scale 1.5 (150%): the table
absorbs the slack and the create/folder inputs + footer stay on-screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the glass card + boxed title bar from every dialog so content floats
directly on the blur backdrop under a plain h6 heading — the Manage-Portfolio
look — unifying the whole modal system on one visual language (reverses the
earlier card-for-small / float-for-large split).
- Framework: the positional BeginOverlayDialog overload (used by all ~34 card
dialogs) now defaults to floatingContent + plainHeading (draw_helpers.h). The
9 spec-form BlurFloat dialogs are unchanged; any dialog can still opt back to a
card via an explicit OverlayStyle::GlassCard spec. Authored widths are kept
(floating never resizes), so width-coupled layouts are unaffected.
- Dedup: the framework h6 title made each dialog's own repeated title heading
redundant. Removed the duplicate body heading from Import/Export key, Backup,
daemon update, seed backup, seed migration, Defender-blocked (app.cpp),
Wallets, and the send-confirm popup.
- Close paths: dropping the title-bar close X is covered — every dialog has a
footer button and/or the framework's outside-click dismiss (all pass p_open);
send-confirm keeps Esc + Confirm/Cancel.
Verified via the headless full UI sweep on dark + light: 17 modal surfaces float
card-less with a single clean heading, nothing clipped, close controls present.
Build clean, ctest passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the drag/resize dashboard grid on the Market tab's portfolio with
three full-width row styles — compact (single-line), detailed (two-line),
and value-hero — cycled with Left/Right arrows like the Overview layouts.
Rows live in an internal scroll region bounded to the portfolio's height
budget, so the section stays put as groups are added.
- pfDrawRow renders each group in the selected style, reusing the icon /
value / DRGX / 24h / sparkline pieces; per-entry field toggles honored.
- portfolio_style setting (0/1/2) persisted; arrow-key switcher + toast.
- Portfolio groups gain a per-wallet scope: new groups default to the
active wallet's identity hash; legacy groups (empty scope) stay visible
in every wallet. Scope filter drives both the height budget and the rows.
- Remove the now-dead grid machinery (mktDrawCard, pfComputeGridLayout,
PfCell, PfGridDrag, MktCardCtx + MktCtx grid fields) — net code drop.
- Full UI sweep: seed demo portfolio groups + wavy price history and add
market-rows-{compact,detailed,featured} surfaces so the styles are
captured per skin (snapshot/restore the real portfolio + market data).
- i18n: portfolio_style_{label,compact,detailed,featured} + 8 langs; CJK
subset rebuilt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the wallet-files dialog and localizes the feature.
- Create a new wallet: a name is normalized to wallet-<name>.dat and opened
via switchToWallet; the daemon mints it (seed-backed, -usemnemonic=1) on
first load. Rejects blank names + name collisions.
- Import: out-of-datadir wallets get an Import action that copies the file
into the datadir (never overwriting) under a wallet-*.dat name, then opens
it — the daemon only loads plain filenames from the datadir.
- i18n: the 32 new multi-wallet / per-wallet-contacts / mining-payout strings
translated into all 8 languages (de/es/fr/ja/ko/pt/ru/zh), CJK subset font
rebuilt. Verified: the Chinese Wallets dialog renders with no missing glyphs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second phase of multi-wallet: list wallet files and switch the active one.
- Daemon plumbing: EmbeddedDaemon::setWalletFile -> start() passes -wallet=<name>
(non-default only; skipped during the isolated seed-migration start), and
DaemonController::syncSettings pushes active_wallet_file on each start.
- App::switchToWallet: persist the new active wallet, then stop + restart the
node on -wallet=<name> (rescan only if it was never synced in this datadir).
Reuses the seed-adopt restart coordination; WalletState clears on disconnect
and the P1 identity-scoped caches re-key, so no previous-wallet data leaks.
Guarded: full-node, embedded daemon, not mid-restart, no pending send.
- Wallets dialog (Settings -> Backup & Data -> "Wallets…"): a table of wallet
files (datadir wallet*.dat + user-added folders' *.dat) with size (disk) and
cached balance / address count / last-opened (wallet index), a current/Active
badge, Open (switch), and Add folder. Out-of-datadir wallets show "import to
open" (P3). Added as a sweep surface.
- Mining payout safety: non-destructive warning if the pool-mode worker looks
like a DragonX address not in the current wallet (stale after a switch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>