24 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00
DanSandClaude Opus 4.8 ed675f90d8 feat(i18n): add English source + 8-language translations for 193 UI strings
Adds the i18n source for 193 previously-hardcoded UI strings (the
security / PIN / lock flow, first-run seed-backup wizard, witness-rebuild
shutdown dialog, and balance / mining / explorer labels) so they can be
localized. The TR() call-site wrapping rides in the accompanying UI commit.

- 193 keys added to loadBuiltinEnglish() (the English source of truth),
  including one pre-existing missing key (copied_to_clipboard).
- Additive res/lang/{de,es,fr,ja,ko,pt,ru,zh}.json (+193 each); every
  translation's printf format-signature was validated against English
  (0 mismatches) so the runtime validator accepts them.
- Rebuilt res/fonts/NotoSansCJK-Subset.ttf to cover ~30 new Han/Hangul
  glyphs introduced by the zh/ja/ko translations (tofu-free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:36:50 -05:00
DanSandClaude Opus 4.8 4492aa3425 fix(sync): prioritize getblockchaininfo and pause chat scans while behind
The node kept falling behind the network near the tip because sync
DETECTION was starved: getblockchaininfo was queued behind the
O(mapWallet) wallet RPCs (z_gettotalbalance / z_listunspent), so
longestchain went stale, the wallet decided it was "synced", and it
resumed hammering cs_main — a feedback loop.

- Issue getblockchaininfo FIRST each cycle and skip the balance/address/
  tx refresh entirely while behind, so sync state (and kSyncProfile)
  updates before any heavy wallet scan runs.
- Gate the two chat note scans (refreshChatNoteBudgetNode /
  fastScanChatMemos) on effectivelySyncing() and the active page, so chat
  memo scanning no longer competes with block connection during sync.
- Windows debug.log tailer: reset the read offset when dragonxd truncates
  the log on startup (it was stranding at Block:0 with no witness/rescan
  progress).
- Tests cover the getblockchaininfo-first ordering and behind-skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:36:30 -05:00
DanSandClaude Opus 4.8 56d93b6128 fix(shutdown): don't warn "node is rebuilding" for routine witness activity / on v1.3.0
The shutdown guard fired its "Node is rebuilding its witness cache" prompt almost always on an
active wallet: the daemon does a per-tx VerifyAndSetInitialWitness as each newly received wallet
tx lands during normal sync, and those markers (sparse — minutes apart — with no matching
"rebuilt N note witness cache(s) … in Xms") kept the heuristic latched "active".

- daemonWitnessRebuildActive() now requires the last progress marker to be part of the CURRENT
  log activity (within ~15s of the newest log line, via a same-log timestamp delta), so routine
  minutes-old per-tx witness sets no longer count as an ongoing rebuild.
- shouldConfirmDaemonStop() suppresses the prompt entirely on v1.3.0+ daemons (version >= 1030000):
  they checkpoint witness-rescan progress, so stopping mid-rebuild resumes on the next start rather
  than redoing it — the warning's "restarts it (several minutes)" premise no longer holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 02:21:49 -05:00
DanSandClaude Opus 4.8 398fb274fa fix(ui): only toast "Blockchain rescan complete" for user-initiated rescans
The daemon-output parser treats autonomous background witness rebuilds as a rescan (they set
state_.sync.rescanning via the foundWitness branch), so one completing fired "Blockchain rescan
complete" even though the user never started a rescan — most visibly after a minimize, where a
whole rebuild's start+finish arrives in one batch.

Add user_initiated_rescan_ (atomic — some triggers run on worker threads), set it at the wallet's
real rescan triggers (the Rescan button, a -rescan/salvage/zap/reindex restart, key import, seed
migration), and gate the three "rescan complete" toasts on it, clearing it when shown. The
rescan/witness progress state machine is untouched — only the toast is gated — so background
rebuilds no longer announce a completed rescan while genuine user rescans still do (and can't
double-toast).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 01:18:57 -05:00
DanSandClaude Opus 4.8 0343d48c13 feat(ui): keep the wallet syncing while minimized
Previously the main loop just did SDL_Delay+continue when minimized, skipping app.update() —
so the wallet stopped draining RPC results, ticking the refresh scheduler, and reconnecting
until it was restored (and a large daemon-output backlog piled up, which is what produced the
spurious "rescan complete" toast on restore).

Now app.update() runs while minimized (it only reads GetIO/GetTime/IsAnyItemActive, all valid
outside a NewFrame) with a real-clock DeltaTime, skipping only the ImGui frame + GPU present,
throttled to ~5 Hz so CPU stays near-idle. Also clamp io.DeltaTime at the top of App::update()
so a long minimize (or machine sleep) can't report a huge delta and fire every refresh/animation
timer at once on the next update. No backlog now builds, so skipDaemonOutputBacklog becomes a
harmless no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 01:09:50 -05:00
DanSandClaude Opus 4.8 a3892c0fd3 fix(ui): stop spurious "Blockchain rescan complete" toast after restoring from minimize
While minimized the main loop skips the frame body (SDL_Delay+continue), so App::update() —
which drains the daemon output via outputSince(daemon_output_offset_) — never runs and the
offset isn't advanced. On restore the whole accumulated backlog is parsed in one batch: a
background witness rebuild's progress lines (parsed as a rescan → state_.sync.rescanning=true)
AND its "rebuilt … in Xms" completion (parsed as finished) arrive together and fire
"Blockchain rescan complete" for a scan the user never initiated.

On WINDOW_RESTORED, discard the daemon-output backlog (advance the offset to the current end,
via App::skipDaemonOutputBacklog) before the resumed update parses it — so only new output is
parsed. Genuine user-initiated rescans still surface completion via the getrescaninfo monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:49:06 -05:00
DanSandClaude Opus 4.8 f7df315695 fix(ui): remove the "Taking longer than expected" startup stall notice
It added clutter to the loading screen (the yellow title + two lines of explanatory text).
The live daemon-output panel below it is the real progress signal. connect_stall_since_ stays
maintained in app_network.cpp for connection bookkeeping; it just no longer drives any on-screen
text (loading_stall_* i18n strings + util::connectHasStalled are now unused).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:15:27 -05:00
DanSandClaude Opus 4.8 aec996a9ce feat(mining): host a RandomX stratum pool from the node (v1.3.0+)
Add an opt-in "Host a mining pool (stratum)" toggle so a v1.3.0+ node can run its native
RandomX stratum server for other miners to point at. Settings toggle + optional allow-IP/CIDR;
passes -stratum (+ -stratumallowip) to the daemon launch args, mirroring the -maxconnections
plumbing (EmbeddedDaemon::setStratumHosting <- DaemonController::syncSettings <- Settings).

Backwards compatible / safe by default:
- UI gated on daemon_version >= 1030000, so it's never offered where it would do nothing.
- The launch flag is harmless on older daemons (they ignore unknown args), and the toggle can
  only be enabled while connected to a v1.3.0+ node anyway.
- Blank allow-IP => the daemon serves loopback only (its safe default); entering a subnet opens
  it to that LAN, with an explicit exposure warning in the UI.
- Takes effect on the next daemon start/restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 23:19:48 -05:00
DanSandClaude Opus 4.8 90e02b1ddd feat(ui): surface daemon DEGRADED mode + v1.3.0 auto-shield status
Rec3 — v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (existing funds spendable,
but no new HD-key derivation) instead of aborting. Add a daemon-log classifier
(walletOpenedDegraded) + detectWalletDegraded(), warned once per session. Pre-1.3.0 daemons
never emit that line, so it's a no-op there.

O1 — probe z_autoshieldstatus once per connection (now decoupled from our own toggle/balance) and,
in Settings, show whether the node handles auto-shield itself (+ its destination, or the daemon's
disabled_reason). The checkbox now governs only the wallet's fallback shielder, which defers to the
node. Nothing renders on pre-1.3.0 daemons (no such RPC), so behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 22:48:46 -05:00
DanSandClaude Opus 4.8 ef8ceeaf9a feat(net): seed via the round-robin DNS record + node1/node5 (backwards-compatible)
Replace the wallet's stale hardcoded -addnode list (node. + node1-4.dragonx.is — a drifted
subset with a bogus bare 'node.') with the daemon's authoritative vSeeds: seed.dragonx.is (a
round-robin A record over the live seed set, so it self-updates without a wallet release) plus
node1/node5 as static fallbacks. Applied in both seeding sites: the launch args
(embedded_daemon.cpp) and the generated DRAGONX.conf (connection.cpp).

Kept (not deleted) — pre-1.3.0 daemons had broken peer discovery and rely on these -addnode
entries to find peers at all; plain hostname resolution works on every daemon version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 22:48:46 -05:00
DanSandClaude Opus 4.8 ba1d760bb3 fix(autoshield): defer to the daemon's own coinbase auto-shield on v1.3.0+
v1.3.0 daemons auto-shield coinbase by default (when the HD seed is recoverable — which
every ObsidianDragon-created wallet is, via -usemnemonic=1). The wallet also ran its own
client-side auto-shield every refresh tick, so both raced for the same coinbase UTXOs and
split funds across different z-addresses (the wallet picks the first z_listaddresses entry;
the daemon uses a seed-hardened derivation).

Probe z_autoshieldstatus once per connection (while synced, so the daemon is past warmup)
and skip the wallet's client-side shield when the daemon reports it active. Fail-closed: a
pre-1.3.0 daemon has no such RPC, so the probe returns active=false and the wallet keeps
shielding — no regression on the currently-bundled v1.0.3. Re-probes on reconnect (handles
a live daemon upgrade/swap).

Follow-up (not done): drive the Settings "Auto-shield" toggle + disabled_reason from
z_autoshieldstatus (O1) — needs runtime verification of the RPC fields on a live v1.3.0 node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-31 22:33:12 -05:00
DanSandClaude Opus 4.8 0942691eb3 fix(win): route shell-outs through a windowless helper (no cmd.exe flash)
_popen/_popen-style shell-outs flash a cmd.exe console window on Windows. Add
Platform::runHiddenCapture() — CreateProcess + CREATE_NO_WINDOW capturing stdout on
Windows, popen on POSIX — and route the remaining shell-outs through it:
- GPU-aware idle detection (getGpuUtilization: "where nvidia-smi" / "nvidia-smi --query-gpu")
- xmrig discovery + version (findXmrigBinary "where xmrig.exe"; "<bin> --version", stderr merged)
- wallet-rebuild helper (app_network) — keeps its exit-code check via the new exitCode out-param

None of these are on the launch path (that was the daemon spawn, fixed in a2f84be); each
would flash a console only when it ran (idle-GPU mining, mining tab, wallet recovery).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:58:54 -05:00
DanSandClaude Opus 4.8 a2f84be2d4 fix(win): stop console-window flash on launch (spawn daemon with CREATE_NO_WINDOW)
The embedded daemon was launched with CREATE_NEW_CONSOLE + SW_HIDE. CREATE_NEW_CONSOLE
allocates a console window that flashes on screen before SW_HIDE hides it — visible as a
console-window flash every time the wallet starts dragonxd (i.e. on launch). Switch to
CREATE_NO_WINDOW (the console child gets no window at all, matching the xmrig launcher);
dragonxd logs to debug.log, not a console, so nothing is lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:43:12 -05:00
DanSandClaude Opus 4.8 7e8b99a82b feat(shutdown): confirm before stopping the daemon mid witness-cache rebuild
Stopping dragonxd while it's rebuilding the Sapling witness cache discards the
in-progress work — BuildWitnessCache aborts on shutdown without persisting — so
the next launch redoes a multi-minute rebuild (the "Activating best chain…" hang).
This bites especially with stop_external_daemon enabled, where wallet exit sends
the node a stop.

beginShutdown() now defers when it would StopDaemon while a rebuild is active and
shows a confirm modal: "Keep node running & quit" (DisconnectOnly — leaves it up
to finish), "Stop anyway & quit", or "Cancel". Rebuild detection reads the
debug.log tail markers (Cleared witness data / Setting Initial Sapling Witness /
Reading blocks for witness rebuild, vs. the "rebuilt … in …ms" / abort lines).
The gate lives entirely in beginShutdown()/render() — no SDL event-loop changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:32:03 -05:00
DanSandClaude Opus 4.8 29274c2f48 fix(ui): trim verbose startup notice; show daemon output on shutdown for external daemons
- Loading "taking longer than expected" notice: shorten the body + hint so the
  startup screen reads less wordy (same info, ~half the text).
- Shutdown screen: when the wallet attached to an EXTERNAL daemon (no captured
  stdout — debug_log_path_ is only set when we spawn it), the "dragonxd output"
  panel was always empty, leaving just a spinner. Fall back to tailing the
  daemon's debug.log so the user can watch the node flush the block index and
  exit. Adds App::tailDaemonDebugLog() (best-effort, reads only the file tail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 23:09:17 -05:00
DanSandClaude Opus 4.8 870793433b fix(sync): stop large-wallet balance polling from starving block connection
On a fully-shielded (ac_private=1) chain, z_gettotalbalance is O(mapWallet) and
holds the daemon's cs_main for its whole duration — ~20s on a ~5k-tx wallet. The
Overview refresh polled it every ~2s (twice: minconf 0 and 1), so cs_main was
held almost continuously, starving the single block-connection thread: the node
connected blocks only in the gaps between polls and could fall further behind
the tip than it caught up (observed live: gap growing 58→100 blocks while the
GUI was open, one core pegged on GetFilteredNotes, 22 idle, ~17 B/s download).

Two hardening changes on top of the existing "skip balance while syncing" guard:

- Hysteresis: keep the low-impact sync profile (and balance suppression) for a
  short settle window after catching up, so a large-wallet scan can't
  immediately re-starve connection and bounce the node back into syncing. Armed
  only on the syncing→caught-up edge, so a wallet synced from the start is never
  throttled at connect (effectivelySyncing()).

- Adaptive balance cadence: time each z_gettotalbalance scan and require the
  next poll to wait at least (cost / 10%), so balance scanning never occupies
  more than ~10% of wall-clock. Cheap wallets are unaffected (the tab's Core
  timer stays the cadence); a ~20s scan backs off to ~200s. Wallet mutations
  (send/shield) force the next poll through so the user's own action updates the
  balance immediately (balanceRefreshDue()).

getblockchaininfo keeps its normal cadence throughout, so sync progress stays
live. Build + test_phase4 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-30 22:45:51 -05:00
DanSandClaude Opus 4.8 08cfeb0e08 feat(ui): rework the consolidate/merge modal
Make Merge to Address actually serve wallet-bloat consolidation and be far less
opaque. New ShieldDialog::showConsolidate() preset (used by the large-wallet
Settings banner + alert action) frames it as "Consolidate funds" and targets
shielded notes — the bloat the nudge warns about.

- Source selector: consolidate shielded notes (ANY_SAPLING), transparent
  (ANY_TADDR), or both (*) — previously hardcoded to ANY_TADDR, which never
  reduced the shielded-witness bloat. Batch limit now applies to the right side.
- Scope: on open, count spendable UTXOs + notes (listunspent / z_listunspent)
  and show "N transparent + M shielded · ~X DRGX"; warn "repeat to finish" when
  the set exceeds one batch.
- Destination auto-selects the best spendable z-address (button enabled by
  default); empty wallets get an inline "Create shielded address" (z_getnewaddress).
- Advanced disclosure hides Fee + "Max inputs per batch" (renamed from the "UTXO
  Limit" jargon) with sane defaults.
- Inline confirm step before the fund-moving call (amount + input count + dest).
- Live progress: self-polls z_getoperationstatus to show Consolidating… →
  Done/Failed, replacing the raw opid + manual "Check status" button.

All three merge entry points now use the typed showMerge()/showConsolidate()
(no stale-static leaks from direct show(MergeToAddress)). Shield-coinbase mode
keeps working. New i18n keys fall back to English.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 22:33:59 -05:00
DanSandClaude Opus 4.8 5daf2d83b6 feat(ui): large-wallet nudge as a one-time toast + clickable alert
Extend the wallet-bloat warning beyond the Settings banner: when wallet.dat
first crosses 500 MB (full-node, synced), fire a one-time warning toast plus a
clickable "Consolidate notes…" entry in the bell/alerts panel that opens Merge
to Address. The persisted large_wallet_warned flag keeps it once-only and
re-arms if the file later shrinks back under the threshold.

- AlertRecord gains an optional onClick + actionHint; Notifications::action()
  pushes a toast and a clickable history entry. renderAlertHistoryPanel() now
  renders the accent action link (under the message) and measures true content
  height so wrapped messages + the link aren't clipped.
- App::maybeWarnLargeWallet() (mirrors maybeRemindSeedBackup) runs once per
  launch from update(); reuses the existing wallet_size_warn/consolidate strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 18:09:36 -05:00
DanSandClaude Opus 4.8 6d26ccd0ed feat(ui): large-wallet nudge in Node & Security
The BDB wallet.dat bloats with shielded-note witness data and never shrinks
in place, so a mining/shielded wallet can grow past 500 MB. Below the Wallet
Size row, show a one-line amber hint once wallet.dat crosses 500 MB with a
"Consolidate notes…" shortcut that opens the Merge to Address (z_mergetoaddress)
dialog. Full-node only (lite has no wallet.dat here); threshold is a single
named constant. i18n keys fall back to English for non-English locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 17:41:25 -05:00
DanSandClaude Opus 4.8 a7514becbc feat(ui): credit The DragonX Developers in the About tab
Add "The DragonX Developers" to the About-tab credits (after The Hush
Developers), acknowledging the DragonX chain/daemon this wallet drives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 17:17:03 -05:00
DanSandClaude Opus 4.8 558cfcbe56 fix(ui): restore ObsidianDragon logo in header and About tab
ensureLogoTexture() rasterized the embedded DragonX SVG into logo_tex_ and
returned early (added in 1752500 "themed DragonX logo"), so the app/product
branding — the top-left header (app.cpp AddImage) and the About tab
(getLogoTexture) — showed the DragonX coin mark instead of the ObsidianDragon
logo. Drop that step so logo_tex_ resolves via the intended path: active-skin
override → ui.toml header-icon → bundled ObsidianDragon dark/light PNG (disk,
then embedded RESOURCE_LOGO). The DragonX SVG stays for coin_logo_tex_ (balance
card) and drgx_emoji_tex_ (chat emoji), which are the currency mark and correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 17:06:04 -05:00
DanSandClaude Opus 4.8 d0bd55b9c1 feat(ui): settings polish — button retune, daemon card, RPC 2-row, chat preview
Settings tabs brought closer to the approved mockup:
- ActionButton/renderCardButton retune (settings-scoped): 7px radius, 9px
  padX, Primary → accent-outline chip, Secondary/card buttons more defined.
- Daemon-binary card: compact status right-aligned on the DAEMON BINARY
  heading (Up to date / Version differs / Not installed), filled/rounded
  status box, neutral danger divider (was alarming red), roomier spacing.
- RPC Connection: two-row column-aligned layout (Host | Port, then
  Username | Password) so the password no longer clips off the card edge.
- Chat settings tab: live conversation preview below the Appearance /
  Messaging cards; "Focus input on open" checkbox reflowed onto the console
  color-toggle row.
- Debug Options: "Current theme only" toggle restricts either screenshot
  sweep to the active theme instead of cycling every skin.
- Tabs fill the full content width (content-max-width cap disabled) and the
  sidebar nav panel centers within the true visible area.
- i18n: new keys for the above (untranslated keys fall back to English).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 16:59:03 -05:00
DanSandClaude Opus 4.8 b37d3d97b6 fix(send/receive): unify card width, justify receive footer, fix recipient-row button height/clip/glyph
Send and Receive are now consistent in layout, and the Send recipient row's buttons
render correctly.

Card envelope (Send ⇄ Receive consistency):
- Add Layout::mainComposeCardBox(availW) — a single shared source for the compose card's
  width + centering (fill the available column up to content-max-width, then center). Both
  tabs derive their card from it, so they can't drift again. Previously Send capped at
  760dp and Receive at 860dp, so the Send card rendered ~150px narrower on any window wider
  than ~860dp; now they fill available width identically.

Receive:
- Justify the footer buttons edge-to-edge (equal shares over the live count) instead of
  left-clustering with dead space, matching Send's full-width footer rhythm.
- Build the address-dropdown preview to the combo's real pixel width so the trailing
  balance ("— 12.00000000 DRGX") no longer hard-clips at 150% (was char-count truncation).

Send recipient row (input | Paste | contacts-icon):
- Pin the contacts icon button to the frame height so the larger iconMed font doesn't
  auto-size it taller than Paste/the input.
- Reserve the real ItemSpacing.x gaps (not the smaller spacingSm token) so the row no
  longer overshoots the card and clips the icon's right border.

draw_helpers (root cause, app-wide):
- TactileButton's icon path measured/drew the label INCLUDING the "##id" suffix (which
  CalcTextSizeA/AddText don't strip the way ImGui's text render does), shoving the glyph
  off-center-left. Strip at "##" before measuring/drawing. Corrects any icon button that
  passes an explicit size and a "##id" label; no-op for labels without "##".

Verified via headless sweeps at 1.0x and 1.5x, plus a real 3800px-wide render (both cards
byte-identical at L=1174/R=2773). ctest 1/1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-19 16:10:02 -05:00
66 changed files with 5321 additions and 1797 deletions
+4
View File
@@ -521,6 +521,8 @@ set(APP_SOURCES
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
src/ui/windows/faq_dialog.cpp
src/ui/windows/faq_content.cpp
src/ui/windows/key_export_dialog.cpp
src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp
@@ -657,6 +659,8 @@ set(APP_HEADERS
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h
src/ui/windows/faq_dialog.h
src/ui/windows/faq_content.h
src/ui/windows/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h
Binary file not shown.
+217 -17
View File
@@ -60,6 +60,80 @@
"amount_label": "Betrag:",
"animate_avatars": "Avatare animieren",
"appearance": "ERSCHEINUNGSBILD",
"appx_back": "Zurück",
"appx_back_up_seed_phrase_title": "Sichern Sie Ihre Seed-Phrase",
"appx_birthday_block_height": "Geburtstag (Blockhöhe): %llu — sichern Sie auch dies.",
"appx_blockchain_data_deleted": "Blockchain-Daten gelöscht (%d Elemente). Der Daemon wird neu gestartet, um sich erneut mit dem Netzwerk zu synchronisieren.",
"appx_blockchain_maintenance_in_progress": "Eine Blockchain-Wartung läuft bereits.",
"appx_blockchain_rescan_complete": "Blockchain-Neuscan abgeschlossen",
"appx_bootstrap_complete_reconciling": "Bootstrap abgeschlossen — Ihr Wallet wird mit den neuen Chain-Daten abgeglichen.",
"appx_cancel": "Abbrechen",
"appx_cleaning_up": "Aufräumen...",
"appx_confirm_your_backup": "Backup bestätigen",
"appx_copied_clipboard_autoclears": "Kopiert — Zwischenablage wird in 45s automatisch geleert",
"appx_copy": "Kopieren",
"appx_could_not_start_restore": "Wiederherstellung konnte nicht gestartet werden",
"appx_create_failed_prefix": "Erstellen fehlgeschlagen: ",
"appx_creating_your_wallet": "Ihr Wallet wird erstellt…",
"appx_daemon_error": "Daemon-Fehler",
"appx_daemon_reinstall_in_progress": "Die Daemon-Neuinstallation läuft bereits.",
"appx_disconnecting": "Verbindung wird getrennt...",
"appx_done": "Fertig",
"appx_dragonxd_output": "dragonxd-Ausgabe",
"appx_encrypting_wallet": "Wallet wird verschlüsselt...",
"appx_fullnode_lifecycle_unavailable_lite": "Full-Node-Lebenszyklusaktionen sind im Lite-Build nicht verfügbar",
"appx_installing_bundled_daemon": "Mitgelieferter Daemon wird installiert — der Node stoppt, aktualisiert und startet neu...",
"appx_invalid_payment_uri_prefix": "Ungültige Zahlungs-URI: ",
"appx_ive_written_it_down": "Ich habe sie notiert",
"appx_keep_node_running_and_quit": "Node weiterlaufen lassen & beenden",
"appx_last_block_n": "Letzter Block: %d",
"appx_last_used_wallet_not_found_prefix": "Ihre zuletzt verwendete Wallet-Datei (",
"appx_last_used_wallet_not_found_suffix": ") wurde nicht gefunden — stattdessen wurde das Standard-Wallet geöffnet. Wenn Sie sie verschoben haben, stellen Sie sie wieder her und wechseln Sie über die Wallet-Liste zurück.",
"appx_low_spec_mode_disabled": "Low-Spec-Modus deaktiviert",
"appx_low_spec_mode_enabled": "Low-Spec-Modus aktiviert",
"appx_miner_stopped_prefix": "Miner gestoppt: ",
"appx_miner_stopped_unexpectedly": "Miner wurde unerwartet gestoppt.",
"appx_n_min_n_sec": "%d Min %d Sek",
"appx_n_seconds": "%d Sekunden",
"appx_no_bundled_daemon_to_install": "Dieser Build enthält keinen mitgelieferten Daemon zur Installation",
"appx_no_embedded_daemon_to_install": "Dieser Build enthält keinen eingebetteten Daemon zur Installation",
"appx_node_busy_restarting": "Der Node ist mit dem Neustart beschäftigt — versuchen Sie es gleich erneut.",
"appx_node_rebuilding_witness_cache": "Node baut seinen Witness-Cache neu auf",
"appx_not_next_word": " — das ist nicht das nächste Wort",
"appx_payment_request_loaded": "Zahlungsanforderung geladen",
"appx_pool_miner_connected_and_hashing": "Pool-Miner verbunden und aktiv.",
"appx_progress_n_of_n": "Fortschritt: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Sapling-Note-Witnesses werden neu aufgebaut…",
"appx_rebuilding_witness_cache_blocks_left": "Witness-Cache wird neu aufgebaut %.0f%% — %d Blöcke verbleiben",
"appx_rebuilding_witness_cache_pct": "Witness-Cache wird neu aufgebaut %.0f%%",
"appx_recovery_phrase_word_count": "Die Wiederherstellungsphrase sollte 24 Wörter haben — Sie haben %d.",
"appx_restarting_daemon_rescan_flag": "Daemon wird mit -rescan-Flag neu gestartet...",
"appx_restarting_daemon_zapwallettxes": "Daemon wird mit -zapwallettxes=2 neu gestartet (Wallet-Reparatur)...",
"appx_restoring_your_wallet": "Ihr Wallet wird wiederhergestellt…",
"appx_seed_backup_warning": "Diese 24 Wörter sind die EINZIGE Möglichkeit, Ihr Wallet wiederherzustellen. Notieren Sie sie in der richtigen Reihenfolge, bewahren Sie sie offline auf und geben Sie sie niemals weiter. Wenn Sie sie verlieren, sind Ihre Guthaben für immer verloren.",
"appx_seed_not_backed_up_warning": "Sie haben Ihren Seed nicht gesichert — Guthaben könnten verloren gehen. Trotzdem überspringen?",
"appx_sending_stop_command_to_daemon": "Stopp-Befehl wird an Daemon gesendet...",
"appx_setting_initial_sapling_witnesses": "Initiale Sapling-Witnesses werden gesetzt %.0f%%",
"appx_shutdown_complete": "Herunterfahren abgeschlossen",
"appx_simple_background_disabled": "Einfacher Hintergrund deaktiviert",
"appx_simple_background_enabled": "Einfacher Hintergrund aktiviert",
"appx_skip": "Überspringen",
"appx_skip_anyway": "Trotzdem überspringen",
"appx_still_status_prefix": "Weiterhin \"",
"appx_still_status_suffix": "\" — ein erzwungenes Beenden jetzt kann die Chain-Daten beschädigen.",
"appx_stop_anyway_and_quit": "Trotzdem stoppen & beenden",
"appx_stopping_daemon_deleting_blockchain": "Daemon wird gestoppt und Blockchain-Daten werden gelöscht...",
"appx_stopping_node_discards_rebuild": "Wenn Sie den Node jetzt stoppen, wird der laufende Neuaufbau verworfen und beim nächsten Öffnen des Wallets neu gestartet (mehrere Minuten). Sie können den Node stattdessen weiterlaufen lassen.",
"appx_stopping_pool_miner": "Pool-Miner wird gestoppt...",
"appx_syncing_pct_block_n_of_n": "Synchronisierung %.1f%% — Block %d / %d",
"appx_tap_words_in_order": "Tippen Sie die Wörter in der richtigen Reihenfolge an, um zu bestätigen, dass Sie sie gespeichert haben.",
"appx_theme_effects_disabled": "Design-Effekte deaktiviert",
"appx_theme_effects_enabled": "Design-Effekte aktiviert",
"appx_theme_prefix": "Design: ",
"appx_use_settings_restart_daemon_hint": "Verwenden Sie Einstellungen > Daemon neu starten, um es erneut zu versuchen",
"appx_waiting_for_daemon_to_encrypt_wallet": "Warten, bis der Daemon das Wallet verschlüsselt...",
"appx_wallet_created_and_backed_up": "Wallet erstellt und gesichert.",
"appx_wallet_open_failed_prefix": "Öffnen des Wallets fehlgeschlagen: ",
"auto_shield": "Mining automatisch abschirmen",
"av_intro": "Mining-Software wird oft als potenziell unerwünscht eingestuft. Führen Sie diese Schritte aus, um das Pool-Mining zu aktivieren:",
"av_open_security": "Windows-Sicherheit öffnen",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender hat den Miner blockiert",
"available": "Verfügbar",
"backup_backing_up": "Sicherung läuft...",
"backup_col_backup": "SICHERUNG",
"backup_col_export": "EXPORTIEREN",
"backup_col_import": "IMPORTIEREN & WIEDERHERSTELLEN",
"backup_create": "Sicherung erstellen",
"backup_created": "Wallet-Sicherung erstellt",
"backup_data": "SICHERUNG & DATEN",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Abgeschirmt: %.8f",
"balance_syncing_pct": "Synchronisiere %.1f%%",
"balance_transparent_fmt": "Transparent: %.8f",
"baltab_market": "Markt",
"baltab_market_price_4dp": "Markt: $%.4f",
"baltab_market_price_8dp": "Markt: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% des Gesamtbetrags · %d Z-addr",
"baltab_shielded": "Geschützt",
"baltab_shielded_amount": "Geschützt %.8f",
"baltab_t_addresses_count": "%d T-Adressen",
"baltab_total_balance": "Gesamtguthaben",
"baltab_transparent": "Transparent",
"baltab_transparent_amount": "Transparent %.8f",
"ban": "Sperren",
"banned_peers": "Gesperrte Peers",
"block": "Block",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Quadrat",
"contacts_shape_tab": "Reiter",
"copied": "Kopiert!",
"copied_to_clipboard": "In die Zwischenablage kopiert",
"copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren",
@@ -423,6 +512,7 @@
"daemon_bundled": "Gebündelt",
"daemon_install_bundled": "Gebündelten installieren",
"daemon_installed": "Installiert",
"daemon_maintenance_label": "WARTUNG",
"daemon_none_bundled": "keiner in diesem Build",
"daemon_not_installed": "nicht installiert",
"daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "Der Download wird vor der Installation anhand der veröffentlichten SHA-256-Prüfsumme des Releases und einer fest hinterlegten ed25519-Signatur verifiziert.",
"daemon_update_verifying": "Wird verifiziert…",
"daemon_update_version": "Version:",
"daemon_updates_label": "AKTUALISIERUNGEN",
"daemon_version": "Daemon",
"dark": "Dunkel",
"data_stale_prefix": "Aktualisiert",
@@ -588,6 +679,63 @@
"general": "Allgemein",
"generating": "Wird generiert",
"go_to_receive": "Zum Empfangen",
"grpa_current_block_paren": "(Aktuell: %d)",
"grpa_days_ago": "vor %lld Tagen",
"grpa_dbg_addrman": "Verfolgung und Verwaltung von Peer-Adressen",
"grpa_dbg_alert": "Meldungen des Alarmsystems",
"grpa_dbg_bench": "Benchmark-Zeiten für Operationen",
"grpa_dbg_coindb": "Lese-/Schreiboperationen der Coin-Datenbank",
"grpa_dbg_db": "Berkeley-DB-Operationen",
"grpa_dbg_estimatefee": "Algorithmus zur Gebührenschätzung",
"grpa_dbg_http": "Aktivität des HTTP-RPC-Servers",
"grpa_dbg_libevent": "Libevent-Netzwerkbibliothek",
"grpa_dbg_lock": "Debugging von Lock-Konflikten",
"grpa_dbg_mempool": "Aktivität des Transaktions-Mempools",
"grpa_dbg_net": "Netzwerkverbindungen und -nachrichten",
"grpa_dbg_paymentdisclosure": "Zahlungsoffenlegungsprotokoll",
"grpa_dbg_pow": "Proof-of-Work-Mining-Aktivität",
"grpa_dbg_proxy": "SOCKS5-Proxy-Verbindungen",
"grpa_dbg_prune": "Block-Pruning-Operationen",
"grpa_dbg_rand": "Zufallszahlengenerierung",
"grpa_dbg_reindex": "Fortschritt der Blockchain-Neuindizierung",
"grpa_dbg_rpc": "Verarbeitung von RPC-Befehlen",
"grpa_dbg_selectcoins": "Coin-Auswahl für Transaktionen",
"grpa_dbg_tor": "Tor-Integration und Circuit-Informationen",
"grpa_dbg_zmq": "ZeroMQ-Benachrichtigungssystem",
"grpa_dbg_zrpc": "Geschützte (z-addr) RPC-Operationen",
"grpa_enter_private_key_to_import": "Geben Sie einen privaten Schlüssel zum Importieren ein.",
"grpa_error_prefix": "Fehler: ",
"grpa_hr_ago": "vor %lld Std",
"grpa_invalid_response_from_daemon": "Ungültige Antwort vom Daemon",
"grpa_invalid_suffix": " (ungültig)",
"grpa_min_ago": "vor %lld Min",
"grpa_sec_ago": "vor %lld Sek",
"grpa_seed_demo_chat": "Seed-Demo-Chat",
"grpa_showing_first_100_of": "... die ersten 100 von %d werden angezeigt",
"grpa_tab_about": "Über",
"grpa_tab_appearance": "Darstellung",
"grpa_tab_backup_data": "Backup & Daten",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorer",
"grpa_tab_node_security": "Node & Sicherheit",
"grpa_tab_wallet": "Wallet",
"grpa_unexpected_getblockhash_result": "unerwartetes getblockhash-Ergebnis",
"grpb_copy": "Kopieren",
"grpb_max": "Max",
"grpb_new_badge_suffix": " [NEU]",
"grpb_preview_msg_payment_through": "Ist die Zahlung durchgegangen? 🙂",
"grpb_preview_msg_sending_rest": "Sende jetzt den Rest 👍",
"grpb_preview_msg_yep_confirmed": "Ja — gerade bestätigt ✅",
"grpb_selected_suffix": "\n(ausgewählt)",
"grpb_tooltip_address_balance": "%s\nGuthaben: %.8f %s%s",
"grpb_undo_clear": "Löschen rückgängig",
"grpc_benchmark_inconclusive": "Benchmark nicht aussagekräftig: es wurden keine Hashrate-Werte aufgezeichnet. Prüfen Sie die Pool-Verbindung und versuchen Sie es erneut.",
"grpc_benchmark_takes_secs": "Der Benchmark dauert ~%ds und unterbricht das Mining. Erneut klicken zum Starten.",
"grpc_bootstrap_failed": "Bootstrap fehlgeschlagen",
"grpc_bootstrap_not_initialized": "Bootstrap nicht initialisiert",
"grpc_hashrate_fee": "%s %s%% Gebühr",
"grpc_key_not_available": "Schlüssel für diese Adresse nicht verfügbar",
"grpc_na": "N/V",
"height": "Höhe",
"help": "Hilfe",
"hidden_tag": " (versteckt)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Warten auf dragonxd — %s",
"sb_warming_up": "Aufwärmen...",
"sb_witness_cache": "Zeugen werden neu aufgebaut",
"scale_effects": "SKALIERUNG & EFFEKTE",
"screenshot_open_dir": "Speicherort öffnen",
"screenshot_sweep": "Screenshot-Durchlauf ausführen",
"screenshot_sweep_desc": "Durchläuft jedes Design über jeden Tab und speichert von jedem einen Screenshot in tab-spezifischen Unterordnern im Screenshots-Ordner des Konfigurationsverzeichnisses (überschreibt den vorherigen Durchlauf). Läuft einige Sekunden.",
"screenshot_sweep_full": "Vollständiger UI-Durchlauf",
"search_icons": "Symbole suchen...",
"search_placeholder": "Suchen...",
"sec_changing_passphrase": "Passphrase wird geändert...",
"sec_changing_pin": "PIN wird geändert...",
"sec_couldnt_lock_wallet": "Wallet konnte nicht gesperrt werden — es ist weiterhin entsperrt. Prüfen Sie die Daemon-Verbindung.",
"sec_encrypted_backup_suffix": "\nVerschlüsseltes Backup: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Wallet wird verschlüsselt...",
"sec_encryption_did_not_complete": "Die Wallet-Verschlüsselung wurde nicht abgeschlossen — Ihr Wallet ist NICHT verschlüsselt. Öffnen Sie die Einstellungen, um die Verschlüsselung abzuschließen.",
"sec_encryption_failed_prefix": "Verschlüsselung fehlgeschlagen: ",
"sec_failed_prefix": "Fehlgeschlagen: ",
"sec_failed_to_create_vault": "Tresor konnte nicht erstellt werden",
"sec_importing_keys_rescanning": "Schlüssel werden importiert & Blockchain neu gescannt — Wallet ist währenddessen nutzbar",
"sec_incorrect_current_pin": "Aktuelle PIN falsch",
"sec_incorrect_passphrase_decrypt": "Falsche Passphrase",
"sec_incorrect_passphrase_pin_setup": "Falsche Passphrase",
"sec_incorrect_pin_remove": "Falsche PIN",
"sec_internal_error_change_pin": "Interner Fehler",
"sec_internal_error_remove_pin": "Interner Fehler",
"sec_mode_passphrase": " Passphrase",
"sec_not_connected_to_daemon": "Nicht mit Daemon verbunden",
"sec_not_connected_to_daemon_pin": "Nicht mit Daemon verbunden",
"sec_passphrase_changed_successfully": "Passphrase erfolgreich geändert",
"sec_pin_changed_successfully": "PIN erfolgreich geändert",
"sec_pin_removed": "PIN entfernt",
"sec_pin_set_successfully": "PIN erfolgreich festgelegt",
"sec_restart_daemon_for_encryption": "Bitte starten Sie Ihren Daemon neu, damit die Verschlüsselung wirksam wird.",
"sec_too_many_attempts_wait": "Zu viele Versuche. Warten Sie %.0f Sekunden...",
"sec_total_elapsed_fmt": "Gesamtdauer: %dm %02ds",
"sec_unlock_button": "Entsperren",
"sec_unlock_failed_prefix": "Entsperren fehlgeschlagen: ",
"sec_unlocking_fmt": "Entsperren%s",
"sec_use_passphrase_instead": "Stattdessen Passphrase verwenden",
"sec_use_pin_instead": "Stattdessen PIN verwenden",
"sec_verifying_passphrase": "Passphrase wird überprüft...",
"sec_verifying_pin": "PIN wird überprüft...",
"sec_wallet_decrypted_all_keys_imported": "Wallet erfolgreich entschlüsselt! Alle Schlüssel importiert.",
"sec_wallet_encrypted_and_pin_set": "Wallet verschlüsselt & PIN festgelegt",
"sec_wallet_encrypted_but_pin_vault_failed": "Wallet verschlüsselt, aber PIN-Tresor fehlgeschlagen",
"sec_wallet_encrypted_restarting_daemon": "Wallet verschlüsselt. Daemon wird neu gestartet...",
"sec_wallet_encrypted_successfully": "Wallet erfolgreich verschlüsselt",
"sec_wallet_locked_title": "Wallet gesperrt",
"security": "SICHERHEIT",
"seed_backup_button": "Wiederherstellungsphrase",
"seed_backup_close": "Schließen",
@@ -1316,12 +1504,12 @@
"settings": "Einstellungen",
"settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.",
"settings_acrylic_level": "Acrylstufe:",
"settings_address_book": "Adressbuch...",
"settings_address_book": "Adressbuch…",
"settings_auto_detected": "Automatisch erkannt aus DRAGONX.conf",
"settings_auto_lock": "AUTO-SPERRE",
"settings_auto_shield_desc": "Transparente Guthaben automatisch an geschirmte Adressen verschieben",
"settings_auto_shield_funds": "Transparente Guthaben automatisch abschirmen",
"settings_backup": "Sicherung...",
"settings_backup": "Sicherung…",
"settings_block_explorer_urls": "Block-Explorer-URLs",
"settings_builtin": "Integriert",
"settings_change_passphrase": "Passphrase ändern",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Diagnose kopieren",
"settings_copyright": "Copyright 2024-2026 DragonX-Entwickler | GPLv3-Lizenz",
"settings_custom": "Benutzerdefiniert",
"settings_data_dir": "Datenverzeichnis:",
"settings_data_dir": "Datenverzeichnis",
"settings_debug_changed": "Debug-Kategorien geändert — Daemon neu starten zum Anwenden",
"settings_debug_restart_note": "Änderungen werden nach einem Neustart des Daemons wirksam.",
"settings_debug_select": "Kategorien auswählen, um Daemon-Fehlerprotokollierung zu aktivieren (-debug= Flags).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Verschlüsseln Sie zuerst die Wallet, um PIN zu aktivieren",
"settings_encrypt_wallet": "Wallet verschlüsseln",
"settings_explorer_hint": "URLs sollten einen abschließenden Schrägstrich enthalten. Die txid/Adresse wird angehängt.",
"settings_export_all": "Alle exportieren...",
"settings_export_csv": "CSV exportieren...",
"settings_export_key": "Schlüssel exportieren...",
"settings_export_all": "Alle exportieren…",
"settings_export_csv": "CSV exportieren…",
"settings_export_key": "Schlüssel exportieren…",
"settings_gradient_bg": "Hintergrund-Verlauf",
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
"settings_idle_after": "nach",
"settings_import_key": "Privaten Schlüssel importieren...",
"settings_import_viewkey": "Anzeigeschlüssel importieren...",
"settings_import_key": "Privaten Schlüssel importieren…",
"settings_import_viewkey": "Anzeigeschlüssel importieren…",
"settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
"settings_lock_now": "Jetzt sperren",
"settings_locked": "Gesperrt",
"settings_merge_to_address": "An Adresse zusammenführen...",
"settings_merge_to_address": "An Adresse zusammenführen…",
"settings_noise_opacity": "Rauschdichte:",
"settings_not_connected": "Nicht mit dem Daemon verbunden",
"settings_not_encrypted": "Nicht verschlüsselt",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Einstellungen von der Festplatte neu geladen",
"settings_remove_encryption": "Verschlüsselung entfernen",
"settings_remove_pin": "PIN entfernen",
"settings_request_payment": "Zahlung anfordern...",
"settings_request_payment": "Zahlung anfordern…",
"settings_rescan_desc": "Blockchain nach fehlenden Transaktionen neu scannen",
"settings_restart_daemon": "Daemon neu starten",
"settings_rpc_connection": "RPC-Verbindung",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Geschirmten Transaktionsverlauf lokal speichern",
"settings_saved": "Einstellungen gespeichert",
"settings_set_pin": "PIN festlegen",
"settings_shield_mining": "Mining abschirmen...",
"settings_shield_mining": "Mining abschirmen…",
"settings_solid_colors_desc": "Feste Farben anstelle von Unschärfe-Effekten verwenden (Barrierefreiheit)",
"settings_theme_refreshed": "Themenliste aktualisiert",
"settings_tor_desc": "Alle Verbindungen für erhöhte Privatsphäre über Tor leiten",
"settings_unlocked": "Entsperrt",
"settings_use_tor_network": "Tor für Netzwerkverbindungen verwenden",
"settings_validate_address": "Adresse überprüfen...",
"settings_validate_address": "Adresse überprüfen…",
"settings_visual_effects": "Visuelle Effekte",
"settings_wallet_file_size": "Wallet-Dateigröße: %s",
"settings_wallet_info": "Wallet-Informationen",
"settings_wallet_location": "Wallet-Speicherort: %s",
"settings_wallet_maintenance": "Wallet-Wartung",
"settings_wallet_not_found": "Wallet-Datei nicht gefunden",
"settings_wallet_size_label": "Wallet-Größe:",
"settings_wallet_size_label": "Wallet-Größe",
"settings_ztx_cleared": "Z-Transaktionsverlauf gelöscht",
"settings_ztx_not_found": "Keine Verlaufsdatei gefunden",
"setup_wizard": "Einrichtungsassistent",
@@ -1455,6 +1643,17 @@
"sweep_to": "Gefegt an:",
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
"sweep_tx": "Transaktion:",
"swin_connection_failed": "Verbindung fehlgeschlagen: ",
"swin_connection_successful": "Verbindung erfolgreich!\ndragonxd-Version: ",
"swin_invalid_suffix": " (ungültig)",
"swin_no_history_file_found": "Keine Verlaufsdatei gefunden",
"swin_rescan_failed": "Neuscan fehlgeschlagen: ",
"swin_rescan_started_from_block": "Neuscan gestartet ab Block ",
"swin_rescan_to": " bis ",
"swin_rpc_client_not_initialized": "RPC-Client nicht initialisiert",
"swin_settings_saved": "Einstellungen gespeichert",
"swin_theme_list_refreshed": "Design-Liste aktualisiert",
"swin_ztx_history_cleared": "Z-Transaktionsverlauf gelöscht",
"switch_corrupt_body": "Diese Wallet scheint beschädigt zu sein – der Knoten konnte sie nicht öffnen. Aus einem Backup wiederherstellen, neu erstellen oder eine Reparatur versuchen.",
"switch_corrupt_repair": "Reparatur versuchen (Salvage)",
"switch_progress_background": "Im Hintergrund fortsetzen",
@@ -1494,6 +1693,7 @@
"to_upper": "AN",
"tools": "WERKZEUGE",
"tools_actions": "Werkzeuge & Aktionen...",
"tools_actions_hdr": "WERKZEUGE & AKTIONEN",
"total": "Gesamt",
"total_balance_label": "Gesamtguthaben",
"transaction_id": "TRANSAKTIONS-ID",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
"tt_block_explorer": "Den DragonX Block-Explorer im Browser öffnen",
"tt_blur": "Unschärfe-Stärke (0%% = aus, 100%% = maximum)",
"tt_blur": "Unschärfe-Stärke (0% = aus, 100% = maximum)",
"tt_change_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
"tt_change_pin": "Ihre Entsperr-PIN ändern",
"tt_chat_bubble_accent": "Akzentfarbe für deine ausgehenden Nachrichtenblasen (oder dem aktuellen Theme folgen)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)",
"tt_noise": "Körnungstextur-Intensität (0%% = aus, 100%% = maximum)",
"tt_noise": "Körnungstextur-Intensität (0% = aus, 100% = maximum)",
"tt_open_app_dir": "Den ObsidianDragon-Ordner (Einstellungen, Themes, Logs) im Dateimanager öffnen",
"tt_open_data_dir": "Den Ordner mit Ihren Wallet- und Blockchain-Daten im Dateimanager öffnen",
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Hotkey: Ctrl+Links/Rechts zum Wechseln der Themes",
"tt_tor": "Daemon-Verbindungen für Anonymität über das Tor-Netzwerk leiten",
"tt_tx_url": "Basis-URL zum Anzeigen von Transaktionen in einem Block-Explorer",
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100%% = vollständig undurchsichtig, niedriger = durchsichtiger)",
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100% = vollständig undurchsichtig, niedriger = durchsichtiger)",
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
@@ -1831,4 +2031,4 @@
"your_addresses": "Ihre Adressen",
"z_address": "Z-Adresse",
"z_addresses": "Z-Adressen"
}
}
+217 -17
View File
@@ -60,6 +60,80 @@
"amount_label": "Cantidad:",
"animate_avatars": "Animar avatares",
"appearance": "APARIENCIA",
"appx_back": "Atrás",
"appx_back_up_seed_phrase_title": "Haz una copia de tu frase semilla",
"appx_birthday_block_height": "Fecha de creación (altura de bloque): %llu — guárdala también.",
"appx_blockchain_data_deleted": "Datos de la cadena de bloques eliminados (%d elementos). El daemon se está reiniciando para volver a sincronizar desde la red.",
"appx_blockchain_maintenance_in_progress": "Ya hay una operación de mantenimiento de la cadena de bloques en curso.",
"appx_blockchain_rescan_complete": "Reescaneo de la cadena de bloques completado",
"appx_bootstrap_complete_reconciling": "Arranque inicial completado; reconciliando tu cartera con los nuevos datos de la cadena.",
"appx_cancel": "Cancelar",
"appx_cleaning_up": "Limpiando...",
"appx_confirm_your_backup": "Confirma tu copia de seguridad",
"appx_copied_clipboard_autoclears": "Copiado; el portapapeles se borra automáticamente en 45 s",
"appx_copy": "Copiar",
"appx_could_not_start_restore": "No se pudo iniciar la restauración",
"appx_create_failed_prefix": "Error al crear: ",
"appx_creating_your_wallet": "Creando tu cartera…",
"appx_daemon_error": "Error del daemon",
"appx_daemon_reinstall_in_progress": "La reinstalación del daemon ya está en curso.",
"appx_disconnecting": "Desconectando...",
"appx_done": "Listo",
"appx_dragonxd_output": "Salida de dragonxd",
"appx_encrypting_wallet": "Cifrando la cartera...",
"appx_fullnode_lifecycle_unavailable_lite": "Las acciones de ciclo de vida del nodo completo no están disponibles en la versión lite",
"appx_installing_bundled_daemon": "Instalando el daemon incluido; el nodo se detendrá, se actualizará y se reiniciará...",
"appx_invalid_payment_uri_prefix": "URI de pago no válida: ",
"appx_ive_written_it_down": "Ya la anoté",
"appx_keep_node_running_and_quit": "Mantener el nodo y salir",
"appx_last_block_n": "Último bloque: %d",
"appx_last_used_wallet_not_found_prefix": "Tu último archivo de cartera usado (",
"appx_last_used_wallet_not_found_suffix": ") no se encontró; se abrió la cartera predeterminada en su lugar. Si lo moviste, restáuralo y vuelve a él desde la lista de carteras.",
"appx_low_spec_mode_disabled": "Modo de bajos recursos desactivado",
"appx_low_spec_mode_enabled": "Modo de bajos recursos activado",
"appx_miner_stopped_prefix": "Minero detenido: ",
"appx_miner_stopped_unexpectedly": "El minero se detuvo inesperadamente.",
"appx_n_min_n_sec": "%d min %d s",
"appx_n_seconds": "%d segundos",
"appx_no_bundled_daemon_to_install": "Esta versión no incluye un daemon para instalar",
"appx_no_embedded_daemon_to_install": "Esta versión no incluye un daemon integrado para instalar",
"appx_node_busy_restarting": "El nodo está ocupado reiniciándose; inténtalo de nuevo en un momento.",
"appx_node_rebuilding_witness_cache": "El nodo está reconstruyendo su caché de testigos",
"appx_not_next_word": " — esa no es la siguiente palabra",
"appx_payment_request_loaded": "Solicitud de pago cargada",
"appx_pool_miner_connected_and_hashing": "Minero de pool conectado y calculando hashes.",
"appx_progress_n_of_n": "Progreso: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Reconstruyendo testigos de notas Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Reconstruyendo la caché de testigos %.0f%% — %d bloques restantes",
"appx_rebuilding_witness_cache_pct": "Reconstruyendo la caché de testigos %.0f%%",
"appx_recovery_phrase_word_count": "La frase de recuperación debe tener 24 palabras; tú tienes %d.",
"appx_restarting_daemon_rescan_flag": "Reiniciando el daemon con la opción -rescan...",
"appx_restarting_daemon_zapwallettxes": "Reiniciando el daemon con -zapwallettxes=2 (reparación de cartera)...",
"appx_restoring_your_wallet": "Restaurando tu cartera…",
"appx_seed_backup_warning": "Estas 24 palabras son la ÚNICA forma de restaurar tu cartera. Anótalas en orden, guárdalas sin conexión y nunca las compartas. Si las pierdes, tus fondos se perderán para siempre.",
"appx_seed_not_backed_up_warning": "No has hecho una copia de tu semilla; podrías perder tus fondos. ¿Omitir de todos modos?",
"appx_sending_stop_command_to_daemon": "Enviando el comando de parada al daemon...",
"appx_setting_initial_sapling_witnesses": "Estableciendo testigos Sapling iniciales %.0f%%",
"appx_shutdown_complete": "Apagado completado",
"appx_simple_background_disabled": "Fondo simple desactivado",
"appx_simple_background_enabled": "Fondo simple activado",
"appx_skip": "Omitir",
"appx_skip_anyway": "Omitir de todos modos",
"appx_still_status_prefix": "Aún \"",
"appx_still_status_suffix": "\" — forzar el cierre ahora podría dañar los datos de la cadena.",
"appx_stop_anyway_and_quit": "Detener de todos modos y salir",
"appx_stopping_daemon_deleting_blockchain": "Deteniendo el daemon y eliminando los datos de la cadena de bloques...",
"appx_stopping_node_discards_rebuild": "Detener el nodo ahora descarta la reconstrucción en curso y la reinicia (varios minutos) la próxima vez que abras la cartera. También puedes dejar el nodo en ejecución.",
"appx_stopping_pool_miner": "Deteniendo el minero de pool...",
"appx_syncing_pct_block_n_of_n": "Sincronizando %.1f%% — Bloque %d / %d",
"appx_tap_words_in_order": "Toca las palabras en el orden correcto para confirmar que las guardaste.",
"appx_theme_effects_disabled": "Efectos de tema desactivados",
"appx_theme_effects_enabled": "Efectos de tema activados",
"appx_theme_prefix": "Tema: ",
"appx_use_settings_restart_daemon_hint": "Usa Ajustes > Reiniciar daemon para intentarlo de nuevo",
"appx_waiting_for_daemon_to_encrypt_wallet": "Esperando a que el daemon cifre la cartera...",
"appx_wallet_created_and_backed_up": "Cartera creada y respaldada.",
"appx_wallet_open_failed_prefix": "Error al abrir la cartera: ",
"auto_shield": "Auto-proteger minería",
"av_intro": "El software de minería suele marcarse como potencialmente no deseado. Sigue estos pasos para habilitar la minería en pool:",
"av_open_security": "Abrir Seguridad de Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender bloqueó el minero",
"available": "Disponible",
"backup_backing_up": "Respaldando...",
"backup_col_backup": "COPIA DE SEGURIDAD",
"backup_col_export": "EXPORTAR",
"backup_col_import": "IMPORTAR Y RESTAURAR",
"backup_create": "Crear Respaldo",
"backup_created": "Respaldo de cartera creado",
"backup_data": "RESPALDO Y DATOS",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Protegido: %.8f",
"balance_syncing_pct": "Sincronizando %.1f%%",
"balance_transparent_fmt": "Transparente: %.8f",
"baltab_market": "Mercado",
"baltab_market_price_4dp": "Mercado: $%.4f",
"baltab_market_price_8dp": "Mercado: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% del total · %d Z-addr",
"baltab_shielded": "Blindado",
"baltab_shielded_amount": "Blindado %.8f",
"baltab_t_addresses_count": "%d T-addresses",
"baltab_total_balance": "Saldo total",
"baltab_transparent": "Transparente",
"baltab_transparent_amount": "Transparente %.8f",
"ban": "Bloquear",
"banned_peers": "Nodos Bloqueados",
"block": "Bloque",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Cuadrado",
"contacts_shape_tab": "Pestaña",
"copied": "¡Copiado!",
"copied_to_clipboard": "Copiado al portapapeles",
"copy": "Copiar",
"copy_address": "Copiar Dirección Completa",
"copy_error": "Copiar Error",
@@ -423,6 +512,7 @@
"daemon_bundled": "Incluido",
"daemon_install_bundled": "Instalar integrado",
"daemon_installed": "Instalado",
"daemon_maintenance_label": "MANTENIMIENTO",
"daemon_none_bundled": "ninguno en esta compilación",
"daemon_not_installed": "no instalado",
"daemon_status_differ": "El binario instalado difiere de la versión incluida.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "La descarga se verifica frente al SHA-256 publicado de la versión y una firma ed25519 fijada antes de instalarla.",
"daemon_update_verifying": "Verificando…",
"daemon_update_version": "Versión:",
"daemon_updates_label": "ACTUALIZACIONES",
"daemon_version": "Daemon",
"dark": "Oscuro",
"data_stale_prefix": "Actualizado",
@@ -588,6 +679,63 @@
"general": "General",
"generating": "Generando",
"go_to_receive": "Ir a Recibir",
"grpa_current_block_paren": "(Actual: %d)",
"grpa_days_ago": "hace %lld días",
"grpa_dbg_addrman": "Seguimiento y gestión de direcciones de pares",
"grpa_dbg_alert": "Mensajes del sistema de alertas",
"grpa_dbg_bench": "Tiempos de referencia de las operaciones",
"grpa_dbg_coindb": "Operaciones de lectura/escritura de la base de datos de monedas",
"grpa_dbg_db": "Operaciones de Berkeley DB",
"grpa_dbg_estimatefee": "Algoritmo de estimación de comisiones",
"grpa_dbg_http": "Actividad del servidor RPC HTTP",
"grpa_dbg_libevent": "Biblioteca de red Libevent",
"grpa_dbg_lock": "Depuración de contención de bloqueos",
"grpa_dbg_mempool": "Actividad del pool de memoria de transacciones",
"grpa_dbg_net": "Conexiones y mensajes de red",
"grpa_dbg_paymentdisclosure": "Protocolo de divulgación de pagos",
"grpa_dbg_pow": "Actividad de minería por prueba de trabajo",
"grpa_dbg_proxy": "Conexiones de proxy SOCKS5",
"grpa_dbg_prune": "Operaciones de poda de bloques",
"grpa_dbg_rand": "Generación de números aleatorios",
"grpa_dbg_reindex": "Progreso de reindexación de la cadena de bloques",
"grpa_dbg_rpc": "Procesamiento de comandos RPC",
"grpa_dbg_selectcoins": "Selección de monedas para transacciones",
"grpa_dbg_tor": "Integración de Tor e información de circuitos",
"grpa_dbg_zmq": "Sistema de notificaciones ZeroMQ",
"grpa_dbg_zrpc": "Operaciones RPC blindadas (z-addr)",
"grpa_enter_private_key_to_import": "Introduce una clave privada para importar.",
"grpa_error_prefix": "Error: ",
"grpa_hr_ago": "hace %lld h",
"grpa_invalid_response_from_daemon": "Respuesta no válida del daemon",
"grpa_invalid_suffix": " (no válido)",
"grpa_min_ago": "hace %lld min",
"grpa_sec_ago": "hace %lld s",
"grpa_seed_demo_chat": "Chat de demostración con semilla",
"grpa_showing_first_100_of": "... mostrando los primeros 100 de %d",
"grpa_tab_about": "Acerca de",
"grpa_tab_appearance": "Apariencia",
"grpa_tab_backup_data": "Copia de seguridad y datos",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorador",
"grpa_tab_node_security": "Nodo y seguridad",
"grpa_tab_wallet": "Cartera",
"grpa_unexpected_getblockhash_result": "resultado inesperado de getblockhash",
"grpb_copy": "Copiar",
"grpb_max": "Máx.",
"grpb_new_badge_suffix": " [NUEVO]",
"grpb_preview_msg_payment_through": "¿Se realizó el pago? 🙂",
"grpb_preview_msg_sending_rest": "Enviando el resto ahora 👍",
"grpb_preview_msg_yep_confirmed": "Sí — acabo de confirmarlo ✅",
"grpb_selected_suffix": "\n(seleccionado)",
"grpb_tooltip_address_balance": "%s\nSaldo: %.8f %s%s",
"grpb_undo_clear": "Deshacer borrado",
"grpc_benchmark_inconclusive": "Prueba de rendimiento no concluyente: no se registraron muestras de tasa de hash. Comprueba la conexión con el pool e inténtalo de nuevo.",
"grpc_benchmark_takes_secs": "La prueba de rendimiento tarda ~%ds e interrumpe la minería. Haz clic de nuevo para empezar.",
"grpc_bootstrap_failed": "Error en el arranque inicial",
"grpc_bootstrap_not_initialized": "Arranque inicial no inicializado",
"grpc_hashrate_fee": "%s %s%% de comisión",
"grpc_key_not_available": "Clave no disponible para esta dirección",
"grpc_na": "N/D",
"height": "Altura",
"help": "Ayuda",
"hidden_tag": " (oculto)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Esperando a dragonxd — %s",
"sb_warming_up": "Calentando...",
"sb_witness_cache": "Reconstruyendo testigos",
"scale_effects": "ESCALA Y EFECTOS",
"screenshot_open_dir": "Abrir ubicación",
"screenshot_sweep": "Ejecutar barrido de capturas",
"screenshot_sweep_desc": "Recorre cada tema en cada pestaña y guarda una captura de pantalla de cada una en subcarpetas por pestaña dentro de la carpeta de capturas del directorio de configuración (sobrescribiendo el barrido anterior). Se ejecuta durante unos segundos.",
"screenshot_sweep_full": "Barrido completo de la interfaz",
"search_icons": "Buscar iconos...",
"search_placeholder": "Buscar...",
"sec_changing_passphrase": "Cambiando la frase de contraseña...",
"sec_changing_pin": "Cambiando el PIN...",
"sec_couldnt_lock_wallet": "No se pudo bloquear la cartera; sigue desbloqueada. Comprueba la conexión con el daemon.",
"sec_encrypted_backup_suffix": "\nCopia de seguridad cifrada: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Cifrando la cartera...",
"sec_encryption_did_not_complete": "El cifrado de la cartera no se completó; tu cartera NO está cifrada. Abre Ajustes para terminar de cifrarla.",
"sec_encryption_failed_prefix": "Error de cifrado: ",
"sec_failed_prefix": "Error: ",
"sec_failed_to_create_vault": "No se pudo crear la bóveda",
"sec_importing_keys_rescanning": "Importando claves y volviendo a escanear la cadena de bloques; la cartera se puede usar mientras tanto",
"sec_incorrect_current_pin": "PIN actual incorrecto",
"sec_incorrect_passphrase_decrypt": "Frase de contraseña incorrecta",
"sec_incorrect_passphrase_pin_setup": "Frase de contraseña incorrecta",
"sec_incorrect_pin_remove": "PIN incorrecto",
"sec_internal_error_change_pin": "Error interno",
"sec_internal_error_remove_pin": "Error interno",
"sec_mode_passphrase": " Frase de contraseña",
"sec_not_connected_to_daemon": "Sin conexión con el daemon",
"sec_not_connected_to_daemon_pin": "Sin conexión con el daemon",
"sec_passphrase_changed_successfully": "Frase de contraseña cambiada correctamente",
"sec_pin_changed_successfully": "PIN cambiado correctamente",
"sec_pin_removed": "PIN eliminado",
"sec_pin_set_successfully": "PIN configurado correctamente",
"sec_restart_daemon_for_encryption": "Reinicia el daemon para que el cifrado surta efecto.",
"sec_too_many_attempts_wait": "Demasiados intentos. Espera %.0f segundos...",
"sec_total_elapsed_fmt": "Tiempo total: %dm %02ds",
"sec_unlock_button": "Desbloquear",
"sec_unlock_failed_prefix": "Error al desbloquear: ",
"sec_unlocking_fmt": "Desbloqueando%s",
"sec_use_passphrase_instead": "Usar frase de contraseña en su lugar",
"sec_use_pin_instead": "Usar PIN en su lugar",
"sec_verifying_passphrase": "Verificando la frase de contraseña...",
"sec_verifying_pin": "Verificando el PIN...",
"sec_wallet_decrypted_all_keys_imported": "¡Cartera descifrada correctamente! Todas las claves importadas.",
"sec_wallet_encrypted_and_pin_set": "Cartera cifrada y PIN configurado",
"sec_wallet_encrypted_but_pin_vault_failed": "Cartera cifrada, pero falló la bóveda del PIN",
"sec_wallet_encrypted_restarting_daemon": "Cartera cifrada. Reiniciando el daemon...",
"sec_wallet_encrypted_successfully": "Cartera cifrada correctamente",
"sec_wallet_locked_title": "Cartera bloqueada",
"security": "SEGURIDAD",
"seed_backup_button": "Frase de recuperación",
"seed_backup_close": "Cerrar",
@@ -1316,12 +1504,12 @@
"settings": "Ajustes",
"settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.",
"settings_acrylic_level": "Nivel de acrílico:",
"settings_address_book": "Libreta de direcciones...",
"settings_address_book": "Libreta de direcciones…",
"settings_auto_detected": "Autodetectado de DRAGONX.conf",
"settings_auto_lock": "BLOQUEO AUTOMÁTICO",
"settings_auto_shield_desc": "Mover automáticamente fondos transparentes a direcciones blindadas",
"settings_auto_shield_funds": "Blindar fondos transparentes automáticamente",
"settings_backup": "Respaldo...",
"settings_backup": "Respaldo…",
"settings_block_explorer_urls": "URLs del explorador de bloques",
"settings_builtin": "Integrado",
"settings_change_passphrase": "Cambiar contraseña",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Copiar diagnósticos",
"settings_copyright": "Copyright 2024-2026 Desarrolladores de DragonX | Licencia GPLv3",
"settings_custom": "Personalizado",
"settings_data_dir": "Dir. de datos:",
"settings_data_dir": "Dir. de datos",
"settings_debug_changed": "Categorías de depuración cambiadas — reinicie el daemon para aplicar",
"settings_debug_restart_note": "Los cambios surten efecto después de reiniciar el daemon.",
"settings_debug_select": "Seleccione categorías para habilitar el registro de depuración del daemon (flags -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Primero cifre la billetera para habilitar el PIN",
"settings_encrypt_wallet": "Cifrar billetera",
"settings_explorer_hint": "Las URLs deben incluir una barra final. Se añadirá el txid/dirección.",
"settings_export_all": "Exportar todo...",
"settings_export_csv": "Exportar CSV...",
"settings_export_key": "Exportar clave...",
"settings_export_all": "Exportar todo…",
"settings_export_csv": "Exportar CSV…",
"settings_export_key": "Exportar clave…",
"settings_gradient_bg": "Fondo degradado",
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
"settings_idle_after": "después de",
"settings_import_key": "Importar Clave Privada...",
"settings_import_viewkey": "Importar clave de visualización...",
"settings_import_key": "Importar Clave Privada…",
"settings_import_viewkey": "Importar clave de visualización…",
"settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
"settings_lock_now": "Bloquear ahora",
"settings_locked": "Bloqueado",
"settings_merge_to_address": "Fusionar a dirección...",
"settings_merge_to_address": "Fusionar a dirección…",
"settings_noise_opacity": "Opacidad de ruido:",
"settings_not_connected": "No conectado al daemon",
"settings_not_encrypted": "Sin cifrar",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Configuración recargada desde el disco",
"settings_remove_encryption": "Quitar cifrado",
"settings_remove_pin": "Quitar PIN",
"settings_request_payment": "Solicitar pago...",
"settings_request_payment": "Solicitar pago…",
"settings_rescan_desc": "Reescanear la cadena de bloques en busca de transacciones faltantes",
"settings_restart_daemon": "Reiniciar daemon",
"settings_rpc_connection": "Conexión RPC",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Guardar historial de transacciones blindadas localmente",
"settings_saved": "Configuración guardada",
"settings_set_pin": "Establecer PIN",
"settings_shield_mining": "Blindar minería...",
"settings_shield_mining": "Blindar minería…",
"settings_solid_colors_desc": "Usar colores sólidos en lugar de efectos de desenfoque (accesibilidad)",
"settings_theme_refreshed": "Lista de temas actualizada",
"settings_tor_desc": "Enrutar todas las conexiones a través de Tor para mayor privacidad",
"settings_unlocked": "Desbloqueado",
"settings_use_tor_network": "Usar Tor para conexiones de red",
"settings_validate_address": "Validar dirección...",
"settings_validate_address": "Validar dirección…",
"settings_visual_effects": "Efectos visuales",
"settings_wallet_file_size": "Tamaño del archivo de billetera: %s",
"settings_wallet_info": "Información de billetera",
"settings_wallet_location": "Ubicación de billetera: %s",
"settings_wallet_maintenance": "Mantenimiento de billetera",
"settings_wallet_not_found": "Archivo de billetera no encontrado",
"settings_wallet_size_label": "Tamaño de billetera:",
"settings_wallet_size_label": "Tamaño de billetera",
"settings_ztx_cleared": "Historial de transacciones Z borrado",
"settings_ztx_not_found": "No se encontró archivo de historial",
"setup_wizard": "Asistente de Configuración",
@@ -1455,6 +1643,17 @@
"sweep_to": "Barrido a:",
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
"sweep_tx": "Transacción:",
"swin_connection_failed": "Error de conexión: ",
"swin_connection_successful": "¡Conexión correcta!\nVersión de dragonxd: ",
"swin_invalid_suffix": " (no válido)",
"swin_no_history_file_found": "No se encontró ningún archivo de historial",
"swin_rescan_failed": "Error en el reescaneo: ",
"swin_rescan_started_from_block": "Reescaneo iniciado desde el bloque ",
"swin_rescan_to": " hasta ",
"swin_rpc_client_not_initialized": "Cliente RPC no inicializado",
"swin_settings_saved": "Ajustes guardados",
"swin_theme_list_refreshed": "Lista de temas actualizada",
"swin_ztx_history_cleared": "Historial de transacciones Z borrado",
"switch_corrupt_body": "Esta cartera parece dañada: el nodo no pudo abrirla. Restáurala desde una copia de seguridad, vuelve a crearla o intenta repararla.",
"switch_corrupt_repair": "Intentar reparar (salvage)",
"switch_progress_background": "Continuar en segundo plano",
@@ -1494,6 +1693,7 @@
"to_upper": "PARA",
"tools": "HERRAMIENTAS",
"tools_actions": "Herramientas y Acciones...",
"tools_actions_hdr": "HERRAMIENTAS Y ACCIONES",
"total": "Total",
"total_balance_label": "Saldo Total",
"transaction_id": "ID DE TRANSACCIÓN",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad",
"tt_backup": "Crear una copia de seguridad de su wallet.dat",
"tt_block_explorer": "Abrir el explorador de bloques DragonX en su navegador",
"tt_blur": "Cantidad de desenfoque (0%% = apagado, 100%% = máximo)",
"tt_blur": "Cantidad de desenfoque (0% = apagado, 100% = máximo)",
"tt_change_pass": "Cambiar la contraseña de cifrado de la billetera",
"tt_change_pin": "Cambiar su PIN de desbloqueo",
"tt_chat_bubble_accent": "Color de acento para tus burbujas de mensaje salientes (o sigue el tema actual)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
"tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)",
"tt_noise": "Intensidad de textura granulada (0%% = apagado, 100%% = máximo)",
"tt_noise": "Intensidad de textura granulada (0% = apagado, 100% = máximo)",
"tt_open_app_dir": "Abrir la carpeta de ObsidianDragon (configuración, temas, registros) en el explorador de archivos",
"tt_open_data_dir": "Abre en el gestor de archivos la carpeta con los datos de tu cartera y de la blockchain",
"tt_open_dir": "Clic para abrir en explorador de archivos",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Atajo: Ctrl+Izquierda/Derecha para cambiar temas",
"tt_tor": "Enrutar conexiones del daemon a través de la red Tor para anonimato",
"tt_tx_url": "URL base para ver transacciones en un explorador de bloques",
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100%% = totalmente opaco, menor = más transparente)",
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100% = totalmente opaco, menor = más transparente)",
"tt_validate": "Comprobar si una dirección DragonX es válida",
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
@@ -1831,4 +2031,4 @@
"your_addresses": "Sus Direcciones",
"z_address": "Dirección Z",
"z_addresses": "Direcciones Z"
}
}
+217 -17
View File
@@ -60,6 +60,80 @@
"amount_label": "Montant :",
"animate_avatars": "Animer les avatars",
"appearance": "APPARENCE",
"appx_back": "Retour",
"appx_back_up_seed_phrase_title": "Sauvegardez votre phrase de récupération",
"appx_birthday_block_height": "Naissance (hauteur de bloc) : %llu — sauvegardez-la également.",
"appx_blockchain_data_deleted": "Données de la blockchain supprimées (%d éléments). Le démon redémarre pour se resynchroniser depuis le réseau.",
"appx_blockchain_maintenance_in_progress": "Une opération de maintenance de la blockchain est déjà en cours.",
"appx_blockchain_rescan_complete": "Nouvelle analyse de la blockchain terminée",
"appx_bootstrap_complete_reconciling": "Amorçage terminé — réconciliation de votre portefeuille avec les nouvelles données de la chaîne.",
"appx_cancel": "Annuler",
"appx_cleaning_up": "Nettoyage...",
"appx_confirm_your_backup": "Confirmez votre sauvegarde",
"appx_copied_clipboard_autoclears": "Copié — le presse-papiers s'efface automatiquement dans 45 s",
"appx_copy": "Copier",
"appx_could_not_start_restore": "Impossible de démarrer la restauration",
"appx_create_failed_prefix": "Échec de la création : ",
"appx_creating_your_wallet": "Création de votre portefeuille…",
"appx_daemon_error": "Erreur du démon",
"appx_daemon_reinstall_in_progress": "La réinstallation du démon est déjà en cours.",
"appx_disconnecting": "Déconnexion...",
"appx_done": "Terminé",
"appx_dragonxd_output": "Sortie de dragonxd",
"appx_encrypting_wallet": "Chiffrement du portefeuille...",
"appx_fullnode_lifecycle_unavailable_lite": "Les actions de cycle de vie du nœud complet ne sont pas disponibles dans la version allégée",
"appx_installing_bundled_daemon": "Installation du démon intégré — le nœud va s'arrêter, se mettre à jour, puis redémarrer...",
"appx_invalid_payment_uri_prefix": "URI de paiement invalide : ",
"appx_ive_written_it_down": "Je les ai notés",
"appx_keep_node_running_and_quit": "Laisser le nœud actif et quitter",
"appx_last_block_n": "Dernier bloc : %d",
"appx_last_used_wallet_not_found_prefix": "Votre dernier fichier de portefeuille utilisé (",
"appx_last_used_wallet_not_found_suffix": ") est introuvable — le portefeuille par défaut a été ouvert à la place. Si vous l'avez déplacé, restaurez-le et revenez à celui-ci depuis la liste des portefeuilles.",
"appx_low_spec_mode_disabled": "Mode faibles ressources désactivé",
"appx_low_spec_mode_enabled": "Mode faibles ressources activé",
"appx_miner_stopped_prefix": "Mineur arrêté : ",
"appx_miner_stopped_unexpectedly": "Le mineur s'est arrêté de manière inattendue.",
"appx_n_min_n_sec": "%d min %d s",
"appx_n_seconds": "%d secondes",
"appx_no_bundled_daemon_to_install": "Cette version ne contient aucun démon intégré à installer",
"appx_no_embedded_daemon_to_install": "Cette version ne contient aucun démon embarqué à installer",
"appx_node_busy_restarting": "Le nœud est occupé à redémarrer — réessayez dans un instant.",
"appx_node_rebuilding_witness_cache": "Le nœud reconstruit son cache de témoins",
"appx_not_next_word": " — ce n'est pas le mot suivant",
"appx_payment_request_loaded": "Demande de paiement chargée",
"appx_pool_miner_connected_and_hashing": "Mineur de pool connecté et en cours de hachage.",
"appx_progress_n_of_n": "Progression : %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Reconstruction des témoins de notes Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Reconstruction du cache de témoins %.0f%% — %d blocs restants",
"appx_rebuilding_witness_cache_pct": "Reconstruction du cache de témoins %.0f%%",
"appx_recovery_phrase_word_count": "La phrase de récupération doit comporter 24 mots — vous en avez %d.",
"appx_restarting_daemon_rescan_flag": "Redémarrage du démon avec l'option -rescan...",
"appx_restarting_daemon_zapwallettxes": "Redémarrage du démon avec -zapwallettxes=2 (réparation du portefeuille)...",
"appx_restoring_your_wallet": "Restauration de votre portefeuille…",
"appx_seed_backup_warning": "Ces 24 mots sont le SEUL moyen de restaurer votre portefeuille. Notez-les dans l'ordre, conservez-les hors ligne et ne les partagez jamais. Si vous les perdez, vos fonds seront perdus à jamais.",
"appx_seed_not_backed_up_warning": "Vous n'avez pas sauvegardé votre phrase de récupération — des fonds pourraient être perdus. Ignorer quand même ?",
"appx_sending_stop_command_to_daemon": "Envoi de la commande d'arrêt au démon...",
"appx_setting_initial_sapling_witnesses": "Définition des témoins Sapling initiaux %.0f%%",
"appx_shutdown_complete": "Arrêt terminé",
"appx_simple_background_disabled": "Arrière-plan simple désactivé",
"appx_simple_background_enabled": "Arrière-plan simple activé",
"appx_skip": "Ignorer",
"appx_skip_anyway": "Ignorer quand même",
"appx_still_status_prefix": "Toujours « ",
"appx_still_status_suffix": " » — forcer la fermeture maintenant peut corrompre les données de la chaîne.",
"appx_stop_anyway_and_quit": "Arrêter quand même et quitter",
"appx_stopping_daemon_deleting_blockchain": "Arrêt du démon et suppression des données de la blockchain...",
"appx_stopping_node_discards_rebuild": "Arrêter le nœud maintenant abandonne la reconstruction en cours et la relance (plusieurs minutes) à la prochaine ouverture du portefeuille. Vous pouvez plutôt laisser le nœud en marche.",
"appx_stopping_pool_miner": "Arrêt du mineur de pool...",
"appx_syncing_pct_block_n_of_n": "Synchronisation %.1f%% — Bloc %d / %d",
"appx_tap_words_in_order": "Touchez les mots dans le bon ordre pour confirmer que vous les avez enregistrés.",
"appx_theme_effects_disabled": "Effets de thème désactivés",
"appx_theme_effects_enabled": "Effets de thème activés",
"appx_theme_prefix": "Thème : ",
"appx_use_settings_restart_daemon_hint": "Utilisez Paramètres > Redémarrer le démon pour réessayer",
"appx_waiting_for_daemon_to_encrypt_wallet": "En attente du chiffrement du portefeuille par le démon...",
"appx_wallet_created_and_backed_up": "Portefeuille créé et sauvegardé.",
"appx_wallet_open_failed_prefix": "Échec de l'ouverture du portefeuille : ",
"auto_shield": "Auto-blindage du minage",
"av_intro": "Les logiciels de minage sont souvent signalés comme potentiellement indésirables. Suivez ces étapes pour activer le minage en pool :",
"av_open_security": "Ouvrir Sécurité Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender a bloqué le mineur",
"available": "Disponible",
"backup_backing_up": "Sauvegarde en cours...",
"backup_col_backup": "SAUVEGARDE",
"backup_col_export": "EXPORTER",
"backup_col_import": "IMPORTER ET RESTAURER",
"backup_create": "Créer une sauvegarde",
"backup_created": "Sauvegarde du portefeuille créée",
"backup_data": "SAUVEGARDE & DONNÉES",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Blindé : %.8f",
"balance_syncing_pct": "Synchronisation %.1f%%",
"balance_transparent_fmt": "Transparent : %.8f",
"baltab_market": "Marché",
"baltab_market_price_4dp": "Marché : %.4f $",
"baltab_market_price_8dp": "Marché : %.8f $",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% du total · %d Z-addr",
"baltab_shielded": "Protégé",
"baltab_shielded_amount": "Protégé %.8f",
"baltab_t_addresses_count": "%d T-addresses",
"baltab_total_balance": "Solde total",
"baltab_transparent": "Transparent",
"baltab_transparent_amount": "Transparent %.8f",
"ban": "Bannir",
"banned_peers": "Pairs bannis",
"block": "Bloc",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Carré",
"contacts_shape_tab": "Onglet",
"copied": "Copié !",
"copied_to_clipboard": "Copié dans le presse-papiers",
"copy": "Copier",
"copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur",
@@ -423,6 +512,7 @@
"daemon_bundled": "Intégré",
"daemon_install_bundled": "Installer la version intégrée",
"daemon_installed": "Installé",
"daemon_maintenance_label": "MAINTENANCE",
"daemon_none_bundled": "aucun dans cette version",
"daemon_not_installed": "non installé",
"daemon_status_differ": "Le binaire installé diffère de la version intégrée.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "Le téléchargement est vérifié par rapport au SHA-256 publié de la version et à une signature ed25519 épinglée avant l'installation.",
"daemon_update_verifying": "Vérification…",
"daemon_update_version": "Version :",
"daemon_updates_label": "MISES À JOUR",
"daemon_version": "Daemon",
"dark": "Sombre",
"data_stale_prefix": "Mis à jour",
@@ -588,6 +679,63 @@
"general": "Général",
"generating": "Génération",
"go_to_receive": "Aller à Recevoir",
"grpa_current_block_paren": "(Actuel : %d)",
"grpa_days_ago": "il y a %lld jours",
"grpa_dbg_addrman": "Suivi et gestion des adresses des pairs",
"grpa_dbg_alert": "Messages du système d'alerte",
"grpa_dbg_bench": "Chronométrage des performances des opérations",
"grpa_dbg_coindb": "Opérations de lecture/écriture de la base de données des pièces",
"grpa_dbg_db": "Opérations Berkeley DB",
"grpa_dbg_estimatefee": "Algorithme d'estimation des frais",
"grpa_dbg_http": "Activité du serveur RPC HTTP",
"grpa_dbg_libevent": "Bibliothèque réseau Libevent",
"grpa_dbg_lock": "Débogage de la contention des verrous",
"grpa_dbg_mempool": "Activité du pool mémoire des transactions",
"grpa_dbg_net": "Connexions et messages réseau",
"grpa_dbg_paymentdisclosure": "Protocole de divulgation de paiement",
"grpa_dbg_pow": "Activité de minage par preuve de travail",
"grpa_dbg_proxy": "Connexions proxy SOCKS5",
"grpa_dbg_prune": "Opérations d'élagage des blocs",
"grpa_dbg_rand": "Génération de nombres aléatoires",
"grpa_dbg_reindex": "Progression de la réindexation de la blockchain",
"grpa_dbg_rpc": "Traitement des commandes RPC",
"grpa_dbg_selectcoins": "Sélection des pièces pour les transactions",
"grpa_dbg_tor": "Intégration de Tor et informations de circuit",
"grpa_dbg_zmq": "Système de notification ZeroMQ",
"grpa_dbg_zrpc": "Opérations RPC protégées (z-addr)",
"grpa_enter_private_key_to_import": "Saisissez une clé privée à importer.",
"grpa_error_prefix": "Erreur : ",
"grpa_hr_ago": "il y a %lld h",
"grpa_invalid_response_from_daemon": "Réponse invalide du démon",
"grpa_invalid_suffix": " (invalide)",
"grpa_min_ago": "il y a %lld min",
"grpa_sec_ago": "il y a %lld s",
"grpa_seed_demo_chat": "Chat de démonstration seed",
"grpa_showing_first_100_of": "... affichage des 100 premiers sur %d",
"grpa_tab_about": "À propos",
"grpa_tab_appearance": "Apparence",
"grpa_tab_backup_data": "Sauvegarde et données",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorateur",
"grpa_tab_node_security": "Nœud et sécurité",
"grpa_tab_wallet": "Portefeuille",
"grpa_unexpected_getblockhash_result": "résultat getblockhash inattendu",
"grpb_copy": "Copier",
"grpb_max": "Max",
"grpb_new_badge_suffix": " [NOUVEAU]",
"grpb_preview_msg_payment_through": "Le paiement est-il passé ? 🙂",
"grpb_preview_msg_sending_rest": "J'envoie le reste maintenant 👍",
"grpb_preview_msg_yep_confirmed": "Oui — je viens de confirmer ✅",
"grpb_selected_suffix": "\n(sélectionné)",
"grpb_tooltip_address_balance": "%s\nSolde : %.8f %s%s",
"grpb_undo_clear": "Annuler l'effacement",
"grpc_benchmark_inconclusive": "Benchmark non concluant : aucun échantillon de taux de hachage n'a été enregistré. Vérifiez la connexion au pool et réessayez.",
"grpc_benchmark_takes_secs": "Le benchmark prend environ %d s et interrompt le minage. Cliquez de nouveau pour démarrer.",
"grpc_bootstrap_failed": "Échec de l'amorçage",
"grpc_bootstrap_not_initialized": "Amorçage non initialisé",
"grpc_hashrate_fee": "%s %s%% de frais",
"grpc_key_not_available": "Clé non disponible pour cette adresse",
"grpc_na": "N/D",
"height": "Hauteur",
"help": "Aide",
"hidden_tag": " (masqué)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "En attente de dragonxd — %s",
"sb_warming_up": "Démarrage...",
"sb_witness_cache": "Reconstruction des témoins",
"scale_effects": "ÉCHELLE ET EFFETS",
"screenshot_open_dir": "Ouvrir l'emplacement",
"screenshot_sweep": "Lancer la capture d'écran",
"screenshot_sweep_desc": "Parcourt chaque thème sur chaque onglet et enregistre une capture d'écran de chacun dans des sous-dossiers par onglet, sous le dossier screenshots du répertoire de configuration (en écrasant le balayage précédent). Dure quelques secondes.",
"screenshot_sweep_full": "Balayage complet de l'interface",
"search_icons": "Rechercher des icônes...",
"search_placeholder": "Rechercher...",
"sec_changing_passphrase": "Modification de la phrase secrète...",
"sec_changing_pin": "Modification du PIN...",
"sec_couldnt_lock_wallet": "Impossible de verrouiller le portefeuille — il reste déverrouillé. Vérifiez la connexion au démon.",
"sec_encrypted_backup_suffix": "\nSauvegarde chiffrée : wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Chiffrement du portefeuille...",
"sec_encryption_did_not_complete": "Le chiffrement du portefeuille ne s'est pas terminé — votre portefeuille N'est PAS chiffré. Ouvrez les Paramètres pour terminer le chiffrement.",
"sec_encryption_failed_prefix": "Échec du chiffrement : ",
"sec_failed_prefix": "Échec : ",
"sec_failed_to_create_vault": "Échec de la création du coffre",
"sec_importing_keys_rescanning": "Importation des clés et nouvelle analyse de la blockchain — le portefeuille reste utilisable pendant l'opération",
"sec_incorrect_current_pin": "PIN actuel incorrect",
"sec_incorrect_passphrase_decrypt": "Phrase secrète incorrecte",
"sec_incorrect_passphrase_pin_setup": "Phrase secrète incorrecte",
"sec_incorrect_pin_remove": "PIN incorrect",
"sec_internal_error_change_pin": "Erreur interne",
"sec_internal_error_remove_pin": "Erreur interne",
"sec_mode_passphrase": " Phrase secrète",
"sec_not_connected_to_daemon": "Non connecté au démon",
"sec_not_connected_to_daemon_pin": "Non connecté au démon",
"sec_passphrase_changed_successfully": "Phrase secrète modifiée avec succès",
"sec_pin_changed_successfully": "PIN modifié avec succès",
"sec_pin_removed": "PIN supprimé",
"sec_pin_set_successfully": "PIN défini avec succès",
"sec_restart_daemon_for_encryption": "Veuillez redémarrer votre démon pour que le chiffrement prenne effet.",
"sec_too_many_attempts_wait": "Trop de tentatives. Patientez %.0f secondes...",
"sec_total_elapsed_fmt": "Temps total écoulé : %dm %02ds",
"sec_unlock_button": "Déverrouiller",
"sec_unlock_failed_prefix": "Échec du déverrouillage : ",
"sec_unlocking_fmt": "Déverrouillage%s",
"sec_use_passphrase_instead": "Utiliser une phrase secrète",
"sec_use_pin_instead": "Utiliser un PIN",
"sec_verifying_passphrase": "Vérification de la phrase secrète...",
"sec_verifying_pin": "Vérification du PIN...",
"sec_wallet_decrypted_all_keys_imported": "Portefeuille déchiffré avec succès ! Toutes les clés ont été importées.",
"sec_wallet_encrypted_and_pin_set": "Portefeuille chiffré et PIN défini",
"sec_wallet_encrypted_but_pin_vault_failed": "Portefeuille chiffré mais échec du coffre PIN",
"sec_wallet_encrypted_restarting_daemon": "Portefeuille chiffré. Redémarrage du démon...",
"sec_wallet_encrypted_successfully": "Portefeuille chiffré avec succès",
"sec_wallet_locked_title": "Portefeuille verrouillé",
"security": "SÉCURITÉ",
"seed_backup_button": "Phrase de récupération",
"seed_backup_close": "Fermer",
@@ -1316,12 +1504,12 @@
"settings": "Paramètres",
"settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.",
"settings_acrylic_level": "Niveau acrylique :",
"settings_address_book": "Carnet d'adresses...",
"settings_address_book": "Carnet d'adresses…",
"settings_auto_detected": "Détecté automatiquement depuis DRAGONX.conf",
"settings_auto_lock": "VERROUILLAGE AUTO",
"settings_auto_shield_desc": "Déplacer automatiquement les fonds transparents vers des adresses blindées",
"settings_auto_shield_funds": "Blindage automatique des fonds transparents",
"settings_backup": "Sauvegarde...",
"settings_backup": "Sauvegarde…",
"settings_block_explorer_urls": "URLs de l'explorateur de blocs",
"settings_builtin": "Intégré",
"settings_change_passphrase": "Changer la phrase secrète",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Copier les diagnostics",
"settings_copyright": "Copyright 2024-2026 Développeurs DragonX | Licence GPLv3",
"settings_custom": "Personnalisé",
"settings_data_dir": "Rép. de données :",
"settings_data_dir": "Rép. de données ",
"settings_debug_changed": "Catégories de débogage modifiées — redémarrez le daemon pour appliquer",
"settings_debug_restart_note": "Les modifications prennent effet après le redémarrage du daemon.",
"settings_debug_select": "Sélectionnez les catégories pour activer la journalisation de débogage du daemon (flags -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Chiffrez d'abord le portefeuille pour activer le PIN",
"settings_encrypt_wallet": "Chiffrer le portefeuille",
"settings_explorer_hint": "Les URLs doivent inclure une barre oblique finale. Le txid/adresse sera ajouté.",
"settings_export_all": "Tout exporter...",
"settings_export_csv": "Exporter CSV...",
"settings_export_key": "Exporter la clé...",
"settings_export_all": "Tout exporter…",
"settings_export_csv": "Exporter CSV…",
"settings_export_key": "Exporter la clé…",
"settings_gradient_bg": "Fond dégradé",
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
"settings_idle_after": "après",
"settings_import_key": "Importer une clé privée...",
"settings_import_viewkey": "Importer la clé de visualisation...",
"settings_import_key": "Importer une clé privée…",
"settings_import_viewkey": "Importer la clé de visualisation…",
"settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
"settings_lock_now": "Verrouiller maintenant",
"settings_locked": "Verrouillé",
"settings_merge_to_address": "Fusionner vers l'adresse...",
"settings_merge_to_address": "Fusionner vers l'adresse…",
"settings_noise_opacity": "Opacité du bruit :",
"settings_not_connected": "Non connecté au démon",
"settings_not_encrypted": "Non chiffré",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Paramètres rechargés depuis le disque",
"settings_remove_encryption": "Supprimer le chiffrement",
"settings_remove_pin": "Supprimer le PIN",
"settings_request_payment": "Demander un paiement...",
"settings_request_payment": "Demander un paiement…",
"settings_rescan_desc": "Rescanner la blockchain pour les transactions manquantes",
"settings_restart_daemon": "Redémarrer le daemon",
"settings_rpc_connection": "Connexion RPC",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Enregistrer l'historique des transactions blindées localement",
"settings_saved": "Paramètres enregistrés",
"settings_set_pin": "Définir le PIN",
"settings_shield_mining": "Blindage minage...",
"settings_shield_mining": "Blindage minage…",
"settings_solid_colors_desc": "Utiliser des couleurs unies au lieu des effets de flou (accessibilité)",
"settings_theme_refreshed": "Liste des thèmes actualisée",
"settings_tor_desc": "Acheminer toutes les connexions via Tor pour une confidentialité renforcée",
"settings_unlocked": "Déverrouillé",
"settings_use_tor_network": "Utiliser Tor pour les connexions réseau",
"settings_validate_address": "Valider l'adresse...",
"settings_validate_address": "Valider l'adresse…",
"settings_visual_effects": "Effets visuels",
"settings_wallet_file_size": "Taille du fichier portefeuille : %s",
"settings_wallet_info": "Informations du portefeuille",
"settings_wallet_location": "Emplacement du portefeuille : %s",
"settings_wallet_maintenance": "Maintenance du portefeuille",
"settings_wallet_not_found": "Fichier portefeuille introuvable",
"settings_wallet_size_label": "Taille du portefeuille :",
"settings_wallet_size_label": "Taille du portefeuille ",
"settings_ztx_cleared": "Historique des transactions Z effacé",
"settings_ztx_not_found": "Aucun fichier d'historique trouvé",
"setup_wizard": "Assistant de configuration",
@@ -1455,6 +1643,17 @@
"sweep_to": "Balayé vers :",
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
"sweep_tx": "Transaction :",
"swin_connection_failed": "Échec de la connexion : ",
"swin_connection_successful": "Connexion réussie !\nVersion de dragonxd : ",
"swin_invalid_suffix": " (invalide)",
"swin_no_history_file_found": "Aucun fichier d'historique trouvé",
"swin_rescan_failed": "Échec de la nouvelle analyse : ",
"swin_rescan_started_from_block": "Nouvelle analyse démarrée à partir du bloc ",
"swin_rescan_to": " à ",
"swin_rpc_client_not_initialized": "Client RPC non initialisé",
"swin_settings_saved": "Paramètres enregistrés",
"swin_theme_list_refreshed": "Liste des thèmes actualisée",
"swin_ztx_history_cleared": "Historique des transactions Z effacé",
"switch_corrupt_body": "Ce portefeuille semble corrompu — le nœud n'a pas pu l'ouvrir. Restaurez-le depuis une sauvegarde, recréez-le ou tentez de le réparer.",
"switch_corrupt_repair": "Tenter une réparation (salvage)",
"switch_progress_background": "Continuer en arrière-plan",
@@ -1494,6 +1693,7 @@
"to_upper": "À",
"tools": "OUTILS",
"tools_actions": "Outils & Actions...",
"tools_actions_hdr": "OUTILS ET ACTIONS",
"total": "Total",
"total_balance_label": "Solde total",
"transaction_id": "ID DE TRANSACTION",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité",
"tt_backup": "Créer une sauvegarde de votre wallet.dat",
"tt_block_explorer": "Ouvrir l'explorateur de blocs DragonX dans votre navigateur",
"tt_blur": "Quantité de flou (0%% = désactivé, 100%% = maximum)",
"tt_blur": "Quantité de flou (0% = désactivé, 100% = maximum)",
"tt_change_pass": "Changer la phrase secrète de chiffrement du portefeuille",
"tt_change_pin": "Changer votre PIN de déverrouillage",
"tt_chat_bubble_accent": "Couleur d'accent de vos bulles de message sortantes (ou suivre le thème actuel)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
"tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)",
"tt_noise": "Intensité de texture grainée (0%% = désactivé, 100%% = maximum)",
"tt_noise": "Intensité de texture grainée (0% = désactivé, 100% = maximum)",
"tt_open_app_dir": "Ouvrir le dossier ObsidianDragon (paramètres, thèmes, journaux) dans le gestionnaire de fichiers",
"tt_open_data_dir": "Ouvrir le dossier contenant les données de votre portefeuille et de la blockchain dans le gestionnaire de fichiers",
"tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Raccourci : Ctrl+Gauche/Droite pour changer de thème",
"tt_tor": "Acheminer les connexions du daemon via le réseau Tor pour l'anonymat",
"tt_tx_url": "URL de base pour consulter les transactions dans un explorateur de blocs",
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100%% = entièrement opaque, plus bas = plus transparent)",
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100% = entièrement opaque, plus bas = plus transparent)",
"tt_validate": "Vérifier si une adresse DragonX est valide",
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
@@ -1831,4 +2031,4 @@
"your_addresses": "Vos adresses",
"z_address": "Adresse Z",
"z_addresses": "Adresses Z"
}
}
+215 -15
View File
@@ -60,6 +60,80 @@
"amount_label": "金額:",
"animate_avatars": "アバターをアニメーション",
"appearance": "外観",
"appx_back": "戻る",
"appx_back_up_seed_phrase_title": "シードフレーズをバックアップ",
"appx_birthday_block_height": "誕生日(ブロック高): %llu — これもバックアップしてください。",
"appx_blockchain_data_deleted": "ブロックチェーンデータを削除しました(%d件)。ネットワークから再同期するため、デーモンを再起動しています。",
"appx_blockchain_maintenance_in_progress": "ブロックチェーンのメンテナンス操作がすでに進行中です。",
"appx_blockchain_rescan_complete": "ブロックチェーンの再スキャンが完了しました",
"appx_bootstrap_complete_reconciling": "ブートストラップが完了しました。新しいチェーンデータとウォレットを照合しています。",
"appx_cancel": "キャンセル",
"appx_cleaning_up": "クリーンアップしています...",
"appx_confirm_your_backup": "バックアップを確認",
"appx_copied_clipboard_autoclears": "コピーしました — クリップボードは45秒後に自動的にクリアされます",
"appx_copy": "コピー",
"appx_could_not_start_restore": "復元を開始できませんでした",
"appx_create_failed_prefix": "作成に失敗しました: ",
"appx_creating_your_wallet": "ウォレットを作成しています…",
"appx_daemon_error": "デーモンエラー",
"appx_daemon_reinstall_in_progress": "デーモンの再インストールがすでに進行中です。",
"appx_disconnecting": "切断しています...",
"appx_done": "完了",
"appx_dragonxd_output": "dragonxd の出力",
"appx_encrypting_wallet": "ウォレットを暗号化しています...",
"appx_fullnode_lifecycle_unavailable_lite": "フルノードのライフサイクル操作はライトビルドでは利用できません",
"appx_installing_bundled_daemon": "同梱デーモンをインストールしています。ノードが停止、更新、再起動されます...",
"appx_invalid_payment_uri_prefix": "無効な支払いURI: ",
"appx_ive_written_it_down": "書き留めました",
"appx_keep_node_running_and_quit": "ノードを起動したまま終了",
"appx_last_block_n": "最新ブロック: %d",
"appx_last_used_wallet_not_found_prefix": "前回使用したウォレットファイル(",
"appx_last_used_wallet_not_found_suffix": ")が見つかりませんでした。代わりにデフォルトのウォレットを開きました。移動した場合は、元に戻してからウォレット一覧で切り替えてください。",
"appx_low_spec_mode_disabled": "低スペックモードを無効にしました",
"appx_low_spec_mode_enabled": "低スペックモードを有効にしました",
"appx_miner_stopped_prefix": "マイナーが停止しました: ",
"appx_miner_stopped_unexpectedly": "マイナーが予期せず停止しました。",
"appx_n_min_n_sec": "%d分 %d秒",
"appx_n_seconds": "%d秒",
"appx_no_bundled_daemon_to_install": "このビルドにはインストールできる同梱デーモンがありません",
"appx_no_embedded_daemon_to_install": "このビルドにはインストールできる組み込みデーモンがありません",
"appx_node_busy_restarting": "ノードが再起動中でビジー状態です。少し待ってから再試行してください。",
"appx_node_rebuilding_witness_cache": "ノードがウィットネスキャッシュを再構築しています",
"appx_not_next_word": " — 次の単語ではありません",
"appx_payment_request_loaded": "支払いリクエストを読み込みました",
"appx_pool_miner_connected_and_hashing": "プールマイナーが接続し、ハッシュ計算中です。",
"appx_progress_n_of_n": "進捗: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Saplingノートウィットネスを再構築しています…",
"appx_rebuilding_witness_cache_blocks_left": "ウィットネスキャッシュを再構築中 %.0f%% — 残り%dブロック",
"appx_rebuilding_witness_cache_pct": "ウィットネスキャッシュを再構築中 %.0f%%",
"appx_recovery_phrase_word_count": "リカバリーフレーズは24単語である必要があります。現在は%d単語です。",
"appx_restarting_daemon_rescan_flag": "-rescanフラグ付きでデーモンを再起動しています...",
"appx_restarting_daemon_zapwallettxes": "-zapwallettxes=2(ウォレット修復)付きでデーモンを再起動しています...",
"appx_restoring_your_wallet": "ウォレットを復元しています…",
"appx_seed_backup_warning": "この24個の単語は、ウォレットを復元する唯一の手段です。順番どおりに書き留め、オフラインで保管し、決して他人に教えないでください。紛失すると、資金は永久に失われます。",
"appx_seed_not_backed_up_warning": "シードをバックアップしていません。資金を失う可能性があります。それでもスキップしますか?",
"appx_sending_stop_command_to_daemon": "デーモンに停止コマンドを送信しています...",
"appx_setting_initial_sapling_witnesses": "初期Saplingウィットネスを設定中 %.0f%%",
"appx_shutdown_complete": "シャットダウンが完了しました",
"appx_simple_background_disabled": "シンプル背景を無効にしました",
"appx_simple_background_enabled": "シンプル背景を有効にしました",
"appx_skip": "スキップ",
"appx_skip_anyway": "それでもスキップ",
"appx_still_status_prefix": "まだ「",
"appx_still_status_suffix": "」です — 今強制終了するとチェーンデータが破損する可能性があります。",
"appx_stop_anyway_and_quit": "それでも停止して終了",
"appx_stopping_daemon_deleting_blockchain": "デーモンを停止し、ブロックチェーンデータを削除しています...",
"appx_stopping_node_discards_rebuild": "今ノードを停止すると、進行中の再構築が破棄され、次回ウォレットを開いたときに再度実行されます(数分かかります)。代わりにノードを起動したままにすることもできます。",
"appx_stopping_pool_miner": "プールマイナーを停止しています...",
"appx_syncing_pct_block_n_of_n": "同期中 %.1f%% — ブロック %d / %d",
"appx_tap_words_in_order": "保存したことを確認するため、単語を正しい順番でタップしてください。",
"appx_theme_effects_disabled": "テーマ効果を無効にしました",
"appx_theme_effects_enabled": "テーマ効果を有効にしました",
"appx_theme_prefix": "テーマ: ",
"appx_use_settings_restart_daemon_hint": "設定 > デーモンを再起動 から再試行してください",
"appx_waiting_for_daemon_to_encrypt_wallet": "デーモンによるウォレットの暗号化を待っています...",
"appx_wallet_created_and_backed_up": "ウォレットを作成し、バックアップしました。",
"appx_wallet_open_failed_prefix": "ウォレットを開けませんでした: ",
"auto_shield": "マイニング自動シールド",
"av_intro": "マイニングソフトウェアは、望ましくない可能性があるものとしてフラグが立てられることがよくあります。プールマイニングを有効にするには、次の手順に従ってください。",
"av_open_security": "Windows セキュリティを開く",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender がマイナーをブロックしました",
"available": "利用可能",
"backup_backing_up": "バックアップ中...",
"backup_col_backup": "バックアップ",
"backup_col_export": "エクスポート",
"backup_col_import": "インポートと復元",
"backup_create": "バックアップを作成",
"backup_created": "ウォレットのバックアップを作成しました",
"backup_data": "バックアップとデータ",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "シールド: %.8f",
"balance_syncing_pct": "同期中 %.1f%%",
"balance_transparent_fmt": "透明: %.8f",
"baltab_market": "市場",
"baltab_market_price_4dp": "市場価格: $%.4f",
"baltab_market_price_8dp": "市場価格: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "全体の%.0f%% · %d Z-addr",
"baltab_shielded": "シールド",
"baltab_shielded_amount": "シールド %.8f",
"baltab_t_addresses_count": "%d 個のT-address",
"baltab_total_balance": "合計残高",
"baltab_transparent": "透明",
"baltab_transparent_amount": "透明 %.8f",
"ban": "ブロック",
"banned_peers": "ブロック済みピア",
"block": "ブロック",
@@ -411,6 +499,7 @@
"contacts_shape_square": "四角",
"contacts_shape_tab": "左タブ",
"copied": "コピーしました!",
"copied_to_clipboard": "クリップボードにコピーしました",
"copy": "コピー",
"copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー",
@@ -423,6 +512,7 @@
"daemon_bundled": "バンドル版",
"daemon_install_bundled": "バンドル版をインストール",
"daemon_installed": "インストール済み",
"daemon_maintenance_label": "メンテナンス",
"daemon_none_bundled": "このビルドにはなし",
"daemon_not_installed": "未インストール",
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
"daemon_update_verifying": "検証中…",
"daemon_update_version": "バージョン:",
"daemon_updates_label": "アップデート",
"daemon_version": "デーモン",
"dark": "ダーク",
"data_stale_prefix": "更新",
@@ -588,6 +679,63 @@
"general": "一般",
"generating": "生成中",
"go_to_receive": "受信へ移動",
"grpa_current_block_paren": "(現在: %d)",
"grpa_days_ago": "%lld日前",
"grpa_dbg_addrman": "ピアアドレスの追跡と管理",
"grpa_dbg_alert": "アラートシステムのメッセージ",
"grpa_dbg_bench": "操作のベンチマーク計測",
"grpa_dbg_coindb": "コインデータベースの読み書き操作",
"grpa_dbg_db": "Berkeley DB の操作",
"grpa_dbg_estimatefee": "手数料見積もりアルゴリズム",
"grpa_dbg_http": "HTTP RPCサーバーの動作",
"grpa_dbg_libevent": "Libevent ネットワークライブラリ",
"grpa_dbg_lock": "ロック競合のデバッグ",
"grpa_dbg_mempool": "トランザクションメモリプールの動作",
"grpa_dbg_net": "ネットワーク接続とメッセージ",
"grpa_dbg_paymentdisclosure": "支払い開示プロトコル",
"grpa_dbg_pow": "プルーフオブワークのマイニング動作",
"grpa_dbg_proxy": "SOCKS5 プロキシ接続",
"grpa_dbg_prune": "ブロックのプルーニング操作",
"grpa_dbg_rand": "乱数生成",
"grpa_dbg_reindex": "ブロックチェーンの再インデックス進捗",
"grpa_dbg_rpc": "RPCコマンドの処理",
"grpa_dbg_selectcoins": "トランザクション用のコイン選択",
"grpa_dbg_tor": "Tor統合とサーキット情報",
"grpa_dbg_zmq": "ZeroMQ 通知システム",
"grpa_dbg_zrpc": "シールド(z-addr)RPC操作",
"grpa_enter_private_key_to_import": "インポートする秘密鍵を入力してください。",
"grpa_error_prefix": "エラー: ",
"grpa_hr_ago": "%lld時間前",
"grpa_invalid_response_from_daemon": "デーモンからの応答が無効です",
"grpa_invalid_suffix": "(無効)",
"grpa_min_ago": "%lld分前",
"grpa_sec_ago": "%lld秒前",
"grpa_seed_demo_chat": "デモチャットのシード",
"grpa_showing_first_100_of": "... %d件中の最初の100件を表示",
"grpa_tab_about": "情報",
"grpa_tab_appearance": "外観",
"grpa_tab_backup_data": "バックアップとデータ",
"grpa_tab_chat": "チャット",
"grpa_tab_explorer": "エクスプローラー",
"grpa_tab_node_security": "ノードとセキュリティ",
"grpa_tab_wallet": "ウォレット",
"grpa_unexpected_getblockhash_result": "予期しない getblockhash の結果",
"grpb_copy": "コピー",
"grpb_max": "最大",
"grpb_new_badge_suffix": " [新規]",
"grpb_preview_msg_payment_through": "支払いは完了しましたか? 🙂",
"grpb_preview_msg_sending_rest": "残りを今送金します 👍",
"grpb_preview_msg_yep_confirmed": "はい — たった今確認できました ✅",
"grpb_selected_suffix": "\n(選択済み)",
"grpb_tooltip_address_balance": "%s\n残高: %.8f %s%s",
"grpb_undo_clear": "消去を元に戻す",
"grpc_benchmark_inconclusive": "ベンチマークの結果が不明確です: ハッシュレートのサンプルが記録されませんでした。プール接続を確認して再試行してください。",
"grpc_benchmark_takes_secs": "ベンチマークには約%d秒かかり、マイニングが中断されます。開始するにはもう一度クリックしてください。",
"grpc_bootstrap_failed": "ブートストラップに失敗しました",
"grpc_bootstrap_not_initialized": "ブートストラップが初期化されていません",
"grpc_hashrate_fee": "%s 手数料 %s%%",
"grpc_key_not_available": "このアドレスの鍵は利用できません",
"grpc_na": "該当なし",
"height": "高さ",
"help": "ヘルプ",
"hidden_tag": " (非表示)",
@@ -1225,12 +1373,52 @@
"sb_waiting_daemon_err": "dragonxd を待機中 — %s",
"sb_warming_up": "ウォームアップ中...",
"sb_witness_cache": "ウィットネスを再構築中",
"scale_effects": "スケールとエフェクト",
"screenshot_open_dir": "場所を開く",
"screenshot_sweep": "スクリーンショットスイープを実行",
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
"screenshot_sweep_full": "UI全体スイープ",
"search_icons": "アイコンを検索...",
"search_placeholder": "検索...",
"sec_changing_passphrase": "パスフレーズを変更しています...",
"sec_changing_pin": "PINを変更しています...",
"sec_couldnt_lock_wallet": "ウォレットをロックできませんでした。まだロック解除された状態です。デーモンの接続を確認してください。",
"sec_encrypted_backup_suffix": "\n暗号化バックアップ: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "ウォレットを暗号化しています...",
"sec_encryption_did_not_complete": "ウォレットの暗号化が完了しませんでした。ウォレットは暗号化されていません。設定を開いて暗号化を完了してください。",
"sec_encryption_failed_prefix": "暗号化に失敗しました: ",
"sec_failed_prefix": "失敗しました: ",
"sec_failed_to_create_vault": "ボールトの作成に失敗しました",
"sec_importing_keys_rescanning": "鍵をインポートし、ブロックチェーンを再スキャンしています。処理中もウォレットは使用できます",
"sec_incorrect_current_pin": "現在のPINが正しくありません",
"sec_incorrect_passphrase_decrypt": "パスフレーズが正しくありません",
"sec_incorrect_passphrase_pin_setup": "パスフレーズが正しくありません",
"sec_incorrect_pin_remove": "PINが正しくありません",
"sec_internal_error_change_pin": "内部エラー",
"sec_internal_error_remove_pin": "内部エラー",
"sec_mode_passphrase": " パスフレーズ",
"sec_not_connected_to_daemon": "デーモンに接続されていません",
"sec_not_connected_to_daemon_pin": "デーモンに接続されていません",
"sec_passphrase_changed_successfully": "パスフレーズを変更しました",
"sec_pin_changed_successfully": "PINを変更しました",
"sec_pin_removed": "PINを削除しました",
"sec_pin_set_successfully": "PINを設定しました",
"sec_restart_daemon_for_encryption": "暗号化を有効にするには、デーモンを再起動してください。",
"sec_too_many_attempts_wait": "試行回数が多すぎます。%.0f秒お待ちください...",
"sec_total_elapsed_fmt": "合計経過時間: %d分 %02d秒",
"sec_unlock_button": "ロック解除",
"sec_unlock_failed_prefix": "ロック解除に失敗しました: ",
"sec_unlocking_fmt": "ロック解除中%s",
"sec_use_passphrase_instead": "代わりにパスフレーズを使用",
"sec_use_pin_instead": "代わりにPINを使用",
"sec_verifying_passphrase": "パスフレーズを確認しています...",
"sec_verifying_pin": "PINを確認しています...",
"sec_wallet_decrypted_all_keys_imported": "ウォレットの復号に成功しました。すべての鍵をインポートしました。",
"sec_wallet_encrypted_and_pin_set": "ウォレットを暗号化し、PINを設定しました",
"sec_wallet_encrypted_but_pin_vault_failed": "ウォレットは暗号化されましたが、PINボールトの作成に失敗しました",
"sec_wallet_encrypted_restarting_daemon": "ウォレットを暗号化しました。デーモンを再起動しています...",
"sec_wallet_encrypted_successfully": "ウォレットを暗号化しました",
"sec_wallet_locked_title": "ウォレットがロックされています",
"security": "セキュリティ",
"seed_backup_button": "シードフレーズ",
"seed_backup_close": "閉じる",
@@ -1313,12 +1501,12 @@
"settings": "設定",
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
"settings_acrylic_level": "アクリルレベル:",
"settings_address_book": "アドレス帳...",
"settings_address_book": "アドレス帳…",
"settings_auto_detected": "DRAGONX.conf から自動検出",
"settings_auto_lock": "オートロック",
"settings_auto_shield_desc": "透明資金を自動的にシールドアドレスに移動",
"settings_auto_shield_funds": "透明資金を自動シールド",
"settings_backup": "バックアップ...",
"settings_backup": "バックアップ…",
"settings_block_explorer_urls": "ブロックエクスプローラーURL",
"settings_builtin": "内蔵",
"settings_change_passphrase": "パスフレーズを変更",
@@ -1340,18 +1528,18 @@
"settings_encrypt_first_pin": "PIN を有効にするには、まずウォレットを暗号化してください",
"settings_encrypt_wallet": "ウォレットを暗号化",
"settings_explorer_hint": "URLには末尾のスラッシュを含めてください。txid/アドレスが追加されます。",
"settings_export_all": "すべてエクスポート...",
"settings_export_csv": "CSV エクスポート...",
"settings_export_key": "鍵をエクスポート...",
"settings_export_all": "すべてエクスポート…",
"settings_export_csv": "CSV エクスポート…",
"settings_export_key": "鍵をエクスポート…",
"settings_gradient_bg": "グラデーション背景",
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
"settings_idle_after": "経過後",
"settings_import_key": "秘密鍵をインポート...",
"settings_import_viewkey": "閲覧鍵をインポート...",
"settings_import_key": "秘密鍵をインポート…",
"settings_import_viewkey": "閲覧鍵をインポート…",
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
"settings_lock_now": "今すぐロック",
"settings_locked": "ロック済み",
"settings_merge_to_address": "アドレスにマージ...",
"settings_merge_to_address": "アドレスにマージ…",
"settings_noise_opacity": "ノイズ不透明度:",
"settings_not_connected": "デーモンに接続されていません",
"settings_not_encrypted": "暗号化されていません",
@@ -1367,7 +1555,7 @@
"settings_reloaded": "ディスクから設定を再読み込みしました",
"settings_remove_encryption": "暗号化を解除",
"settings_remove_pin": "PIN を削除",
"settings_request_payment": "支払い請求...",
"settings_request_payment": "支払い請求…",
"settings_rescan_desc": "欠落したトランザクションのためにブロックチェーンを再スキャン",
"settings_restart_daemon": "デーモンを再起動",
"settings_rpc_connection": "RPC 接続",
@@ -1378,13 +1566,13 @@
"settings_save_shielded_local": "シールドトランザクション履歴をローカルに保存",
"settings_saved": "設定を保存しました",
"settings_set_pin": "PIN を設定",
"settings_shield_mining": "マイニングシールド...",
"settings_shield_mining": "マイニングシールド…",
"settings_solid_colors_desc": "ぼかし効果の代わりに単色を使用(アクセシビリティ)",
"settings_theme_refreshed": "テーマ一覧を更新しました",
"settings_tor_desc": "プライバシー向上のため全接続を Tor 経由にする",
"settings_unlocked": "ロック解除",
"settings_use_tor_network": "ネットワーク接続に Tor を使用",
"settings_validate_address": "アドレス検証...",
"settings_validate_address": "アドレス検証…",
"settings_visual_effects": "視覚効果",
"settings_wallet_file_size": "ウォレットファイルサイズ:%s",
"settings_wallet_info": "ウォレット情報",
@@ -1452,6 +1640,17 @@
"sweep_to": "集約先:",
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
"sweep_tx": "取引:",
"swin_connection_failed": "接続に失敗しました: ",
"swin_connection_successful": "接続に成功しました!\ndragonxd バージョン: ",
"swin_invalid_suffix": "(無効)",
"swin_no_history_file_found": "履歴ファイルが見つかりません",
"swin_rescan_failed": "再スキャンに失敗しました: ",
"swin_rescan_started_from_block": "再スキャンを開始しました。開始ブロック ",
"swin_rescan_to": " 〜 ",
"swin_rpc_client_not_initialized": "RPCクライアントが初期化されていません",
"swin_settings_saved": "設定を保存しました",
"swin_theme_list_refreshed": "テーマ一覧を更新しました",
"swin_ztx_history_cleared": "Zトランザクション履歴を消去しました",
"switch_corrupt_body": "このウォレットは破損しているようです。ノードが開けませんでした。バックアップから復元するか、作り直すか、修復を試してください。",
"switch_corrupt_repair": "修復を試す(salvage)",
"switch_progress_background": "バックグラウンドで続行",
@@ -1491,6 +1690,7 @@
"to_upper": "宛先",
"tools": "ツール",
"tools_actions": "ツールとアクション...",
"tools_actions_hdr": "ツールと操作",
"total": "合計",
"total_balance_label": "総残高",
"transaction_id": "取引ID",
@@ -1514,7 +1714,7 @@
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
"tt_backup": "wallet.dat のバックアップを作成",
"tt_block_explorer": "ブラウザで DragonX ブロックエクスプローラーを開く",
"tt_blur": "ぼかし量(0%% = オフ、100%% = 最大)",
"tt_blur": "ぼかし量(0% = オフ、100% = 最大)",
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
"tt_change_pin": "アンロック PIN を変更",
"tt_chat_bubble_accent": "送信メッセージの吹き出しのアクセントカラー(または現在のテーマに従う)",
@@ -1577,7 +1777,7 @@
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
"tt_merge": "複数の UTXO を一つのアドレスに統合",
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
"tt_noise": "グレインテクスチャ強度(0%% = オフ、100%% = 最大)",
"tt_noise": "グレインテクスチャ強度(0% = オフ、100% = 最大)",
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
@@ -1616,7 +1816,7 @@
"tt_theme_hotkey": "ホットキー:Ctrl+左/右でテーマを切り替え",
"tt_tor": "匿名性のためにデーモン接続を Tor ネットワーク経由でルーティング",
"tt_tx_url": "ブロックエクスプローラーでトランザクションを表示するためのベース URL",
"tt_ui_opacity": "カードとサイドバーの不透明度(100%% = 完全不透明、低い = より透過)",
"tt_ui_opacity": "カードとサイドバーの不透明度(100% = 完全不透明、低い = より透過)",
"tt_validate": "DragonX アドレスが有効かどうかを確認",
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
@@ -1828,4 +2028,4 @@
"your_addresses": "あなたのアドレス",
"z_address": "Zアドレス",
"z_addresses": "Zアドレス"
}
}
+217 -17
View File
@@ -60,6 +60,80 @@
"amount_label": "금액:",
"animate_avatars": "아바타 애니메이션",
"appearance": "외관",
"appx_back": "뒤로",
"appx_back_up_seed_phrase_title": "시드 문구 백업",
"appx_birthday_block_height": "생성 시점(블록 높이): %llu — 이것도 함께 백업하세요.",
"appx_blockchain_data_deleted": "블록체인 데이터를 삭제했습니다(%d개 항목). 네트워크에서 다시 동기화하기 위해 데몬을 재시작합니다.",
"appx_blockchain_maintenance_in_progress": "블록체인 유지 관리 작업이 이미 진행 중입니다.",
"appx_blockchain_rescan_complete": "블록체인 다시 스캔 완료",
"appx_bootstrap_complete_reconciling": "부트스트랩 완료 — 새 체인 데이터와 지갑을 대조하는 중입니다.",
"appx_cancel": "취소",
"appx_cleaning_up": "정리하는 중...",
"appx_confirm_your_backup": "백업 확인",
"appx_copied_clipboard_autoclears": "복사됨 — 클립보드가 45초 후 자동으로 지워집니다",
"appx_copy": "복사",
"appx_could_not_start_restore": "복구를 시작할 수 없습니다",
"appx_create_failed_prefix": "생성 실패: ",
"appx_creating_your_wallet": "지갑을 생성하는 중…",
"appx_daemon_error": "데몬 오류",
"appx_daemon_reinstall_in_progress": "데몬 재설치가 이미 진행 중입니다.",
"appx_disconnecting": "연결 해제 중...",
"appx_done": "완료",
"appx_dragonxd_output": "dragonxd 출력",
"appx_encrypting_wallet": "지갑 암호화 중...",
"appx_fullnode_lifecycle_unavailable_lite": "풀노드 수명 주기 작업은 라이트 빌드에서 사용할 수 없습니다",
"appx_installing_bundled_daemon": "번들 데몬을 설치하는 중 — 노드가 중지, 업데이트 후 다시 시작됩니다...",
"appx_invalid_payment_uri_prefix": "잘못된 결제 URI: ",
"appx_ive_written_it_down": "적어 두었습니다",
"appx_keep_node_running_and_quit": "노드 유지하고 종료",
"appx_last_block_n": "마지막 블록: %d",
"appx_last_used_wallet_not_found_prefix": "마지막으로 사용한 지갑 파일(",
"appx_last_used_wallet_not_found_suffix": ")을 찾을 수 없어 기본 지갑을 대신 열었습니다. 옮기셨다면 복원한 후 지갑 목록에서 다시 전환하세요.",
"appx_low_spec_mode_disabled": "저사양 모드 비활성화됨",
"appx_low_spec_mode_enabled": "저사양 모드 활성화됨",
"appx_miner_stopped_prefix": "채굴기 중지됨: ",
"appx_miner_stopped_unexpectedly": "채굴기가 예기치 않게 중지되었습니다.",
"appx_n_min_n_sec": "%d분 %d초",
"appx_n_seconds": "%d초",
"appx_no_bundled_daemon_to_install": "이 빌드에는 설치할 번들 데몬이 없습니다",
"appx_no_embedded_daemon_to_install": "이 빌드에는 설치할 내장 데몬이 없습니다",
"appx_node_busy_restarting": "노드가 재시작 중입니다 — 잠시 후 다시 시도하세요.",
"appx_node_rebuilding_witness_cache": "노드가 위트니스 캐시를 재구성하는 중입니다",
"appx_not_next_word": " — 다음 단어가 아닙니다",
"appx_payment_request_loaded": "결제 요청을 불러왔습니다",
"appx_pool_miner_connected_and_hashing": "풀 채굴기가 연결되어 해싱 중입니다.",
"appx_progress_n_of_n": "진행 상황: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Sapling 노트 위트니스 재구성 중…",
"appx_rebuilding_witness_cache_blocks_left": "위트니스 캐시 재구성 중 %.0f%% — %d개 블록 남음",
"appx_rebuilding_witness_cache_pct": "위트니스 캐시 재구성 중 %.0f%%",
"appx_recovery_phrase_word_count": "복구 문구는 24개 단어여야 합니다 — 현재 %d개입니다.",
"appx_restarting_daemon_rescan_flag": "-rescan 플래그로 데몬을 다시 시작하는 중...",
"appx_restarting_daemon_zapwallettxes": "-zapwallettxes=2로 데몬을 다시 시작하는 중(지갑 복구)...",
"appx_restoring_your_wallet": "지갑을 복구하는 중…",
"appx_seed_backup_warning": "이 24개 단어는 지갑을 복구할 수 있는 유일한 방법입니다. 순서대로 적어 오프라인에 보관하고 절대 공유하지 마세요. 잃어버리면 자금을 영원히 되찾을 수 없습니다.",
"appx_seed_not_backed_up_warning": "시드를 아직 백업하지 않았습니다 — 자금을 잃을 수 있습니다. 그래도 건너뛰시겠습니까?",
"appx_sending_stop_command_to_daemon": "데몬에 중지 명령을 보내는 중...",
"appx_setting_initial_sapling_witnesses": "초기 Sapling 위트니스 설정 중 %.0f%%",
"appx_shutdown_complete": "종료 완료",
"appx_simple_background_disabled": "단순 배경 비활성화됨",
"appx_simple_background_enabled": "단순 배경 활성화됨",
"appx_skip": "건너뛰기",
"appx_skip_anyway": "그래도 건너뛰기",
"appx_still_status_prefix": "아직 \"",
"appx_still_status_suffix": "\" 상태입니다 — 지금 강제 종료하면 체인 데이터가 손상될 수 있습니다.",
"appx_stop_anyway_and_quit": "그래도 중지하고 종료",
"appx_stopping_daemon_deleting_blockchain": "데몬을 중지하고 블록체인 데이터를 삭제하는 중...",
"appx_stopping_node_discards_rebuild": "지금 노드를 중지하면 진행 중인 재구성이 취소되며, 다음에 지갑을 열 때 다시 시작됩니다(몇 분 소요). 대신 노드를 계속 실행할 수 있습니다.",
"appx_stopping_pool_miner": "풀 채굴기를 중지하는 중...",
"appx_syncing_pct_block_n_of_n": "동기화 중 %.1f%% — 블록 %d / %d",
"appx_tap_words_in_order": "저장한 내용을 확인하도록 단어를 올바른 순서대로 탭하세요.",
"appx_theme_effects_disabled": "테마 효과 비활성화됨",
"appx_theme_effects_enabled": "테마 효과 활성화됨",
"appx_theme_prefix": "테마: ",
"appx_use_settings_restart_daemon_hint": "설정 > 데몬 다시 시작을 사용해 다시 시도하세요",
"appx_waiting_for_daemon_to_encrypt_wallet": "데몬이 지갑을 암호화하기를 기다리는 중...",
"appx_wallet_created_and_backed_up": "지갑이 생성되고 백업되었습니다.",
"appx_wallet_open_failed_prefix": "지갑 열기 실패: ",
"auto_shield": "채굴 자동 차폐",
"av_intro": "채굴 소프트웨어는 종종 잠재적으로 원치 않는 항목으로 표시됩니다. 풀 채굴을 활성화하려면 다음 단계를 따르세요:",
"av_open_security": "Windows 보안 열기",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender가 채굴기를 차단했습니다",
"available": "사용 가능",
"backup_backing_up": "백업 중...",
"backup_col_backup": "백업",
"backup_col_export": "내보내기",
"backup_col_import": "가져오기 및 복원",
"backup_create": "백업 생성",
"backup_created": "지갑 백업이 생성되었습니다",
"backup_data": "백업 및 데이터",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "차폐: %.8f",
"balance_syncing_pct": "동기화 중 %.1f%%",
"balance_transparent_fmt": "투명: %.8f",
"baltab_market": "시세",
"baltab_market_price_4dp": "시세: $%.4f",
"baltab_market_price_8dp": "시세: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24시간",
"baltab_pct_of_total_zaddr": "전체의 %.0f%% · Z-addr %d개",
"baltab_shielded": "실드됨",
"baltab_shielded_amount": "실드됨 %.8f",
"baltab_t_addresses_count": "T-address %d개",
"baltab_total_balance": "총 잔액",
"baltab_transparent": "투명",
"baltab_transparent_amount": "투명 %.8f",
"ban": "차단",
"banned_peers": "차단된 피어",
"block": "블록",
@@ -411,6 +499,7 @@
"contacts_shape_square": "사각형",
"contacts_shape_tab": "왼쪽 탭",
"copied": "복사됨!",
"copied_to_clipboard": "클립보드에 복사됨",
"copy": "복사",
"copy_address": "전체 주소 복사",
"copy_error": "오류 복사",
@@ -423,6 +512,7 @@
"daemon_bundled": "번들",
"daemon_install_bundled": "번들 버전 설치",
"daemon_installed": "설치됨",
"daemon_maintenance_label": "유지 관리",
"daemon_none_bundled": "이 빌드에 없음",
"daemon_not_installed": "설치되지 않음",
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
"daemon_update_verifying": "확인 중…",
"daemon_update_version": "버전:",
"daemon_updates_label": "업데이트",
"daemon_version": "데몬",
"dark": "다크",
"data_stale_prefix": "업데이트",
@@ -588,6 +679,63 @@
"general": "일반",
"generating": "생성 중",
"go_to_receive": "수신으로 이동",
"grpa_current_block_paren": "(현재: %d)",
"grpa_days_ago": "%lld일 전",
"grpa_dbg_addrman": "피어 주소 추적 및 관리",
"grpa_dbg_alert": "경고 시스템 메시지",
"grpa_dbg_bench": "작업 벤치마크 시간 측정",
"grpa_dbg_coindb": "코인 데이터베이스 읽기/쓰기 작업",
"grpa_dbg_db": "Berkeley DB 작업",
"grpa_dbg_estimatefee": "수수료 추정 알고리즘",
"grpa_dbg_http": "HTTP RPC 서버 활동",
"grpa_dbg_libevent": "Libevent 네트워킹 라이브러리",
"grpa_dbg_lock": "락 경합 디버깅",
"grpa_dbg_mempool": "트랜잭션 메모리 풀 활동",
"grpa_dbg_net": "네트워크 연결 및 메시지",
"grpa_dbg_paymentdisclosure": "결제 공개 프로토콜",
"grpa_dbg_pow": "작업 증명 채굴 활동",
"grpa_dbg_proxy": "SOCKS5 프록시 연결",
"grpa_dbg_prune": "블록 정리 작업",
"grpa_dbg_rand": "난수 생성",
"grpa_dbg_reindex": "블록체인 재색인 진행 상황",
"grpa_dbg_rpc": "RPC 명령 처리",
"grpa_dbg_selectcoins": "트랜잭션용 코인 선택",
"grpa_dbg_tor": "Tor 연동 및 회로 정보",
"grpa_dbg_zmq": "ZeroMQ 알림 시스템",
"grpa_dbg_zrpc": "실드(z-addr) RPC 작업",
"grpa_enter_private_key_to_import": "가져올 개인 키를 입력하세요.",
"grpa_error_prefix": "오류: ",
"grpa_hr_ago": "%lld시간 전",
"grpa_invalid_response_from_daemon": "데몬으로부터 잘못된 응답",
"grpa_invalid_suffix": " (유효하지 않음)",
"grpa_min_ago": "%lld분 전",
"grpa_sec_ago": "%lld초 전",
"grpa_seed_demo_chat": "시드 데모 채팅",
"grpa_showing_first_100_of": "... %d개 중 처음 100개 표시",
"grpa_tab_about": "정보",
"grpa_tab_appearance": "화면 표시",
"grpa_tab_backup_data": "백업 및 데이터",
"grpa_tab_chat": "채팅",
"grpa_tab_explorer": "탐색기",
"grpa_tab_node_security": "노드 및 보안",
"grpa_tab_wallet": "지갑",
"grpa_unexpected_getblockhash_result": "예기치 않은 getblockhash 결과",
"grpb_copy": "복사",
"grpb_max": "최대",
"grpb_new_badge_suffix": " [신규]",
"grpb_preview_msg_payment_through": "결제가 완료됐나요? 🙂",
"grpb_preview_msg_sending_rest": "지금 나머지를 보낼게요 👍",
"grpb_preview_msg_yep_confirmed": "네 — 방금 확인했어요 ✅",
"grpb_selected_suffix": "\n(선택됨)",
"grpb_tooltip_address_balance": "%s\n잔액: %.8f %s%s",
"grpb_undo_clear": "지우기 취소",
"grpc_benchmark_inconclusive": "벤치마크 결과가 불확실합니다: 해시레이트 샘플이 기록되지 않았습니다. 풀 연결을 확인한 후 다시 시도하세요.",
"grpc_benchmark_takes_secs": "벤치마크는 약 %d초가 걸리며 채굴을 중단합니다. 시작하려면 다시 클릭하세요.",
"grpc_bootstrap_failed": "부트스트랩 실패",
"grpc_bootstrap_not_initialized": "부트스트랩이 초기화되지 않았습니다",
"grpc_hashrate_fee": "%s 수수료 %s%%",
"grpc_key_not_available": "이 주소에 대한 키를 사용할 수 없습니다",
"grpc_na": "해당 없음",
"height": "높이",
"help": "도움말",
"hidden_tag": " (숨김)",
@@ -1227,12 +1375,52 @@
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
"sb_warming_up": "워밍업 중...",
"sb_witness_cache": "증인 재구축 중",
"scale_effects": "배율 및 효과",
"screenshot_open_dir": "위치 열기",
"screenshot_sweep": "스크린샷 스윕 실행",
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
"screenshot_sweep_full": "전체 UI 스윕",
"search_icons": "아이콘 검색...",
"search_placeholder": "검색...",
"sec_changing_passphrase": "암호문 변경 중...",
"sec_changing_pin": "PIN 변경 중...",
"sec_couldnt_lock_wallet": "지갑을 잠글 수 없습니다 — 아직 잠금 해제 상태입니다. 데몬 연결을 확인하세요.",
"sec_encrypted_backup_suffix": "\n암호화된 백업: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "지갑 암호화 중...",
"sec_encryption_did_not_complete": "지갑 암호화가 완료되지 않았습니다 — 지갑이 암호화되지 않은 상태입니다. 설정을 열어 암호화를 완료하세요.",
"sec_encryption_failed_prefix": "암호화 실패: ",
"sec_failed_prefix": "실패: ",
"sec_failed_to_create_vault": "볼트 생성에 실패했습니다",
"sec_importing_keys_rescanning": "키를 가져오고 블록체인을 다시 스캔하는 중 — 진행 중에도 지갑을 사용할 수 있습니다",
"sec_incorrect_current_pin": "현재 PIN이 올바르지 않습니다",
"sec_incorrect_passphrase_decrypt": "잘못된 암호문",
"sec_incorrect_passphrase_pin_setup": "잘못된 암호문",
"sec_incorrect_pin_remove": "잘못된 PIN",
"sec_internal_error_change_pin": "내부 오류",
"sec_internal_error_remove_pin": "내부 오류",
"sec_mode_passphrase": " 암호문",
"sec_not_connected_to_daemon": "데몬에 연결되지 않음",
"sec_not_connected_to_daemon_pin": "데몬에 연결되지 않음",
"sec_passphrase_changed_successfully": "암호문이 성공적으로 변경되었습니다",
"sec_pin_changed_successfully": "PIN이 성공적으로 변경되었습니다",
"sec_pin_removed": "PIN이 제거되었습니다",
"sec_pin_set_successfully": "PIN이 성공적으로 설정되었습니다",
"sec_restart_daemon_for_encryption": "암호화를 적용하려면 데몬을 다시 시작하세요.",
"sec_too_many_attempts_wait": "시도 횟수가 너무 많습니다. %.0f초 기다리세요...",
"sec_total_elapsed_fmt": "총 경과 시간: %d분 %02d초",
"sec_unlock_button": "잠금 해제",
"sec_unlock_failed_prefix": "잠금 해제 실패: ",
"sec_unlocking_fmt": "잠금 해제 중%s",
"sec_use_passphrase_instead": "대신 암호문 사용",
"sec_use_pin_instead": "대신 PIN 사용",
"sec_verifying_passphrase": "암호문 확인 중...",
"sec_verifying_pin": "PIN 확인 중...",
"sec_wallet_decrypted_all_keys_imported": "지갑 복호화 완료! 모든 키를 가져왔습니다.",
"sec_wallet_encrypted_and_pin_set": "지갑 암호화 및 PIN 설정 완료",
"sec_wallet_encrypted_but_pin_vault_failed": "지갑은 암호화되었으나 PIN 볼트 설정에 실패했습니다",
"sec_wallet_encrypted_restarting_daemon": "지갑이 암호화되었습니다. 데몬을 다시 시작하는 중...",
"sec_wallet_encrypted_successfully": "지갑이 성공적으로 암호화되었습니다",
"sec_wallet_locked_title": "지갑 잠김",
"security": "보안",
"seed_backup_button": "시드 문구",
"seed_backup_close": "닫기",
@@ -1315,12 +1503,12 @@
"settings": "설정",
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
"settings_acrylic_level": "아크릴 레벨:",
"settings_address_book": "주소록...",
"settings_address_book": "주소록…",
"settings_auto_detected": "DRAGONX.conf에서 자동 감지",
"settings_auto_lock": "자동 잠금",
"settings_auto_shield_desc": "투명 자금을 자동으로 차폐 주소로 이동",
"settings_auto_shield_funds": "투명 자금 자동 차폐",
"settings_backup": "백업...",
"settings_backup": "백업…",
"settings_block_explorer_urls": "블록 탐색기 URL",
"settings_builtin": "내장",
"settings_change_passphrase": "비밀번호 변경",
@@ -1334,7 +1522,7 @@
"settings_copy_diagnostics": "진단 정보 복사",
"settings_copyright": "Copyright 2024-2026 DragonX 개발자 | GPLv3 라이선스",
"settings_custom": "사용자 지정",
"settings_data_dir": "데이터 디렉터리:",
"settings_data_dir": "데이터 디렉터리",
"settings_debug_changed": "디버그 카테고리가 변경되었습니다 — 데몬을 재시작하여 적용",
"settings_debug_restart_note": "변경 사항은 데몬을 다시 시작한 후에 적용됩니다.",
"settings_debug_select": "데몬 디버그 로깅을 활성화할 카테고리를 선택하세요 (-debug= 플래그).",
@@ -1342,18 +1530,18 @@
"settings_encrypt_first_pin": "PIN을 활성화하려면 먼저 지갑을 암호화하세요",
"settings_encrypt_wallet": "지갑 암호화",
"settings_explorer_hint": "URL에 후행 슬래시를 포함해야 합니다. txid/주소가 추가됩니다.",
"settings_export_all": "모두 내보내기...",
"settings_export_csv": "CSV 내보내기...",
"settings_export_key": "키 내보내기...",
"settings_export_all": "모두 내보내기…",
"settings_export_csv": "CSV 내보내기…",
"settings_export_key": "키 내보내기…",
"settings_gradient_bg": "그라데이션 배경",
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
"settings_idle_after": "후",
"settings_import_key": "개인 키 가져오기...",
"settings_import_viewkey": "조회 키 가져오기...",
"settings_import_key": "개인 키 가져오기…",
"settings_import_viewkey": "조회 키 가져오기…",
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
"settings_lock_now": "지금 잠금",
"settings_locked": "잠김",
"settings_merge_to_address": "주소로 병합...",
"settings_merge_to_address": "주소로 병합…",
"settings_noise_opacity": "노이즈 불투명도:",
"settings_not_connected": "데몬에 연결되지 않음",
"settings_not_encrypted": "암호화되지 않음",
@@ -1369,7 +1557,7 @@
"settings_reloaded": "디스크에서 설정을 다시 불러왔습니다",
"settings_remove_encryption": "암호화 제거",
"settings_remove_pin": "PIN 제거",
"settings_request_payment": "결제 요청...",
"settings_request_payment": "결제 요청…",
"settings_rescan_desc": "누락된 거래를 찾기 위해 블록체인 재스캔",
"settings_restart_daemon": "데몬 재시작",
"settings_rpc_connection": "RPC 연결",
@@ -1380,20 +1568,20 @@
"settings_save_shielded_local": "차폐 거래 기록을 로컬에 저장",
"settings_saved": "설정이 저장되었습니다",
"settings_set_pin": "PIN 설정",
"settings_shield_mining": "채굴 차폐...",
"settings_shield_mining": "채굴 차폐…",
"settings_solid_colors_desc": "블러 효과 대신 단색 사용 (접근성)",
"settings_theme_refreshed": "테마 목록을 새로고침했습니다",
"settings_tor_desc": "향상된 개인 정보 보호를 위해 모든 연결을 Tor를 통해 라우팅",
"settings_unlocked": "잠금 해제",
"settings_use_tor_network": "네트워크 연결에 Tor 사용",
"settings_validate_address": "주소 확인...",
"settings_validate_address": "주소 확인…",
"settings_visual_effects": "시각 효과",
"settings_wallet_file_size": "지갑 파일 크기: %s",
"settings_wallet_info": "지갑 정보",
"settings_wallet_location": "지갑 위치: %s",
"settings_wallet_maintenance": "지갑 유지보수",
"settings_wallet_not_found": "지갑 파일을 찾을 수 없음",
"settings_wallet_size_label": "지갑 크기:",
"settings_wallet_size_label": "지갑 크기",
"settings_ztx_cleared": "Z-거래 내역이 삭제되었습니다",
"settings_ztx_not_found": "내역 파일을 찾을 수 없습니다",
"setup_wizard": "설정 마법사",
@@ -1454,6 +1642,17 @@
"sweep_to": "쓸어담은 주소:",
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
"sweep_tx": "거래:",
"swin_connection_failed": "연결 실패: ",
"swin_connection_successful": "연결에 성공했습니다!\ndragonxd 버전: ",
"swin_invalid_suffix": " (유효하지 않음)",
"swin_no_history_file_found": "기록 파일을 찾을 수 없습니다",
"swin_rescan_failed": "다시 스캔 실패: ",
"swin_rescan_started_from_block": "다시 스캔이 시작된 블록 ",
"swin_rescan_to": " ~ ",
"swin_rpc_client_not_initialized": "RPC 클라이언트가 초기화되지 않았습니다",
"swin_settings_saved": "설정이 저장되었습니다",
"swin_theme_list_refreshed": "테마 목록을 새로고침했습니다",
"swin_ztx_history_cleared": "Z-트랜잭션 기록이 삭제되었습니다",
"switch_corrupt_body": "이 지갑이 손상된 것 같습니다. 노드가 열 수 없습니다. 백업에서 복원하거나 다시 만들거나 복구를 시도하세요.",
"switch_corrupt_repair": "복구 시도(salvage)",
"switch_progress_background": "백그라운드에서 계속",
@@ -1493,6 +1692,7 @@
"to_upper": "받는 곳",
"tools": "도구",
"tools_actions": "도구 및 작업...",
"tools_actions_hdr": "도구 및 작업",
"total": "합계",
"total_balance_label": "총 잔액",
"transaction_id": "거래 ID",
@@ -1516,7 +1716,7 @@
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
"tt_backup": "wallet.dat 백업 만들기",
"tt_block_explorer": "브라우저에서 DragonX 블록 탐색기 열기",
"tt_blur": "블러 양 (0%% = 끔, 100%% = 최대)",
"tt_blur": "블러 양 (0% = 끔, 100% = 최대)",
"tt_change_pass": "지갑 암호화 비밀번호 변경",
"tt_change_pin": "잠금 해제 PIN 변경",
"tt_chat_bubble_accent": "보내는 메시지 말풍선의 강조 색상(또는 현재 테마를 따름)",
@@ -1579,7 +1779,7 @@
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
"tt_noise": "그레인 텍스처 강도 (0% = 끔, 100% = 최대)",
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
@@ -1618,7 +1818,7 @@
"tt_theme_hotkey": "단축키: Ctrl+왼쪽/오른쪽으로 테마 전환",
"tt_tor": "익명성을 위해 데몬 연결을 Tor 네트워크를 통해 라우팅",
"tt_tx_url": "블록 탐색기에서 거래를 보기 위한 기본 URL",
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100%% = 완전 불투명, 낮을수록 더 투명)",
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100% = 완전 불투명, 낮을수록 더 투명)",
"tt_validate": "DragonX 주소가 유효한지 확인",
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
@@ -1830,4 +2030,4 @@
"your_addresses": "내 주소",
"z_address": "Z 주소",
"z_addresses": "Z 주소"
}
}
+217 -17
View File
@@ -60,6 +60,80 @@
"amount_label": "Valor:",
"animate_avatars": "Animar avatares",
"appearance": "APARÊNCIA",
"appx_back": "Voltar",
"appx_back_up_seed_phrase_title": "Faça o backup da sua frase-semente",
"appx_birthday_block_height": "Nascimento (altura do bloco): %llu — faça o backup disto também.",
"appx_blockchain_data_deleted": "Dados da blockchain excluídos (%d itens). O daemon está reiniciando para ressincronizar com a rede.",
"appx_blockchain_maintenance_in_progress": "Uma operação de manutenção da blockchain já está em andamento.",
"appx_blockchain_rescan_complete": "Reescaneamento da blockchain concluído",
"appx_bootstrap_complete_reconciling": "Bootstrap concluído — reconciliando sua carteira com os novos dados da chain.",
"appx_cancel": "Cancelar",
"appx_cleaning_up": "Limpando...",
"appx_confirm_your_backup": "Confirme seu backup",
"appx_copied_clipboard_autoclears": "Copiado — a área de transferência será limpa em 45s",
"appx_copy": "Copiar",
"appx_could_not_start_restore": "Não foi possível iniciar a restauração",
"appx_create_failed_prefix": "Falha na criação: ",
"appx_creating_your_wallet": "Criando sua carteira…",
"appx_daemon_error": "Erro do Daemon",
"appx_daemon_reinstall_in_progress": "A reinstalação do daemon já está em andamento.",
"appx_disconnecting": "Desconectando...",
"appx_done": "Concluído",
"appx_dragonxd_output": "saída do dragonxd",
"appx_encrypting_wallet": "Criptografando a carteira...",
"appx_fullnode_lifecycle_unavailable_lite": "As ações de ciclo de vida do nó completo não estão disponíveis na versão lite",
"appx_installing_bundled_daemon": "Instalando o daemon incluído — o nó vai parar, atualizar e reiniciar...",
"appx_invalid_payment_uri_prefix": "URI de pagamento inválida: ",
"appx_ive_written_it_down": "Já anotei",
"appx_keep_node_running_and_quit": "Manter o nó em execução e sair",
"appx_last_block_n": "Último bloco: %d",
"appx_last_used_wallet_not_found_prefix": "Seu último arquivo de carteira usado (",
"appx_last_used_wallet_not_found_suffix": ") não foi encontrado — a carteira padrão foi aberta em seu lugar. Se você o moveu, restaure-o e volte a ele pela lista de carteiras.",
"appx_low_spec_mode_disabled": "Modo de baixo desempenho desativado",
"appx_low_spec_mode_enabled": "Modo de baixo desempenho ativado",
"appx_miner_stopped_prefix": "Minerador parado: ",
"appx_miner_stopped_unexpectedly": "O minerador parou inesperadamente.",
"appx_n_min_n_sec": "%d min %d s",
"appx_n_seconds": "%d segundos",
"appx_no_bundled_daemon_to_install": "Esta versão não tem daemon incluído para instalar",
"appx_no_embedded_daemon_to_install": "Esta versão não tem daemon embutido para instalar",
"appx_node_busy_restarting": "O nó está ocupado reiniciando — tente novamente em instantes.",
"appx_node_rebuilding_witness_cache": "O nó está reconstruindo seu cache de testemunhas",
"appx_not_next_word": " — essa não é a próxima palavra",
"appx_payment_request_loaded": "Solicitação de pagamento carregada",
"appx_pool_miner_connected_and_hashing": "Minerador de pool conectado e processando hashes.",
"appx_progress_n_of_n": "Progresso: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Reconstruindo testemunhas de notas Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Reconstruindo cache de testemunhas %.0f%% — %d blocos restantes",
"appx_rebuilding_witness_cache_pct": "Reconstruindo cache de testemunhas %.0f%%",
"appx_recovery_phrase_word_count": "A frase de recuperação deve ter 24 palavras — você tem %d.",
"appx_restarting_daemon_rescan_flag": "Reiniciando o daemon com a flag -rescan...",
"appx_restarting_daemon_zapwallettxes": "Reiniciando o daemon com -zapwallettxes=2 (reparo da carteira)...",
"appx_restoring_your_wallet": "Restaurando sua carteira…",
"appx_seed_backup_warning": "Estas 24 palavras são a ÚNICA forma de restaurar sua carteira. Anote-as na ordem, guarde-as offline e nunca as compartilhe. Se perdê-las, seus fundos desaparecem para sempre.",
"appx_seed_not_backed_up_warning": "Você não fez o backup da sua semente — os fundos podem ser perdidos. Pular mesmo assim?",
"appx_sending_stop_command_to_daemon": "Enviando comando de parada ao daemon...",
"appx_setting_initial_sapling_witnesses": "Definindo testemunhas Sapling iniciais %.0f%%",
"appx_shutdown_complete": "Encerramento concluído",
"appx_simple_background_disabled": "Fundo simples desativado",
"appx_simple_background_enabled": "Fundo simples ativado",
"appx_skip": "Pular",
"appx_skip_anyway": "Pular mesmo assim",
"appx_still_status_prefix": "Ainda \"",
"appx_still_status_suffix": "\" — forçar o encerramento agora pode corromper os dados da chain.",
"appx_stop_anyway_and_quit": "Parar mesmo assim e sair",
"appx_stopping_daemon_deleting_blockchain": "Parando o daemon e excluindo os dados da blockchain...",
"appx_stopping_node_discards_rebuild": "Parar o nó agora descarta a reconstrução em andamento e a reinicia (vários minutos) na próxima vez que você abrir a carteira. Em vez disso, você pode manter o nó em execução.",
"appx_stopping_pool_miner": "Parando o minerador de pool...",
"appx_syncing_pct_block_n_of_n": "Sincronizando %.1f%% — Bloco %d / %d",
"appx_tap_words_in_order": "Toque nas palavras na ordem correta para confirmar que você as salvou.",
"appx_theme_effects_disabled": "Efeitos de tema desativados",
"appx_theme_effects_enabled": "Efeitos de tema ativados",
"appx_theme_prefix": "Tema: ",
"appx_use_settings_restart_daemon_hint": "Use Configurações > Reiniciar Daemon para tentar novamente",
"appx_waiting_for_daemon_to_encrypt_wallet": "Aguardando o daemon criptografar a carteira...",
"appx_wallet_created_and_backed_up": "Carteira criada e com backup feito.",
"appx_wallet_open_failed_prefix": "Falha ao abrir a carteira: ",
"auto_shield": "Auto-blindar mineração",
"av_intro": "Softwares de mineração costumam ser sinalizados como potencialmente indesejados. Siga estes passos para habilitar a mineração em pool:",
"av_open_security": "Abrir Segurança do Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender bloqueou o minerador",
"available": "Disponível",
"backup_backing_up": "Fazendo backup...",
"backup_col_backup": "BACKUP",
"backup_col_export": "EXPORTAR",
"backup_col_import": "IMPORTAR E RESTAURAR",
"backup_create": "Criar Backup",
"backup_created": "Backup da carteira criado",
"backup_data": "BACKUP & DADOS",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Blindado: %.8f",
"balance_syncing_pct": "Sincronizando %.1f%%",
"balance_transparent_fmt": "Transparente: %.8f",
"baltab_market": "Mercado",
"baltab_market_price_4dp": "Mercado: $%.4f",
"baltab_market_price_8dp": "Mercado: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24h",
"baltab_pct_of_total_zaddr": "%.0f%% do total · %d Z-addr",
"baltab_shielded": "Blindado",
"baltab_shielded_amount": "Blindado %.8f",
"baltab_t_addresses_count": "%d T-addresses",
"baltab_total_balance": "Saldo Total",
"baltab_transparent": "Transparente",
"baltab_transparent_amount": "Transparente %.8f",
"ban": "Banir",
"banned_peers": "Pares Banidos",
"block": "Bloco",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Quadrado",
"contacts_shape_tab": "Aba",
"copied": "Copiado!",
"copied_to_clipboard": "Copiado para a área de transferência",
"copy": "Copiar",
"copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro",
@@ -423,6 +512,7 @@
"daemon_bundled": "Empacotado",
"daemon_install_bundled": "Instalar incluído",
"daemon_installed": "Instalado",
"daemon_maintenance_label": "MANUTENÇÃO",
"daemon_none_bundled": "nenhum nesta build",
"daemon_not_installed": "não instalado",
"daemon_status_differ": "O binário instalado difere da versão empacotada.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "O download é verificado contra o SHA-256 publicado do lançamento e uma assinatura ed25519 fixada antes da instalação.",
"daemon_update_verifying": "Verificando…",
"daemon_update_version": "Versão:",
"daemon_updates_label": "ATUALIZAÇÕES",
"daemon_version": "Daemon",
"dark": "Escuro",
"data_stale_prefix": "Atualizado",
@@ -588,6 +679,63 @@
"general": "Geral",
"generating": "Gerando",
"go_to_receive": "Ir para Receber",
"grpa_current_block_paren": "(Atual: %d)",
"grpa_days_ago": "há %lld dias",
"grpa_dbg_addrman": "Rastreamento e gerenciamento de endereços de pares",
"grpa_dbg_alert": "Mensagens do sistema de alertas",
"grpa_dbg_bench": "Medições de tempo de benchmark das operações",
"grpa_dbg_coindb": "Operações de leitura/gravação do banco de dados de moedas",
"grpa_dbg_db": "Operações do Berkeley DB",
"grpa_dbg_estimatefee": "Algoritmo de estimativa de taxa",
"grpa_dbg_http": "Atividade do servidor RPC HTTP",
"grpa_dbg_libevent": "Biblioteca de rede Libevent",
"grpa_dbg_lock": "Depuração de contenção de bloqueios",
"grpa_dbg_mempool": "Atividade do pool de memória de transações",
"grpa_dbg_net": "Conexões e mensagens de rede",
"grpa_dbg_paymentdisclosure": "Protocolo de divulgação de pagamento",
"grpa_dbg_pow": "Atividade de mineração de prova de trabalho",
"grpa_dbg_proxy": "Conexões de proxy SOCKS5",
"grpa_dbg_prune": "Operações de poda de blocos",
"grpa_dbg_rand": "Geração de números aleatórios",
"grpa_dbg_reindex": "Progresso da reindexação da blockchain",
"grpa_dbg_rpc": "Processamento de comandos RPC",
"grpa_dbg_selectcoins": "Seleção de moedas para transações",
"grpa_dbg_tor": "Integração com Tor e informações de circuito",
"grpa_dbg_zmq": "Sistema de notificações ZeroMQ",
"grpa_dbg_zrpc": "Operações RPC blindadas (z-address)",
"grpa_enter_private_key_to_import": "Insira uma chave privada para importar.",
"grpa_error_prefix": "Erro: ",
"grpa_hr_ago": "há %lld h",
"grpa_invalid_response_from_daemon": "Resposta inválida do daemon",
"grpa_invalid_suffix": " (inválido)",
"grpa_min_ago": "há %lld min",
"grpa_sec_ago": "há %lld s",
"grpa_seed_demo_chat": "Chat de demonstração da semente",
"grpa_showing_first_100_of": "... mostrando os primeiros 100 de %d",
"grpa_tab_about": "Sobre",
"grpa_tab_appearance": "Aparência",
"grpa_tab_backup_data": "Backup e Dados",
"grpa_tab_chat": "Chat",
"grpa_tab_explorer": "Explorer",
"grpa_tab_node_security": "Nó e Segurança",
"grpa_tab_wallet": "Carteira",
"grpa_unexpected_getblockhash_result": "resultado inesperado de getblockhash",
"grpb_copy": "Copiar",
"grpb_max": "Máx",
"grpb_new_badge_suffix": " [NOVO]",
"grpb_preview_msg_payment_through": "O pagamento foi concluído? 🙂",
"grpb_preview_msg_sending_rest": "Enviando o restante agora 👍",
"grpb_preview_msg_yep_confirmed": "Sim — acabei de confirmar ✅",
"grpb_selected_suffix": "\n(selecionado)",
"grpb_tooltip_address_balance": "%s\nSaldo: %.8f %s%s",
"grpb_undo_clear": "Desfazer Limpar",
"grpc_benchmark_inconclusive": "Benchmark inconclusivo: nenhuma amostra de taxa de hash foi registrada. Verifique a conexão com a pool e tente novamente.",
"grpc_benchmark_takes_secs": "O benchmark leva cerca de %ds e interrompe a mineração. Clique novamente para iniciar.",
"grpc_bootstrap_failed": "Falha no bootstrap",
"grpc_bootstrap_not_initialized": "Bootstrap não inicializado",
"grpc_hashrate_fee": "%s %s%% de taxa",
"grpc_key_not_available": "Chave não disponível para este endereço",
"grpc_na": "N/D",
"height": "Altura",
"help": "Ajuda",
"hidden_tag": " (oculto)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Aguardando dragonxd — %s",
"sb_warming_up": "Aquecendo...",
"sb_witness_cache": "Reconstruindo testemunhas",
"scale_effects": "ESCALA E EFEITOS",
"screenshot_open_dir": "Abrir local",
"screenshot_sweep": "Executar varredura de capturas de tela",
"screenshot_sweep_desc": "Percorre cada tema em cada aba e salva uma captura de tela de cada um em subpastas por aba dentro da pasta de capturas de tela do diretório de configuração (sobrescrevendo a varredura anterior). É executado por alguns segundos.",
"screenshot_sweep_full": "Varredura completa da interface",
"search_icons": "Pesquisar ícones...",
"search_placeholder": "Pesquisar...",
"sec_changing_passphrase": "Alterando a frase-senha...",
"sec_changing_pin": "Alterando o PIN...",
"sec_couldnt_lock_wallet": "Não foi possível bloquear a carteira — ela continua desbloqueada. Verifique a conexão com o daemon.",
"sec_encrypted_backup_suffix": "\nBackup criptografado: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Criptografando a carteira...",
"sec_encryption_did_not_complete": "A criptografia da carteira não foi concluída — sua carteira NÃO está criptografada. Abra as Configurações para terminar de criptografá-la.",
"sec_encryption_failed_prefix": "Falha na criptografia: ",
"sec_failed_prefix": "Falhou: ",
"sec_failed_to_create_vault": "Falha ao criar o cofre",
"sec_importing_keys_rescanning": "Importando chaves e reescaneando a blockchain — a carteira pode ser usada durante o processo",
"sec_incorrect_current_pin": "PIN atual incorreto",
"sec_incorrect_passphrase_decrypt": "Frase-senha incorreta",
"sec_incorrect_passphrase_pin_setup": "Frase-senha incorreta",
"sec_incorrect_pin_remove": "PIN incorreto",
"sec_internal_error_change_pin": "Erro interno",
"sec_internal_error_remove_pin": "Erro interno",
"sec_mode_passphrase": " Frase-senha",
"sec_not_connected_to_daemon": "Não conectado ao daemon",
"sec_not_connected_to_daemon_pin": "Não conectado ao daemon",
"sec_passphrase_changed_successfully": "Frase-senha alterada com sucesso",
"sec_pin_changed_successfully": "PIN alterado com sucesso",
"sec_pin_removed": "PIN removido",
"sec_pin_set_successfully": "PIN definido com sucesso",
"sec_restart_daemon_for_encryption": "Reinicie o daemon para que a criptografia entre em vigor.",
"sec_too_many_attempts_wait": "Tentativas em excesso. Aguarde %.0f segundos...",
"sec_total_elapsed_fmt": "Tempo total: %dm %02ds",
"sec_unlock_button": "Desbloquear",
"sec_unlock_failed_prefix": "Falha ao desbloquear: ",
"sec_unlocking_fmt": "Desbloqueando%s",
"sec_use_passphrase_instead": "Usar frase-senha",
"sec_use_pin_instead": "Usar PIN",
"sec_verifying_passphrase": "Verificando a frase-senha...",
"sec_verifying_pin": "Verificando o PIN...",
"sec_wallet_decrypted_all_keys_imported": "Carteira descriptografada com sucesso! Todas as chaves importadas.",
"sec_wallet_encrypted_and_pin_set": "Carteira criptografada e PIN definido",
"sec_wallet_encrypted_but_pin_vault_failed": "Carteira criptografada, mas o cofre do PIN falhou",
"sec_wallet_encrypted_restarting_daemon": "Carteira criptografada. Reiniciando o daemon...",
"sec_wallet_encrypted_successfully": "Carteira criptografada com sucesso",
"sec_wallet_locked_title": "Carteira Bloqueada",
"security": "SEGURANÇA",
"seed_backup_button": "Frase de recuperação",
"seed_backup_close": "Fechar",
@@ -1316,12 +1504,12 @@
"settings": "Ajustes",
"settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.",
"settings_acrylic_level": "Nível acrílico:",
"settings_address_book": "Livro de endereços...",
"settings_address_book": "Livro de endereços…",
"settings_auto_detected": "Detectado automaticamente de DRAGONX.conf",
"settings_auto_lock": "BLOQUEIO AUTOMÁTICO",
"settings_auto_shield_desc": "Mover automaticamente fundos transparentes para endereços blindados",
"settings_auto_shield_funds": "Blindar fundos transparentes automaticamente",
"settings_backup": "Backup...",
"settings_backup": "Backup…",
"settings_block_explorer_urls": "URLs do explorador de blocos",
"settings_builtin": "Integrado",
"settings_change_passphrase": "Alterar frase secreta",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Copiar diagnósticos",
"settings_copyright": "Copyright 2024-2026 Desenvolvedores DragonX | Licença GPLv3",
"settings_custom": "Personalizado",
"settings_data_dir": "Dir. de dados:",
"settings_data_dir": "Dir. de dados",
"settings_debug_changed": "Categorias de depuração alteradas — reinicie o daemon para aplicar",
"settings_debug_restart_note": "As alterações entram em vigor após reiniciar o daemon.",
"settings_debug_select": "Selecione categorias para ativar o registro de depuração do daemon (flags -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Encripte a carteira primeiro para ativar o PIN",
"settings_encrypt_wallet": "Encriptar carteira",
"settings_explorer_hint": "As URLs devem incluir uma barra final. O txid/endereço será adicionado.",
"settings_export_all": "Exportar tudo...",
"settings_export_csv": "Exportar CSV...",
"settings_export_key": "Exportar chave...",
"settings_export_all": "Exportar tudo…",
"settings_export_csv": "Exportar CSV…",
"settings_export_key": "Exportar chave…",
"settings_gradient_bg": "Fundo gradiente",
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
"settings_idle_after": "após",
"settings_import_key": "Importar Chave Privada...",
"settings_import_viewkey": "Importar chave de visualização...",
"settings_import_key": "Importar Chave Privada…",
"settings_import_viewkey": "Importar chave de visualização…",
"settings_language_note": "Nota: Alguns textos requerem reinício para atualizar",
"settings_lock_now": "Bloquear agora",
"settings_locked": "Bloqueado",
"settings_merge_to_address": "Fundir para endereço...",
"settings_merge_to_address": "Fundir para endereço…",
"settings_noise_opacity": "Opacidade do ruído:",
"settings_not_connected": "Não conectado ao daemon",
"settings_not_encrypted": "Não encriptado",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Configurações recarregadas do disco",
"settings_remove_encryption": "Remover encriptação",
"settings_remove_pin": "Remover PIN",
"settings_request_payment": "Solicitar pagamento...",
"settings_request_payment": "Solicitar pagamento…",
"settings_rescan_desc": "Reescanear a blockchain em busca de transações ausentes",
"settings_restart_daemon": "Reiniciar daemon",
"settings_rpc_connection": "Conexão RPC",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Salvar histórico de transações blindadas localmente",
"settings_saved": "Configurações salvas",
"settings_set_pin": "Definir PIN",
"settings_shield_mining": "Blindar mineração...",
"settings_shield_mining": "Blindar mineração…",
"settings_solid_colors_desc": "Usar cores sólidas em vez de efeitos de desfoque (acessibilidade)",
"settings_theme_refreshed": "Lista de temas atualizada",
"settings_tor_desc": "Rotear todas as conexões através do Tor para maior privacidade",
"settings_unlocked": "Desbloqueado",
"settings_use_tor_network": "Usar Tor para conexões de rede",
"settings_validate_address": "Validar endereço...",
"settings_validate_address": "Validar endereço…",
"settings_visual_effects": "Efeitos visuais",
"settings_wallet_file_size": "Tamanho do arquivo da carteira: %s",
"settings_wallet_info": "Informações da carteira",
"settings_wallet_location": "Localização da carteira: %s",
"settings_wallet_maintenance": "Manutenção da carteira",
"settings_wallet_not_found": "Arquivo da carteira não encontrado",
"settings_wallet_size_label": "Tamanho da carteira:",
"settings_wallet_size_label": "Tamanho da carteira",
"settings_ztx_cleared": "Histórico de transações Z limpo",
"settings_ztx_not_found": "Nenhum arquivo de histórico encontrado",
"setup_wizard": "Assistente de Configuração",
@@ -1455,6 +1643,17 @@
"sweep_to": "Varrido para:",
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
"sweep_tx": "Transação:",
"swin_connection_failed": "Falha na conexão: ",
"swin_connection_successful": "Conexão bem-sucedida!\nversão do dragonxd: ",
"swin_invalid_suffix": " (inválido)",
"swin_no_history_file_found": "Nenhum arquivo de histórico encontrado",
"swin_rescan_failed": "Falha no reescaneamento: ",
"swin_rescan_started_from_block": "Reescaneamento iniciado a partir do bloco ",
"swin_rescan_to": " até ",
"swin_rpc_client_not_initialized": "Cliente RPC não inicializado",
"swin_settings_saved": "Configurações salvas",
"swin_theme_list_refreshed": "Lista de temas atualizada",
"swin_ztx_history_cleared": "Histórico de transações Z limpo",
"switch_corrupt_body": "Esta carteira parece corrompida — o nó não conseguiu abri-la. Restaure de um backup, recrie-a ou tente repará-la.",
"switch_corrupt_repair": "Tentar reparar (salvage)",
"switch_progress_background": "Continuar em segundo plano",
@@ -1494,6 +1693,7 @@
"to_upper": "PARA",
"tools": "FERRAMENTAS",
"tools_actions": "Ferramentas e Ações...",
"tools_actions_hdr": "FERRAMENTAS E AÇÕES",
"total": "Total",
"total_balance_label": "Saldo Total",
"transaction_id": "ID DA TRANSAÇÃO",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
"tt_backup": "Criar um backup do seu wallet.dat",
"tt_block_explorer": "Abrir o explorador de blocos DragonX no seu navegador",
"tt_blur": "Quantidade de desfoque (0%% = desligado, 100%% = máximo)",
"tt_blur": "Quantidade de desfoque (0% = desligado, 100% = máximo)",
"tt_change_pass": "Alterar a frase secreta de encriptação da carteira",
"tt_change_pin": "Alterar seu PIN de desbloqueio",
"tt_chat_bubble_accent": "Cor de destaque para seus balões de mensagem enviados (ou seguir o tema atual)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
"tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)",
"tt_noise": "Intensidade de textura granulada (0%% = desligado, 100%% = máximo)",
"tt_noise": "Intensidade de textura granulada (0% = desligado, 100% = máximo)",
"tt_open_app_dir": "Abrir a pasta ObsidianDragon (configurações, temas, logs) no gerenciador de arquivos",
"tt_open_data_dir": "Abrir a pasta com os dados da sua carteira e da blockchain no gerenciador de arquivos",
"tt_open_dir": "Clique para abrir no explorador de arquivos",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Atalho: Ctrl+Esquerda/Direita para alternar temas",
"tt_tor": "Rotear conexões do daemon através da rede Tor para anonimato",
"tt_tx_url": "URL base para visualizar transações em um explorador de blocos",
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100%% = totalmente opaco, menor = mais transparente)",
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100% = totalmente opaco, menor = mais transparente)",
"tt_validate": "Verificar se um endereço DragonX é válido",
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
@@ -1831,4 +2031,4 @@
"your_addresses": "Seus Endereços",
"z_address": "Endereço Z",
"z_addresses": "Endereços Z"
}
}
+217 -17
View File
@@ -60,6 +60,80 @@
"amount_label": "Сумма:",
"animate_avatars": "Анимировать аватары",
"appearance": "ВНЕШНИЙ ВИД",
"appx_back": "Назад",
"appx_back_up_seed_phrase_title": "Сохраните seed-фразу",
"appx_birthday_block_height": "Дата создания (высота блока): %llu — сохраните и её.",
"appx_blockchain_data_deleted": "Данные блокчейна удалены (%d элементов). Демон перезапускается для повторной синхронизации с сетью.",
"appx_blockchain_maintenance_in_progress": "Операция обслуживания блокчейна уже выполняется.",
"appx_blockchain_rescan_complete": "Повторное сканирование блокчейна завершено",
"appx_bootstrap_complete_reconciling": "Начальная загрузка завершена — сверка кошелька с новыми данными цепи.",
"appx_cancel": "Отмена",
"appx_cleaning_up": "Очистка...",
"appx_confirm_your_backup": "Подтвердите резервную копию",
"appx_copied_clipboard_autoclears": "Скопировано — буфер обмена очистится через 45 с",
"appx_copy": "Копировать",
"appx_could_not_start_restore": "Не удалось начать восстановление",
"appx_create_failed_prefix": "Ошибка создания: ",
"appx_creating_your_wallet": "Создание кошелька…",
"appx_daemon_error": "Ошибка демона",
"appx_daemon_reinstall_in_progress": "Переустановка демона уже выполняется.",
"appx_disconnecting": "Отключение...",
"appx_done": "Готово",
"appx_dragonxd_output": "вывод dragonxd",
"appx_encrypting_wallet": "Шифрование кошелька...",
"appx_fullnode_lifecycle_unavailable_lite": "Действия жизненного цикла полного узла недоступны в lite-сборке",
"appx_installing_bundled_daemon": "Установка встроенного демона — узел остановится, обновится и перезапустится...",
"appx_invalid_payment_uri_prefix": "Неверный платёжный URI: ",
"appx_ive_written_it_down": "Я записал её",
"appx_keep_node_running_and_quit": "Оставить узел и выйти",
"appx_last_block_n": "Последний блок: %d",
"appx_last_used_wallet_not_found_prefix": "Ваш последний использованный файл кошелька (",
"appx_last_used_wallet_not_found_suffix": ") не найден — вместо него открыт кошелёк по умолчанию. Если вы его переместили, восстановите файл и переключитесь обратно из списка кошельков.",
"appx_low_spec_mode_disabled": "Режим слабого оборудования выключен",
"appx_low_spec_mode_enabled": "Режим слабого оборудования включён",
"appx_miner_stopped_prefix": "Майнер остановлен: ",
"appx_miner_stopped_unexpectedly": "Майнер неожиданно остановился.",
"appx_n_min_n_sec": "%d мин %d сек",
"appx_n_seconds": "%d сек",
"appx_no_bundled_daemon_to_install": "В этой сборке нет встроенного демона для установки",
"appx_no_embedded_daemon_to_install": "В этой сборке нет встроенного демона для установки",
"appx_node_busy_restarting": "Узел занят перезапуском — попробуйте ещё раз через мгновение.",
"appx_node_rebuilding_witness_cache": "Узел перестраивает кэш свидетелей",
"appx_not_next_word": " — это не следующее слово",
"appx_payment_request_loaded": "Запрос на оплату загружен",
"appx_pool_miner_connected_and_hashing": "Пул-майнер подключён и вычисляет хеши.",
"appx_progress_n_of_n": "Прогресс: %d / %d",
"appx_rebuilding_sapling_note_witnesses": "Перестройка свидетелей заметок Sapling…",
"appx_rebuilding_witness_cache_blocks_left": "Перестройка кэша свидетелей %.0f%% — осталось блоков: %d",
"appx_rebuilding_witness_cache_pct": "Перестройка кэша свидетелей %.0f%%",
"appx_recovery_phrase_word_count": "Фраза восстановления должна содержать 24 слова — у вас %d.",
"appx_restarting_daemon_rescan_flag": "Перезапуск демона с флагом -rescan...",
"appx_restarting_daemon_zapwallettxes": "Перезапуск демона с -zapwallettxes=2 (восстановление кошелька)...",
"appx_restoring_your_wallet": "Восстановление кошелька…",
"appx_seed_backup_warning": "Эти 24 слова — ЕДИНСТВЕННЫЙ способ восстановить кошелёк. Запишите их по порядку, храните офлайн и никому не сообщайте. Если вы их потеряете, средства пропадут навсегда.",
"appx_seed_not_backed_up_warning": "Вы не сохранили seed-фразу — средства могут быть потеряны. Всё равно пропустить?",
"appx_sending_stop_command_to_daemon": "Отправка команды остановки демону...",
"appx_setting_initial_sapling_witnesses": "Установка начальных свидетелей Sapling %.0f%%",
"appx_shutdown_complete": "Завершение работы выполнено",
"appx_simple_background_disabled": "Простой фон выключен",
"appx_simple_background_enabled": "Простой фон включён",
"appx_skip": "Пропустить",
"appx_skip_anyway": "Всё равно пропустить",
"appx_still_status_prefix": "Всё ещё \"",
"appx_still_status_suffix": "\" — принудительный выход сейчас может повредить данные цепи.",
"appx_stop_anyway_and_quit": "Всё равно остановить и выйти",
"appx_stopping_daemon_deleting_blockchain": "Остановка демона и удаление данных блокчейна...",
"appx_stopping_node_discards_rebuild": "Остановка узла сейчас отменит текущую перестройку и запустит её заново (несколько минут) при следующем открытии кошелька. Вместо этого можно оставить узел работать.",
"appx_stopping_pool_miner": "Остановка пул-майнера...",
"appx_syncing_pct_block_n_of_n": "Синхронизация %.1f%% — Блок %d / %d",
"appx_tap_words_in_order": "Нажимайте слова в правильном порядке, чтобы подтвердить, что вы их сохранили.",
"appx_theme_effects_disabled": "Эффекты темы выключены",
"appx_theme_effects_enabled": "Эффекты темы включены",
"appx_theme_prefix": "Тема: ",
"appx_use_settings_restart_daemon_hint": "Откройте Настройки > Перезапустить демон, чтобы повторить попытку",
"appx_waiting_for_daemon_to_encrypt_wallet": "Ожидание шифрования кошелька демоном...",
"appx_wallet_created_and_backed_up": "Кошелёк создан, резервная копия сохранена.",
"appx_wallet_open_failed_prefix": "Не удалось открыть кошелёк: ",
"auto_shield": "Авто-экранирование майнинга",
"av_intro": "Программы для майнинга часто помечаются как потенциально нежелательные. Выполните эти шаги, чтобы включить пул-майнинг:",
"av_open_security": "Открыть Безопасность Windows",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender заблокировал майнер",
"available": "Доступно",
"backup_backing_up": "Создание резервной копии...",
"backup_col_backup": "РЕЗЕРВНАЯ КОПИЯ",
"backup_col_export": "ЭКСПОРТ",
"backup_col_import": "ИМПОРТ И ВОССТАНОВЛЕНИЕ",
"backup_create": "Создать резервную копию",
"backup_created": "Резервная копия кошелька создана",
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "Экранировано: %.8f",
"balance_syncing_pct": "Синхронизация %.1f%%",
"balance_transparent_fmt": "Прозрачный: %.8f",
"baltab_market": "Рынок",
"baltab_market_price_4dp": "Рынок: $%.4f",
"baltab_market_price_8dp": "Рынок: $%.8f",
"baltab_pct_change_24h": "%s%.1f%% за 24ч",
"baltab_pct_of_total_zaddr": "%.0f%% от общего · %d Z-addr",
"baltab_shielded": "Скрытый",
"baltab_shielded_amount": "Скрытый %.8f",
"baltab_t_addresses_count": "%d T-адресов",
"baltab_total_balance": "Общий баланс",
"baltab_transparent": "Прозрачный",
"baltab_transparent_amount": "Прозрачный %.8f",
"ban": "Заблокировать",
"banned_peers": "Заблокированные узлы",
"block": "Блок",
@@ -411,6 +499,7 @@
"contacts_shape_square": "Квадрат",
"contacts_shape_tab": "Вкладка",
"copied": "Скопировано!",
"copied_to_clipboard": "Скопировано в буфер обмена",
"copy": "Копировать",
"copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку",
@@ -423,6 +512,7 @@
"daemon_bundled": "Встроенный",
"daemon_install_bundled": "Установить встроенную",
"daemon_installed": "Установлено",
"daemon_maintenance_label": "ОБСЛУЖИВАНИЕ",
"daemon_none_bundled": "нет в этой сборке",
"daemon_not_installed": "не установлен",
"daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.",
"daemon_update_verifying": "Проверка…",
"daemon_update_version": "Версия:",
"daemon_updates_label": "ОБНОВЛЕНИЯ",
"daemon_version": "Демон",
"dark": "Тёмная",
"data_stale_prefix": "Обновлено",
@@ -588,6 +679,63 @@
"general": "Общие",
"generating": "Генерация",
"go_to_receive": "Перейти к получению",
"grpa_current_block_paren": "(Текущий: %d)",
"grpa_days_ago": "%lld дн назад",
"grpa_dbg_addrman": "Отслеживание и управление адресами узлов",
"grpa_dbg_alert": "Сообщения системы оповещений",
"grpa_dbg_bench": "Замеры производительности операций",
"grpa_dbg_coindb": "Операции чтения/записи базы монет",
"grpa_dbg_db": "Операции Berkeley DB",
"grpa_dbg_estimatefee": "Алгоритм оценки комиссии",
"grpa_dbg_http": "Активность HTTP RPC-сервера",
"grpa_dbg_libevent": "Сетевая библиотека Libevent",
"grpa_dbg_lock": "Отладка конкуренции блокировок",
"grpa_dbg_mempool": "Активность пула транзакций в памяти",
"grpa_dbg_net": "Сетевые соединения и сообщения",
"grpa_dbg_paymentdisclosure": "Протокол раскрытия платежей",
"grpa_dbg_pow": "Активность майнинга proof-of-work",
"grpa_dbg_proxy": "Соединения через прокси SOCKS5",
"grpa_dbg_prune": "Операции обрезки блоков",
"grpa_dbg_rand": "Генерация случайных чисел",
"grpa_dbg_reindex": "Прогресс переиндексации блокчейна",
"grpa_dbg_rpc": "Обработка RPC-команд",
"grpa_dbg_selectcoins": "Выбор монет для транзакций",
"grpa_dbg_tor": "Интеграция Tor и сведения о цепочках",
"grpa_dbg_zmq": "Система уведомлений ZeroMQ",
"grpa_dbg_zrpc": "Скрытые (z-addr) RPC-операции",
"grpa_enter_private_key_to_import": "Введите приватный ключ для импорта.",
"grpa_error_prefix": "Ошибка: ",
"grpa_hr_ago": "%lld ч назад",
"grpa_invalid_response_from_daemon": "Неверный ответ от демона",
"grpa_invalid_suffix": " (неверный)",
"grpa_min_ago": "%lld мин назад",
"grpa_sec_ago": "%lld сек назад",
"grpa_seed_demo_chat": "Демо-чат seed",
"grpa_showing_first_100_of": "... показаны первые 100 из %d",
"grpa_tab_about": "О программе",
"grpa_tab_appearance": "Внешний вид",
"grpa_tab_backup_data": "Резервное копирование и данные",
"grpa_tab_chat": "Чат",
"grpa_tab_explorer": "Обозреватель",
"grpa_tab_node_security": "Узел и безопасность",
"grpa_tab_wallet": "Кошелёк",
"grpa_unexpected_getblockhash_result": "неожиданный результат getblockhash",
"grpb_copy": "Копировать",
"grpb_max": "Макс",
"grpb_new_badge_suffix": " [НОВОЕ]",
"grpb_preview_msg_payment_through": "Платёж прошёл? 🙂",
"grpb_preview_msg_sending_rest": "Отправляю остаток 👍",
"grpb_preview_msg_yep_confirmed": "Да — только что подтвердил ✅",
"grpb_selected_suffix": "\n(выбрано)",
"grpb_tooltip_address_balance": "%s\nБаланс: %.8f %s%s",
"grpb_undo_clear": "Отменить очистку",
"grpc_benchmark_inconclusive": "Бенчмарк не дал результата: не записано ни одной выборки хешрейта. Проверьте соединение с пулом и повторите попытку.",
"grpc_benchmark_takes_secs": "Бенчмарк занимает ~%dс и прерывает майнинг. Нажмите ещё раз для запуска.",
"grpc_bootstrap_failed": "Ошибка начальной загрузки",
"grpc_bootstrap_not_initialized": "Начальная загрузка не инициализирована",
"grpc_hashrate_fee": "%s комиссия %s%%",
"grpc_key_not_available": "Ключ недоступен для этого адреса",
"grpc_na": "Н/Д",
"height": "Высота",
"help": "Справка",
"hidden_tag": " (скрыт)",
@@ -1228,12 +1376,52 @@
"sb_waiting_daemon_err": "Ожидание dragonxd — %s",
"sb_warming_up": "Прогрев...",
"sb_witness_cache": "Перестроение свидетелей",
"scale_effects": "МАСШТАБ И ЭФФЕКТЫ",
"screenshot_open_dir": "Открыть расположение",
"screenshot_sweep": "Запустить прогон скриншотов",
"screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.",
"screenshot_sweep_full": "Полный обход интерфейса",
"search_icons": "Поиск значков...",
"search_placeholder": "Поиск...",
"sec_changing_passphrase": "Смена парольной фразы...",
"sec_changing_pin": "Смена PIN...",
"sec_couldnt_lock_wallet": "Не удалось заблокировать кошелёк — он всё ещё разблокирован. Проверьте соединение с демоном.",
"sec_encrypted_backup_suffix": "\nЗашифрованная резервная копия: wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "Шифрование кошелька...",
"sec_encryption_did_not_complete": "Шифрование кошелька не завершено — ваш кошелёк НЕ зашифрован. Откройте настройки, чтобы завершить шифрование.",
"sec_encryption_failed_prefix": "Ошибка шифрования: ",
"sec_failed_prefix": "Ошибка: ",
"sec_failed_to_create_vault": "Не удалось создать хранилище",
"sec_importing_keys_rescanning": "Импорт ключей и повторное сканирование блокчейна — кошелёк доступен во время выполнения",
"sec_incorrect_current_pin": "Неверный текущий PIN",
"sec_incorrect_passphrase_decrypt": "Неверная парольная фраза",
"sec_incorrect_passphrase_pin_setup": "Неверная парольная фраза",
"sec_incorrect_pin_remove": "Неверный PIN",
"sec_internal_error_change_pin": "Внутренняя ошибка",
"sec_internal_error_remove_pin": "Внутренняя ошибка",
"sec_mode_passphrase": " Парольная фраза",
"sec_not_connected_to_daemon": "Нет соединения с демоном",
"sec_not_connected_to_daemon_pin": "Нет соединения с демоном",
"sec_passphrase_changed_successfully": "Парольная фраза успешно изменена",
"sec_pin_changed_successfully": "PIN успешно изменён",
"sec_pin_removed": "PIN удалён",
"sec_pin_set_successfully": "PIN успешно задан",
"sec_restart_daemon_for_encryption": "Перезапустите демон, чтобы шифрование вступило в силу.",
"sec_too_many_attempts_wait": "Слишком много попыток. Подождите %.0f сек...",
"sec_total_elapsed_fmt": "Всего прошло: %dм %02dс",
"sec_unlock_button": "Разблокировать",
"sec_unlock_failed_prefix": "Ошибка разблокировки: ",
"sec_unlocking_fmt": "Разблокировка%s",
"sec_use_passphrase_instead": "Использовать парольную фразу",
"sec_use_pin_instead": "Использовать PIN",
"sec_verifying_passphrase": "Проверка парольной фразы...",
"sec_verifying_pin": "Проверка PIN...",
"sec_wallet_decrypted_all_keys_imported": "Кошелёк успешно расшифрован! Все ключи импортированы.",
"sec_wallet_encrypted_and_pin_set": "Кошелёк зашифрован, PIN задан",
"sec_wallet_encrypted_but_pin_vault_failed": "Кошелёк зашифрован, но не удалось создать хранилище PIN",
"sec_wallet_encrypted_restarting_daemon": "Кошелёк зашифрован. Перезапуск демона...",
"sec_wallet_encrypted_successfully": "Кошелёк успешно зашифрован",
"sec_wallet_locked_title": "Кошелёк заблокирован",
"security": "БЕЗОПАСНОСТЬ",
"seed_backup_button": "Сид-фраза",
"seed_backup_close": "Закрыть",
@@ -1316,12 +1504,12 @@
"settings": "Настройки",
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
"settings_acrylic_level": "Уровень акрила:",
"settings_address_book": "Адресная книга...",
"settings_address_book": "Адресная книга…",
"settings_auto_detected": "Автоопределено из DRAGONX.conf",
"settings_auto_lock": "АВТОБЛОКИРОВКА",
"settings_auto_shield_desc": "Автоматически перемещать прозрачные средства на экранированные адреса",
"settings_auto_shield_funds": "Автоматически экранировать прозрачные средства",
"settings_backup": "Резервная копия...",
"settings_backup": "Резервная копия…",
"settings_block_explorer_urls": "URL-адреса обозревателя блоков",
"settings_builtin": "Встроенные",
"settings_change_passphrase": "Сменить пароль",
@@ -1335,7 +1523,7 @@
"settings_copy_diagnostics": "Копировать диагностику",
"settings_copyright": "Copyright 2024-2026 Разработчики DragonX | Лицензия GPLv3",
"settings_custom": "Пользовательские",
"settings_data_dir": "Каталог данных:",
"settings_data_dir": "Каталог данных",
"settings_debug_changed": "Категории отладки изменены — перезапустите демон для применения",
"settings_debug_restart_note": "Изменения вступают в силу после перезапуска демона.",
"settings_debug_select": "Выберите категории для включения журнала отладки демона (флаги -debug=).",
@@ -1343,18 +1531,18 @@
"settings_encrypt_first_pin": "Сначала зашифруйте кошелёк, чтобы включить PIN",
"settings_encrypt_wallet": "Зашифровать кошелёк",
"settings_explorer_hint": "URL-адреса должны заканчиваться косой чертой. Txid/адрес будет добавлен.",
"settings_export_all": "Экспортировать все...",
"settings_export_csv": "Экспорт CSV...",
"settings_export_key": "Экспортировать ключ...",
"settings_export_all": "Экспортировать все…",
"settings_export_csv": "Экспорт CSV…",
"settings_export_key": "Экспортировать ключ…",
"settings_gradient_bg": "Градиент фона",
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
"settings_idle_after": "через",
"settings_import_key": "Импорт приватного ключа...",
"settings_import_viewkey": "Импортировать ключ просмотра...",
"settings_import_key": "Импорт приватного ключа…",
"settings_import_viewkey": "Импортировать ключ просмотра…",
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
"settings_lock_now": "Заблокировать сейчас",
"settings_locked": "Заблокирован",
"settings_merge_to_address": "Объединить на адрес...",
"settings_merge_to_address": "Объединить на адрес…",
"settings_noise_opacity": "Непрозрачность шума:",
"settings_not_connected": "Нет соединения с демоном",
"settings_not_encrypted": "Не зашифрован",
@@ -1370,7 +1558,7 @@
"settings_reloaded": "Настройки перезагружены с диска",
"settings_remove_encryption": "Удалить шифрование",
"settings_remove_pin": "Удалить PIN",
"settings_request_payment": "Запросить платёж...",
"settings_request_payment": "Запросить платёж…",
"settings_rescan_desc": "Пересканировать блокчейн для поиска пропущенных транзакций",
"settings_restart_daemon": "Перезапустить демон",
"settings_rpc_connection": "RPC-соединение",
@@ -1381,20 +1569,20 @@
"settings_save_shielded_local": "Сохранять историю защищённых транзакций локально",
"settings_saved": "Настройки сохранены",
"settings_set_pin": "Установить PIN",
"settings_shield_mining": "Экранировать майнинг...",
"settings_shield_mining": "Экранировать майнинг…",
"settings_solid_colors_desc": "Использовать сплошные цвета вместо эффектов размытия (доступность)",
"settings_theme_refreshed": "Список тем обновлён",
"settings_tor_desc": "Маршрутизировать все соединения через Tor для повышения конфиденциальности",
"settings_unlocked": "Разблокирован",
"settings_use_tor_network": "Использовать Tor для сетевых подключений",
"settings_validate_address": "Проверить адрес...",
"settings_validate_address": "Проверить адрес…",
"settings_visual_effects": "Визуальные эффекты",
"settings_wallet_file_size": "Размер файла кошелька: %s",
"settings_wallet_info": "Информация о кошельке",
"settings_wallet_location": "Расположение кошелька: %s",
"settings_wallet_maintenance": "Обслуживание кошелька",
"settings_wallet_not_found": "Файл кошелька не найден",
"settings_wallet_size_label": "Размер кошелька:",
"settings_wallet_size_label": "Размер кошелька",
"settings_ztx_cleared": "История Z-транзакций очищена",
"settings_ztx_not_found": "Файл истории не найден",
"setup_wizard": "Мастер настройки",
@@ -1455,6 +1643,17 @@
"sweep_to": "Переведено на:",
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
"sweep_tx": "Транзакция:",
"swin_connection_failed": "Ошибка соединения: ",
"swin_connection_successful": "Соединение установлено!\nВерсия dragonxd: ",
"swin_invalid_suffix": " (неверный)",
"swin_no_history_file_found": "Файл истории не найден",
"swin_rescan_failed": "Ошибка повторного сканирования: ",
"swin_rescan_started_from_block": "Повторное сканирование начато с блока ",
"swin_rescan_to": " до ",
"swin_rpc_client_not_initialized": "RPC-клиент не инициализирован",
"swin_settings_saved": "Настройки сохранены",
"swin_theme_list_refreshed": "Список тем обновлён",
"swin_ztx_history_cleared": "История Z-транзакций очищена",
"switch_corrupt_body": "Похоже, этот кошелёк повреждён — узел не смог его открыть. Восстановите из резервной копии, создайте заново или попробуйте восстановить.",
"switch_corrupt_repair": "Попробовать восстановить (salvage)",
"switch_progress_background": "Продолжить в фоне",
@@ -1494,6 +1693,7 @@
"to_upper": "КОМУ",
"tools": "УТИЛИТЫ",
"tools_actions": "Инструменты и действия...",
"tools_actions_hdr": "ИНСТРУМЕНТЫ И ДЕЙСТВИЯ",
"total": "Итого",
"total_balance_label": "Общий баланс",
"transaction_id": "ID ТРАНЗАКЦИИ",
@@ -1517,7 +1717,7 @@
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
"tt_backup": "Создать резервную копию вашего wallet.dat",
"tt_block_explorer": "Открыть обозреватель блоков DragonX в браузере",
"tt_blur": "Степень размытия (0%% = выкл., 100%% = максимум)",
"tt_blur": "Степень размытия (0% = выкл., 100% = максимум)",
"tt_change_pass": "Сменить пароль шифрования кошелька",
"tt_change_pin": "Изменить PIN-код разблокировки",
"tt_chat_bubble_accent": "Акцентный цвет для ваших исходящих пузырьков сообщений (или следовать текущей теме)",
@@ -1580,7 +1780,7 @@
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
"tt_merge": "Объединить несколько UTXO в один адрес",
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)",
"tt_noise": "Интенсивность зернистой текстуры (0% = выкл., 100% = максимум)",
"tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере",
"tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна",
"tt_open_dir": "Нажмите, чтобы открыть в проводнике",
@@ -1619,7 +1819,7 @@
"tt_theme_hotkey": "Горячая клавиша: Ctrl+Влево/Вправо для переключения тем",
"tt_tor": "Маршрутизировать подключения демона через сеть Tor для анонимности",
"tt_tx_url": "Базовый URL для просмотра транзакций в обозревателе блоков",
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100%% = полностью непрозрачно, ниже = прозрачнее)",
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100% = полностью непрозрачно, ниже = прозрачнее)",
"tt_validate": "Проверить, действителен ли адрес DragonX",
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
@@ -1831,4 +2031,4 @@
"your_addresses": "Ваши адреса",
"z_address": "Z-адрес",
"z_addresses": "Z-адреса"
}
}
+215 -15
View File
@@ -60,6 +60,80 @@
"amount_label": "金额:",
"animate_avatars": "动画头像",
"appearance": "外观",
"appx_back": "返回",
"appx_back_up_seed_phrase_title": "备份你的助记词",
"appx_birthday_block_height": "生日(区块高度):%llu — 也请一并备份。",
"appx_blockchain_data_deleted": "区块链数据已删除(%d 项)。守护进程正在重启,以从网络重新同步。",
"appx_blockchain_maintenance_in_progress": "已有区块链维护操作正在进行中。",
"appx_blockchain_rescan_complete": "区块链重新扫描完成",
"appx_bootstrap_complete_reconciling": "引导数据完成——正在将你的钱包与新的链数据进行核对。",
"appx_cancel": "取消",
"appx_cleaning_up": "正在清理…",
"appx_confirm_your_backup": "确认你的备份",
"appx_copied_clipboard_autoclears": "已复制——剪贴板将在 45 秒后自动清空",
"appx_copy": "复制",
"appx_could_not_start_restore": "无法开始恢复",
"appx_create_failed_prefix": "创建失败:",
"appx_creating_your_wallet": "正在创建你的钱包…",
"appx_daemon_error": "守护进程错误",
"appx_daemon_reinstall_in_progress": "守护进程重新安装已在进行中。",
"appx_disconnecting": "正在断开连接…",
"appx_done": "完成",
"appx_dragonxd_output": "dragonxd 输出",
"appx_encrypting_wallet": "正在加密钱包…",
"appx_fullnode_lifecycle_unavailable_lite": "全节点生命周期操作在轻量版中不可用",
"appx_installing_bundled_daemon": "正在安装捆绑守护进程——节点将停止、更新并重启…",
"appx_invalid_payment_uri_prefix": "无效的支付 URI:",
"appx_ive_written_it_down": "我已抄写完毕",
"appx_keep_node_running_and_quit": "保持节点运行并退出",
"appx_last_block_n": "最新区块:%d",
"appx_last_used_wallet_not_found_prefix": "找不到你上次使用的钱包文件(",
"appx_last_used_wallet_not_found_suffix": ")——已改为打开默认钱包。如果你移动了它,请恢复该文件,然后从钱包列表中切换回来。",
"appx_low_spec_mode_disabled": "已禁用低配模式",
"appx_low_spec_mode_enabled": "已启用低配模式",
"appx_miner_stopped_prefix": "矿工已停止:",
"appx_miner_stopped_unexpectedly": "矿工意外停止。",
"appx_n_min_n_sec": "%d 分 %d 秒",
"appx_n_seconds": "%d 秒",
"appx_no_bundled_daemon_to_install": "此版本没有可安装的捆绑守护进程",
"appx_no_embedded_daemon_to_install": "此版本没有可安装的内嵌守护进程",
"appx_node_busy_restarting": "节点正忙于重启——请稍后再试。",
"appx_node_rebuilding_witness_cache": "节点正在重建其见证缓存",
"appx_not_next_word": " — 这不是下一个词",
"appx_payment_request_loaded": "支付请求已加载",
"appx_pool_miner_connected_and_hashing": "矿池矿工已连接并正在计算哈希。",
"appx_progress_n_of_n": "进度:%d / %d",
"appx_rebuilding_sapling_note_witnesses": "正在重建 Sapling 票据见证…",
"appx_rebuilding_witness_cache_blocks_left": "正在重建见证缓存 %.0f%% —— 剩余 %d 个区块",
"appx_rebuilding_witness_cache_pct": "正在重建见证缓存 %.0f%%",
"appx_recovery_phrase_word_count": "助记词应为 24 个——你输入了 %d 个。",
"appx_restarting_daemon_rescan_flag": "正在以 -rescan 标志重启守护进程…",
"appx_restarting_daemon_zapwallettxes": "正在以 -zapwallettxes=2 重启守护进程(钱包修复)…",
"appx_restoring_your_wallet": "正在恢复你的钱包…",
"appx_seed_backup_warning": "这 24 个词是恢复钱包的唯一方式。请按顺序抄写、离线保存,切勿泄露给他人。一旦丢失,你的资金将永远无法找回。",
"appx_seed_not_backed_up_warning": "你尚未备份助记词——资金可能丢失。仍要跳过吗?",
"appx_sending_stop_command_to_daemon": "正在向守护进程发送停止命令…",
"appx_setting_initial_sapling_witnesses": "正在设置初始 Sapling 见证 %.0f%%",
"appx_shutdown_complete": "关闭完成",
"appx_simple_background_disabled": "已禁用简约背景",
"appx_simple_background_enabled": "已启用简约背景",
"appx_skip": "跳过",
"appx_skip_anyway": "仍要跳过",
"appx_still_status_prefix": "仍处于“",
"appx_still_status_suffix": "”——现在强制退出可能损坏链数据。",
"appx_stop_anyway_and_quit": "仍要停止并退出",
"appx_stopping_daemon_deleting_blockchain": "正在停止守护进程并删除区块链数据…",
"appx_stopping_node_discards_rebuild": "现在停止节点会丢弃正在进行的重建,下次打开钱包时将重新开始(需要几分钟)。你也可以让节点继续运行。",
"appx_stopping_pool_miner": "正在停止矿池矿工…",
"appx_syncing_pct_block_n_of_n": "正在同步 %.1f%% —— 区块 %d / %d",
"appx_tap_words_in_order": "按正确顺序点击这些词,以确认你已保存。",
"appx_theme_effects_disabled": "已禁用主题特效",
"appx_theme_effects_enabled": "已启用主题特效",
"appx_theme_prefix": "主题:",
"appx_use_settings_restart_daemon_hint": "请使用 设置 > 重启守护进程 重试",
"appx_waiting_for_daemon_to_encrypt_wallet": "正在等待守护进程加密钱包…",
"appx_wallet_created_and_backed_up": "钱包已创建并完成备份。",
"appx_wallet_open_failed_prefix": "钱包打开失败:",
"auto_shield": "自动屏蔽挖矿",
"av_intro": "挖矿软件经常被标记为潜在有害程序。请按照以下步骤启用矿池挖矿:",
"av_open_security": "打开 Windows 安全中心",
@@ -74,6 +148,9 @@
"av_title": "Windows Defender 已阻止矿工程序",
"available": "可用",
"backup_backing_up": "正在备份...",
"backup_col_backup": "备份",
"backup_col_export": "导出",
"backup_col_import": "导入与恢复",
"backup_create": "创建备份",
"backup_created": "钱包备份已创建",
"backup_data": "备份与数据",
@@ -97,6 +174,17 @@
"balance_shielded_fmt": "屏蔽:%.8f",
"balance_syncing_pct": "同步中 %.1f%%",
"balance_transparent_fmt": "透明:%.8f",
"baltab_market": "市价",
"baltab_market_price_4dp": "市价:$%.4f",
"baltab_market_price_8dp": "市价:$%.8f",
"baltab_pct_change_24h": "%s%.1f%% 24小时",
"baltab_pct_of_total_zaddr": "占总额 %.0f%% · %d 个 Z-addr",
"baltab_shielded": "隐蔽",
"baltab_shielded_amount": "隐蔽 %.8f",
"baltab_t_addresses_count": "%d 个 T-address",
"baltab_total_balance": "总余额",
"baltab_transparent": "透明",
"baltab_transparent_amount": "透明 %.8f",
"ban": "封禁",
"banned_peers": "已封禁节点",
"block": "区块",
@@ -411,6 +499,7 @@
"contacts_shape_square": "方形",
"contacts_shape_tab": "左标签",
"copied": "已复制!",
"copied_to_clipboard": "已复制到剪贴板",
"copy": "复制",
"copy_address": "复制完整地址",
"copy_error": "复制错误",
@@ -423,6 +512,7 @@
"daemon_bundled": "内置",
"daemon_install_bundled": "安装内置版本",
"daemon_installed": "已安装",
"daemon_maintenance_label": "维护",
"daemon_none_bundled": "此版本未内置",
"daemon_not_installed": "未安装",
"daemon_status_differ": "已安装的程序文件与内置版本不同。",
@@ -458,6 +548,7 @@
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
"daemon_update_verifying": "正在验证…",
"daemon_update_version": "版本:",
"daemon_updates_label": "更新",
"daemon_version": "守护进程",
"dark": "深色",
"data_stale_prefix": "更新于",
@@ -588,6 +679,63 @@
"general": "常规",
"generating": "正在生成",
"go_to_receive": "前往接收",
"grpa_current_block_paren": "(当前:%d)",
"grpa_days_ago": "%lld 天前",
"grpa_dbg_addrman": "对等节点地址追踪与管理",
"grpa_dbg_alert": "警报系统消息",
"grpa_dbg_bench": "操作的基准计时",
"grpa_dbg_coindb": "币数据库读写操作",
"grpa_dbg_db": "Berkeley DB 操作",
"grpa_dbg_estimatefee": "手续费估算算法",
"grpa_dbg_http": "HTTP RPC 服务器活动",
"grpa_dbg_libevent": "Libevent 网络库",
"grpa_dbg_lock": "锁竞争调试",
"grpa_dbg_mempool": "交易内存池活动",
"grpa_dbg_net": "网络连接与消息",
"grpa_dbg_paymentdisclosure": "支付披露协议",
"grpa_dbg_pow": "工作量证明挖矿活动",
"grpa_dbg_proxy": "SOCKS5 代理连接",
"grpa_dbg_prune": "区块修剪操作",
"grpa_dbg_rand": "随机数生成",
"grpa_dbg_reindex": "区块链重新索引进度",
"grpa_dbg_rpc": "RPC 命令处理",
"grpa_dbg_selectcoins": "交易的币选择",
"grpa_dbg_tor": "Tor 集成与线路信息",
"grpa_dbg_zmq": "ZeroMQ 通知系统",
"grpa_dbg_zrpc": "隐蔽(z-addr)RPC 操作",
"grpa_enter_private_key_to_import": "请输入要导入的私钥。",
"grpa_error_prefix": "错误:",
"grpa_hr_ago": "%lld 小时前",
"grpa_invalid_response_from_daemon": "守护进程返回了无效响应",
"grpa_invalid_suffix": "(无效)",
"grpa_min_ago": "%lld 分钟前",
"grpa_sec_ago": "%lld 秒前",
"grpa_seed_demo_chat": "种子演示聊天",
"grpa_showing_first_100_of": "…显示 %d 项中的前 100 项",
"grpa_tab_about": "关于",
"grpa_tab_appearance": "外观",
"grpa_tab_backup_data": "备份与数据",
"grpa_tab_chat": "聊天",
"grpa_tab_explorer": "浏览器",
"grpa_tab_node_security": "节点与安全",
"grpa_tab_wallet": "钱包",
"grpa_unexpected_getblockhash_result": "意外的 getblockhash 结果",
"grpb_copy": "复制",
"grpb_max": "最大",
"grpb_new_badge_suffix": " [新]",
"grpb_preview_msg_payment_through": "付款到账了吗?🙂",
"grpb_preview_msg_sending_rest": "现在把剩下的发过去 👍",
"grpb_preview_msg_yep_confirmed": "到了——刚刚确认 ✅",
"grpb_selected_suffix": "\n(已选)",
"grpb_tooltip_address_balance": "%s\n余额:%.8f %s%s",
"grpb_undo_clear": "撤销清除",
"grpc_benchmark_inconclusive": "基准测试无结果:未记录到任何算力样本。请检查矿池连接后重试。",
"grpc_benchmark_takes_secs": "基准测试约需 %d 秒并会中断挖矿。再次点击以开始。",
"grpc_bootstrap_failed": "引导失败",
"grpc_bootstrap_not_initialized": "引导未初始化",
"grpc_hashrate_fee": "%s %s%% 手续费",
"grpc_key_not_available": "此地址无可用密钥",
"grpc_na": "不适用",
"height": "高度",
"help": "帮助",
"hidden_tag": " (已隐藏)",
@@ -1226,12 +1374,52 @@
"sb_waiting_daemon_err": "等待 dragonxd — %s",
"sb_warming_up": "正在预热...",
"sb_witness_cache": "正在重建见证",
"scale_effects": "缩放与效果",
"screenshot_open_dir": "打开位置",
"screenshot_sweep": "运行截图批处理",
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
"screenshot_sweep_full": "完整 UI 遍历",
"search_icons": "搜索图标...",
"search_placeholder": "搜索...",
"sec_changing_passphrase": "正在更改密码短语…",
"sec_changing_pin": "正在更改 PIN…",
"sec_couldnt_lock_wallet": "无法锁定钱包——它仍处于解锁状态。请检查守护进程连接。",
"sec_encrypted_backup_suffix": "\n加密备份:wallet.dat.encrypted.bak",
"sec_encrypting_wallet": "正在加密钱包…",
"sec_encryption_did_not_complete": "钱包加密未完成——你的钱包尚未加密。请打开设置以完成加密。",
"sec_encryption_failed_prefix": "加密失败:",
"sec_failed_prefix": "失败:",
"sec_failed_to_create_vault": "创建保险库失败",
"sec_importing_keys_rescanning": "正在导入密钥并重新扫描区块链——期间钱包仍可使用",
"sec_incorrect_current_pin": "当前 PIN 错误",
"sec_incorrect_passphrase_decrypt": "密码短语错误",
"sec_incorrect_passphrase_pin_setup": "密码短语错误",
"sec_incorrect_pin_remove": "PIN 错误",
"sec_internal_error_change_pin": "内部错误",
"sec_internal_error_remove_pin": "内部错误",
"sec_mode_passphrase": " 密码短语",
"sec_not_connected_to_daemon": "未连接到守护进程",
"sec_not_connected_to_daemon_pin": "未连接到守护进程",
"sec_passphrase_changed_successfully": "密码短语更改成功",
"sec_pin_changed_successfully": "PIN 更改成功",
"sec_pin_removed": "PIN 已移除",
"sec_pin_set_successfully": "PIN 设置成功",
"sec_restart_daemon_for_encryption": "请重启守护进程以使加密生效。",
"sec_too_many_attempts_wait": "尝试次数过多。请等待 %.0f 秒…",
"sec_total_elapsed_fmt": "总耗时:%d 分 %02d 秒",
"sec_unlock_button": "解锁",
"sec_unlock_failed_prefix": "解锁失败:",
"sec_unlocking_fmt": "正在解锁%s",
"sec_use_passphrase_instead": "改用密码短语",
"sec_use_pin_instead": "改用 PIN",
"sec_verifying_passphrase": "正在验证密码短语…",
"sec_verifying_pin": "正在验证 PIN…",
"sec_wallet_decrypted_all_keys_imported": "钱包解密成功!所有密钥已导入。",
"sec_wallet_encrypted_and_pin_set": "钱包已加密并已设置 PIN",
"sec_wallet_encrypted_but_pin_vault_failed": "钱包已加密,但 PIN 保险库创建失败",
"sec_wallet_encrypted_restarting_daemon": "钱包已加密。正在重启守护进程…",
"sec_wallet_encrypted_successfully": "钱包加密成功",
"sec_wallet_locked_title": "钱包已锁定",
"security": "安全",
"seed_backup_button": "助记词",
"seed_backup_close": "关闭",
@@ -1314,12 +1502,12 @@
"settings": "设置",
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
"settings_acrylic_level": "亚克力级别:",
"settings_address_book": "地址簿...",
"settings_address_book": "地址簿…",
"settings_auto_detected": "从 DRAGONX.conf 自动检测",
"settings_auto_lock": "自动锁定",
"settings_auto_shield_desc": "自动将透明资金转移到屏蔽地址",
"settings_auto_shield_funds": "自动屏蔽透明资金",
"settings_backup": "备份...",
"settings_backup": "备份…",
"settings_block_explorer_urls": "区块浏览器网址",
"settings_builtin": "内置",
"settings_change_passphrase": "更改密码",
@@ -1341,18 +1529,18 @@
"settings_encrypt_first_pin": "请先加密钱包以启用 PIN",
"settings_encrypt_wallet": "加密钱包",
"settings_explorer_hint": "URL 应包含尾部斜杠。将自动附加 txid/地址。",
"settings_export_all": "全部导出...",
"settings_export_csv": "导出 CSV...",
"settings_export_key": "导出密钥...",
"settings_export_all": "全部导出…",
"settings_export_csv": "导出 CSV…",
"settings_export_key": "导出密钥…",
"settings_gradient_bg": "渐变背景",
"settings_gradient_desc": "用平滑渐变替换纹理背景",
"settings_idle_after": "之后",
"settings_import_key": "导入私钥...",
"settings_import_viewkey": "导入查看密钥...",
"settings_import_key": "导入私钥…",
"settings_import_viewkey": "导入查看密钥…",
"settings_language_note": "注意:部分文本需要重启才能更新",
"settings_lock_now": "立即锁定",
"settings_locked": "已锁定",
"settings_merge_to_address": "合并到地址...",
"settings_merge_to_address": "合并到地址…",
"settings_noise_opacity": "噪点不透明度:",
"settings_not_connected": "未连接到守护进程",
"settings_not_encrypted": "未加密",
@@ -1368,7 +1556,7 @@
"settings_reloaded": "已从磁盘重新加载设置",
"settings_remove_encryption": "移除加密",
"settings_remove_pin": "移除 PIN",
"settings_request_payment": "请求付款...",
"settings_request_payment": "请求付款…",
"settings_rescan_desc": "重新扫描区块链以查找丢失的交易",
"settings_restart_daemon": "重启守护进程",
"settings_rpc_connection": "RPC 连接",
@@ -1379,13 +1567,13 @@
"settings_save_shielded_local": "将屏蔽交易历史保存到本地",
"settings_saved": "设置已保存",
"settings_set_pin": "设置 PIN",
"settings_shield_mining": "屏蔽挖矿...",
"settings_shield_mining": "屏蔽挖矿…",
"settings_solid_colors_desc": "使用纯色代替模糊效果(无障碍功能)",
"settings_theme_refreshed": "主题列表已刷新",
"settings_tor_desc": "通过 Tor 路由所有连接以增强隐私",
"settings_unlocked": "已解锁",
"settings_use_tor_network": "使用 Tor 进行网络连接",
"settings_validate_address": "验证地址...",
"settings_validate_address": "验证地址…",
"settings_visual_effects": "视觉效果",
"settings_wallet_file_size": "钱包文件大小:%s",
"settings_wallet_info": "钱包信息",
@@ -1453,6 +1641,17 @@
"sweep_to": "归集到:",
"sweep_toggle": "归集到我的钱包(不保留密钥)",
"sweep_tx": "交易:",
"swin_connection_failed": "连接失败:",
"swin_connection_successful": "连接成功!\ndragonxd 版本:",
"swin_invalid_suffix": "(无效)",
"swin_no_history_file_found": "未找到历史文件",
"swin_rescan_failed": "重新扫描失败:",
"swin_rescan_started_from_block": "重新扫描已开始,起始区块 ",
"swin_rescan_to": " 至 ",
"swin_rpc_client_not_initialized": "RPC 客户端未初始化",
"swin_settings_saved": "设置已保存",
"swin_theme_list_refreshed": "主题列表已刷新",
"swin_ztx_history_cleared": "Z 交易历史已清除",
"switch_corrupt_body": "此钱包似乎已损坏——节点无法打开它。请从备份恢复、重新创建,或尝试修复。",
"switch_corrupt_repair": "尝试修复(salvage)",
"switch_progress_background": "在后台继续",
@@ -1492,6 +1691,7 @@
"to_upper": "至",
"tools": "工具",
"tools_actions": "工具与操作...",
"tools_actions_hdr": "工具与操作",
"total": "合计",
"total_balance_label": "总余额",
"transaction_id": "交易 ID",
@@ -1515,7 +1715,7 @@
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
"tt_backup": "创建 wallet.dat 的备份",
"tt_block_explorer": "在浏览器中打开 DragonX 区块浏览器",
"tt_blur": "模糊程度(0%% = 关闭,100%% = 最大)",
"tt_blur": "模糊程度(0% = 关闭,100% = 最大)",
"tt_change_pass": "更改钱包加密密码",
"tt_change_pin": "更改您的解锁 PIN",
"tt_chat_bubble_accent": "你发出的消息气泡的强调色(或跟随当前主题)",
@@ -1578,7 +1778,7 @@
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
"tt_merge": "将多个 UTXO 合并到一个地址",
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
"tt_noise": "颗粒纹理强度(0%% = 关闭,100%% = 最大)",
"tt_noise": "颗粒纹理强度(0% = 关闭,100% = 最大)",
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
"tt_open_dir": "点击在文件管理器中打开",
@@ -1617,7 +1817,7 @@
"tt_theme_hotkey": "快捷键:Ctrl+左/右箭头切换主题",
"tt_tor": "通过 Tor 网络路由守护进程连接以实现匿名",
"tt_tx_url": "在区块浏览器中查看交易的基础 URL",
"tt_ui_opacity": "卡片和侧边栏不透明度(100%% = 完全不透明,越低越透明)",
"tt_ui_opacity": "卡片和侧边栏不透明度(100% = 完全不透明,越低越透明)",
"tt_validate": "检查 DragonX 地址是否有效",
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
@@ -1829,4 +2029,4 @@
"your_addresses": "您的地址",
"z_address": "Z 地址",
"z_addresses": "Z 地址"
}
}
+5
View File
@@ -965,6 +965,11 @@ edition-label = { position = 120 }
link-button = { width = 100, font = "button-sm" }
close-button = { width = 120, font = "button", align = "center" }
[dialogs.faq]
width = 860.0
height = 660.0
window = { width = 860, height = 660 }
[dialogs.settings]
width = 600.0
height = 550.0
+394 -191
View File
File diff suppressed because it is too large Load Diff
+86
View File
@@ -163,6 +163,13 @@ public:
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
// Daemon (v1.3.0+) coinbase auto-shield status, from z_autoshieldstatus. "Not probed" / all-false on
// pre-1.3.0 daemons (no such RPC) — callers treat that as "the wallet handles auto-shield itself".
bool daemonAutoShieldProbed() const { return daemon_autoshield_probed_; }
bool daemonAutoShieldActive() const { return daemon_autoshield_active_; }
const std::string& daemonAutoShieldAddress() const { return daemon_autoshield_address_; }
const std::string& daemonAutoShieldDisabledReason() const { return daemon_autoshield_disabled_reason_; }
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
// for the "Copy diagnostics" action. Contains no secrets.
std::string buildDiagnosticsReport();
@@ -175,6 +182,21 @@ public:
*/
void renderShutdownScreen();
/**
* @brief Tail the last N lines of the daemon's debug.log (best-effort, reads only the file tail).
* Fallback for the shutdown screen when we have no captured stdout — e.g. an external daemon we
* attached to rather than spawned — so the user can still see the node flushing/exiting.
*/
std::vector<std::string> tailDaemonDebugLog(int maxLines) const;
// True when the daemon's debug.log shows an in-progress Sapling witness-cache rebuild (best-effort
// heuristic). Stopping the daemon during one discards it and forces a multi-minute redo next launch.
bool daemonWitnessRebuildActive() const;
// Whether beginShutdown() should pause and confirm before stopping the daemon (rebuild in progress).
bool shouldConfirmDaemonStop() const;
// The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render().
void renderDaemonStopConfirm();
/**
* @brief Render loading overlay in content area while daemon is starting/syncing
* @param contentH Height of the content area child window
@@ -371,6 +393,10 @@ public:
// Force refresh
void refreshNow();
// Called on window restore: drop the daemon-output backlog that accumulated while minimized (the
// per-frame update loop was paused), so a background witness rebuild that started+finished during
// the minimize isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
void skipDaemonOutputBacklog();
void refreshMiningInfo();
void refreshPeerInfo();
void refreshMarketData();
@@ -408,6 +434,9 @@ public:
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
void startFullUiSweep();
std::string screenshotFullDir() const;
// Debug option: restrict either sweep to just the currently-active theme instead of cycling all.
bool sweepCurrentThemeOnly() const { return sweep_current_theme_only_; }
void setSweepCurrentThemeOnly(bool v) { sweep_current_theme_only_ = v; }
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
@@ -433,6 +462,7 @@ public:
return 0;
}
void showAboutDialog() { show_about_ = true; }
void showFaqDialog() { show_faq_ = true; }
// Legacy tab compat — maps int to NavPage
void setCurrentTab(int tab);
@@ -771,6 +801,7 @@ private:
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
bool chat_note_scan_in_flight_ = false;
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
double chat_note_scan_ms_ = 0.0; // measured cost of the last note scan (adaptive back-off)
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
@@ -780,6 +811,9 @@ private:
// Coordinator helpers (both variants unless noted; see app_network.cpp).
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
@@ -814,6 +848,7 @@ private:
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup();
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
void maybeWarnLargeWallet(); // full-node: wallet.dat past bloat threshold → one-time toast + clickable alert
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
@@ -847,6 +882,8 @@ private:
void fastScanChatMemos();
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
double chat_fast_scan_last_ = 0.0; // ImGui time of the last 0-conf fast scan (adaptive back-off)
double chat_fast_scan_ms_ = 0.0; // measured cost of the last fast scan (z_listreceivedbyaddress)
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
@@ -900,6 +937,11 @@ private:
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
std::thread shutdown_thread_;
// Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm)
bool pending_shutdown_confirm_ = false; // a quit is deferred, waiting to open the confirm modal
bool daemon_stop_confirm_open_ = false; // the confirm modal is currently showing
bool shutdown_confirmed_ = false; // user chose to proceed — bypass the guard on re-entry
bool shutdown_keep_daemon_override_ = false; // user chose "keep node running" for this shutdown only
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
@@ -934,6 +976,8 @@ private:
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
bool wallet_auto_recovered_ = false; // a salvage happened this session
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
@@ -991,6 +1035,7 @@ private:
bool show_demo_window_ = false;
bool show_settings_ = false;
bool show_about_ = false;
bool show_faq_ = false;
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
@@ -1004,6 +1049,7 @@ private:
bool seed_backup_loading_ = false;
bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
bool large_wallet_checked_ = false; // gate: stat wallet.dat for the bloat nudge once per launch
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
@@ -1097,6 +1143,22 @@ private:
bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
// Sync-settle hysteresis + adaptive balance-poll throttle. Balance polling (z_gettotalbalance) is
// O(mapWallet) and holds the daemon's cs_main, which starves block connection on a large shielded
// wallet — so we keep the low-impact profile briefly after catching up, and back the balance poll
// off in proportion to its own measured cost. See effectivelySyncing() / balanceRefreshDue().
bool was_core_syncing_ = false; // previous Core-refresh sync state, to detect the caught-up edge
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
// starving block connection near the tip (where effectivelySyncing() reads false). See
// addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
// refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
double last_address_scan_ms_ = 0.0; // measured cost of the last address scan
double last_tx_scan_ms_ = 0.0; // measured cost of the last history scan
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
@@ -1114,6 +1176,7 @@ private:
// Debug screenshot sweep state.
bool screenshot_sweep_active_ = false;
bool sweep_current_theme_only_ = false; // Debug Options: sweep only the active theme
bool sweep_capture_this_frame_ = false;
int sweep_skin_idx_ = 0;
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
@@ -1207,6 +1270,16 @@ private:
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};
// v1.3.0+ daemons auto-shield coinbase themselves; probe z_autoshieldstatus once per connection and
// defer the wallet's own client-side auto-shield when the daemon is doing it (otherwise both race for
// the same coinbase UTXOs and split funds across different z-addresses). Fail-closed: a pre-1.3.0
// daemon lacks the RPC → active stays false → the wallet keeps shielding client-side (no regression).
bool daemon_autoshield_probed_ = false;
bool daemon_autoshield_active_ = false;
std::atomic<bool> daemon_autoshield_probe_inflight_{false};
std::string daemon_autoshield_address_; // z_autoshieldstatus fields (O1); empty on old daemons
std::string daemon_autoshield_disabled_reason_; // daemon's reason auto-shield is off (e.g. seed not recoverable)
bool daemon_autoshield_seed_recoverable_ = false;
// P4: Incremental transaction cache
int last_tx_block_height_ = -1; // block height at last full tx fetch
@@ -1245,6 +1318,11 @@ private:
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false;
// True only for a rescan the WALLET/USER initiated (the Rescan button, a -rescan/salvage/zap/reindex
// restart, key import, seed migration) — not an autonomous background witness rebuild the daemon does
// on its own. Gates the "Blockchain rescan complete" toast so background rebuilds don't fire it;
// cleared when the toast is shown.
std::atomic<bool> user_initiated_rescan_{false}; // atomic: some rescan triggers run on worker threads
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false;
@@ -1399,6 +1477,7 @@ private:
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
void detectWalletDegraded(); // scan daemon output for a DEGRADED-mode open; warn once/session
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
@@ -1433,6 +1512,13 @@ private:
void refreshPrice();
void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page);
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
bool addressRefreshDue() const; // same adaptive back-off for the address scan (z_listunspent)
bool txRefreshDue() const; // same adaptive back-off for the history scan (z_listreceivedbyaddress)
// Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
// lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;
+264 -29
View File
@@ -35,6 +35,7 @@
#include "rpc/connection.h"
#include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning
#include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch
#include "ui/windows/shield_dialog.h" // ui::ShieldDialog — Merge to Address shortcut from the bloat nudge
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
#include <cctype>
@@ -248,6 +249,21 @@ void App::detectWalletAutoRecovery()
VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n");
}
// v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (funds spendable) instead of aborting,
// but can't derive NEW HD keys — z_getnewaddress / z_shieldcoinbase / t->z z_sendmany fail with "HD seed
// not found". Only a startup log line signals it, so scan the captured output and warn once. Pre-1.3.0
// daemons never emit it, so this is a no-op there (backwards compatible).
void App::detectWalletDegraded()
{
if (wallet_degraded_warned_) return;
if (!isUsingEmbeddedDaemon() || !daemon_controller_ || !daemon_controller_->daemon()) return;
if (!daemon::walletOpenedDegraded(daemon_controller_->daemon()->getOutput())) return;
wallet_degraded_ = true;
wallet_degraded_warned_ = true;
ui::Notifications::instance().warning(TR("wallet_degraded_notify"), 30.0f);
VERBOSE_LOGF("[recovery] Daemon opened wallet in DEGRADED mode — new-key derivation disabled\n");
}
void App::tryConnect()
{
// Lite builds have no full node / RPC daemon, so never run the RPC connection state machine
@@ -258,6 +274,7 @@ void App::tryConnect()
// Catch a startup wallet salvage as soon as it appears in the node's output — independent of whether
// the node ever finishes starting or connects (skip only while an orchestrated swap is mid-flight).
if (!daemon_restarting_) detectWalletAutoRecovery();
if (!daemon_restarting_) detectWalletDegraded();
if (connection_in_progress_) return;
@@ -713,6 +730,11 @@ void App::onDisconnected(const std::string& reason)
wallet_seed_status_ = WalletSeedStatus::Unknown;
wallet_seed_status_attempts_ = 0;
// Re-probe whether the daemon auto-shields coinbase on the next connect — it may have been
// upgraded/swapped (e.g. v1.0.3 which has no z_autoshieldstatus -> v1.3.0 which auto-shields).
daemon_autoshield_probed_ = false;
daemon_autoshield_active_ = false;
// Clear RPC result caches
viewtx_cache_.clear();
confirmed_tx_cache_.clear();
@@ -836,13 +858,58 @@ void App::applyRefreshPolicy(ui::NavPage page)
// While the daemon is syncing, override the per-tab cadence with the low-impact sync profile so
// the wallet stops contending for the daemon's cs_main lock (frequent getpeerinfo / per-block
// transaction scans / balance polls slow block connection). This makes every tab sync as fast
// as the Console tab does today. Reverts to the per-tab profile once sync finishes.
refresh_policy_syncing_ = state_.sync.syncing;
// as the Console tab does today. effectivelySyncing() keeps this profile on briefly after catching
// up (hysteresis) so a large-wallet scan can't immediately re-starve connection and bounce the
// node back into "syncing". Reverts to the per-tab profile once the settle window passes.
refresh_policy_syncing_ = effectivelySyncing();
network_refresh_.setIntervals(refresh_policy_syncing_
? services::RefreshScheduler::kSyncProfile
: getIntervalsForPage(page));
}
// True while the node is behind, and for a short settle window after it first catches up. The settle
// window is armed only on the syncing→caught-up edge (see the Core refresh callback), so a wallet that
// was synced from the start is never throttled at connect — only a node that just finished catching up.
bool App::effectivelySyncing() const
{
if (state_.sync.syncing) return true;
if (sync_settle_until_ == 0) return false; // no pending settle → genuinely caught up
return std::time(nullptr) < sync_settle_until_;
}
// Adaptive throttle: the next balance poll must wait at least (lastScanCost / kBalanceDutyCycle) since
// the last one, so balance scanning can never occupy more than ~kBalanceDutyCycle of wall-clock. A
// cheap wallet (sub-cadence cost) is unaffected — the tab's Core timer stays the real cadence; a ~20s
// scan on a large wallet backs off to roughly every ~200s instead of every 2s, freeing cs_main for
// block connection. A wallet mutation bypasses this via force_balance_refresh_.
bool App::scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const
{
constexpr double kScanDutyCycle = 0.10;
if (lastUpdate == 0) return true; // never fetched
if (lastScanMs <= 0.0) return true; // no cost measured yet
const double minInterval = (lastScanMs / 1000.0) / kScanDutyCycle;
return std::difftime(std::time(nullptr), static_cast<std::time_t>(lastUpdate)) >= minInterval;
}
bool App::balanceRefreshDue() const
{
return scanRefreshDue(state_.last_balance_update, last_balance_scan_ms_);
}
// The address scan (z_listunspent) and history scan (z_listreceivedbyaddress) are just as O(mapWallet)
// and cs_main-bound as the balance scan, so they get the same adaptive back-off. This is what keeps a
// large synced wallet from re-scanning them every tab cadence — the near-tip starvation the balance-only
// throttle missed (only the periodic poll is gated; explicit refreshes call the refresh directly).
bool App::addressRefreshDue() const
{
return scanRefreshDue(state_.last_address_update, last_address_scan_ms_);
}
bool App::txRefreshDue() const
{
return scanRefreshDue(state_.last_tx_update, last_tx_scan_ms_);
}
bool App::currentPageNeedsWalletDataRefresh() const
{
using NP = ui::NavPage;
@@ -1317,6 +1384,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
if (salvage && daemon_controller_) daemon_controller_->setSalvageOnNextStart(true); // repair a corrupt wallet
else if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // (salvage implies rescan)
if (salvage || needRescan) user_initiated_rescan_ = true; // wallet-triggered repair/rescan → completion should toast
// Start ONCE. Do NOT retry-spawn: a second start while the first is still shutting down leaves
// two dragonxd holding wallet.dat against each other (BDB "Failed to rename … Error"). The
// stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid
@@ -1632,7 +1700,10 @@ void App::refreshCoreData()
transactions_dirty_ = true;
last_tx_block_height_ = -1;
invalidateShieldedHistoryScanProgress(true);
ui::Notifications::instance().success("Blockchain rescan complete");
if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
}
NetworkRefreshService::applyConnectionInfoResult(state_, result.info);
@@ -1665,9 +1736,15 @@ void App::refreshCoreData()
? fast_rpc_.get() : rpc_.get();
if (!w || !rpc) return;
ui::NavPage tracePage = current_page_;
// Skip the balance call while syncing (it's incomplete anyway and takes the wallet lock +
// cs_main). Captured on the main thread to avoid reading state_ off the worker thread.
const bool includeBalance = !state_.sync.syncing;
// Decide whether to include the balance call (z_gettotalbalance — O(mapWallet), holds cs_main).
// Suppress it (a) while syncing or within the post-sync settle window, so it can't starve block
// connection, and (b) unless enough time has elapsed given the LAST scan's measured cost, so a
// large shielded wallet backs off automatically instead of re-scanning every couple of seconds.
// A wallet mutation (send/shield) forces the next poll through so the user's own action updates the
// balance immediately. Captured on the main thread to avoid reading state_ off the worker thread.
const bool includeBalance = !effectivelySyncing() &&
(force_balance_refresh_ || balanceRefreshDue());
if (includeBalance) force_balance_refresh_ = false;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb {
AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh"));
@@ -1677,6 +1754,27 @@ void App::refreshCoreData()
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
applyPendingSendBalanceDeltas(true);
// Feed the adaptive balance throttle + sync-settle hysteresis. Record the last scan's
// cost (0 when balance was skipped), and arm the settle window only on the
// syncing→caught-up edge so a wallet synced from the start is never throttled at connect.
if (result.balanceScanMs > 0.0) last_balance_scan_ms_ = result.balanceScanMs;
// Sticky-behind hysteresis: hold the low-impact profile for kSyncSettleSeconds after the
// LAST time we read "behind" — not only on the caught-up edge. While actually syncing we
// re-arm every Core tick, so the profile stays engaged continuously; once we truly reach
// the tip (within the 2-block tolerance in applyCoreRefreshResult) we stop re-arming and it
// lapses after the window. This is what breaks the near-tip feedback loop: a single stale or
// optimistic "caught up" reading can no longer immediately unleash the heavy O(mapWallet)
// scans and re-starve the tail (which dropped us behind again → oscillation). With the
// scans suppressed, kSyncProfile keeps sync detection itself cheap, so we keep getting fresh
// "behind" readings and stay latched until genuinely caught up. Window must exceed the
// worst-case Core-refresh interval so it can't lapse between two behind readings mid-catchup.
const bool nowSyncing = state_.sync.syncing;
constexpr double kSyncSettleSeconds = 30.0;
if (nowSyncing || was_core_syncing_) {
sync_settle_until_ = std::time(nullptr) + static_cast<std::time_t>(kSyncSettleSeconds);
}
was_core_syncing_ = nowSyncing;
// Mid-session connection-loss detection. During normal operation, both core
// RPCs failing together means the daemon connection is dead (a busy daemon
// fails them individually, not both at once). Warmup is excluded — both fail
@@ -1695,9 +1793,40 @@ void App::refreshCoreData()
}
}
// Auto-shield transparent funds if enabled
if (result.balanceOk && settings_ && settings_->getAutoShield() &&
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing &&
// Probe the daemon's auto-shield status once per connection — independent of our own
// toggle/balance — so both the defer-gate below and the Settings UI (O1) can read it.
// Only when synced (daemon past warmup). Fail-closed: a pre-1.3.0 daemon lacks the RPC,
// so the probe leaves active=false and the wallet keeps shielding client-side.
if (result.balanceOk && !state_.sync.syncing && !daemon_autoshield_probed_ && worker_ &&
!daemon_autoshield_probe_inflight_.exchange(true)) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
bool active = false, seedRecoverable = false;
std::string addr, reason;
try {
auto st = rpc_->call("z_autoshieldstatus", json::array());
if (st.is_object()) {
if (st.contains("autoshield")) active = st["autoshield"].get<bool>();
if (st.contains("autoshieldaddress")) addr = st["autoshieldaddress"].get<std::string>();
if (st.contains("disabled_reason")) reason = st["disabled_reason"].get<std::string>();
if (st.contains("seed_recoverable")) seedRecoverable = st["seed_recoverable"].get<bool>();
}
} catch (...) {} // pre-1.3.0 daemon: no such method — leave defaults (inactive)
return [this, active, addr, reason, seedRecoverable]() {
daemon_autoshield_active_ = active;
daemon_autoshield_address_ = addr;
daemon_autoshield_disabled_reason_ = reason;
daemon_autoshield_seed_recoverable_ = seedRecoverable;
daemon_autoshield_probed_ = true;
daemon_autoshield_probe_inflight_ = false;
};
});
}
// Auto-shield transparent funds — but defer to the daemon's own coinbase auto-shielder
// (v1.3.0+) when it's active, so we don't double-shield and split funds across z-addrs.
const bool autoShieldEligible = result.balanceOk && settings_ && settings_->getAutoShield() &&
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing;
if (autoShieldEligible && daemon_autoshield_probed_ && !daemon_autoshield_active_ &&
!auto_shield_pending_.exchange(true)) {
std::string targetZAddr;
for (const auto& addr : state_.addresses) {
@@ -1770,7 +1899,13 @@ void App::refreshAddressData()
return [this, previousAddressCount, previousWalletIdentity, result = std::move(result)]() mutable {
const bool addrListOk = result.addressListOk; // capture before the move
const double scanMs = result.scanMs; // feed the adaptive throttle (addressRefreshDue)
if (scanMs > 0.0) last_address_scan_ms_ = scanMs;
// #3: this scan already listed the wallet's unspent notes — feed the chat send-budget from it
// (dedups the dedicated chat z_listunspent). Grab it before `result` is moved below.
auto unspentNotes = std::move(result.unspentNotes);
NetworkRefreshService::applyAddressRefreshResult(state_, std::move(result));
updateChatNoteBudgetFromUnspent(unspentNotes);
// Mark the address list as loaded ONLY if enumeration actually succeeded — a swallowed
// z_listaddresses/getaddressesbyaccount failure returns a falsely-short list, and stamping it
// would let the empty-wallet warning trust a spurious 0 count (see maybeWarnEmptyWallet…).
@@ -1829,6 +1964,13 @@ void App::refreshTransactionData()
transactionSnapshot.maxShieldedReceiveScans = shieldedReceiveScanBudget(current_page_);
transactionSnapshot.shieldedScanTipTolerance =
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
// When fully synced, widen the re-scan tolerance so the O(mapWallet) per-address history scan runs
// every several blocks instead of ~every 2 — the heaviest residual synced-state cs_main cost. The
// balance poll still surfaces incoming funds on its own cadence; only the detailed history list lags a
// few minutes. (During catch-up the tx refresh is suppressed by kSyncProfile, so this is synced-only.)
if (!effectivelySyncing())
transactionSnapshot.shieldedScanTipTolerance =
std::max(transactionSnapshot.shieldedScanTipTolerance, 8);
ui::NavPage tracePage = current_page_;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
@@ -1839,6 +1981,7 @@ void App::refreshTransactionData()
return [this, result = std::move(result)]() mutable {
bool shieldedScanComplete = result.shieldedScanComplete;
if (result.scanMs > 0.0) last_tx_scan_ms_ = result.scanMs; // feed the adaptive throttle (txRefreshDue)
std::size_t nextShieldedScanStartIndex = result.nextShieldedScanStartIndex;
auto shieldedScanHeights = std::move(result.shieldedScanHeights);
NetworkRefreshService::TransactionCacheUpdate cacheUpdate{
@@ -1904,6 +2047,9 @@ void App::refreshRecentTransactionData()
transactionSnapshot.maxShieldedReceiveScans = 1;
transactionSnapshot.shieldedScanTipTolerance =
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
if (!effectivelySyncing()) // synced: widen the re-scan tolerance (see refreshTransactionData)
transactionSnapshot.shieldedScanTipTolerance =
std::max(transactionSnapshot.shieldedScanTipTolerance, 8);
ui::NavPage tracePage = current_page_;
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
@@ -3433,9 +3579,25 @@ void App::refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model)
void App::refreshChatNoteBudgetNode() {
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
if (!state_.connected || !rpc_ || !worker_ || state_.isLocked()) return;
// Never contend the daemon's cs_main while the node is catching up. z_listunspent is O(mapWallet)
// and holds cs_main for its whole duration — seconds on a large wallet — so an ungated per-few-second
// chat scan here starves block connection and keeps the node from ever pinning to the tip. Chat
// send-readiness can wait until we're synced; this budget is only advisory (the send path re-checks).
if (effectivelySyncing()) return;
// Only pre-warm the send-budget while the user is actually in chat. Off the Chat tab this heavy
// z_listunspent is unnecessary — and on a wallet-data tab, refreshAddressData's own z_listunspent
// already feeds the budget for free (updateChatNoteBudgetFromUnspent), so this dedicated scan is a
// fallback for the Chat tab (where the address refresh doesn't run).
if (current_page_ != ui::NavPage::Chat) return;
if (chat_note_scan_in_flight_) return; // one scan at a time
const double now = ImGui::GetTime();
if (now - chat_note_scan_last_ < 4.0) return; // rate limit; self-throttles via the in-flight flag too
// Adaptive back-off keyed on the last scan's own cost: on a large wallet where the scan takes many
// seconds, this stretches the interval so the scan can't occupy more than ~10% of wall-clock (same
// duty-cycle discipline as the balance/address/tx polls). A cheap wallet stays at the 4s floor.
double minInterval = 4.0;
if (chat_note_scan_ms_ > 0.0)
minInterval = std::max(4.0, (chat_note_scan_ms_ / 1000.0) / 0.10);
if (now - chat_note_scan_last_ < minInterval) return;
chat_note_scan_last_ = now;
chat_note_scan_in_flight_ = true;
@@ -3446,6 +3608,7 @@ void App::refreshChatNoteBudgetNode() {
int verified = 0, pipeline = 0;
std::uint64_t verifiedZat = 0;
bool ok = false;
const auto scanStart = std::chrono::steady_clock::now();
try {
rpc::RPCClient::TraceScope trace("HushChat / note-buffer scan");
nlohmann::json notes = rpc_->call("z_listunspent", nlohmann::json::array({0})); // 0 = include maturing
@@ -3465,9 +3628,12 @@ void App::refreshChatNoteBudgetNode() {
}
}
} catch (const std::exception&) {}
return [this, scanGen, ok, verified, pipeline, verifiedZat]() {
const double scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return [this, scanGen, ok, verified, pipeline, verifiedZat, scanMs]() {
if (scanGen != chat_session_generation_) return; // wallet switched — drop (reset cleared the flag)
chat_note_scan_in_flight_ = false;
if (scanMs > 0.0) chat_note_scan_ms_ = scanMs; // feed the adaptive back-off
if (!ok) return; // z_listunspent unavailable — leave caches as-is
chat_note_model_seen_ = true;
chat_verified_note_budget_ = verified;
@@ -3482,6 +3648,36 @@ void App::refreshChatNoteBudgetNode() {
});
}
// #3: derive the chat send-budget from an already-collected z_listunspent (refreshAddressData's scan),
// so we don't issue a duplicate z_listunspent. Runs on the main thread; the notes are pre-parsed to the
// fields we need (amount / locked / TRUE confirmations), so there's no RPC and no worker hop. Mirrors the
// apply in refreshChatNoteBudgetNode and stamps chat_note_scan_last_ so the dedicated scan backs off.
void App::updateChatNoteBudgetFromUnspent(
const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes) {
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
const int confsNeeded = chatConfsRequired();
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
int verified = 0, pipeline = 0;
std::uint64_t verifiedZat = 0;
for (const auto& nz : unspentNotes) {
if (nz.locked) continue; // tied up by an in-flight send
const std::uint64_t amtZat = chatDrgxToZat(nz.amount);
if (amtZat < minVal) continue; // skip dust below a fee
++pipeline; // unspent self-note (verified or maturing)
if (nz.confirmations >= confsNeeded) { ++verified; verifiedZat += amtZat; }
}
chat_note_model_seen_ = true;
chat_verified_note_budget_ = verified;
chat_pipeline_note_count_ = pipeline;
chat_verified_shielded_zat_ = verifiedZat;
chat_note_scan_last_ = ImGui::GetTime(); // counts as a fresh scan → dedicated scan backs off
if (chat_split_outstanding_ &&
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
chat_split_outstanding_ = false;
}
}
void App::enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId) {
QueuedChatOp op;
op.kind = kind;
@@ -3723,15 +3919,28 @@ void App::fastScanChatMemos()
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
if (!state_.connected || !rpc_ || !worker_) return;
if (chat_fast_scan_in_flight_) return; // don't stack RPCs if a previous scan is still running
// z_listreceivedbyaddress is O(mapWallet) (the daemon iterates every wtx even when filtered to one
// address) — seconds on a large wallet — so this 0-conf poll must NOT run while the node is catching
// up, and even when synced it backs off to ~10% wall-clock keyed on its own cost. The block-tip
// harvest still ingests every message on confirmation; this only trades away mempool-speed delivery.
if (effectivelySyncing()) return;
// Mempool-speed 0-conf delivery is only worth its heavy cost while the user is watching chat. Off the
// Chat tab, incoming messages still arrive via the confirmed block-tip harvest (+ toast) within ~1 block.
if (current_page_ != ui::NavPage::Chat) return;
const double nowT = ImGui::GetTime();
if (chat_fast_scan_ms_ > 0.0 &&
nowT - chat_fast_scan_last_ < (chat_fast_scan_ms_ / 1000.0) / 0.10) return;
const std::string addr = chatReplyZaddr();
if (addr.empty()) return;
chat_fast_scan_last_ = nowT;
chat_fast_scan_in_flight_ = true;
const int scanGen = chat_session_generation_; // guard: drop the result if the wallet switches/locks
worker_->post([this, addr, scanGen]() -> rpc::RPCWorker::MainCb {
std::vector<chat::HushChatTransactionMetadata> metadata;
int rawMemoCount = 0; // received notes carrying a memo at the reply addr (0-conf visibility signal)
std::string scanError;
const auto scanStart = std::chrono::steady_clock::now();
try {
rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan");
nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool
@@ -3762,12 +3971,15 @@ void App::fastScanChatMemos()
}
} catch (const std::exception& e) { scanError = e.what(); }
const int parsedCount = static_cast<int>(metadata.size());
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError]() mutable {
const double scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError, scanMs]() mutable {
// The wallet was switched/locked between post and now — this metadata belongs to the previous
// session. resetChatSession already reset the in-flight flag, so just drop; clearing it here
// would clobber a new session's own in-flight scan (mirrors the broadcast/identity guards).
if (scanGen != chat_session_generation_) return;
chat_fast_scan_in_flight_ = false;
if (scanMs > 0.0) chat_fast_scan_ms_ = scanMs; // feed the adaptive back-off
// Diagnostic (console App/chat channel): log when the memo-note count at the reply address
// CHANGES, so a stable mempool doesn't spam every 2.5s. Reveals whether inbound 0-conf messages
// are reaching us at all, and how many parse as chat vs are unrelated memos.
@@ -4232,6 +4444,36 @@ void App::maybeRemindSeedBackup()
});
}
// One-time nudge (full-node) when the BDB wallet.dat has bloated past the threshold. Berkeley DB never
// shrinks in place and shielded-note witness data accumulates, so a mining/shielded wallet can grow
// unbounded. Fires ONCE (persisted flag) a warning toast + a clickable "Consolidate notes…" entry in the
// bell/alert panel that opens Merge to Address; re-arms if the file later drops back under the threshold.
void App::maybeWarnLargeWallet()
{
if (capture_mode_ || lite_wallet_) return; // no live nags during a UI sweep; lite has no wallet.dat
if (!supportsFullNodeLifecycleActions() || !settings_) return;
if (!state_.connected || !state_.encryption_state_known) return;
if (state_.warming_up || state_.daemon_initializing || !state_.sync.isSynced()) return;
if (large_wallet_checked_) return; // stat wallet.dat at most once per launch
large_wallet_checked_ = true;
static constexpr uint64_t kWalletBloatWarnBytes = 500ull * 1024 * 1024; // 500 MB (matches the Settings banner)
const std::string walletPath = util::Platform::getDragonXDataDir() + "/wallet.dat";
const uint64_t sz = util::Platform::getFileSize(walletPath);
if (sz <= kWalletBloatWarnBytes) {
// Re-arm the one-time warning if the file shrank back under the threshold (e.g. after a fresh seed wallet).
if (settings_->getLargeWalletWarned()) { settings_->setLargeWalletWarned(false); settings_->save(); }
return;
}
if (settings_->getLargeWalletWarned()) return; // already warned once for this bloat episode
settings_->setLargeWalletWarned(true);
settings_->save();
ui::Notifications::instance().action(
TR("wallet_size_warn"), ui::NotificationType::Warning,
[]() { ui::ShieldDialog::showConsolidate(); },
TR("wallet_size_consolidate"), 12.0f);
}
// Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it
// happened on a prior run, or under an external daemon whose startup output we never captured, so
// detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in
@@ -4703,7 +4945,7 @@ void App::beginAdoptSeedWallet()
// 3. Rescan on next start (only if the swap happened) and bring the daemon back up —
// unless we're quitting, in which case don't resurrect it.
if (swapDone && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
if (swapDone && daemon_controller_) { daemon_controller_->setRescanOnNextStart(true); user_initiated_rescan_ = true; }
if (!shutting_down_) {
// We stopped the daemon ourselves (port_free) — clear the adopted-external latch so the
// relaunched process is treated as owned (stop/isRunning/exit behave normally afterward).
@@ -4967,21 +5209,12 @@ void App::rebuildWalletDatabase()
const std::string tmpOut = datadir + "/wallet.rebuilt-" + std::string(ts) + ".tmp";
{ std::error_code ec; fs::remove(tmpOut, ec); } // helper uses DB_EXCL — path must be fresh
// 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line.
std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\"";
#ifdef _WIN32
cmd = "\"" + cmd + "\""; // cmd.exe strips the outermost quotes
FILE* fp = _popen(cmd.c_str(), "r");
#else
FILE* fp = popen(cmd.c_str(), "r");
#endif
std::string jout;
if (fp) { char b[512]; while (std::fgets(b, sizeof b, fp)) jout += b; }
#ifdef _WIN32
const int rc = fp ? _pclose(fp) : -1;
#else
const int rc = fp ? pclose(fp) : -1;
#endif
// 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line. Windowless
// (runHiddenCapture) so a wallet rebuild never flashes a cmd.exe console; it runs the
// helper via CreateProcess directly on Windows, so no cmd.exe outer-quote wrap is needed.
const std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\"";
int rc = -1;
const std::string jout = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/false, &rc);
DEBUG_LOGF("[App] wallet-rebuild helper rc=%d out=%s\n", rc, jout.c_str());
// 4. Verify-before-swap: the output must be a readable BDB with the fund-critical keys.
@@ -5011,7 +5244,7 @@ void App::rebuildWalletDatabase()
fs::rename(datadir + "/database", datadir + "/database.prerebuild-" + std::string(ts) + ".bak", e2);
for (const auto& e : fs::directory_iterator(datadir, e2))
if (e.path().filename().string().rfind("__db.", 0) == 0) { std::error_code e3; fs::remove(e.path(), e3); }
if (daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
if (daemon_controller_) { daemon_controller_->setRescanOnNextStart(true); user_initiated_rescan_ = true; }
}
}
}
@@ -5269,6 +5502,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
// Force transaction list refresh so the sent tx appears immediately
transactions_dirty_ = true;
last_tx_block_height_ = -1;
force_balance_refresh_ = true; // the user's own send must update the balance now, past the throttle
network_refresh_.markWalletMutationRefresh();
// z_sendmany only returned an opid: the transaction is built/signed/
// broadcast asynchronously by the daemon. Defer the user-facing
@@ -5438,6 +5672,7 @@ void App::runtimeRescan(int startHeight)
runtime_rescan_active_ = true;
state_.sync.rescanning = true;
rescan_confirmed_active_ = true;
user_initiated_rescan_ = true; // user clicked Rescan → completion should toast
state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "...";
transactions_dirty_ = true;
+44 -45
View File
@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
TR("sec_restart_daemon_for_encryption"));
}
}
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (result.encrypted) {
return [this]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
// Immediately update local encryption state so the
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
}
ui::Notifications::instance().info(
"Wallet encrypted successfully", 5.0f);
TR("sec_wallet_encrypted_successfully"), 5.0f);
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error(
"Encryption failed: " + err);
std::string(TR("sec_encryption_failed_prefix")) + err);
// Return to passphrase entry on failure
if (show_encrypt_dialog_ &&
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
std::string pin = std::move(deferredEncryption.pin);
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
if (result.pinStored) {
settings_->setPinEnabled(true);
settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
} else {
ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed");
TR("sec_wallet_encrypted_but_pin_vault_failed"));
}
} else {
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
}
wallet_security_.clearDeferredEncryption();
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error("Encryption failed: " + err);
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
wallet_security_.clearDeferredEncryption();
};
}
@@ -531,7 +531,7 @@ void App::lockWallet() {
if (!lock_failure_warned_) {
lock_failure_warned_ = true;
ui::Notifications::instance().warning(
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
TR("sec_couldnt_lock_wallet"), 12.0f);
}
}
};
@@ -541,7 +541,7 @@ void App::lockWallet() {
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Changing passphrase...";
encrypt_status_ = TR("sec_changing_passphrase");
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
unlockTransactionHistoryCacheWithPassphrase(newPass);
storeTransactionHistoryCacheIfAvailable();
ui::Notifications::instance().info("Passphrase changed successfully");
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
} else {
encrypt_status_ = "Failed: " + err_msg;
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
}
util::SecureVault::secureZero(newPass.data(), newPass.size());
};
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
!encryption_incomplete_warned_) {
encryption_incomplete_warned_ = true;
ui::Notifications::instance().warning(
"Wallet encryption did not complete — your wallet is NOT encrypted. "
"Open Settings to finish encrypting it.", 30.0f);
TR("sec_encryption_did_not_complete"), 30.0f);
}
if (state_.transactions.empty()) {
loadTransactionHistoryCacheIfAvailable();
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
ImU32 textCol = ui::material::OnSurface();
{
const char* title = "Wallet Locked";
const char* title = TR("sec_wallet_locked_title");
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
char msg[128];
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) {
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
const char* switchLabel = lock_use_pin_
? "Use passphrase instead"
: "Use PIN instead";
? TR("sec_use_passphrase_instead")
: TR("sec_use_pin_instead");
// Current mode indicator — icon with icon font, text with caption font
ImFont* iconFont = ui::material::Type().iconSmall();
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
if (lock_unlock_in_progress_) {
// Animated spinner dots
char msg[64];
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
r->call("walletpassphrase", {passphrase, timeout});
rpcOk = true;
} else {
rpcErr = "Not connected to daemon";
rpcErr = TR("sec_not_connected_to_daemon");
}
} catch (const std::exception& e) {
rpcErr = e.what();
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
// so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
applyUnlockFailure("Unlock failed: " + rpcErr);
applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
}
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
if (!unlock.ok) {
return [this]() {
wallet_security_workflow_.failEntry("Incorrect passphrase");
wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
};
}
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
});
ui::Notifications::instance().info(
"Importing keys & rescanning blockchain — wallet is usable while this runs",
TR("sec_importing_keys_rescanning"),
8.0f);
};
});
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
wallet_security_workflow_.finishImport();
ui::Notifications::instance().error(
err +
"\nEncrypted backup: wallet.dat.encrypted.bak",
TR("sec_encrypted_backup_suffix"),
12.0f);
};
});
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
refreshPeerInfo();
ui::Notifications::instance().success(
"Wallet decrypted successfully! All keys imported.",
TR("sec_wallet_decrypted_all_keys_imported"),
8.0f);
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
};
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
int tMins = (int)(totalElapsed / 60);
int tSecs = (int)(totalElapsed % 60);
ImGui::Spacing();
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
}
// ---- Phase 2: Success ----
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying passphrase...";
pin_status_ = TR("sec_verifying_passphrase");
// Verify passphrase + store vault on worker thread to avoid
// blocking the UI with Argon2id key derivation.
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
return [this]() {
pin_status_ = "Incorrect passphrase";
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
pin_in_progress_ = false;
};
}
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_setup_ = false;
ui::Notifications::instance().info("PIN set successfully");
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
} else {
pin_status_ = "Failed to create vault";
pin_status_ = TR("sec_failed_to_create_vault");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Not connected to daemon";
pin_status_ = TR("sec_not_connected_to_daemon_pin");
pin_in_progress_ = false;
}
}
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Changing PIN...";
pin_status_ = TR("sec_changing_pin");
std::string oldPin(pin_old_buf_);
std::string newPinCopy = newPin;
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_change_ = false;
ui::Notifications::instance().info("PIN changed successfully");
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
} else {
pin_status_ = "Incorrect current PIN";
pin_status_ = TR("sec_incorrect_current_pin");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_change_pin");
pin_in_progress_ = false;
}
}
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying PIN...";
pin_status_ = TR("sec_verifying_pin");
std::string oldPin(pin_old_buf_);
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_remove_ = false;
ui::Notifications::instance().info("PIN removed");
ui::Notifications::instance().info(TR("sec_pin_removed"));
} else {
pin_status_ = "Incorrect PIN";
pin_status_ = TR("sec_incorrect_pin_remove");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_remove_pin");
pin_in_progress_ = false;
}
}
+19
View File
@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
add("modal-faq", ui::NavPage::Overview,
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
add("modal-encrypt", ui::NavPage::Settings,
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
@@ -704,9 +706,26 @@ void App::startSweepImpl(bool full)
if (sk.valid) sweep_skins_.push_back(sk.id);
if (sweep_skins_.empty()) return;
// Debug Options "Current theme only": sweep just the active skin instead of cycling every theme.
if (sweep_current_theme_only_)
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
// under review instead of all surfaces x every skin.
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
sweep_full_ = full;
if (full) { capture_mode_ = true; installDemoWalletData(); }
buildSweepCatalog();
if (!sweepOnlyStr.empty()) {
std::vector<SweepTarget> keep;
for (const auto& t : sweep_targets_)
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
sweep_targets_.swap(keep);
}
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
+6
View File
@@ -232,6 +232,7 @@ bool Settings::load(const std::string& path)
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
empty_wallet_warning_acked_.clear();
for (const auto& w : j["empty_wallet_warning_acked"])
@@ -251,6 +252,8 @@ bool Settings::load(const std::string& path)
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
loadScalar(j, "max_connections", max_connections_);
loadScalar(j, "stratum_host_enabled", stratum_host_enabled_);
loadScalar(j, "stratum_allowip", stratum_allowip_);
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) {
@@ -506,6 +509,7 @@ bool Settings::save(const std::string& path)
}
j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_;
j["large_wallet_warned"] = large_wallet_warned_;
j["empty_wallet_warning_acked"] = json::array();
for (const auto& w : empty_wallet_warning_acked_)
j["empty_wallet_warning_acked"].push_back(w);
@@ -523,6 +527,8 @@ bool Settings::save(const std::string& path)
j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
j["stratum_host_enabled"] = stratum_host_enabled_;
j["stratum_allowip"] = stratum_allowip_;
{
json lite = json::object();
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
+12
View File
@@ -330,6 +330,10 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
bool getLargeWalletWarned() const { return large_wallet_warned_; }
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
// warning has been dismissed. Keyed per active wallet file so switching to a different empty
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
@@ -396,6 +400,11 @@ public:
// Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Host a RandomX stratum pool from the node (v1.3.0+ daemons). Empty allow-IP = loopback only (safe).
bool getStratumHost() const { return stratum_host_enabled_; }
void setStratumHost(bool v) { stratum_host_enabled_ = v; }
const std::string& getStratumAllowIp() const { return stratum_allowip_; }
void setStratumAllowIp(const std::string& v) { stratum_allowip_ = v; }
// Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
@@ -597,6 +606,7 @@ private:
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
bool large_wallet_warned_ = false;
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
bool encryption_pending_ = false;
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
@@ -612,6 +622,8 @@ private:
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
bool stratum_host_enabled_ = false; // host a RandomX stratum pool from the node (v1.3.0+ daemons)
std::string stratum_allowip_; // -stratumallowip filter (empty = daemon default: loopback only)
// Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here.
+1
View File
@@ -26,6 +26,7 @@ void DaemonController::syncSettings(const config::Settings* settings)
if (!settings) return;
daemon_->setDebugCategories(settings->getDebugCategories());
daemon_->setMaxConnections(settings->getMaxConnections());
daemon_->setStratumHosting(settings->getStratumHost(), settings->getStratumAllowIp());
std::string walletFile = settings->getActiveWalletFile();
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
+10
View File
@@ -49,6 +49,16 @@ inline bool walletAutoRecovered(const std::string& out)
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside
}
// True when dragonxd (v1.3.0+) opened the wallet in DEGRADED mode: a wallet.dat that lost its hdchain
// record (e.g. an old `-salvagewallet` output) now OPENS — existing keys stay intact and spendable —
// instead of aborting, but the daemon can no longer derive NEW HD keys, so z_getnewaddress /
// z_shieldcoinbase / a t->z z_sendmany fail with "HD seed not found". The only signal is a startup log
// line; pre-1.3.0 daemons never emit it, so this classifier is naturally a no-op against them.
inline bool walletOpenedDegraded(const std::string& out)
{
return out.find("Wallet opened in DEGRADED mode") != std::string::npos;
}
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
inline long long parseWalletSalvageBakTs(const std::string& name)
{
+32 -8
View File
@@ -205,11 +205,13 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-ac_reward=300000000",
"-ac_blocktime=36",
"-ac_private=1",
"-addnode=node.dragonx.is",
// Seeds: seed.dragonx.is is a round-robin A record over the live seed set (self-updates without
// a wallet release), with node1/node5 as static fallbacks — mirrors the daemon's own vSeeds.
// Plain -addnode hostname resolution works on EVERY daemon version, and is load-bearing for
// pre-1.3.0 daemons whose built-in peer discovery was broken (they rely on these to find peers).
"-addnode=seed.dragonx.is",
"-addnode=node1.dragonx.is",
"-addnode=node2.dragonx.is",
"-addnode=node3.dragonx.is",
"-addnode=node4.dragonx.is",
"-addnode=node5.dragonx.is",
"-experimentalfeatures",
"-developerencryptwallet",
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
@@ -543,6 +545,15 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-maxconnections=" + std::to_string(max_connections_));
}
// Host a RandomX stratum pool from this node (-stratum). Only v1.3.0+ daemons implement it; older
// ones ignore the unknown flag (no fatal arg check), and the Settings toggle is gated on daemon
// version, so this is only enabled against a daemon that supports it. Without -stratumallowip the
// daemon serves loopback only (safe default); a subnet opens it to that LAN.
if (stratum_enabled_) {
args.push_back("-stratum");
if (!stratum_allowip_.empty()) args.push_back("-stratumallowip=" + stratum_allowip_);
}
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
// non-default name so the common case's command line is unchanged; skip during an isolated
// start (seed migration manages its own throwaway wallet).
@@ -689,7 +700,10 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
debug_log_path_.c_str(), debug_log_offset_);
}
// Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE).
// Launch daemon windowless. Use CREATE_NO_WINDOW (NOT CREATE_NEW_CONSOLE): CREATE_NEW_CONSOLE
// allocates a console window that briefly flashes on screen before SW_HIDE can hide it, which is
// visible as a console-window flash on wallet launch. CREATE_NO_WINDOW gives the console child no
// window at all (same approach as the xmrig launcher). The daemon logs to debug.log, not a console.
// The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
// — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
STARTUPINFOA si;
@@ -699,7 +713,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
ZeroMemory(&pi, sizeof(pi));
char* cmd_line = _strdup(cmd.c_str());
BOOL success = CreateProcessA(
NULL,
@@ -707,7 +721,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
CREATE_NO_WINDOW,
NULL,
work_dir.c_str(),
&si,
@@ -802,11 +816,21 @@ void EmbeddedDaemon::drainOutput()
}
size_t currentSize = static_cast<size_t>(fileSize.QuadPart);
// Truncation / rotation detection. dragonxd shrinks debug.log on startup (ShrinkDebugFile keeps
// only the tail once it has grown large), so the file can become SMALLER than where we left off.
// Our offset was set to the PRE-shrink size at spawn, so without this reset it stays stranded ahead
// of the freshly-truncated file and we read NOTHING for the whole session — no block height (status
// bar shows "Block: 0") and, worse, no witness-rebuild progress, since the "Setting Initial Sapling
// Witness …" lines the warmup progress bar parses only exist in debug.log on Windows. On a shrink,
// restart from the new beginning; the parser is monotonic and converges as it reaches current output.
if (currentSize < debug_log_offset_) {
debug_log_offset_ = 0;
}
if (currentSize <= debug_log_offset_) {
CloseHandle(hFile);
return; // No new data
}
// Seek to where we left off
LARGE_INTEGER seekPos;
seekPos.QuadPart = static_cast<LONGLONG>(debug_log_offset_);
+3
View File
@@ -182,6 +182,7 @@ public:
* @brief Set maximum peer connections (0 = use daemon default)
*/
void setMaxConnections(int v) { max_connections_ = v; }
void setStratumHosting(bool enabled, const std::string& allowIp) { stratum_enabled_ = enabled; stratum_allowip_ = allowIp; }
/**
* @brief Request a blockchain rescan on the next daemon start
@@ -311,6 +312,8 @@ private:
std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default
bool stratum_enabled_ = false; // -stratum: host a RandomX pool (v1.3.0+; older daemons ignore it)
std::string stratum_allowip_; // -stratumallowip subnet (empty = daemon default: loopback only)
std::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
+14 -41
View File
@@ -23,6 +23,7 @@
#include <curl/curl.h>
#include "../util/logger.h"
#include "../util/platform.h"
#include "../util/pool_registry.h"
#ifdef _WIN32
@@ -145,32 +146,18 @@ std::string XmrigManager::findXmrigBinary() {
return path;
}
// Fallback: system PATH
// Fallback: system PATH — windowless so it never flashes a console.
#ifdef _WIN32
FILE* f = _popen("where xmrig.exe 2>nul", "r");
std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
#else
FILE* f = popen("which xmrig 2>/dev/null", "r");
#endif
if (f) {
char line[512];
if (fgets(line, sizeof(line), f)) {
std::string s(line);
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
s.pop_back();
if (!s.empty() && fs::exists(s)) {
#ifdef _WIN32
_pclose(f);
#else
pclose(f);
#endif
return s;
}
}
#ifdef _WIN32
_pclose(f);
#else
pclose(f);
std::string out = util::Platform::runHiddenCapture("which xmrig");
#endif
{
std::string s = out;
const auto nl = s.find_first_of("\r\n"); // first line only
if (nl != std::string::npos) s.erase(nl);
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
if (!s.empty() && fs::exists(s)) return s;
}
return {};
@@ -927,24 +914,10 @@ void XmrigManager::startVersionDetection()
const bool binShellSafe =
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
if (binShellSafe) {
const std::string cmd = "\"" + bin + "\" --version 2>&1";
#ifdef _WIN32
FILE* fp = _popen(cmd.c_str(), "r");
#else
FILE* fp = popen(cmd.c_str(), "r");
#endif
if (fp) {
std::string out;
char buf[256];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
#ifdef _WIN32
_pclose(fp);
#else
pclose(fp);
#endif
ver = parseMinerVersion(out);
}
// Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
const std::string cmd = "\"" + bin + "\" --version";
const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
if (!out.empty()) ver = parseMinerVersion(out);
}
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
g_installed_ver = ver;
+54 -5
View File
@@ -1263,6 +1263,36 @@ int main(int argc, char* argv[])
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
}
// DEV/TEST hook (dormant unless the env is set): DRAGONX_WIN_GEOM="WxH" forces an exact window
// size, placing it on the largest display that can hold it and bypassing the primary-monitor
// clamp. Lets a headless WSLg sweep render at sizes wider than the 1280 primary (e.g. 2560x1440
// on the mirrored 4K/1440p outputs). Needs the x11 backend so absolute positioning takes effect.
int wantW = 0, wantH = 0;
if (const char* geom = std::getenv("DRAGONX_WIN_GEOM"))
sscanf(geom, "%dx%d", &wantW, &wantH);
if (wantW > 0 && wantH > 0) {
int count = 0;
SDL_DisplayID* disp = SDL_GetDisplays(&count);
SDL_DisplayID best = 0; SDL_Rect bestUsable{0, 0, 0, 0};
for (int i = 0; i < count; ++i) {
SDL_Rect u;
if (!SDL_GetDisplayUsableBounds(disp[i], &u)) continue;
bool fits = (u.w >= wantW && u.h >= wantH);
bool bestFits = (bestUsable.w >= wantW && bestUsable.h >= wantH);
// Prefer a display that fits; among those, the smallest; else the largest available.
if ((fits && !bestFits) ||
(fits && bestFits && (long)u.w * u.h < (long)bestUsable.w * bestUsable.h) ||
(!fits && !bestFits && (long)u.w * u.h > (long)bestUsable.w * bestUsable.h)) {
bestUsable = u; best = disp[i];
}
}
if (disp) SDL_free(disp);
if (best) {
SDL_SetWindowMinimumSize(window, 320, 240);
SDL_SetWindowPosition(window, bestUsable.x + 10, bestUsable.y + 10);
SDL_SetWindowSize(window, wantW, wantH);
}
} else {
// Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
// inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
// window off-screen or bigger than the screen on a same-DPI cold start).
@@ -1281,6 +1311,7 @@ int main(int argc, char* argv[])
DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
curW, curH, newW, newH, currentDpiScale);
}
}
}
#endif
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
@@ -1469,6 +1500,7 @@ int main(int argc, char* argv[])
// WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
int dpiSettleFrames = 0;
SDL_DisplayID lastLoggedDisplay = 0; // [WINLOG] throttle: log MOVED only when the display changes
Uint64 minimizedLastTickMs = 0; // real-clock tick for the minimized "keep syncing" update
{
float s = dragonx::ui::material::Typography::instance().getDpiScale();
int w = 0, h = 0;
@@ -1552,6 +1584,7 @@ int main(int argc, char* argv[])
// Window restored from minimized — trigger immediate data refresh
if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
app.refreshNow();
}
// Handle DPI change that arrived while idle (same logic as poll loop)
@@ -1681,6 +1714,7 @@ int main(int argc, char* argv[])
// Window restored from minimized — trigger immediate data refresh
if (event.type == SDL_EVENT_WINDOW_RESTORED &&
event.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
app.refreshNow();
}
// Handle DPI/display scale changes (e.g. window dragged to a
@@ -1701,13 +1735,28 @@ int main(int argc, char* argv[])
// Check if window is minimized
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
// Still check shouldQuit while minimized to avoid hang
if (app.shouldQuit()) {
running = false;
}
SDL_Delay(10);
// Keep the wallet syncing while minimized: run the logic update (drains RPC results, ticks the
// refresh scheduler, keeps the daemon connection/reconnect + sync status live) but skip the
// ImGui frame + GPU present since nothing is visible. app.update() only reads
// GetIO()/GetTime()/IsAnyItemActive() — all valid outside a frame — so it's safe without a
// NewFrame; feed it a real-clock DeltaTime (NewFrame, which normally sets it, is skipped) and
// let app.update() clamp it. Throttled to ~5 Hz so CPU stays near-idle (refresh cadences are
// seconds-scale). shouldQuit is still checked so a quit request never hangs behind minimize.
Uint64 nowMs = SDL_GetTicks();
float minDelta = (minimizedLastTickMs == 0) ? 0.001f
: (float)(nowMs - minimizedLastTickMs) / 1000.0f;
minimizedLastTickMs = nowMs;
ImGui::GetIO().DeltaTime = (minDelta > 0.0f) ? minDelta : 0.001f;
try {
app.update();
} catch (const std::exception& e) {
DEBUG_LOGF("[Main] minimized app.update() threw: %s\n", e.what());
} catch (...) {}
if (app.shouldQuit()) running = false;
SDL_Delay(200);
continue;
}
minimizedLastTickMs = 0; // visible again — reset the minimized clock
// --- PerfLog: begin frame ---
dragonx::util::PerfLog::instance().beginFrame();
+4 -4
View File
@@ -456,11 +456,11 @@ bool Connection::createDefaultConfig(const std::string& path)
file << "exportdir=" << dataDir << "\n";
file << "experimentalfeatures=1\n";
file << "developerencryptwallet=1\n";
file << "addnode=node.dragonx.is\n";
// Round-robin DNS seed (self-updating) + static fallbacks; mirrors the daemon's vSeeds and keeps
// pre-1.3.0 daemons (broken peer discovery) able to find peers. Works on every daemon version.
file << "addnode=seed.dragonx.is\n";
file << "addnode=node1.dragonx.is\n";
file << "addnode=node2.dragonx.is\n";
file << "addnode=node3.dragonx.is\n";
file << "addnode=node4.dragonx.is\n";
file << "addnode=node5.dragonx.is\n";
file.close();
+64 -9
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <map>
@@ -294,8 +295,37 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
json blockInfo;
bool balanceOk = false;
bool blockOk = false;
double balanceScanMs = 0.0;
if (includeBalance) {
// getblockchaininfo FIRST — it's cheap, and the sync state it returns gates everything else
// (the balance/address/tx throttles and kSyncProfile). Running it BEFORE the O(mapWallet) balance
// scan keeps sync detection from being delayed behind (or, under contention, starved by) that
// scan — the failure mode where the wallet kept reading "synced" while actually falling behind,
// so kSyncProfile never engaged. See effectivelySyncing()'s sticky-behind latch.
try {
blockInfo = rpc.call("getblockchaininfo", json::array());
blockOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
// If getblockchaininfo shows we're behind (same 2-block tolerance as applyCoreRefreshResult), skip
// the balance scan this cycle regardless of includeBalance: the balance is incomplete mid-sync
// anyway, and skipping it lets the sync-state update — and hence kSyncProfile — take effect at the
// end of THIS (now-cheap) task instead of being delayed ~20s behind the scan. This is what lets the
// sticky-behind latch engage promptly the first time the node falls behind.
bool behind = false;
if (blockOk && blockInfo.is_object()) {
const long long b = blockInfo.value("blocks", 0LL);
const long long lc = blockInfo.value("longestchain", 0LL);
if (lc > 0 && b < lc - 2) behind = true;
}
if (includeBalance && !behind) {
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
// (balanceRefreshDue()), keeping balance scans from starving block connection.
const auto balanceStart = std::chrono::steady_clock::now();
try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater
totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
balanceOk = true;
@@ -305,16 +335,13 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
} catch (...) {}
balanceScanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - balanceStart).count();
}
try {
blockInfo = rpc.call("getblockchaininfo", json::array());
blockOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
return parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
result.balanceScanMs = balanceScanMs;
return result;
}
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
@@ -593,6 +620,10 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
const AddressRefreshSnapshot& snapshot)
{
AddressRefreshResult result;
// Time the whole scan — z_listunspent (and per-address z_getbalance fallback) hold the daemon's
// cs_main for the duration, seconds on a large shielded wallet. The measured cost feeds the
// caller's adaptive throttle so the address poll can't starve block connection.
const auto scanStart = std::chrono::steady_clock::now();
try {
json zList = rpc.call("z_listaddresses", json::array());
@@ -634,6 +665,22 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
try {
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
// Retain a minimal view so a downstream consumer (chat note-budget) can reuse this scan instead
// of issuing its own z_listunspent. rawconfirmations is the TRUE depth; `confirmations` is
// dPoW-clamped to 1 and understates it.
if (unspent.is_array()) {
result.unspentNotes.reserve(unspent.size());
for (const auto& nz : unspent) {
if (!nz.is_object()) continue;
UnspentNoteLite lite;
lite.amount = nz.value("amount", 0.0);
lite.locked = nz.value("locked", false);
lite.confirmations = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
? nz["rawconfirmations"].get<int>()
: nz.value("confirmations", 0);
result.unspentNotes.push_back(lite);
}
}
} catch (const std::exception& e) {
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
for (auto& info : result.shieldedAddresses) {
@@ -663,6 +710,8 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
DEBUG_LOGF("listunspent error: %s\n", e.what());
}
result.scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return result;
}
@@ -854,6 +903,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
result.blockHeight = currentBlockHeight;
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
result.shieldedScanHeights = snapshot.shieldedScanHeights;
// Time the whole scan — the per-address z_listreceivedbyaddress pass is O(mapWallet) and holds the
// daemon's cs_main. The measured cost feeds the caller's adaptive throttle (txRefreshDue()) so the
// routine full history rescan can't starve block connection on a large wallet.
const auto scanStart = std::chrono::steady_clock::now();
std::set<std::string> knownTxids;
HushChatMemoOutputMap hushChatReceivedOutputs;
@@ -1034,6 +1087,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
}
sortTransactionsNewestFirst(result.transactions);
result.scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
return result;
}
+18
View File
@@ -111,6 +111,7 @@ public:
std::optional<double> verificationProgress;
std::optional<int> longestChain;
std::optional<int> notarized;
double balanceScanMs = 0.0; // wall-clock spent in z_gettotalbalance this refresh (0 if balance skipped)
};
struct MiningRefreshResult {
@@ -146,6 +147,14 @@ public:
std::string errorMessage;
};
// Minimal per-note view of a z_listunspent entry — just what a downstream consumer needs to derive
// spendable-note budgets without re-scanning. Kept UI/feature-agnostic (no chat specifics here).
struct UnspentNoteLite {
double amount = 0.0; // note value, DRGX
bool locked = false; // tied up by an in-flight send
int confirmations = 0; // TRUE depth (rawconfirmations when present, else confirmations)
};
struct AddressRefreshResult {
std::vector<AddressInfo> shieldedAddresses;
std::vector<AddressInfo> transparentAddresses;
@@ -153,6 +162,12 @@ public:
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. the
// empty-wallet warning) must not trust a 0 count unless this is true.
bool addressListOk = true;
// Wall-clock spent in the address scan (dominated by z_listunspent — O(mapWallet), holds the
// daemon's cs_main). Lets the caller throttle how often it polls (addressRefreshDue()).
double scanMs = 0.0;
// The wallet's unspent notes from this same z_listunspent scan, so a consumer (e.g. the chat
// note-budget) can be fed for free instead of running its own duplicate z_listunspent.
std::vector<UnspentNoteLite> unspentNotes;
};
struct AddressRefreshSnapshot {
@@ -204,6 +219,9 @@ public:
std::size_t shieldedAddressCount = 0;
std::unordered_map<std::string, int> shieldedScanHeights;
bool shieldedScanComplete = true;
// Wall-clock spent in the history scan (z_listreceivedbyaddress — O(mapWallet), holds cs_main).
// Lets the caller throttle the routine full rescan by its measured cost (txRefreshDue()).
double scanMs = 0.0;
};
struct OperationStatusPollResult {
+12 -8
View File
@@ -5,6 +5,7 @@
#include "theme_effects.h"
#include "low_spec.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
void ThemeEffects::loadFromTheme() {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
auto eff = [&](const char* name) {
return S.drawElement("effects", name);
};
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
// ---- Shimmer ----
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f) * dp;
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
// Shimmer color: read from the schema's color resolver
@@ -124,7 +126,7 @@ void ThemeEffects::loadFromTheme() {
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
auto glowColorElem = eff("glow-pulse-color");
if (!glowColorElem.color.empty()) {
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
@@ -136,7 +138,7 @@ void ThemeEffects::loadFromTheme() {
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
auto edgeColorElem = eff("edge-trace-color");
if (!edgeColorElem.color.empty()) {
@@ -149,7 +151,7 @@ void ThemeEffects::loadFromTheme() {
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
auto emberColorElem = eff("ember-rise-color");
if (!emberColorElem.color.empty()) {
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
auto gbColorA = eff("gradient-border-color-a");
if (!gbColorA.color.empty()) {
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
@@ -694,6 +696,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
float w = pMax.x - pMin.x;
float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return;
@@ -707,7 +710,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
// Deterministic pseudo-random x position per particle
// Simple hash: sin of large prime multiples
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway
float x = pMin.x + w * xHash + xDrift;
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
@@ -745,6 +748,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
ImGuiViewport* vp = ImGui::GetMainViewport();
float vpW = vp->WorkSize.x;
float vpH = vp->WorkSize.y;
@@ -765,7 +769,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
xHash = xHash - (int)xHash; // fractional part
if (xHash < 0) xHash += 1.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;
float x = vpX + vpW * xHash + xDrift;
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
+17 -5
View File
@@ -173,11 +173,23 @@ inline float kSidePanelMinWidth() { return schema::UI().drawElement("panels",
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
// Overall content-column cap: the max width a tab's content should occupy before it is centered in wider
// windows. Prevents cards/forms/tables (which all derive their size from the content child's width) from
// stretching edge-to-edge at wide/ultrawide widths. <= 0 disables (fill full width). Tunable via ui.toml
// [layout] content-max-width; default is generous so data-dense screens stay comfortable.
inline float kContentMaxWidth() { return schema::UI().drawElement("layout", "content-max-width").sizeOr(1600.0f) * dpiScale(); }
// Overall content-column cap: the max width a tab's content occupies before it is centered in wider
// windows. <= 0 disables the cap so tab content fills ALL available horizontal width (the default —
// requested so large windows don't leave a big empty gutter on the right). Set a positive
// ui.toml [layout] content-max-width to re-enable a centered readable column.
inline float kContentMaxWidth() { return schema::UI().drawElement("layout", "content-max-width").sizeOr(0.0f) * dpiScale(); }
// Shared compose-card envelope for the Send + Receive tabs (and any tab wanting the same box): fill the
// available column up to the content-max-width cap, then center the leftover as margin. Both tabs MUST
// derive their card width/offset from this so the two envelopes stay byte-for-byte identical — they
// previously drifted (Send capped at 760dp, Receive at 860dp), so the Send card rendered narrower than
// Receive on any window wider than ~860dp. Returns {width, offsetX} in the same units as availW.
struct CardBox { float width; float offsetX; };
inline CardBox mainComposeCardBox(float availW) {
float cap = kContentMaxWidth();
float w = (cap > 0.0f) ? std::min(availW, cap) : availW; // cap <= 0 -> fill full width
return CardBox{ w, std::max(0.0f, (availW - w) * 0.5f) };
}
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
+19 -13
View File
@@ -544,11 +544,16 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0),
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
// For icon fonts, manually draw centered icon after getting button rect
// For icon fonts, manually draw centered icon after getting button rect. Measure/draw only the
// VISIBLE label (up to the "##id" separator): CalcTextSizeA/AddText don't strip "##" the way
// ImGui's own text render does, so an id suffix like "##pickContact" would inflate textSz and
// shove the glyph left off-center (and try to draw the notdef id chars).
if (isIconFont && size.x > 0 && size.y > 0) {
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label);
const char* labelEnd = label;
while (*labelEnd && !(labelEnd[0] == '#' && labelEnd[1] == '#')) ++labelEnd;
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label, labelEnd);
ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f);
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label);
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label, labelEnd);
}
float rounding = ImGui::GetStyle().FrameRounding;
@@ -920,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
// Draw a full-height rounded rect with card rounding (left corners)
// and clip to stripe width so the shape follows the corner radius.
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
float stripeW = 4.0f;
float stripeW = 4.0f * Layout::dpiScale();
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
ImDrawFlags_RoundCornersLeft);
@@ -1277,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
float winWidth = ImGui::GetWindowWidth();
float barHeight = 36.0f;
const float dp = Layout::dpiScale();
float barHeight = 36.0f * dp;
// Get accent color from theme if not provided
if (!accent_col) {
@@ -1301,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImFont* titleFont = Type().subtitle1();
ImGui::PushFont(titleFont);
ImVec2 titleSize = ImGui::CalcTextSize(title);
float titleX = barMin.x + 16.0f;
float titleX = barMin.x + 16.0f * dp;
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
ImGui::PopFont();
// Close button (X) on right side
if (p_open) {
float btnSize = 24.0f;
float btnX = barMax.x - btnSize - 12.0f;
float btnSize = 24.0f * dp;
float btnX = barMax.x - btnSize - 12.0f * dp;
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
@@ -1322,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Button background on hover
if (hovered) {
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f * dp);
}
// Draw X icon
@@ -1343,7 +1349,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
}
// Reserve space for title bar so content starts below it
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
return closeClicked;
}
@@ -1643,7 +1649,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
// tiles, chat) while staying translucent rather than opaque.
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
}
@@ -1657,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Content child.
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28 * dp, 20 * dp) : ImVec2(28 * dp, 24 * dp));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
// content overflowed the viewport); otherwise the child auto-resizes to its content.
+8 -6
View File
@@ -107,7 +107,7 @@ inline float ActionButtonWidth(const char* label, const char* icon, float minWid
ImFont* lf = Type().button();
ImFont* icf = Type().iconSmall();
const float dp = Layout::dpiScale();
const float padX = 12.0f * dp, gap = 6.0f * dp;
const float padX = 9.0f * dp, gap = 6.0f * dp; // mockup .btn padding: 9px horizontal
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
const float iconW = (icon && icon[0] && icf) ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
const float w = padX * 2.0f + iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
@@ -133,19 +133,21 @@ inline bool ActionButton(const char* id, const char* label, const char* icon, Ac
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImVec2 pMax(pos.x + w, pos.y + h);
const float round = ImGui::GetStyle().FrameRounding;
const float round = 7.0f * dp; // mockup .btn radius: 7px (softer than the global 4px frame)
const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
ImU32 bg = 0, border = 0, fg = OnSurface();
bool glass = false;
switch (tier) {
case ActionTier::Primary:
bg = WithAlpha(Primary(), act ? 255 : (hov ? 245 : 220));
fg = IM_COL32(255, 255, 255, 240);
// Mockup .btn.acc: a dark accent-tinted chip with accent TEXT — not a bright filled button.
bg = WithAlpha(Primary(), hov ? 52 : 38);
border = WithAlpha(Primary(), hov ? 150 : 110);
fg = Primary();
break;
case ActionTier::Secondary:
bg = WithAlpha(OnSurface(), hov ? 26 : 16);
border = WithAlpha(OnSurface(), 40);
bg = WithAlpha(OnSurface(), hov ? 30 : 20);
border = WithAlpha(OnSurface(), 48);
glass = true;
fg = OnSurface();
break;
+14 -2
View File
@@ -31,6 +31,8 @@ struct AlertRecord {
std::string message;
NotificationType type;
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display
std::function<void()> onClick; // optional: makes this bell-panel entry actionable
std::string actionHint; // optional: accent link label rendered for the action
};
struct Notification {
@@ -92,15 +94,25 @@ public:
if (duration < 0.0f) duration = schemaDuration("duration-error", 4.0f);
push(message, NotificationType::Error, duration);
}
// An actionable alert: a normal toast PLUS a clickable entry in the bell/alert-history panel.
// onClick fires when the user clicks the accent `actionHint` link in that panel.
void action(const std::string& message, NotificationType type, std::function<void()> onClick,
const std::string& actionHint, float duration = -1.0f) {
if (duration < 0.0f) duration = schemaDuration("duration-warning", 3.5f);
push(message, type, duration, std::move(onClick), actionHint);
}
void push(const std::string& message, NotificationType type, float duration = 5.0f) {
void push(const std::string& message, NotificationType type, float duration = 5.0f,
std::function<void()> onClick = nullptr, const std::string& actionHint = "") {
notifications_.emplace_back(message, type, duration);
// Retain a copy in the persistent history (the toast above will fade in seconds; this
// survives so the user can review what happened). Thread note: every push is on the UI
// thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock,
// consistent with the rest of this class. Do NOT push from a raw worker thread.
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr))});
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr)),
std::move(onClick), actionHint});
++total_pushed_;
while (history_.size() > kMaxHistory) {
history_.pop_front();
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
float lineW = s_cc.lineW;
// --- Outer glow pass: wider, softer dark edge on top-left ---
float glowExpand = s_cc.glowExpand;
float glowExpand = s_cc.glowExpand * Layout::dpiScale();
int glowA = (int)s_cc.glowAlpha;
float glowLineW = s_cc.glowLineW;
{
@@ -289,6 +289,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
// inner pass gives crisp bevel edge. All use AddRect with rounding so
// every layer follows the rounded corners perfectly — no clip rects needed.
{
const float dp = Layout::dpiScale();
float cx = (mn.x + mx.x) * 0.5f;
float cy = (mn.y + mx.y) * 0.5f;
@@ -304,7 +305,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
BevelPass passes[] = {
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.5f * dp, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
};
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
}
}
if (depth > s_ic.threshold) {
float baseInset = s_ic.inset;
float baseInset = s_ic.inset * Layout::dpiScale();
int shadowMax = (int)(s_ic.maxAlpha * depth);
float fadeRatio = s_ic.fadeRatio;
float bW = mx.x - mn.x - baseInset * 2.0f;
@@ -487,7 +488,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i)
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap;
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (showLabels) {
curY += sectionGap;
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
curY += olFsz + 2.0f + sectionLabelPadBot;
curY += olFsz + 2.0f * dp + sectionLabelPadBot;
} else {
curY += sectionGap * 0.4f;
if (nSeparators < 4) separatorY[nSeparators++] = curY;
@@ -538,11 +539,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float exitRelY = curY + bottomPadding;
float panelH = exitRelY + stripH;
// Vertical centering — offset so panel is centered in the child window
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
if (centerOffset + panelH > contentHeight)
centerOffset = std::max(0.0f, contentHeight - panelH);
// ===================================================================
// PASS 2: Render using computed positions
// ===================================================================
@@ -552,6 +548,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 wp = ImGui::GetWindowPos();
// Vertical centering — center the panel within the child. app.cpp sizes the child (contentHeight)
// to the visible area (child top -> status-bar top) using window-local geometry, so this yields
// equal top/bottom gaps at any height on every platform, no viewport dependency.
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
if (centerOffset + panelH > contentHeight)
centerOffset = std::max(0.0f, contentHeight - panelH);
float panelLeft = wp.x + glassMarginL;
float panelRight = wp.x + sidebarWidth - glassMarginR;
float panelTopY = wp.y + centerOffset;
@@ -651,7 +654,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
fx.drawShimmer(dl, indMin, indMax, btnRnd);
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
}
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f * dp);
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
buttonRects.push_back({indMin, indMax, btnRnd});
}
@@ -682,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
bool itemHasBadge =
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
(item.page == NavPage::Mining && status.miningActive) ||
(item.page == NavPage::Peers && status.peerCount > 0) ||
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
float badgeReserve = 0.0f;
if (itemHasBadge) {
@@ -730,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
badgeCol = Warning(); badgeTextCol = OnWarning();
} else if (item.page == NavPage::Mining && status.miningActive) {
dotOnly = true; badgeCol = Success();
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
badgeCount = status.peerCount;
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
badgeCount = status.chatUnreadCount;
}
+3 -2
View File
@@ -93,7 +93,7 @@ public:
// Arrow
{
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f);
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos();
@@ -312,7 +312,8 @@ private:
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
const float dp = Layout::dpiScale();
float btnW = 120.0f * dp;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false;
+1 -1
View File
@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
} else if (rows.empty()) {
float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
if (ch < 60.0f * dp) ch = 60.0f * dp;
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
+51 -51
View File
@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
// Card height: must fit the Market card's content (overline + price + 24h)
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f);
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f);
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
float marketContentH = cardPadLg
+ ovFont->LegacySize + ovGap
+ sub1->LegacySize + 2.0f * dp
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
// We draw a full-size rounded rect (left corners only) and clip it to the
// stripe width so the shape itself follows the card rounding.
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
ImU32 bd = WithAlpha(fg, 90);
snprintf(buf, sizeof(buf), "%s %s",
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
// Hover → explain what stale means and how old the data actually is.
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
Tooltip("%s", TR("data_stale_tooltip"));
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
}
}
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
{
float privPct = (s_dispTotal > 1e-9)
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr",
snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
privPct, (int)state.z_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
WithAlpha(Success(), 160), buf);
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
ImVec2 ts = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, buf);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
ImVec2 bMin(cMax.x - ts.x - bp * 3,
cMin.y + cardPadLg);
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive);
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
OnSurfaceMedium(), DRAGONX_TICKER);
cy += sub1->LegacySize + valGap;
snprintf(buf, sizeof(buf), "%d T-addresses",
snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
(int)state.t_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
OnSurfaceDisabled(), buf);
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive);
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
[](unsigned char c){ return (char)std::toupper(c); });
float textW = std::max(pSz.x + tickGap + usdSz.x,
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkLeft = cx + textW + sparkGap;
float sparkRight = cMax.x - cardPadLg;
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
bool pos = market.change_24h >= 0;
ImU32 chgCol = pos ? Success()
: Error();
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy), chgCol, buf);
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Market
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Market);
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f);
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
// Shielded legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded);
snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
legendY += capFont->LegacySize + legendLineGap;
// Transparent legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent);
snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
legendY += capFont->LegacySize + legendSectionGap;
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
const auto& market = state.market;
if (market.price_usd > 0) {
if (market.price_usd >= 0.01)
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd);
snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
else
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd);
snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
OnSurfaceMedium(), buf);
legendY += capFont->LegacySize + 4 * dp;
bool pos = market.change_24h >= 0;
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h);
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
pos ? Success() : Error(), buf);
}
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f));
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
// Bottom half: proportion bars
float barY = divY + Layout::spacingSm();
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
// Accent stripe — clipped to tile rounded corners
{
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
ImDrawFlags_RoundCornersLeft);
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
// Click
if (material::IsRectHovered(tMin, tMax)) {
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(tiles[i].nav);
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Font-content floor per row: icon + label + value must fit
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
+ Layout::spacingSm() * 2;
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f);
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
float stackH;
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
};
RowInfo rowInfos[4] = {
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
};
for (int i = 0; i < 4; i++) {
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
if (i == 1 || i == 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float barH = std::max(
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
// Sparkline in the gap between label and 24h change
if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) {
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
ImFont* iconFont = Type().iconSmall();
// Font-content floor per row: caption text + vertical padding
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
float stackH;
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
CellInfo cells[2][2] = {
// Row 0: Total Balance (left), Shielded (right)
{
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
},
// Row 1: Market (left), Transparent (right)
{
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
},
};
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
// Sparkline between label and 24h change
if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) {
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
gaugeCy + sinf(needleAngle) * needleLen);
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f));
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
// Center text: percentage
ImFont* sub1 = Type().subtitle1();
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
}
RenderSyncBar(app, dl, vs);
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f)));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
// Row 2: 3 mini-cards inline
{
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
ImFont* capFont = Type().caption();
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f);
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
// Sparkline between price and percentage
if (market.price_history.size() >= 2) {
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = cx + priceSz.x + sparkGap;
float sparkRightEdge = sparkRight - sparkGap;
if (sparkLeft < sparkRightEdge) {
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImVec2 sepPos = ImGui::GetCursorScreenPos();
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f);
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
float x = sepPos.x;
float endX = sepPos.x + availW;
while (x < endX) {
+4 -4
View File
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_loading = false;
if (!error.empty()) {
s_error = "Error: " + error;
s_error = std::string(TR("grpa_error_prefix")) + error;
return;
}
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_has_data = true;
} else {
s_error = "Invalid response from daemon";
s_error = TR("grpa_invalid_response_from_daemon");
}
}
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
// Current block info
if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
}
ImGui::SameLine();
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) {
error = "unexpected getblockhash result";
error = TR("grpa_unexpected_getblockhash_result");
} else {
block = rpc->call("getblock", {hashResult.get<std::string>()});
}
+2 -2
View File
@@ -144,7 +144,7 @@ private:
if (!s_bootstrap) {
s_state = State::Failed;
s_errorMsg = "Bootstrap not initialized";
s_errorMsg = TR("grpc_bootstrap_not_initialized");
return;
}
@@ -227,7 +227,7 @@ private:
s_state = State::Done;
} else {
s_errorMsg = finalProg.error;
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
s_state = State::Failed;
}
s_bootstrap.reset();
+54 -7
View File
@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
const std::string peer = "Ava";
const PMsg msgs[] = {
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
{ TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
};
const int N = 3;
@@ -1885,20 +1885,37 @@ void ResetChatTab()
s_show_chat_settings = false;
}
void RenderChatSettingsControls(App* app, float contentWidth)
void RenderChatSettingsControls(App* app, float contentWidth, bool drawCards)
{
auto* st = app ? app->settings() : nullptr;
if (!st) return;
const float dp = Layout::dpiScale();
const float ctrlW = 250.0f * dp; // control column width (fits a 3-segment control comfortably)
const float rowGap = 5.0f * dp;
const float rowGap = 10.0f * dp;
// Optionally paint two glass cards (Appearance | Messaging) around our own two columns so the
// Settings tab matches the mockup's card-per-group layout. The chat modal passes drawCards=false
// and keeps its plain single-surface layout — the controls themselves are identical either way.
ImDrawList* cardDL = ImGui::GetWindowDrawList();
const float cardPad = drawCards ? Layout::cardInnerPadding() : 0.0f;
material::GlassPanelSpec cardSpec; cardSpec.rounding = Layout::glassRounding();
float cardTopScr = 0.0f, cardBaseXScr = 0.0f, cardLeftBotScr = 0.0f;
if (drawCards) {
cardTopScr = ImGui::GetCursorScreenPos().y;
cardBaseXScr = ImGui::GetCursorScreenPos().x;
cardDL->ChannelsSplit(2);
cardDL->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardBaseXScr, cardTopScr + cardPad));
ImGui::Indent(cardPad);
}
// Right-align controls to the row's true right edge. The Settings tab renders us inside a GlassCard
// whose content region isn't narrowed to the card padding, so it passes an explicit contentWidth;
// the chat modal's dialog content region is correct, so it passes 0 (auto).
// leftX/rowW define the current column the rows lay out in; retargeted below
// to split Appearance | Messaging into two columns when the card is wide.
float leftX = ImGui::GetCursorPosX();
float rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x;
float rowW = drawCards ? (contentWidth - 2.0f * cardPad)
: ((contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x);
// Label left, control right-aligned within [leftX, leftX+rowW]. Leaves the cursor at the control origin.
auto beginRow = [&](const char* label) {
@@ -1966,7 +1983,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
// Two internal columns when the card is wide enough: Appearance on the left,
// Messaging on the right — fills the width and roughly halves the height.
// (Mirrors the Node & Security card.) Narrow (the chat modal) stays single-column.
const float chatColGap = 24.0f * dp;
const float chatColGap = drawCards ? (Layout::cardGap() + 2.0f * cardPad) : (24.0f * dp);
const bool chatTwoCol = rowW > 760.0f * dp;
const float chatColW = chatTwoCol ? (rowW - chatColGap) * 0.5f : rowW;
const float chatBaseLeftX = leftX;
@@ -2023,6 +2040,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
// line-start holds the column; retarget leftX so controls right-align in it).
if (chatTwoCol) {
chatLeftBottomY = ImGui::GetCursorPosY();
cardLeftBotScr = ImGui::GetCursorScreenPos().y; // left column bottom (screen), for its card panel
ImGui::SetCursorPosY(chatTopY);
ImGui::Indent(chatColW + chatColGap);
leftX = chatBaseLeftX + chatColW + chatColGap;
@@ -2056,12 +2074,41 @@ void RenderChatSettingsControls(App* app, float contentWidth)
}
// Close the two-column band: un-indent and drop below the taller column.
const float cardRightBotScr = ImGui::GetCursorScreenPos().y; // right (or only) column bottom, screen
if (chatTwoCol) {
ImGui::Unindent(chatColW + chatColGap);
const float chatRightBottomY = ImGui::GetCursorPosY();
ImGui::SetCursorPosX(chatBaseLeftX);
ImGui::SetCursorPosY(std::max(chatLeftBottomY, chatRightBottomY));
}
// Paint the glass card(s) behind the content, then merge the channels.
if (drawCards) {
ImGui::Unindent(cardPad);
cardDL->ChannelsSetCurrent(0);
const float cardW = (contentWidth - Layout::cardGap()) * 0.5f;
if (chatTwoCol) {
const float eqBot = std::max(cardLeftBotScr, cardRightBotScr); // equal-height cards (mockup grid stretch)
material::DrawGlassPanel(cardDL, ImVec2(cardBaseXScr, cardTopScr),
ImVec2(cardBaseXScr + cardW, eqBot + cardPad), cardSpec);
material::DrawGlassPanel(cardDL, ImVec2(cardBaseXScr + cardW + Layout::cardGap(), cardTopScr),
ImVec2(cardBaseXScr + contentWidth, eqBot + cardPad), cardSpec);
} else {
material::DrawGlassPanel(cardDL, ImVec2(cardBaseXScr, cardTopScr),
ImVec2(cardBaseXScr + contentWidth, cardRightBotScr + cardPad), cardSpec);
}
cardDL->ChannelsMerge();
const float botScr = chatTwoCol ? std::max(cardLeftBotScr, cardRightBotScr) : cardRightBotScr;
// Reserve the card footprint with a Dummy so the parent scroll region grows to include it
// (a bare SetCursorScreenPos past content warns in ImGui).
ImGui::SetCursorScreenPos(ImVec2(cardBaseXScr, cardTopScr));
ImGui::Dummy(ImVec2(contentWidth, (botScr - cardTopScr) + cardPad));
// Live conversation preview below the two cards (Settings tab only — the chat modal renders
// its own preview column beside these controls, so it passes drawCards=false and skips this).
ImGui::Dummy(ImVec2(0.0f, Layout::spacingMd()));
RenderChatSettingsPreview(app, contentWidth);
}
}
} // namespace ui
+1 -1
View File
@@ -34,7 +34,7 @@ void RenderChatTab(App* app);
* width from the Settings tab (whose GlassCard doesn't narrow the content region). 0 = auto
* (use the current content region, correct inside the chat modal's dialog).
*/
void RenderChatSettingsControls(App* app, float contentWidth = 0.0f);
void RenderChatSettingsControls(App* app, float contentWidth = 0.0f, bool drawCards = false);
/**
* @brief Securely wipe the Chat tab's UI-local state (composer / new-conversation
+1 -1
View File
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
float outputH = ComputeConsoleOutputHeight(
availHeight,
input_height,
schema::UI().drawElement("tabs.console", "output-min-height").size,
schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
ImDrawList* dlOut = ImGui::GetWindowDrawList();
+6 -6
View File
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60)
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
else if (diff < 3600)
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
else if (diff < 86400)
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
else
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
return buf;
}
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
txDL->AddRectFilled(rowStart,
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
WithAlpha(OnSurface(), 10),
S.drawElement("tabs.explorer", "row-rounding").size);
S.drawElement("tabs.explorer", "row-rounding").size * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", txid.c_str());
}
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
}
if (s_detail_txids.size() > 100) {
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
ImGui::TextDisabled("%s", buf);
}
}
+115
View File
@@ -0,0 +1,115 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_content.h"
namespace dragonx {
namespace ui {
namespace faq {
// ── Wallet group ───────────────────────────────────────────────────────────
const std::vector<FaqSubcategory>& walletFaq()
{
static const std::vector<FaqSubcategory> kWallet = {
{ "faq_w_gs_title", {
{ "faq_w_gs_1_q", "faq_w_gs_1_a" },
{ "faq_w_gs_2_q", "faq_w_gs_2_a" },
{ "faq_w_gs_3_q", "faq_w_gs_3_a" },
{ "faq_w_gs_4_q", "faq_w_gs_4_a" },
}},
{ "faq_w_addr_title", {
{ "faq_w_addr_1_q", "faq_w_addr_1_a" },
{ "faq_w_addr_2_q", "faq_w_addr_2_a" },
{ "faq_w_addr_3_q", "faq_w_addr_3_a" },
{ "faq_w_addr_4_q", "faq_w_addr_4_a" },
}},
{ "faq_w_send_title", {
{ "faq_w_send_1_q", "faq_w_send_1_a" },
{ "faq_w_send_2_q", "faq_w_send_2_a" },
{ "faq_w_send_3_q", "faq_w_send_3_a" },
{ "faq_w_send_4_q", "faq_w_send_4_a" },
}},
{ "faq_w_bal_title", {
{ "faq_w_bal_1_q", "faq_w_bal_1_a" },
{ "faq_w_bal_2_q", "faq_w_bal_2_a" },
{ "faq_w_bal_3_q", "faq_w_bal_3_a" },
}},
{ "faq_w_sec_title", {
{ "faq_w_sec_1_q", "faq_w_sec_1_a" },
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
}},
{ "faq_w_seed_title", {
{ "faq_w_seed_1_q", "faq_w_seed_1_a" },
{ "faq_w_seed_2_q", "faq_w_seed_2_a" },
{ "faq_w_seed_3_q", "faq_w_seed_3_a" },
{ "faq_w_seed_4_q", "faq_w_seed_4_a" },
}},
{ "faq_w_chat_title", {
{ "faq_w_chat_1_q", "faq_w_chat_1_a" },
{ "faq_w_chat_2_q", "faq_w_chat_2_a" },
{ "faq_w_chat_3_q", "faq_w_chat_3_a" },
}},
{ "faq_w_set_title", {
{ "faq_w_set_1_q", "faq_w_set_1_a" },
{ "faq_w_set_2_q", "faq_w_set_2_a" },
{ "faq_w_set_3_q", "faq_w_set_3_a" },
{ "faq_w_set_4_q", "faq_w_set_4_a" },
}},
};
return kWallet;
}
// ── Daemon group (full-node only) ──────────────────────────────────────────
const std::vector<FaqSubcategory>& daemonFaq()
{
static const std::vector<FaqSubcategory> kDaemon = {
{ "faq_d_node_title", {
{ "faq_d_node_1_q", "faq_d_node_1_a" },
{ "faq_d_node_2_q", "faq_d_node_2_a" },
{ "faq_d_node_3_q", "faq_d_node_3_a" },
}},
{ "faq_d_sync_title", {
{ "faq_d_sync_1_q", "faq_d_sync_1_a" },
{ "faq_d_sync_2_q", "faq_d_sync_2_a" },
{ "faq_d_sync_3_q", "faq_d_sync_3_a" },
{ "faq_d_sync_4_q", "faq_d_sync_4_a" },
}},
{ "faq_d_mgmt_title", {
{ "faq_d_mgmt_1_q", "faq_d_mgmt_1_a" },
{ "faq_d_mgmt_2_q", "faq_d_mgmt_2_a" },
{ "faq_d_mgmt_3_q", "faq_d_mgmt_3_a" },
}},
{ "faq_d_upd_title", {
{ "faq_d_upd_1_q", "faq_d_upd_1_a" },
{ "faq_d_upd_2_q", "faq_d_upd_2_a" },
{ "faq_d_upd_3_q", "faq_d_upd_3_a" },
}},
{ "faq_d_mine_title", {
{ "faq_d_mine_1_q", "faq_d_mine_1_a" },
{ "faq_d_mine_2_q", "faq_d_mine_2_a" },
{ "faq_d_mine_3_q", "faq_d_mine_3_a" },
{ "faq_d_mine_4_q", "faq_d_mine_4_a" },
}},
{ "faq_d_net_title", {
{ "faq_d_net_1_q", "faq_d_net_1_a" },
{ "faq_d_net_2_q", "faq_d_net_2_a" },
}},
{ "faq_d_perf_title", {
{ "faq_d_perf_1_q", "faq_d_perf_1_a" },
{ "faq_d_perf_2_q", "faq_d_perf_2_a" },
}},
{ "faq_d_trbl_title", {
{ "faq_d_trbl_1_q", "faq_d_trbl_1_a" },
{ "faq_d_trbl_2_q", "faq_d_trbl_2_a" },
{ "faq_d_trbl_3_q", "faq_d_trbl_3_a" },
{ "faq_d_trbl_4_q", "faq_d_trbl_4_a" },
}},
};
return kDaemon;
}
} // namespace faq
} // namespace ui
} // namespace dragonx
+41
View File
@@ -0,0 +1,41 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// FAQ content model. The FAQ screen is data-driven: this header exposes the two
// top-level groups (Wallet, Daemon) as ordered lists of subcategories, each a list
// of question/answer entries. Every string is an i18n KEY (looked up with TR at
// render time), so wording + translations live in src/util/i18n.cpp + res/lang/*.json
// and never require touching UI code. Add a Q&A by appending a {qKey,aKey} pair here
// and its two strings to loadBuiltinEnglish().
#pragma once
#include <vector>
namespace dragonx {
namespace ui {
namespace faq {
// One question and its answer, both i18n keys. The answer may contain "\n\n"
// paragraph breaks; it is rendered wrapped. Keep answers free of printf specifiers
// (%d/%s/…) — the i18n layer rejects translations whose format signature drifts.
struct FaqEntry {
const char* questionKey;
const char* answerKey;
};
// A named group of questions. titleKey is an i18n key for the subcategory header.
struct FaqSubcategory {
const char* titleKey;
std::vector<FaqEntry> entries;
};
// The two top-level groups. daemonFaq() is full-node material and is only shown when
// the build supports full-node lifecycle actions (see App::supportsFullNodeLifecycleActions()).
const std::vector<FaqSubcategory>& walletFaq();
const std::vector<FaqSubcategory>& daemonFaq();
} // namespace faq
} // namespace ui
} // namespace dragonx
+201
View File
@@ -0,0 +1,201 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_dialog.h"
#include "faq_content.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace dragonx {
namespace ui {
namespace {
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
struct FaqDialogState {
int group = 0;
char search[128] = "";
std::unordered_map<std::string, bool> expanded;
};
FaqDialogState s_faq;
bool entryMatches(const faq::FaqEntry& e, const char* query)
{
return util::containsIgnoreCase(TR(e.questionKey), query) ||
util::containsIgnoreCase(TR(e.answerKey), query);
}
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
void renderAnswer(const char* answerKey)
{
ImGui::Indent(Layout::spacingMd());
ImGui::PushFont(material::Type().body2());
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR(answerKey));
ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::Unindent(Layout::spacingMd());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
} // namespace
void RenderFaqDialog(App* app, bool* p_open)
{
auto& S = schema::UI();
auto win = S.window("dialogs.faq");
const float dp = Layout::dpiScale();
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
// viewport so the content flexes + scrolls on small / HiDPI screens.
const float vpH = ImGui::GetMainViewport()->Size.y;
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
material::OverlayDialogSpec spec;
spec.title = TR("faq_title");
spec.p_open = p_open;
spec.style = material::OverlayStyle::BlurFloat;
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
spec.idSuffix = "faq";
if (!material::BeginOverlayDialog(spec)) {
return;
}
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
// fires when the field isn't capturing it).
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("faq_intro"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const float contentW = ImGui::GetContentRegionAvail().x;
// ── Group tabs: Wallet | Daemon ──
{
const int nTabs = daemonAvailable ? 2 : 1;
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
auto tab = [&](const char* label, int idx) {
const bool active = (s_faq.group == idx);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
}
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
if (active) ImGui::PopStyleColor(2);
};
tab(TR("faq_group_wallet"), 0);
if (daemonAvailable) { ImGui::SameLine(); tab(TR("faq_group_daemon"), 1); }
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ── Search ──
ImGui::SetNextItemWidth(contentW);
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
const bool searching = s_faq.search[0] != '\0';
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
material::ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq();
bool anyShown = false;
bool firstSection = true;
for (const auto& subcat : groups) {
// Collect the entries visible under the current search.
std::vector<const faq::FaqEntry*> visible;
for (const auto& e : subcat.entries) {
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
}
if (visible.empty()) continue;
// Section break: generous space above every section after the first, so topic groups read as
// clearly separated bands rather than one uniform list.
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
firstSection = false;
anyShown = true;
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
// above its (brighter, normal-case) question rows.
material::Type().textColored(material::TypeStyle::Overline,
material::Primary(), TR(subcat.titleKey));
{
const ImVec2 rp = ImGui::GetCursorScreenPos();
const float rw = ImGui::GetContentRegionAvail().x;
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
material::Divider(), 1.0f * dp);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
for (const auto* e : visible) {
const float rowW = ImGui::GetContentRegionAvail().x;
if (searching) {
// Search results: show question + answer directly (no collapsing).
ImGui::PushFont(material::Type().subtitle2());
ImGui::TextWrapped("%s", TR(e->questionKey));
ImGui::PopFont();
renderAnswer(e->answerKey);
} else {
bool& exp = s_faq.expanded[e->questionKey];
std::string id = std::string("##faq_") + e->questionKey;
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
material::Type().subtitle2(), material::OnSurface());
if (exp) renderAnswer(e->answerKey);
}
}
}
if (!anyShown) {
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("faq_no_results"));
ImGui::PopStyleColor();
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
// Close button footer (BlurFloat has no ✕ in the heading).
const float closeW = 120.0f * dp;
material::BeginOverlayDialogFooter(closeW, false);
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
material::EndOverlayDialog();
}
} // namespace ui
} // namespace dragonx
+18
View File
@@ -0,0 +1,18 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
// Renders the Help & FAQ modal. Call every frame while *p_open is true; the dialog
// clears *p_open itself on close (✕ / click-outside / Esc). Content is data-driven
// (see faq_content.h) and grouped into Wallet and Daemon tabs; the Daemon tab is
// hidden on builds without full-node lifecycle support.
void RenderFaqDialog(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx
+6 -5
View File
@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
s_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive
} else {
s_error = r.ok ? std::string("Key not available for this address") : r.error;
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
}
wallet::secureWipeLiteSecret(found);
s_fetching = false;
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
} else {
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
const float gap = 12.0f;
const float dp = Layout::dpiScale();
const float gap = 12.0f * dp;
const float avail = ImGui::GetContentRegionAvail().x;
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
float qrSize = avail * 0.30f;
if (qrSize < 200.0f) qrSize = 200.0f;
if (qrSize > 340.0f) qrSize = 340.0f;
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
float keyW = avail - qrSize - gap;
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
if (!sideBySide) keyW = avail;
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
+3 -3
View File
@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
}
} else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
}
if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
Warning(), errorText.c_str());
}
}
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
: (tk == ticks - 1) ? plotRight - lblSz.x
: xpos - lblSz.x * 0.5f;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
}
}
+7 -2
View File
@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
}
};
ImFont* stepFont = Type().iconSmall();
const float fieldH = capFont->LegacySize + 6.0f * dp;
// Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
// capFont+6px, shorter than the box, so they sat misaligned.
const float fieldH = ImGui::GetFrameHeight();
const float sideW = fieldH; // square -/+ buttons
const float fieldW = 46.0f * dp;
const float g = 2.0f * dp;
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
s_benchConfirm = true;
char msg[128];
snprintf(msg, sizeof(msg),
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
TR("grpc_benchmark_takes_secs"),
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
Notifications::instance().warning(msg);
} else {
+2 -2
View File
@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
if (estActive)
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else
snprintf(estVal, sizeof(estVal), "N/A");
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
else if (totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
else
snprintf(sysBuf, sizeof(sysBuf), "N/A");
snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
float sysTextX = barX + barW - textPadX - sysTextW;
+5 -4
View File
@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
? it->second.feePercent
: kp.feePercent;
if (feePct >= 0.0)
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
FormatFeePercent(feePct).c_str());
else
snprintf(right, sizeof(right), "%s", hrStr.c_str());
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
// centered ~1000dp band inside the panel's padded content region; the
// glass panel itself still fills rightW, leftover -> side margin.
const float rightContentW = rightW - pad * 2.0f;
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
const float chartBandX = rightMin.x + pad
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
const float chartBandW = rightContentW;
const float chartBandX = rightMin.x + pad;
// Right panel: live log (if toggled + available) else the sparkline.
if (showLogView) {
+1 -2
View File
@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
}
if (benchmarkUpdate.inconclusive) {
Notifications::instance().warning(
"Benchmark inconclusive: no hashrate samples were recorded. "
"Check the pool connection and try again.");
TR("grpc_benchmark_inconclusive"));
}
}
+4 -4
View File
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
if (tlsCount == totalPeers) {
ImFont* iconFont = Type().iconSmall();
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
}
{
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls"));
+44 -22
View File
@@ -175,6 +175,11 @@ static void RenderAddressDropdown(App* app, float width) {
}
}
// Combo/button widths first — the preview truncates to the combo's real pixel width below.
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width));
float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2;
// Build preview string
if (!app->isConnected()) {
s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected");
@@ -183,18 +188,27 @@ static void RenderAddressDropdown(App* app, float width) {
const auto& addr = state.addresses[s_selected_address_idx];
bool isZ = addr.type == "shielded";
const char* tag = isZ ? "[Z]" : "[T]";
std::string trunc = util::truncateMiddle(addr.address,
static_cast<int>(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size)));
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
// Reserve pixel room for the tag prefix and the trailing balance, then middle-truncate the
// address to whatever remains — measured with the combo's own Body2 font. Char-count
// truncation kept MORE chars as the column widened, so at 150% the scaled font overflowed
// and the combo hard-clipped "— 12.00000000 DRGX" to "— 1"; measuring in pixels keeps the
// balance visible at any scale.
ImFont* comboFont = Type().getFont(TypeStyle::Body2);
float comboFontSz = comboFont->LegacySize;
char prefix[16]; snprintf(prefix, sizeof(prefix), "%s ", tag);
char suffix[64]; snprintf(suffix, sizeof(suffix), " \xe2\x80\x94 %.8f %s", addr.balance, DRAGONX_TICKER);
float fixedW = comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, prefix).x
+ comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, suffix).x;
// Combo interior = dropdownW minus its dropdown-arrow button (~frame height) and both frame paddings.
float addrBudget = dropdownW - ImGui::GetFrameHeight() - ImGui::GetStyle().FramePadding.x * 2.0f - fixedW;
if (addrBudget < 24.0f) addrBudget = 24.0f; // floor: truncate to a stub rather than overflow
std::string trunc = material::TruncateToWidth(addr.address, comboFont, comboFontSz, addrBudget);
snprintf(buf, sizeof(buf), "%s%s%s", prefix, trunc.c_str(), suffix);
s_source_preview = buf;
} else {
s_source_preview = TR("select_receiving_address");
}
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width));
float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2;
ImGui::SetNextItemWidth(dropdownW);
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
@@ -245,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
tag, lblIt->second.c_str(), trunc.c_str(),
addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : "");
isNew ? TR("grpb_new_badge_suffix") : "");
} else {
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : "");
isNew ? TR("grpb_new_badge_suffix") : "");
}
ImGui::PushID(static_cast<int>(i));
@@ -258,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
s_cached_qr_data.clear(); // Force QR regeneration
}
if (ImGui::IsItemHovered()) {
material::Tooltip("%s\nBalance: %.8f %s%s",
material::Tooltip(TR("grpb_tooltip_address_balance"),
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? "\n(selected)" : "");
isCurrent ? TR("grpb_selected_suffix") : "");
}
ImGui::PopID();
}
@@ -584,12 +598,14 @@ void RenderReceiveTab(App* app)
// ================================================================
{
// Cap + center the card so the address/amount column stops stretching while the
// QR plateaus. cardW never exceeds formW (only shrinks); leftover becomes margin.
// QR plateaus. Shared with the Send tab via mainComposeCardBox() so the two card
// envelopes are identical (they must never drift in width/position again).
// The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
float cardDp = Layout::dpiScale();
float cardW = std::min(formW, 860.0f * cardDp);
Layout::CardBox cardBox = Layout::mainComposeCardBox(formW);
float cardW = cardBox.width;
float cardLeftX = ImGui::GetCursorScreenPos().x;
float cardOffsetX = std::max(0.0f, (formW - cardW) * 0.5f);
float cardOffsetX = cardBox.offsetX;
if (cardOffsetX > 0.0f)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
@@ -700,17 +716,17 @@ void RenderReceiveTab(App* app)
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f;
float cx = startX + ss;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
float ay1 = cy - ag;
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag;
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss),
@@ -728,8 +744,8 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; };
Preset presets[] = {
@@ -857,7 +873,7 @@ void RenderReceiveTab(App* app)
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), TR("qr_unavailable"));
@@ -911,7 +927,13 @@ void RenderReceiveTab(App* app)
{
float btnGap = Layout::spacingMd();
float btnH = std::max(schema::UI().drawElement("tabs.receive", "action-btn-min-height").size, schema::UI().drawElement("tabs.receive", "action-btn-height").size * vScale);
float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size, innerW * S.drawElement("tabs.receive", "action-btn-width-ratio").size);
// Justify the footer edge-to-edge like Send's [Review Send][Cancel] row instead of packing
// fixed-width buttons from the left (which left a large dead gap on the right). Split innerW
// into equal shares over the live button count: 2 by default (Clear Request + Explorer),
// 4 when an amount is requested (+ Copy URI + Share).
int nBtns = (s_request_amount > 0 ? 2 : 0) + 2;
float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size,
(innerW - (nBtns - 1) * btnGap) / (float)nBtns);
bool firstBtn = true;
+25 -15
View File
@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
if (suggestions.empty()) return;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size);
const float dp = Layout::dpiScale();
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
for (size_t si = 0; si < suggestions.size(); si++) {
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
for (int fi = 0; fi < 3; fi++) {
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size);
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
bool active = (s_fee_tier == fi);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
// Max button — use caption font to fit bar height
ImGui::SameLine(0, gap);
char maxId[32];
snprintf(maxId, sizeof(maxId), "Max%s", suffix);
snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
s_amount = maxAmount;
s_send_max = true;
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
bool is_valid_address, double available,
const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
const auto& state = app->getWalletState();
double total = s_amount + s_fee;
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing();
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
ClearFormWithUndo();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
@@ -1086,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
char undoId[32];
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot();
Notifications::instance().info(TR("send_form_restored"));
@@ -1272,12 +1275,13 @@ void RenderSendTab(App* app)
float contentStartY = ImGui::GetCursorPosY();
float formAvailW = ImGui::GetContentRegionAvail().x;
// The compose form reads best as a centered fixed-width column, not edge-to-edge.
// Cap the card to a readable form width and center it; the recent-sends list below
// deliberately keeps the full column width (formAvailW).
const float sendDp = Layout::dpiScale();
float formCardW = std::min(formAvailW, 760.0f * sendDp);
float formOffsetX = std::max(0.0f, (formAvailW - formCardW) * 0.5f);
// Fill the available column up to the content-max-width cap, then center. Shared with the
// Receive tab via mainComposeCardBox() so the two card envelopes are identical in width and
// position (Send previously capped at 760dp vs Receive's 860dp, so Send rendered narrower).
// The recent-sends list below deliberately keeps the full column width (formAvailW).
Layout::CardBox formBox = Layout::mainComposeCardBox(formAvailW);
float formCardW = formBox.width;
float formOffsetX = formBox.offsetX;
float formW = formAvailW;
ImGui::BeginGroup();
@@ -1367,7 +1371,10 @@ void RenderSendTab(App* app)
float pasteW = std::max(schema::UI().drawElement("tabs.send", "paste-btn-min-width").size, colW * schema::UI().drawElement("tabs.send", "paste-btn-width-ratio").size);
float contactsW = ImGui::GetFrameHeight(); // compact square icon button for the contact picker
ImGui::PushItemWidth(colW - pasteW - contactsW - Layout::spacingSm() * 2.0f);
// Reserve the TWO real SameLine gaps (each = ItemSpacing.x) between input|Paste|icon.
// Reserving spacingSm (a smaller token) under-counted the gap, so the row overshot colW by
// ~2*(ItemSpacing.x - spacingSm) and the icon's right border clipped past the card edge.
ImGui::PushItemWidth(colW - pasteW - contactsW - ImGui::GetStyle().ItemSpacing.x * 2.0f);
// Show clipboard preview as transparent overlay when paste button is hovered
bool paste_hovered = false;
@@ -1425,9 +1432,12 @@ void RenderSendTab(App* app)
}
}
// Contact picker — pick a saved contact's address as the recipient.
// Contact picker — pick a saved contact's address as the recipient. Pin the height to the
// frame height (== the input + Paste height) so the larger iconMed font doesn't auto-size
// this square button taller than its row-mates. (Passing a non-zero size also routes
// TactileButton through its precise InvisibleButton + centered-glyph path.)
ImGui::SameLine();
if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, 0),
if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, ImGui::GetFrameHeight()),
material::Type().iconMed()))
ImGui::OpenPopup("##ContactPickerPopup");
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));
+12 -12
View File
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string label = skin.name + " (invalid)";
std::string label = skin.name + TR("swin_invalid_suffix");
ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh();
Notifications::instance().info("Theme list refreshed");
Notifications::instance().info(TR("swin_theme_list_refreshed"));
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");
std::string msg = "Connection successful!\ndragonxd version: " + version;
std::string msg = std::string(TR("swin_connection_successful")) + version;
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Connection failed: " + error);
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (error.empty()) {
int start = result.value("start_height", 0);
int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) +
" to " + std::to_string(end);
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
TR("swin_rescan_to") + std::to_string(end);
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Rescan failed: " + error);
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
Notifications::instance().success(TR("swin_ztx_history_cleared"));
} else {
Notifications::instance().info("No history file found");
Notifications::instance().info(TR("swin_no_history_file_found"));
}
s_confirm_clear_ztx = false;
}
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
// Save/Cancel buttons
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
Notifications::instance().success(TR("swin_settings_saved"));
*p_open = false;
}
ImGui::SameLine();
+414 -264
View File
@@ -5,6 +5,7 @@
#include "shield_dialog.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
@@ -15,39 +16,98 @@
#include <vector>
#include <string>
#include <cstdio>
namespace dragonx {
namespace ui {
// Static state
static bool s_open = false;
// ── Static dialog state ─────────────────────────────────────────────────────────────────────────
static bool s_open = false;
static ShieldDialog::Mode s_mode = ShieldDialog::Mode::ShieldCoinbase;
static char s_from_address[512] = "*";
static char s_to_address[512] = "";
static bool s_consolidate = false; // opened from the wallet-bloat nudge (shielded preset + framing)
static int s_src = 2; // merge source: 0 = transparent, 1 = shielded, 2 = both
static char s_from_address[512] = "*";
static char s_to_address[512] = "";
static int s_selected_zaddr_idx = -1;
static double s_fee = DRAGONX_DEFAULT_FEE;
static int s_utxo_limit = 50; // overridden by schema at runtime
static bool s_operation_pending = false;
static int s_utxo_limit = 50; // overridden by schema at runtime
static bool s_advanced = false; // Advanced (fee + batch size) disclosure
static bool s_confirm = false; // inline "confirm before moving funds" phase
static bool s_operation_pending = false;
static bool s_op_terminal = false; // async op reached success/failed — freeze inputs
static std::string s_operation_id;
static std::string s_status_message;
static int s_selected_zaddr_idx = -1;
static double s_last_poll = 0.0; // live-progress self-poll timer (ImGui::GetTime seconds)
// Scope of what can be consolidated (fetched once on open, merge mode only).
static bool s_scope_loading = false;
static bool s_scope_loaded = false;
static int s_t_count = 0, s_z_count = 0;
static double s_t_amount = 0.0, s_z_amount = 0.0;
static bool s_creating_addr = false; // z_getnewaddress in flight (empty state)
static void resetTransient()
{
s_operation_pending = false;
s_op_terminal = false;
s_confirm = false;
s_status_message.clear();
s_operation_id.clear();
s_creating_addr = false;
}
// Count + sum spendable transparent UTXOs and shielded notes so the user can see the scope of a
// consolidation (and how many batches it may take). Read-only; runs off the UI thread.
static void loadScope(App* app)
{
if (!app || !app->worker()) return;
s_scope_loading = true; s_scope_loaded = false;
app->worker()->post([app, rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
int tC = 0, zC = 0; double tA = 0.0, zA = 0.0; std::string error;
try {
rpc::RPCClient::TraceScope trace("Shield dialog / Scope count");
nlohmann::json us = rpc->call("listunspent", nlohmann::json::array({0}));
if (us.is_array()) for (const auto& u : us) {
if (u.value("confirmations", 0) >= 1 && u.value("spendable", true)) { ++tC; tA += u.value("amount", 0.0); }
}
nlohmann::json zs = rpc->call("z_listunspent", nlohmann::json::array({0}));
if (zs.is_array()) for (const auto& z : zs) {
if (z.value("confirmations", 0) >= 1) { ++zC; zA += z.value("amount", 0.0); }
}
} catch (const std::exception& e) { error = e.what(); }
return [tC, zC, tA, zA, error]() {
s_scope_loading = false; s_scope_loaded = error.empty();
s_t_count = tC; s_z_count = zC; s_t_amount = tA; s_z_amount = zA;
// Clamp the source to what actually has inputs (unless the user is mid-op).
const bool tOk = tC > 0, zOk = zC > 0;
if (!s_operation_pending) {
if (s_consolidate && zOk) s_src = 1; // bloat nudge → shielded
else if (s_src == 0 && !tOk) s_src = zOk ? 1 : 2;
else if (s_src == 1 && !zOk) s_src = tOk ? 0 : 2;
else if (!tOk && zOk) s_src = 1;
else if (tOk && !zOk) s_src = 0;
}
};
});
}
void ShieldDialog::show(Mode mode)
{
s_mode = mode;
s_open = true;
s_operation_pending = false;
s_status_message.clear();
s_operation_id.clear();
if (mode == Mode::ShieldCoinbase) {
strncpy(s_from_address, "*", sizeof(s_from_address));
} else {
s_from_address[0] = '\0';
}
s_consolidate = false; // reset preset flags so stale statics don't leak across opens
s_src = 2;
resetTransient();
s_from_address[0] = '\0';
if (mode == Mode::ShieldCoinbase) strncpy(s_from_address, "*", sizeof(s_from_address));
s_to_address[0] = '\0';
s_selected_zaddr_idx = -1;
s_fee = DRAGONX_DEFAULT_FEE;
s_utxo_limit = (int)schema::UI().drawElement("business", "utxo-limit").size;
s_selected_zaddr_idx = -1;
if (s_utxo_limit < 1) s_utxo_limit = 50;
s_advanced = false;
s_scope_loaded = false; s_scope_loading = false;
s_t_count = s_z_count = 0; s_t_amount = s_z_amount = 0.0;
s_last_poll = 0.0;
}
void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
@@ -59,14 +119,146 @@ void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
void ShieldDialog::showMerge()
{
show(Mode::MergeToAddress);
s_consolidate = false;
s_src = 2; // generic merge: both sources
}
void ShieldDialog::showConsolidate()
{
show(Mode::MergeToAddress);
s_consolidate = true;
s_src = 1; // wallet-bloat consolidation targets shielded notes (witness bloat)
}
void ShieldDialog::hide()
{
s_open = false;
s_operation_pending = false;
s_status_message.clear();
s_operation_id.clear();
resetTransient();
}
// Relevant count/amount for the currently-selected merge source.
static int srcCount() { return s_src == 0 ? s_t_count : s_src == 1 ? s_z_count : (s_t_count + s_z_count); }
static double srcAmount() { return s_src == 0 ? s_t_amount : s_src == 1 ? s_z_amount : (s_t_amount + s_z_amount); }
static std::string fmtAmt(double v) { char b[48]; std::snprintf(b, sizeof(b), "%.4f", v); return b; }
static std::string shortAddr(const std::string& a)
{
if (a.size() <= 20) return a;
return a.substr(0, 10) + "…" + a.substr(a.size() - 8);
}
// Auto-pick the best spendable z-address as the default destination (fewest hops for the user).
static void autoSelectDestination(const WalletState& state)
{
if (s_to_address[0] != '\0' || state.z_addresses.empty()) return;
int idx = bestSpendableAddressIndex(state.z_addresses);
if (idx < 0) idx = 0;
s_selected_zaddr_idx = idx;
strncpy(s_to_address, state.z_addresses[idx].address.c_str(), sizeof(s_to_address) - 1);
}
// Fire the actual shield/merge op. Registers the opid with the shared poller (for balance refresh)
// AND kicks the modal's own live-progress poll.
static void submitOperation(App* app)
{
s_operation_pending = true;
s_op_terminal = false;
s_status_message = TR("shield_submitting");
s_last_poll = ImGui::GetTime();
if (s_mode == ShieldDialog::Mode::ShieldCoinbase) {
std::string from(s_from_address), to(s_to_address);
double fee = s_fee; int limit = s_utxo_limit;
if (!app->worker()) return;
app->worker()->post([app, rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb {
nlohmann::json result; std::string error;
try {
rpc::RPCClient::TraceScope trace("Shield dialog / Shield coinbase");
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
} catch (const std::exception& e) { error = e.what(); }
return [app, result, error]() {
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = TR("merge_progress");
Notifications::instance().success(TR("shield_started"));
app->trackOperation(s_operation_id);
} else {
s_operation_pending = false; s_op_terminal = true;
s_status_message = std::string(TR("shield_error_prefix")) + error;
Notifications::instance().error(std::string(TR("shield_send_failed")) + error);
}
};
});
return;
}
// Merge / consolidate. Source → z_mergetoaddress fromaddress selector (this is the fix: shielded
// notes, not just transparent UTXOs — the wallet-bloat the nudge warns about is shielded witnesses).
std::vector<std::string> fromAddrs;
if (s_src == 0) fromAddrs = { "ANY_TADDR" };
else if (s_src == 1) fromAddrs = { "ANY_SAPLING" };
else fromAddrs = { "*" };
std::string to(s_to_address);
double fee = s_fee; int limit = s_utxo_limit;
if (!app->worker()) return;
app->worker()->post([app, rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb {
nlohmann::json addrs = nlohmann::json::array();
for (const auto& a : fromAddrs) addrs.push_back(a);
nlohmann::json result; std::string error;
try {
rpc::RPCClient::TraceScope trace("Shield dialog / Consolidate");
// fromaddrs, toaddr, fee, transparent_limit, shielded_limit — cap both to the batch size.
result = rpc->call("z_mergetoaddress", {addrs, to, fee, limit, limit});
} catch (const std::exception& e) { error = e.what(); }
return [app, result, error]() {
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = TR("merge_progress");
Notifications::instance().success(TR("merge_started"));
app->trackOperation(s_operation_id);
} else {
s_operation_pending = false; s_op_terminal = true;
s_status_message = std::string(TR("shield_error_prefix")) + error;
Notifications::instance().error(std::string(TR("merge_send_failed")) + error);
}
};
});
}
// Live-progress self-poll: while an op is in flight, poll z_getoperationstatus every ~2s so the modal
// shows "Consolidating… → Done/Failed" without a manual button. (The shared poller also tracks it for
// balance refresh; this drives only the inline display.)
static void pollOperation(App* app)
{
if (s_operation_id.empty() || s_op_terminal || !app->worker()) return;
const double now = ImGui::GetTime();
if (now - s_last_poll < 2.0) return;
s_last_poll = now;
std::string opid = s_operation_id;
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
nlohmann::json result; std::string error;
try {
rpc::RPCClient::TraceScope trace("Shield dialog / Op status");
result = rpc->call("z_getoperationstatus", {nlohmann::json::array({opid})});
} catch (const std::exception& e) { error = e.what(); }
return [result, error]() {
if (!error.empty() || !result.is_array() || result.empty()) return; // transient — retry next tick
const auto& op = result[0];
const std::string status = op.value("status", "");
if (status == "success") {
s_operation_pending = false; s_op_terminal = true;
s_status_message = TR("shield_completed");
Notifications::instance().success(TR("shield_merge_done"));
} else if (status == "failed") {
std::string msg = op.value("error", nlohmann::json{}).value("message", std::string(TR("shield_unknown_error")));
s_operation_pending = false; s_op_terminal = true;
s_status_message = std::string(TR("shield_op_failed")) + msg;
Notifications::instance().error(std::string(TR("shield_op_failed")) + msg);
}
// queued / executing → leave the "Consolidating…" message and keep polling.
};
});
}
void ShieldDialog::render(App* app)
@@ -74,263 +266,221 @@ void ShieldDialog::render(App* app)
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.shield");
auto addrLbl = S.label("dialogs.shield", "address-label");
auto addrFrontLbl = S.label("dialogs.shield", "address-front-label");
auto addrBackLbl = S.label("dialogs.shield", "address-back-label");
auto feeInput = S.input("dialogs.shield", "fee-input");
auto utxoInput = S.input("dialogs.shield", "utxo-limit-input");
auto shieldBtn = S.button("dialogs.shield", "shield-button");
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
auto win = S.window("dialogs.shield");
auto addrLbl = S.label("dialogs.shield", "address-label");
auto addrFront = S.label("dialogs.shield", "address-front-label");
auto addrBack = S.label("dialogs.shield", "address-back-label");
auto feeInput = S.input("dialogs.shield", "fee-input");
auto utxoInput = S.input("dialogs.shield", "utxo-limit-input");
auto shieldBtn = S.button("dialogs.shield", "shield-button");
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
const float dp = Layout::dpiScale();
const bool isMerge = (s_mode == Mode::MergeToAddress);
const char* title = (s_mode == Mode::ShieldCoinbase)
? TR("shield_title")
: TR("merge_title");
const char* title = s_consolidate ? TR("consolidate_title")
: isMerge ? TR("merge_title")
: TR("shield_title");
material::OverlayDialogSpec ov;
ov.title = title; ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = win.width; ov.idSuffix = "shielddialog";
if (material::BeginOverlayDialog(ov)) {
const auto& state = app->getWalletState();
if (!material::BeginOverlayDialog(ov)) return;
// Description
if (s_mode == Mode::ShieldCoinbase) {
ImGui::TextWrapped("%s", TR("shield_description"));
} else {
ImGui::TextWrapped("%s", TR("merge_description"));
const auto& state = app->getWalletState();
autoSelectDestination(state);
pollOperation(app);
if (isMerge && !s_scope_loaded && !s_scope_loading && s_operation_id.empty()) loadScope(app);
// ── Description ──────────────────────────────────────────────────────────────────────────────
ImGui::TextWrapped("%s", s_consolidate ? TR("consolidate_desc")
: isMerge ? TR("merge_description")
: TR("shield_description"));
ImGui::Spacing();
const bool opInFlight = !s_operation_id.empty(); // submitted — inputs frozen, showing progress
// ── Merge: scope + source selector ──────────────────────────────────────────────────────────
if (isMerge && !opInFlight) {
if (s_scope_loading) {
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(),
TR("merge_scope_loading"));
} else if (s_scope_loaded) {
char buf[160];
std::snprintf(buf, sizeof(buf), TR("merge_scope_fmt"),
s_t_count, s_z_count, fmtAmt(s_t_amount + s_z_amount).c_str());
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), buf);
}
ImGui::Spacing();
// From address (for shield coinbase)
if (s_mode == Mode::ShieldCoinbase) {
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
// Source selector — only offer the types that actually have inputs.
const bool tOk = s_t_count > 0, zOk = s_z_count > 0;
if (tOk && zOk) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("merge_source"));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::RadioButton(TR("merge_src_shielded"), &s_src, 1); ImGui::SameLine();
ImGui::RadioButton(TR("merge_src_transparent"), &s_src, 0); ImGui::SameLine();
ImGui::RadioButton(TR("merge_src_both"), &s_src, 2);
ImGui::Spacing();
}
// To address (z-address dropdown)
ImGui::Text("%s", TR("shield_to_address"));
// Get z-addresses for dropdown
std::string to_display = s_to_address[0] ? s_to_address : TR("shield_select_z");
if (to_display.length() > static_cast<size_t>(addrLbl.truncate)) {
to_display = to_display.substr(0, addrFrontLbl.truncate) + "..." + to_display.substr(to_display.length() - addrBackLbl.truncate);
}
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("##ToAddr", to_display.c_str())) {
for (size_t i = 0; i < state.z_addresses.size(); i++) {
const auto& addr = state.z_addresses[i];
std::string label = addr.address;
if (label.length() > static_cast<size_t>(addrLbl.truncate)) {
label = label.substr(0, addrFrontLbl.truncate) + "..." + label.substr(label.length() - addrBackLbl.truncate);
}
bool selected = (s_selected_zaddr_idx == static_cast<int>(i));
if (ImGui::Selectable(label.c_str(), selected)) {
s_selected_zaddr_idx = static_cast<int>(i);
strncpy(s_to_address, addr.address.c_str(), sizeof(s_to_address) - 1);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (state.z_addresses.empty()) {
material::Type().textColored(material::TypeStyle::Caption, material::Warning(),
TR("shield_no_zaddr_hint"));
}
ImGui::Spacing();
// Fee + UTXO limit share one row (two columns) to tighten vertical rhythm.
float pairColX = ImGui::GetContentRegionAvail().x * 0.5f;
// Fee (left column)
ImGui::Text("%s", TR("fee_label"));
ImGui::SetNextItemWidth(feeInput.width * Layout::dpiScale());
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
if (s_fee < 0.0) s_fee = 0.0; // no negative fee
if (s_fee > 1.0) s_fee = 1.0; // guard a fat-fingered huge fee (mirrors utxo clamp)
ImGui::SameLine();
ImGui::TextDisabled("DRGX");
// UTXO limit (right column) — hint drops under the input (rather than beside it) since
// "Max UTXOs per operation" is too long to share the narrower half-width column with "DRGX".
ImGui::SameLine(pairColX);
ImGui::BeginGroup();
ImGui::Text("%s", TR("shield_utxo_limit"));
ImGui::SetNextItemWidth(utxoInput.width * Layout::dpiScale());
ImGui::InputInt("##Limit", &s_utxo_limit);
if (s_utxo_limit < 1) s_utxo_limit = 1;
if (s_utxo_limit > 100) s_utxo_limit = 100;
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(),
TR("shield_max_utxos"));
ImGui::EndGroup();
ImGui::Spacing();
// Status message
if (!s_status_message.empty()) {
if (s_operation_pending) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status_message.c_str());
} else {
ImGui::TextWrapped("%s", s_status_message.c_str());
}
ImGui::Spacing();
}
// Buttons — guard on connection/sync like the Send tab (a disconnected or mid-sync submit just
// fails at the daemon with a raw error).
bool sh_connected = app->isConnected();
bool sh_syncing = state.sync.syncing;
bool can_submit = !s_operation_pending && s_to_address[0] != '\0' && sh_connected && !sh_syncing;
// Center the primary + Cancel action row via the shared footer helper. We can't use
// DialogActionFooter here because the primary button carries a disabled-hover tooltip that must
// fire on ITS item (the helper draws primary+Close internally, leaving no hook between them), so
// we keep the two TactileButtons + the interleaved tooltip and only standardize the placement.
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x;
material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false);
if (!can_submit) ImGui::BeginDisabled();
if (material::TactileButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
s_operation_pending = true;
s_status_message = TR("shield_submitting");
if (s_mode == Mode::ShieldCoinbase) {
std::string from(s_from_address), to(s_to_address);
double fee = s_fee;
int limit = s_utxo_limit;
if (app->worker()) {
app->worker()->post([app, rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb {
nlohmann::json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Send tab / Shield coinbase");
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
} catch (const std::exception& e) {
error = e.what();
}
return [app, result, error]() {
s_operation_pending = false;
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = std::string(TR("shield_op_submitted")) + s_operation_id;
Notifications::instance().success(TR("shield_started"));
// Register with the shared poller so an async failure is
// surfaced (and balances refresh) even after this dialog closes.
app->trackOperation(s_operation_id);
} else {
s_status_message = std::string(TR("shield_error_prefix")) + error;
Notifications::instance().error(std::string(TR("shield_send_failed")) + error);
}
};
});
}
} else {
std::vector<std::string> fromAddrs;
fromAddrs.push_back("ANY_TADDR");
std::string to(s_to_address);
double fee = s_fee;
int limit = s_utxo_limit;
if (app->worker()) {
app->worker()->post([app, rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb {
nlohmann::json addrs = nlohmann::json::array();
for (const auto& addr : fromAddrs) addrs.push_back(addr);
nlohmann::json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Send tab / Merge funds");
result = rpc->call("z_mergetoaddress", {addrs, to, fee, 0, limit});
} catch (const std::exception& e) {
error = e.what();
}
return [app, result, error]() {
s_operation_pending = false;
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = std::string(TR("shield_op_submitted")) + s_operation_id;
Notifications::instance().success(TR("merge_started"));
// Register with the shared poller so an async failure is
// surfaced (and balances refresh) even after this dialog closes.
app->trackOperation(s_operation_id);
} else {
s_status_message = std::string(TR("shield_error_prefix")) + error;
Notifications::instance().error(std::string(TR("merge_send_failed")) + error);
}
};
});
}
}
}
if (!can_submit) ImGui::EndDisabled();
if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (!sh_connected) material::Tooltip("%s", TR("send_tooltip_not_connected"));
else if (sh_syncing) material::Tooltip("%s", TR("send_tooltip_syncing"));
else if (s_to_address[0]=='\0') material::Tooltip("%s", TR("shield_select_z"));
}
ImGui::SameLine();
if (material::TactileButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
s_open = false;
}
// Show operation status if we have an opid
if (!s_operation_id.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text(TR("shield_operation_id"), s_operation_id.c_str());
if (material::TactileButton(TR("shield_check_status"), ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
std::string opid = s_operation_id;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
nlohmann::json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Send tab / Shield operation status");
nlohmann::json ids = nlohmann::json::array();
ids.push_back(opid);
result = rpc->call("z_getoperationstatus", {ids});
} catch (const std::exception& e) {
error = e.what();
}
return [result, error]() {
if (error.empty() && result.is_array() && !result.empty()) {
auto& op = result[0];
std::string status = op.value("status", "unknown");
if (status == "success") {
s_status_message = TR("shield_completed");
Notifications::instance().success(TR("shield_merge_done"));
} else if (status == "failed") {
std::string errMsg = op.value("error", nlohmann::json{}).value("message", TR("shield_unknown_error"));
s_status_message = std::string(TR("shield_op_failed")) + errMsg;
Notifications::instance().error(std::string(TR("shield_op_failed")) + errMsg);
} else if (status == "executing") {
s_status_message = TR("shield_in_progress");
} else {
s_status_message = std::string(TR("shield_status_label")) + status;
}
} else if (!error.empty()) {
s_status_message = std::string(TR("shield_status_check_error")) + error;
}
};
});
}
}
}
material::EndOverlayDialog();
}
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
if (!isMerge && !opInFlight) {
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
ImGui::Spacing();
}
// ── Destination (z-address) ─────────────────────────────────────────────────────────────────
if (!opInFlight) {
ImGui::TextUnformatted(TR("shield_to_address"));
if (state.z_addresses.empty()) {
material::Type().textColored(material::TypeStyle::Caption, material::Warning(), TR("shield_no_zaddr_hint"));
ImGui::Spacing();
if (s_creating_addr) {
ImGui::TextDisabled("%s", TR("merge_creating"));
} else if (material::TactileButton(TR("merge_create_zaddr"), ImVec2(0, 0), S.resolveFont(shieldBtn.font))) {
s_creating_addr = true;
if (app->worker()) app->worker()->post([app, rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
std::string addr, error;
try {
rpc::RPCClient::TraceScope trace("Shield dialog / New z-address");
addr = rpc->call("z_getnewaddress", nlohmann::json::array()).get<std::string>();
} catch (const std::exception& e) { error = e.what(); }
return [app, addr, error]() {
s_creating_addr = false;
if (error.empty() && !addr.empty()) {
strncpy(s_to_address, addr.c_str(), sizeof(s_to_address) - 1);
Notifications::instance().success(TR("merge_addr_created"));
} else {
Notifications::instance().error(std::string(TR("shield_error_prefix")) + error);
}
};
});
}
} else {
std::string to_display = s_to_address[0] ? s_to_address : TR("shield_select_z");
if (to_display.length() > static_cast<size_t>(addrLbl.truncate))
to_display = to_display.substr(0, addrFront.truncate) + "..." + to_display.substr(to_display.length() - addrBack.truncate);
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("##ToAddr", to_display.c_str())) {
for (size_t i = 0; i < state.z_addresses.size(); i++) {
std::string label = state.z_addresses[i].address;
if (label.length() > static_cast<size_t>(addrLbl.truncate))
label = label.substr(0, addrFront.truncate) + "..." + label.substr(label.length() - addrBack.truncate);
bool selected = (s_selected_zaddr_idx == static_cast<int>(i));
if (ImGui::Selectable(label.c_str(), selected)) {
s_selected_zaddr_idx = static_cast<int>(i);
strncpy(s_to_address, state.z_addresses[i].address.c_str(), sizeof(s_to_address) - 1);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
ImGui::Spacing();
// ── Advanced (fee + batch size) ─────────────────────────────────────────────────────────
ImDrawList* dl = ImGui::GetWindowDrawList();
material::CollapsibleHeader(dl, "##AdvToggle", TR("merge_advanced"), s_advanced,
ImGui::GetContentRegionAvail().x, material::Type().caption(),
material::OnSurfaceMedium());
if (s_advanced) {
ImGui::Spacing();
ImGui::TextUnformatted(TR("fee_label"));
ImGui::SetNextItemWidth(feeInput.width * dp);
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
if (s_fee < 0.0) s_fee = 0.0;
if (s_fee > 1.0) s_fee = 1.0;
ImGui::SameLine(); ImGui::TextDisabled("DRGX");
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("merge_fee_hint"));
ImGui::Spacing();
ImGui::TextUnformatted(TR("merge_max_inputs"));
ImGui::SetNextItemWidth(utxoInput.width * dp);
ImGui::InputInt("##Limit", &s_utxo_limit);
if (s_utxo_limit < 1) s_utxo_limit = 1;
if (s_utxo_limit > 100) s_utxo_limit = 100;
}
// Batch hint: one run only merges up to the limit; large sets need repeats.
if (isMerge && s_scope_loaded && srcCount() > s_utxo_limit) {
char hb[160];
std::snprintf(hb, sizeof(hb), TR("merge_batch_fmt"), s_utxo_limit);
ImGui::Spacing();
material::Type().textColored(material::TypeStyle::Caption, material::Warning(), hb);
}
ImGui::Spacing();
}
// ── Live progress / status ──────────────────────────────────────────────────────────────────
if (!s_status_message.empty()) {
if (s_operation_pending && !s_op_terminal)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status_message.c_str());
else
ImGui::TextWrapped("%s", s_status_message.c_str());
ImGui::Spacing();
}
// ── Footer ──────────────────────────────────────────────────────────────────────────────────
const bool connected = app->isConnected();
const bool syncing = state.sync.syncing;
const bool haveDest = s_to_address[0] != '\0';
if (opInFlight) {
// After submit: just a Close button (progress shows above; op continues in the background).
material::BeginOverlayDialogFooter(cancelBtn.width, /*drawSeparator=*/false);
if (material::TactileButton(s_op_terminal ? TR("done") : TR("close"),
ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font)))
s_open = false;
material::EndOverlayDialog();
return;
}
const char* primaryLabel = s_confirm ? TR("merge_confirm_btn")
: s_consolidate ? TR("consolidate_funds_btn")
: isMerge ? TR("merge_funds")
: TR("shield_funds");
const char* secondaryLabel = s_confirm ? TR("merge_back") : TR("cancel");
// Confirm summary (inline, before the fund-moving call). Merge/consolidate shows amount + input
// count; shield-coinbase just gets the button relabel (its inputs aren't enumerated here).
if (s_confirm && isMerge) {
char cb[200];
std::snprintf(cb, sizeof(cb), TR("merge_confirm_fmt"),
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
ImGui::TextWrapped("%s", cb);
ImGui::Spacing();
}
bool can_submit = haveDest && connected && !syncing;
if (isMerge && s_scope_loaded && srcCount() == 0) can_submit = false;
float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x;
material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false);
if (!can_submit) ImGui::BeginDisabled();
if (material::TactileButton(primaryLabel, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
if (s_confirm) { submitOperation(app); }
else { s_confirm = true; } // first click → show the confirm summary
}
if (!can_submit) ImGui::EndDisabled();
if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (!connected) material::Tooltip("%s", TR("send_tooltip_not_connected"));
else if (syncing) material::Tooltip("%s", TR("send_tooltip_syncing"));
else if (!haveDest) material::Tooltip("%s", TR("shield_select_z"));
else if (isMerge && srcCount() == 0) material::Tooltip("%s", TR("merge_no_spendable"));
}
ImGui::SameLine();
if (material::TactileButton(secondaryLabel, ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
if (s_confirm) s_confirm = false; // Back → return to the form
else s_open = false; // Cancel → close
}
material::EndOverlayDialog();
}
} // namespace ui
+7 -1
View File
@@ -33,10 +33,16 @@ public:
static void showShieldCoinbase(const std::string& fromAddress = "*");
/**
* @brief Show merge to address dialog
* @brief Show merge to address dialog (generic — both transparent + shielded sources)
*/
static void showMerge();
/**
* @brief Show the consolidate-funds flow preset for wallet-bloat reduction (shielded notes).
* Used by the large-wallet nudges (Settings banner + alert action).
*/
static void showConsolidate();
/**
* @brief Render the dialog (call each frame)
*/
@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::SameLine();
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.address.c_str());
}
}
+2 -2
View File
@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
int idx_map[] = {-1, 1, 0, 2};
int idx = idx_map[type_filter];
float xOff = idx * (cardW + cardGap);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
}
ImGui::Dummy(ImVec2(availWidth, cardH));
+1 -1
View File
@@ -100,7 +100,7 @@ public:
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin
const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
+453 -22
View File
@@ -460,15 +460,23 @@ void I18n::loadBuiltinEnglish()
// Settings sections
strings_["appearance"] = "APPEARANCE";
strings_["theme_language"] = "THEME & LANGUAGE";
strings_["scale_effects"] = "SCALE & EFFECTS";
strings_["advanced_effects"] = "Advanced Effects...";
strings_["tools_actions"] = "Tools & Actions...";
strings_["tools_actions_hdr"] = "TOOLS & ACTIONS";
strings_["wallet_options_hdr"] = "OPTIONS";
strings_["wallet_diagnostics_hdr"] = "DIAGNOSTICS";
strings_["wallet"] = "WALLET";
strings_["node_security"] = "NODE & SECURITY";
strings_["node"] = "NODE";
strings_["security"] = "SECURITY";
strings_["explorer_section"] = "EXPLORER";
strings_["explorer_urls_hdr"] = "URLS";
strings_["about"] = "About";
strings_["backup_data"] = "BACKUP & DATA";
strings_["backup_col_import"] = "IMPORT & RESTORE";
strings_["backup_col_backup"] = "BACKUP";
strings_["backup_col_export"] = "EXPORT";
strings_["balance_layout"] = "Balance Layout";
strings_["low_spec_mode"] = "Low-spec mode";
strings_["simple_background"] = "Simple background";
@@ -496,9 +504,11 @@ void I18n::loadBuiltinEnglish()
strings_["screenshot_sweep"] = "Run screenshot sweep";
strings_["screenshot_sweep_full"] = "Full UI sweep";
strings_["screenshot_open_dir"] = "Open location";
strings_["sweep_current_theme_only"] = "Current theme only";
strings_["tt_sweep_current_theme_only"] = "Sweep only the active theme instead of cycling every theme";
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds.";
strings_["mine_when_idle"] = "Mine when idle";
strings_["setup_wizard"] = "Run Setup Wizard...";
strings_["setup_wizard"] = "Run Setup Wizard…";
// RPC / Explorer settings
strings_["rpc_connection"] = "RPC Connection...";
@@ -512,21 +522,21 @@ void I18n::loadBuiltinEnglish()
strings_["fetch_prices"] = "Fetch price data from CoinGecko";
strings_["block_explorer"] = "Block Explorer";
strings_["test_connection"] = "Test Connection";
strings_["rescan"] = "Rescan Blockchain";
strings_["rescan"] = "Rescan";
// Settings: buttons
strings_["settings_address_book"] = "Address Book...";
strings_["settings_validate_address"] = "Validate Address...";
strings_["settings_request_payment"] = "Request Payment...";
strings_["settings_shield_mining"] = "Shield Mining...";
strings_["settings_merge_to_address"] = "Merge to Address...";
strings_["settings_address_book"] = "Address Book…";
strings_["settings_validate_address"] = "Validate Address…";
strings_["settings_request_payment"] = "Request Payment…";
strings_["settings_shield_mining"] = "Shield Mining…";
strings_["settings_merge_to_address"] = "Merge to Address…";
strings_["settings_clear_ztx"] = "Clear Z-Tx History";
strings_["settings_import_key"] = "Import Private Key...";
strings_["settings_import_viewkey"] = "Import Viewing Key...";
strings_["settings_export_key"] = "Export Key...";
strings_["settings_export_all"] = "Export All...";
strings_["settings_backup"] = "Backup...";
strings_["settings_export_csv"] = "Export CSV...";
strings_["settings_import_key"] = "Import Private Key…";
strings_["settings_import_viewkey"] = "Import Viewing Key…";
strings_["settings_export_key"] = "Export Key…";
strings_["settings_export_all"] = "Export All…";
strings_["settings_backup"] = "Backup…";
strings_["settings_export_csv"] = "Export CSV…";
strings_["settings_encrypt_wallet"] = "Encrypt Wallet";
strings_["settings_change_passphrase"] = "Change Passphrase";
strings_["settings_lock_now"] = "Lock Now";
@@ -655,8 +665,11 @@ void I18n::loadBuiltinEnglish()
strings_["wiz_pin_confirm"] = "Confirm PIN:";
strings_["wiz_pin_invalid"] = "PIN must be 4-8 digits";
strings_["wiz_pin_mismatch"] = "PINs do not match";
strings_["settings_data_dir"] = "Data Dir:";
strings_["settings_wallet_size_label"] = "Wallet Size:";
strings_["settings_data_dir"] = "Data Dir";
strings_["settings_wallet_size_label"] = "Wallet Size";
strings_["wallet_size_warn"] = "This wallet file is large. Consolidating your notes can curb further growth.";
strings_["tt_wallet_size_warn"] = "Shielded wallets grow with each note's witness data — merging many notes into one address reduces it. Back up first.";
strings_["wallet_size_consolidate"] = "Consolidate notes\xE2\x80\xA6";
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf";
strings_["settings_visual_effects"] = "Visual Effects";
@@ -670,7 +683,7 @@ void I18n::loadBuiltinEnglish()
strings_["settings_wallet_info"] = "Wallet Info";
strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
strings_["settings_configure_explorer"] = "Configure external block explorer links";
strings_["settings_auto_lock"] = "AUTO-LOCK";
strings_["settings_auto_lock"] = "Auto-lock";
strings_["timeout_off"] = "Off";
strings_["timeout_1min"] = "1 min";
strings_["timeout_5min"] = "5 min";
@@ -700,9 +713,9 @@ void I18n::loadBuiltinEnglish()
strings_["tt_scanline"] = "CRT scanline effect in console";
strings_["tt_theme_effects"] = "Shimmer, glow, hue-cycling per theme";
strings_["tt_animate_avatars"] = "Play animated (GIF / WebP) contact avatars; off shows the first frame only";
strings_["tt_blur"] = "Blur amount (0%% = off, 100%% = maximum)";
strings_["tt_noise"] = "Grain texture intensity (0%% = off, 100%% = maximum)";
strings_["tt_ui_opacity"] = "Card and sidebar opacity (100%% = fully opaque, lower = more see-through)";
strings_["tt_blur"] = "Blur amount (0% = off, 100% = maximum)";
strings_["tt_noise"] = "Grain texture intensity (0% = off, 100% = maximum)";
strings_["tt_ui_opacity"] = "Card and sidebar opacity (100% = fully opaque, lower = more see-through)";
strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)";
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x). Hotkey: Alt + Scroll Wheel";
strings_["tt_custom_theme"] = "Custom theme active";
@@ -717,6 +730,12 @@ void I18n::loadBuiltinEnglish()
strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity";
strings_["tt_keep_daemon"] = "Daemon will still stop when running the setup wizard";
strings_["tt_stop_external"] = "Applies when connecting to a daemon\nyou started outside this wallet";
strings_["stratum_host_section"] = "MINING POOL HOSTING";
strings_["stratum_host"] = "Host a mining pool (stratum)";
strings_["tt_stratum_host"] = "Run a RandomX stratum pool server on this node so other RandomX miners can point at this computer. Requires a v1.3.0+ node and a daemon restart to apply.";
strings_["stratum_host_hint"] = "Miners connect to this computer on port 22769 (RPC port + 1000) with a RandomX stratum miner. Restart the daemon to apply.";
strings_["stratum_allowip_hint"] = "Allow miners from IP or CIDR (blank = this computer only)";
strings_["stratum_expose_warn"] = "Opens a mining port to the network you allow — only use on a trusted LAN.";
strings_["tt_verbose"] = "Log detailed connection diagnostics,\ndaemon state, and port owner info\nto the Console tab";
strings_["tt_mine_idle"] = "Automatically start mining when the\nsystem is idle (no keyboard/mouse input)";
strings_["tt_idle_delay"] = "How long to wait before starting mining";
@@ -805,12 +824,14 @@ void I18n::loadBuiltinEnglish()
strings_["rescan_detecting"] = "Checking which blocks your node has on disk…";
strings_["rescan_bootstrapped_msg"] = "Your node was bootstrapped, so blocks below the snapshot aren't on disk and a rescan from genesis would fail. Rescan from a height your snapshot includes to reconcile your wallet's spent balance. Your wallet.dat and chain data are not deleted.";
strings_["rescan_from_height"] = "Rescan from block height:";
strings_["repair_wallet"] = "Repair Wallet";
strings_["repair_wallet"] = "Repair";
strings_["tt_repair_wallet"] = "Wipe and rebuild the wallet's transaction records from the blockchain (fixes notes that fail to send after a rescan)";
strings_["confirm_repair_wallet_title"] = "Repair Wallet";
strings_["confirm_repair_wallet_msg"] = "This restarts the daemon with -zapwallettxes=2: it deletes all of the wallet's transaction and note records, then rebuilds them from the blockchain. Use this when transactions fail to build (\"Invalid sapling spend proof\" / \"shielded requirements not met\") even after a full rescan. It takes a long time and the wallet stays offline until it finishes.";
strings_["confirm_repair_wallet_safe"] = "Your keys, addresses and balance are preserved — only the cached transaction records are rebuilt.";
strings_["daemon_binary"] = "Daemon binary";
strings_["daemon_updates_label"] = "UPDATES";
strings_["daemon_maintenance_label"] = "MAINTENANCE";
strings_["daemon_installed"] = "Installed";
strings_["daemon_bundled"] = "Bundled";
strings_["daemon_not_installed"] = "not installed";
@@ -818,6 +839,10 @@ void I18n::loadBuiltinEnglish()
strings_["daemon_status_match"] = "Installed binary matches the bundled version.";
strings_["daemon_status_differ"] = "Installed binary differs from the bundled version.";
strings_["daemon_status_missing"] = "No daemon installed — install the bundled version.";
// Compact status shown right-aligned on the DAEMON BINARY heading row.
strings_["daemon_status_ok"] = "Up to date";
strings_["daemon_status_diff"] = "Version differs";
strings_["daemon_status_none"] = "Not installed";
strings_["daemon_install_bundled"] = "Install bundled";
strings_["tt_daemon_install_bundled"] = "Stop the node, overwrite the installed dragonxd with the version bundled in this wallet build, then restart";
strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon";
@@ -1231,6 +1256,8 @@ void I18n::loadBuiltinEnglish()
strings_["wallet_recovered_restore"] = "Restore the original file instead";
strings_["wallet_recovered_restore_sub"] = "Puts your largest untouched backup back in place, verbatim, then re-scans — slightly faster, but only as complete as that one file was. Your current file is kept as a dated backup either way.";
strings_["wallet_recovered_notify"] = "Your wallet file needed a repair — your original was safely backed up. Open the app to review your options.";
strings_["wallet_degraded_notify"] = "Your wallet opened in reduced-function mode: existing funds are safe and spendable, but creating new addresses and shielding are disabled. Back up your seed phrase and restore it to fully repair the wallet.";
strings_["autoshield_by_node"] = "Auto-shield is handled by your node";
// In-dialog recovery lifecycle (Offer → Working → Done/Failed) + disclosures.
strings_["wallet_recovery_working_label"] = "Working";
strings_["wallet_recovery_done"] = "Done";
@@ -1437,8 +1464,8 @@ void I18n::loadBuiltinEnglish()
strings_["sb_daemon_extract_failed"] = "Failed to write daemon files — check free disk space and permissions.";
strings_["sb_daemon_files_failed"] = "Failed to write daemon files to %s — check free disk space and permissions.";
strings_["loading_stall_title"] = "Taking longer than expected";
strings_["loading_stall_body"] = "The daemon has been initializing for %.0fs. This can be normal after an update or on first launch (loading the block index or rescanning) — it will connect automatically once ready.";
strings_["loading_stall_hint"] = "Still stuck? Open Settings and use Restart Daemon, or check the Console for details.";
strings_["loading_stall_body"] = "Initializing for %.0fs — normal after an update or first launch. Connects automatically when ready.";
strings_["loading_stall_hint"] = "Stuck? Settings → Restart Daemon, or check the Console.";
strings_["sb_plaintext_remote_blocked"] = "Refusing to send RPC credentials over plaintext to a remote host. Add rpcallowplaintext=1 to DRAGONX.conf to allow it, or enable TLS with rpctls=1.";
strings_["rpc_plaintext_remote_warning"] = "Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.";
strings_["settings_open_log_folder"] = "Open log folder";
@@ -1500,6 +1527,7 @@ void I18n::loadBuiltinEnglish()
strings_["about_chain"] = "Chain:";
strings_["about_connections"] = "Connections:";
strings_["about_credits"] = "Credits";
strings_["about_source"] = "Source";
strings_["about_daemon"] = "Daemon:";
strings_["about_debug"] = "Debug";
strings_["about_edition"] = "ImGui Edition";
@@ -1513,6 +1541,175 @@ void I18n::loadBuiltinEnglish()
strings_["about_version"] = "Version:";
strings_["about_website"] = "Website";
// --- Help & FAQ ---
strings_["faq"] = "FAQ";
strings_["faq_title"] = "Help & FAQ";
strings_["faq_intro"] = "Answers about your wallet and the DragonX node. Search, or browse by topic below.";
strings_["faq_search_hint"] = "Search the FAQ\xE2\x80\xA6";
strings_["faq_open_tooltip"] = "Help & FAQ";
strings_["faq_no_results"] = "No results. Try a different search term.";
strings_["faq_group_wallet"] = "Wallet";
strings_["faq_group_daemon"] = "Daemon";
// Wallet subcategory titles
strings_["faq_w_gs_title"] = "Getting Started";
strings_["faq_w_addr_title"] = "Addresses & Privacy";
strings_["faq_w_send_title"] = "Sending & Receiving";
strings_["faq_w_bal_title"] = "Balance & Sync";
strings_["faq_w_sec_title"] = "Security & Encryption";
strings_["faq_w_seed_title"] = "Seed Phrase & Backup";
strings_["faq_w_chat_title"] = "Chat & Contacts";
strings_["faq_w_set_title"] = "Settings & Appearance";
// Daemon subcategory titles
strings_["faq_d_node_title"] = "The Full Node";
strings_["faq_d_sync_title"] = "Sync & Blockchain";
strings_["faq_d_mgmt_title"] = "Node Management";
strings_["faq_d_upd_title"] = "Updating the Node";
strings_["faq_d_mine_title"] = "Mining & Pools";
strings_["faq_d_net_title"] = "Peers & Network";
strings_["faq_d_perf_title"] = "Storage & Performance";
strings_["faq_d_trbl_title"] = "Troubleshooting";
// Wallet > Getting Started
strings_["faq_w_gs_1_q"] = "What is ObsidianDragon?";
strings_["faq_w_gs_1_a"] = "ObsidianDragon is a full-node wallet for DragonX (DRGX). It manages your coins and, on the desktop build, runs a DragonX full node (the daemon) in the background so your wallet verifies the blockchain itself instead of trusting a third-party server.\n\nThink of it as two parts working together: the wallet (keys, balances, sending) and the node (the blockchain and network). The questions here are split the same way \xE2\x80\x94 see the Wallet and Daemon tabs above.";
strings_["faq_w_gs_2_q"] = "How do I create a new wallet?";
strings_["faq_w_gs_2_a"] = "On first launch the setup wizard creates a wallet for you automatically. New wallets are backed by a secret recovery phrase (a list of words). Write that phrase down and keep it offline \xE2\x80\x94 it is the only way to restore your funds if this computer is lost.\n\nYou can review or back up the phrase any time from Settings > Backup & Data > Seed phrase.";
strings_["faq_w_gs_3_q"] = "How do I restore a wallet I already have?";
strings_["faq_w_gs_3_a"] = "Use the seed-phrase restore flow if you have a recovery phrase, or copy an existing wallet.dat file into the wallet's data directory before launch.\n\nAfter restoring, the node re-scans the blockchain to find your past transactions, so your balance and history may take a while to appear the first time.";
strings_["faq_w_gs_4_q"] = "What does the first-run setup do?";
strings_["faq_w_gs_4_a"] = "The wizard lets you pick an appearance/theme, optionally download a bootstrap to speed up the initial blockchain sync, and set up encryption and a PIN. You can skip any step and change all of these later in Settings.";
// Wallet > Addresses & Privacy
strings_["faq_w_addr_1_q"] = "What's the difference between transparent and shielded addresses?";
strings_["faq_w_addr_1_a"] = "Transparent addresses (they start with R or t) work like most coins: the amounts and addresses are public on the blockchain.\n\nShielded addresses (z-addresses, starting with zs) use zero-knowledge cryptography \xE2\x80\x94 the amount, sender, and receiver are encrypted on-chain. Balances held in shielded addresses are private.";
strings_["faq_w_addr_2_q"] = "Which address type should I use?";
strings_["faq_w_addr_2_a"] = "Prefer shielded (z) addresses whenever possible \xE2\x80\x94 they keep your balance and payment history private. DragonX is a privacy coin and shielded is the default for received funds.\n\nUse a transparent address only when a service you interact with cannot handle shielded addresses.";
strings_["faq_w_addr_3_q"] = "Should I reuse an address or make a new one?";
strings_["faq_w_addr_3_a"] = "For shielded addresses, reuse is fine and does not leak history. For transparent addresses, using a fresh address per payment improves privacy. Create new addresses from the Receive tab.";
strings_["faq_w_addr_4_q"] = "What is 'shielding'?";
strings_["faq_w_addr_4_a"] = "Shielding moves coins from a transparent address into a shielded one, making them private. Newly mined coins arrive transparent and are shielded for you automatically.\n\nYou can also shield manually from the wallet; a shield is just a special transaction, so it takes a normal confirmation time to complete.";
// Wallet > Sending & Receiving
strings_["faq_w_send_1_q"] = "How do I send funds?";
strings_["faq_w_send_1_a"] = "Open the Send tab, paste the recipient's address, enter an amount, and confirm. If the wallet is encrypted you'll be asked to unlock it for the transaction.\n\nSends to shielded addresses are private; sends to transparent addresses are public.";
strings_["faq_w_send_2_q"] = "What fee do I pay?";
strings_["faq_w_send_2_a"] = "DragonX fees are very low and are set automatically. The fee is shown before you confirm. You do not normally need to change it.";
strings_["faq_w_send_3_q"] = "Can I attach a message to a payment?";
strings_["faq_w_send_3_a"] = "Yes \xE2\x80\x94 when sending to a shielded (z) address you can include an encrypted memo. Only the recipient can read it. Transparent addresses do not support memos.";
strings_["faq_w_send_4_q"] = "I sent a payment but it says pending \xE2\x80\x94 why?";
strings_["faq_w_send_4_a"] = "A transaction is 'pending' until it is mined into a block and gains confirmations. This usually takes a minute or two. Shielded transactions also need the node to be synced to build the proof.\n\nIf a send stays pending unusually long, check that the node is connected and synced (see the status bar at the bottom of the window).";
// Wallet > Balance & Sync
strings_["faq_w_bal_1_q"] = "Why is my balance 0 or not updating?";
strings_["faq_w_bal_1_a"] = "While the node is still syncing the blockchain, your balance is incomplete and may read 0 \xE2\x80\x94 the wallet hasn't scanned all of your transactions yet. It fills in once syncing finishes.\n\nWatch the status bar at the bottom: it shows the connection state and block height. Once the node is synced, your balance is accurate.";
strings_["faq_w_bal_2_q"] = "What are confirmations?";
strings_["faq_w_bal_2_a"] = "Each new block mined on top of the block containing your transaction adds one confirmation. More confirmations mean the payment is more firmly settled. Received funds become spendable after the first confirmation.";
strings_["faq_w_bal_3_q"] = "What's the difference between total and spendable balance?";
strings_["faq_w_bal_3_a"] = "Total includes funds that are still confirming or are temporarily locked (for example, coins in the middle of being shielded). Spendable is what you can send right now. They converge as transactions confirm.";
// Wallet > Security & Encryption
strings_["faq_w_sec_1_q"] = "How do I encrypt my wallet?";
strings_["faq_w_sec_1_a"] = "Open Settings > Node & Security and set a passphrase (the SECURITY section). Encryption protects your private keys on disk, so someone with access to the files still cannot spend your coins.\n\nChoose a strong passphrase and don't lose it \xE2\x80\x94 there is no way to recover an encrypted wallet without it.";
strings_["faq_w_sec_2_q"] = "What is the PIN / lock screen?";
strings_["faq_w_sec_2_a"] = "The PIN locks the app's screen so balances and actions are hidden when you step away. It's a convenience lock on top of encryption; you set it up during the wizard or in Settings > Node & Security (encrypt the wallet first).";
strings_["faq_w_sec_3_q"] = "I forgot my passphrase \xE2\x80\x94 can it be recovered?";
strings_["faq_w_sec_3_a"] = "No. Wallet encryption cannot be bypassed. If you still have your seed recovery phrase, you can restore the wallet from it into a fresh wallet and set a new passphrase. Without either the passphrase or the seed phrase, the funds cannot be recovered.";
// Wallet > Seed Phrase & Backup
strings_["faq_w_seed_1_q"] = "How do I back up my recovery phrase?";
strings_["faq_w_seed_1_a"] = "Go to Settings > Backup & Data > Seed phrase. Write the words down on paper in order and store them somewhere safe and offline. Anyone with the phrase can spend your coins, so never store it in a photo, email, or cloud note.";
strings_["faq_w_seed_2_q"] = "My wallet has no recovery phrase \xE2\x80\x94 can I add one?";
strings_["faq_w_seed_2_a"] = "Older (legacy) wallets weren't seed-based. The wallet can migrate a legacy wallet into a modern seed-backed one: it creates a new seed wallet and sweeps your funds into it. Look for 'Migrate to seed\xE2\x80\xA6' in Settings > Backup & Data. This feature needs the current daemon version.";
strings_["faq_w_seed_3_q"] = "How do I restore from my recovery phrase?";
strings_["faq_w_seed_3_a"] = "Use the seed-restore flow when setting up a wallet and enter your words in order. The node then re-scans the chain to rebuild your balance and history, which can take a while the first time.";
strings_["faq_w_seed_4_q"] = "Should I also back up wallet.dat?";
strings_["faq_w_seed_4_a"] = "Your seed phrase is the primary backup and is enough to restore everything. A copy of the wallet.dat file is a convenient secondary backup that also preserves labels and settings. Keep any backup offline and private.";
// Wallet > Chat & Contacts
strings_["faq_w_chat_1_q"] = "What is the Chat feature?";
strings_["faq_w_chat_1_a"] = "Chat is an encrypted, on-chain messenger built into the wallet. Messages are sent as private shielded memos, so only you and your contact can read them.";
strings_["faq_w_chat_2_q"] = "How does my chat identity work?";
strings_["faq_w_chat_2_a"] = "Your chat identity is derived from your wallet's recovery phrase, so it travels with your wallet \xE2\x80\x94 restore the wallet and your identity comes back. You don't create a separate account or password.";
strings_["faq_w_chat_3_q"] = "How do contacts work?";
strings_["faq_w_chat_3_a"] = "Add a contact by their address in the Contacts tab, optionally with a name and avatar. Contacts can be kept private to the current wallet or shared across all wallets you open on this computer.";
// Wallet > Settings & Appearance
strings_["faq_w_set_1_q"] = "How do I change the theme?";
strings_["faq_w_set_1_a"] = "Open Settings > Appearance to pick a theme and accent. Several light and dark skins are included, and effects like blur can be toggled off for lower-powered machines.";
strings_["faq_w_set_2_q"] = "Can I change the language?";
strings_["faq_w_set_2_a"] = "Yes \xE2\x80\x94 Settings > Appearance has a language selector. The wallet ships with several translations; anything not yet translated falls back to English.";
strings_["faq_w_set_3_q"] = "The text is too small (or too large) \xE2\x80\x94 can I scale it?";
strings_["faq_w_set_3_a"] = "Use the font-scale option in Settings > Appearance. It scales the whole interface, which is handy on high-resolution (HiDPI) displays where the app might otherwise render small.";
strings_["faq_w_set_4_q"] = "How do I show fiat prices?";
strings_["faq_w_set_4_a"] = "Enable price fetching in Settings. The wallet then shows an approximate fiat value alongside balances and can display a small market chart. Prices are informational only.";
// Daemon > The Full Node
strings_["faq_d_node_1_q"] = "What is the daemon (dragonxd)?";
strings_["faq_d_node_1_a"] = "The daemon, dragonxd, is the DragonX full node. It downloads and verifies the entire blockchain, relays transactions to the network, and answers the wallet's queries. Running your own node means you don't have to trust anyone else's server.";
strings_["faq_d_node_2_q"] = "Embedded vs external node \xE2\x80\x94 what's the difference?";
strings_["faq_d_node_2_a"] = "By default the wallet launches and manages its own bundled node (embedded) \xE2\x80\x94 you don't have to do anything. If you already run dragonxd yourself, the wallet can connect to that external node instead. The wallet detects which situation it's in and behaves accordingly.";
strings_["faq_d_node_3_q"] = "Why run a full node at all?";
strings_["faq_d_node_3_a"] = "A full node validates every rule of the blockchain independently, so your wallet trusts math instead of a third party. It also strengthens the network. The trade-off is disk space and an initial sync.";
// Daemon > Sync & Blockchain
strings_["faq_d_sync_1_q"] = "What does 'Processing blocks' at startup mean?";
strings_["faq_d_sync_1_a"] = "On launch the node loads and validates blocks before it can serve the wallet. The 'Processing blocks / Applying blocks to build the current chain state' screen is that warm-up. It's normal; the wallet becomes usable once the node finishes and reports its height.";
strings_["faq_d_sync_2_q"] = "What is 'Rebuilding witness cache'?";
strings_["faq_d_sync_2_a"] = "For your shielded funds the node keeps cryptographic 'witnesses' that let you spend privately. After certain restarts or updates it rebuilds this cache by re-scanning recent blocks. On a wallet with many transactions this can take several minutes and use extra memory.\n\nIt is not stuck \xE2\x80\x94 let it finish. Force-quitting in the middle just makes it restart the rebuild next time.";
strings_["faq_d_sync_3_q"] = "Why is syncing slow when I'm almost caught up?";
strings_["faq_d_sync_3_a"] = "Near the chain tip, work that scans your wallet (checking balances and notes) competes with the node for the same internal lock, which can slow down connecting the final blocks on a large wallet. Recent versions throttle that scanning so the node can catch up. Staying on a lighter tab (or just waiting) lets it finish faster.";
strings_["faq_d_sync_4_q"] = "How long does the initial sync take?";
strings_["faq_d_sync_4_a"] = "The first sync downloads and verifies the whole chain and can take a while depending on your connection and disk. You can speed it up dramatically by enabling the bootstrap download in the setup wizard, which fetches a recent verified copy of the chain data.";
// Daemon > Node Management
strings_["faq_d_mgmt_1_q"] = "How do I start or stop the node?";
strings_["faq_d_mgmt_1_a"] = "The embedded node starts automatically with the wallet and stops when appropriate, so you normally don't manage it by hand. Advanced controls (including restarting the daemon) live in Settings > Node & Security.";
strings_["faq_d_mgmt_2_q"] = "What does 'close external daemon on exit' do?";
strings_["faq_d_mgmt_2_a"] = "If you connect the wallet to a node you started yourself, this option decides whether closing the wallet also shuts that node down. Leave it off if you want your node to keep running after you close the wallet.";
strings_["faq_d_mgmt_3_q"] = "Where is the blockchain and wallet data stored?";
strings_["faq_d_mgmt_3_a"] = "Node and wallet data live in the DragonX data directory under your user profile (for example, in AppData on Windows or your home folder on Linux/macOS). The blockchain is the large part; keep enough free disk space for it to grow.";
// Daemon > Updating the Node
strings_["faq_d_upd_1_q"] = "How do I update the node?";
strings_["faq_d_upd_1_a"] = "Open Settings > Node & Security > Daemon binary and use 'Check for updates\xE2\x80\xA6'. The wallet downloads the latest verified node build and installs it; the new version takes effect the next time the daemon starts.";
strings_["faq_d_upd_2_q"] = "Can I install a specific node version?";
strings_["faq_d_upd_2_a"] = "Yes \xE2\x80\x94 the update dialog lists every release so you can pick a specific or older build. Be cautious downgrading: an older node may not accept blockchain data written by a newer one and could need a re-index.";
strings_["faq_d_upd_3_q"] = "Is the update safe?";
strings_["faq_d_upd_3_a"] = "Every downloaded node archive is verified against a checksum and a cryptographic signature before it's installed. If verification fails, the wallet refuses the update.";
// Daemon > Mining & Pools
strings_["faq_d_mine_1_q"] = "How do I mine DragonX?";
strings_["faq_d_mine_1_a"] = "The Mining tab lets you mine with your CPU. DragonX uses the RandomX algorithm, which is designed for regular processors. Set the number of threads and start \xE2\x80\x94 rewards accrue to your wallet.";
strings_["faq_d_mine_2_q"] = "Solo vs pool mining \xE2\x80\x94 which should I choose?";
strings_["faq_d_mine_2_a"] = "Solo mining sends rewards straight to you but pays only when you find a block, which is infrequent unless you have a lot of hashrate. Pool mining shares work with others for smaller, steadier payouts. The Mining tab supports both.";
strings_["faq_d_mine_3_q"] = "How do I update the miner?";
strings_["faq_d_mine_3_a"] = "The Mining tab's pool section has an 'Update miner\xE2\x80\xA6' button that downloads, verifies, and installs the latest optimized DragonX miner build. Like the node updater, each download is checksum- and signature-verified before install.";
strings_["faq_d_mine_4_q"] = "Can I host a mining pool?";
strings_["faq_d_mine_4_a"] = "The current node can run a built-in stratum server so other miners can point at your machine. There's a toggle for it under Settings > Node & Security. By default it only listens locally; exposing it to other computers requires opening it up deliberately.";
// Daemon > Peers & Network
strings_["faq_d_net_1_q"] = "The wallet shows no peers or connections \xE2\x80\x94 what's wrong?";
strings_["faq_d_net_1_a"] = "Right after launch the node needs a moment to find peers, so a brief '0 peers' is normal. If it persists, check your internet connection and that a firewall isn't blocking the node. The node finds peers automatically through built-in seeds.";
strings_["faq_d_net_2_q"] = "Is my connection to the network encrypted?";
strings_["faq_d_net_2_a"] = "Yes \xE2\x80\x94 DragonX nodes talk to each other over TLS, so peer connections are encrypted. The wallet also talks to its own node over a secure local channel.";
// Daemon > Storage & Performance
strings_["faq_d_perf_1_q"] = "How much disk does the node use, and can I tune memory?";
strings_["faq_d_perf_1_a"] = "The blockchain is the large item and grows over time, so keep several gigabytes free. The node uses a database cache for speed; the bundled defaults are tuned for typical machines. Advanced users can adjust the node's database cache by editing dbcache in its config file (DRAGONX.conf).";
strings_["faq_d_perf_2_q"] = "How do I make the initial sync faster?";
strings_["faq_d_perf_2_a"] = "Enable the bootstrap download (offered in the setup wizard) to fetch a recent, verified copy of the chain instead of validating every block from scratch. A fast disk (SSD) also helps a lot.";
// Daemon > Troubleshooting
strings_["faq_d_trbl_1_q"] = "The node seems stuck on 'Activating best chain' \xE2\x80\x94 is it frozen?";
strings_["faq_d_trbl_1_a"] = "Usually not. On a large wallet the node can spend several minutes rebuilding shielded witnesses or applying blocks, during which it looks paused and may be slow to answer. Give it time \xE2\x80\x94 the debug log (Settings) shows steady progress if it's working.\n\nAvoid force-quitting during this phase; it restarts the work from scratch next launch.";
strings_["faq_d_trbl_2_q"] = "What is 'degraded mode'?";
strings_["faq_d_trbl_2_a"] = "If a wallet file is recovered but part of its key data is missing, the node may open it in a limited 'degraded' mode where it can't derive new addresses or shield funds. The safest fix is to restore from your seed recovery phrase into a fresh wallet.";
strings_["faq_d_trbl_3_q"] = "When should I re-index or re-scan?";
strings_["faq_d_trbl_3_a"] = "A re-scan makes the wallet re-read the chain to rediscover your transactions (useful after importing keys). A re-index rebuilds the node's blockchain database and is only needed if that database is corrupted or incompatible after a version change. Both can take a while; start them from Settings > Node & Security.";
strings_["faq_d_trbl_4_q"] = "The node is using a lot of memory \xE2\x80\x94 is that normal?";
strings_["faq_d_trbl_4_a"] = "Memory use spikes during heavy work like a witness rebuild or an initial sync, then drops back down once it finishes. Sustained high memory at idle is unusual \xE2\x80\x94 restarting the wallet clears it. On a low-RAM machine, make sure other memory-heavy apps aren't competing.";
// --- Address Book Dialog ---
strings_["address_book_add"] = "Add Address";
strings_["address_book_add_new"] = "Add New";
@@ -2221,6 +2418,29 @@ void I18n::loadBuiltinEnglish()
strings_["merge_funds"] = "Merge Funds";
strings_["merge_started"] = "Merge operation started";
strings_["merge_title"] = "Merge to Address";
// Consolidate-funds flow (rich merge modal + wallet-bloat preset).
strings_["consolidate_title"] = "Consolidate funds";
strings_["consolidate_desc"] = "Combine many small inputs into a single shielded note. Fewer notes means a smaller wallet file and better privacy.";
strings_["consolidate_funds_btn"] = "Consolidate";
strings_["merge_scope_loading"] = "Checking your inputs\xE2\x80\xA6";
strings_["merge_scope_fmt"] = "%d transparent + %d shielded inputs \xC2\xB7 ~%s DRGX spendable";
strings_["merge_source"] = "Consolidate";
strings_["merge_src_transparent"] = "Transparent";
strings_["merge_src_shielded"] = "Shielded";
strings_["merge_src_both"] = "Both";
strings_["merge_batch_fmt"] = "Merges up to %d inputs per run \xE2\x80\x94 repeat to finish the rest.";
strings_["merge_advanced"] = "Advanced";
strings_["merge_max_inputs"] = "Max inputs per batch";
strings_["merge_fee_hint"] = "Network fee for this transaction.";
strings_["merge_create_zaddr"] = "Create shielded address";
strings_["merge_creating"] = "Creating address\xE2\x80\xA6";
strings_["merge_addr_created"] = "Shielded address created.";
strings_["merge_confirm_fmt"] = "Consolidate ~%s DRGX from %d input(s) into %s?";
strings_["merge_confirm_btn"] = "Confirm";
strings_["merge_back"] = "Back";
strings_["merge_progress"] = "Consolidating\xE2\x80\xA6 this can take a few minutes. You can close this window.";
strings_["merge_no_spendable"] = "No spendable inputs to consolidate yet.";
strings_["done"] = "Done";
// --- Transaction Details Dialog ---
strings_["tx_confirmations"] = "%d confirmations";
@@ -2304,6 +2524,217 @@ void I18n::loadBuiltinEnglish()
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
strings_["explorer_no_results"] = "No matching cached blocks";
// ---- i18n audit 2026-09: wrap previously-hardcoded UI strings ----
// app_security.cpp — encryption / PIN / lock flow
strings_["sec_restart_daemon_for_encryption"] = "Please restart your daemon for encryption to take effect.";
strings_["sec_encrypting_wallet"] = "Encrypting wallet...";
strings_["sec_wallet_encrypted_restarting_daemon"] = "Wallet encrypted. Restarting daemon...";
strings_["sec_wallet_encrypted_successfully"] = "Wallet encrypted successfully";
strings_["sec_encryption_failed_prefix"] = "Encryption failed: ";
strings_["sec_wallet_encrypted_and_pin_set"] = "Wallet encrypted & PIN set";
strings_["sec_wallet_encrypted_but_pin_vault_failed"] = "Wallet encrypted but PIN vault failed";
strings_["sec_couldnt_lock_wallet"] = "Couldn't lock the wallet — it is still unlocked. Check the daemon connection.";
strings_["sec_changing_passphrase"] = "Changing passphrase...";
strings_["sec_passphrase_changed_successfully"] = "Passphrase changed successfully";
strings_["sec_failed_prefix"] = "Failed: ";
strings_["sec_encryption_did_not_complete"] = "Wallet encryption did not complete — your wallet is NOT encrypted. Open Settings to finish encrypting it.";
strings_["sec_wallet_locked_title"] = "Wallet Locked";
strings_["sec_too_many_attempts_wait"] = "Too many attempts. Wait %.0f seconds...";
strings_["sec_mode_passphrase"] = " Passphrase";
strings_["sec_use_passphrase_instead"] = "Use passphrase instead";
strings_["sec_use_pin_instead"] = "Use PIN instead";
strings_["sec_unlocking_fmt"] = "Unlocking%s";
strings_["sec_unlock_button"] = "Unlock";
strings_["sec_not_connected_to_daemon"] = "Not connected to daemon";
strings_["sec_unlock_failed_prefix"] = "Unlock failed: ";
strings_["sec_incorrect_passphrase_decrypt"] = "Incorrect passphrase";
strings_["sec_importing_keys_rescanning"] = "Importing keys & rescanning blockchain — wallet is usable while this runs";
strings_["sec_encrypted_backup_suffix"] = "\nEncrypted backup: wallet.dat.encrypted.bak";
strings_["sec_wallet_decrypted_all_keys_imported"] = "Wallet decrypted successfully! All keys imported.";
strings_["sec_total_elapsed_fmt"] = "Total elapsed: %dm %02ds";
strings_["sec_verifying_passphrase"] = "Verifying passphrase...";
strings_["sec_incorrect_passphrase_pin_setup"] = "Incorrect passphrase";
strings_["sec_pin_set_successfully"] = "PIN set successfully";
strings_["sec_failed_to_create_vault"] = "Failed to create vault";
strings_["sec_not_connected_to_daemon_pin"] = "Not connected to daemon";
strings_["sec_changing_pin"] = "Changing PIN...";
strings_["sec_pin_changed_successfully"] = "PIN changed successfully";
strings_["sec_incorrect_current_pin"] = "Incorrect current PIN";
strings_["sec_internal_error_change_pin"] = "Internal error";
strings_["sec_verifying_pin"] = "Verifying PIN...";
strings_["sec_pin_removed"] = "PIN removed";
strings_["sec_incorrect_pin_remove"] = "Incorrect PIN";
strings_["sec_internal_error_remove_pin"] = "Internal error";
// app.cpp — wizard, seed backup, shutdown, daemon/miner lifecycle
strings_["appx_creating_your_wallet"] = "Creating your wallet…";
strings_["appx_create_failed_prefix"] = "Create failed: ";
strings_["appx_back_up_seed_phrase_title"] = "Back up your seed phrase";
strings_["appx_seed_backup_warning"] = "These 24 words are the ONLY way to restore your wallet. Write them down in order, store them offline, and never share them. If you lose them, your funds are gone forever.";
strings_["appx_birthday_block_height"] = "Birthday (block height): %llu — back this up too.";
strings_["appx_ive_written_it_down"] = "I've written it down";
strings_["appx_copy"] = "Copy";
strings_["appx_skip_anyway"] = "Skip anyway";
strings_["appx_skip"] = "Skip";
strings_["appx_seed_not_backed_up_warning"] = "You have not backed up your seed — funds could be lost. Skip anyway?";
strings_["appx_confirm_your_backup"] = "Confirm your backup";
strings_["appx_tap_words_in_order"] = "Tap the words in the correct order to confirm you saved them.";
strings_["appx_progress_n_of_n"] = "Progress: %d / %d";
strings_["appx_not_next_word"] = " — that's not the next word";
strings_["appx_done"] = "Done";
strings_["appx_wallet_created_and_backed_up"] = "Wallet created and backed up.";
strings_["appx_back"] = "Back";
strings_["appx_restoring_your_wallet"] = "Restoring your wallet…";
strings_["appx_recovery_phrase_word_count"] = "Recovery phrase should be 24 words — you have %d.";
strings_["appx_could_not_start_restore"] = "Could not start restore";
strings_["appx_node_rebuilding_witness_cache"] = "Node is rebuilding its witness cache";
strings_["appx_stopping_node_discards_rebuild"] = "Stopping the node now discards the in-progress rebuild and restarts it (several minutes) the next time you open the wallet. You can keep the node running instead.";
strings_["appx_keep_node_running_and_quit"] = "Keep node running & quit";
strings_["appx_stop_anyway_and_quit"] = "Stop anyway & quit";
strings_["appx_cancel"] = "Cancel";
strings_["appx_last_used_wallet_not_found_prefix"] = "Your last-used wallet file (";
strings_["appx_last_used_wallet_not_found_suffix"] = ") was not found — opened the default wallet instead. If you moved it, restore it and switch back from the wallet list.";
strings_["appx_wallet_open_failed_prefix"] = "Wallet open failed: ";
strings_["appx_blockchain_rescan_complete"] = "Blockchain rescan complete";
strings_["appx_miner_stopped_unexpectedly"] = "Miner stopped unexpectedly.";
strings_["appx_miner_stopped_prefix"] = "Miner stopped: ";
strings_["appx_pool_miner_connected_and_hashing"] = "Pool miner connected and hashing.";
strings_["appx_bootstrap_complete_reconciling"] = "Bootstrap complete — reconciling your wallet with the new chain data.";
strings_["appx_blockchain_data_deleted"] = "Blockchain data deleted (%d items). The daemon is restarting to re-sync from the network.";
strings_["appx_invalid_payment_uri_prefix"] = "Invalid payment URI: ";
strings_["appx_payment_request_loaded"] = "Payment request loaded";
strings_["appx_fullnode_lifecycle_unavailable_lite"] = "Full-node lifecycle actions are unavailable in lite build";
strings_["appx_blockchain_maintenance_in_progress"] = "A blockchain maintenance operation is already in progress.";
strings_["appx_node_busy_restarting"] = "The node is busy restarting — try again in a moment.";
strings_["appx_restarting_daemon_rescan_flag"] = "Restarting daemon with -rescan flag...";
strings_["appx_restarting_daemon_zapwallettxes"] = "Restarting daemon with -zapwallettxes=2 (wallet repair)...";
strings_["appx_no_bundled_daemon_to_install"] = "This build has no bundled daemon to install";
strings_["appx_no_embedded_daemon_to_install"] = "This build has no embedded daemon to install";
strings_["appx_daemon_reinstall_in_progress"] = "The daemon reinstall is already in progress.";
strings_["appx_installing_bundled_daemon"] = "Installing bundled daemon — the node will stop, update, and restart...";
strings_["appx_stopping_daemon_deleting_blockchain"] = "Stopping daemon and deleting blockchain data...";
strings_["appx_stopping_pool_miner"] = "Stopping pool miner...";
strings_["appx_disconnecting"] = "Disconnecting...";
strings_["appx_sending_stop_command_to_daemon"] = "Sending stop command to daemon...";
strings_["appx_cleaning_up"] = "Cleaning up...";
strings_["appx_shutdown_complete"] = "Shutdown complete";
strings_["appx_n_seconds"] = "%d seconds";
strings_["appx_n_min_n_sec"] = "%d min %d sec";
strings_["appx_still_status_prefix"] = "Still \"";
strings_["appx_still_status_suffix"] = "\" — force quitting now may corrupt chain data.";
strings_["appx_rebuilding_witness_cache_blocks_left"] = "Rebuilding witness cache %.0f%% — %d blocks left";
strings_["appx_rebuilding_witness_cache_pct"] = "Rebuilding witness cache %.0f%%";
strings_["appx_setting_initial_sapling_witnesses"] = "Setting initial Sapling witnesses %.0f%%";
strings_["appx_rebuilding_sapling_note_witnesses"] = "Rebuilding Sapling note witnesses…";
strings_["appx_syncing_pct_block_n_of_n"] = "Syncing %.1f%% — Block %d / %d";
strings_["appx_last_block_n"] = "Last block: %d";
strings_["appx_encrypting_wallet"] = "Encrypting wallet...";
strings_["appx_waiting_for_daemon_to_encrypt_wallet"] = "Waiting for daemon to encrypt wallet...";
strings_["appx_daemon_error"] = "Daemon Error";
strings_["appx_use_settings_restart_daemon_hint"] = "Use Settings > Restart Daemon to try again";
strings_["appx_copied_clipboard_autoclears"] = "Copied — clipboard auto-clears in 45s";
strings_["appx_dragonxd_output"] = "dragonxd output";
strings_["appx_theme_prefix"] = "Theme: ";
strings_["appx_low_spec_mode_enabled"] = "Low-spec mode enabled";
strings_["appx_low_spec_mode_disabled"] = "Low-spec mode disabled";
strings_["appx_theme_effects_enabled"] = "Theme effects enabled";
strings_["appx_theme_effects_disabled"] = "Theme effects disabled";
strings_["appx_simple_background_enabled"] = "Simple background enabled";
strings_["appx_simple_background_disabled"] = "Simple background disabled";
// balance_tab.cpp
strings_["baltab_shielded_amount"] = "Shielded %.8f";
strings_["baltab_transparent_amount"] = "Transparent %.8f";
strings_["baltab_market_price_4dp"] = "Market: $%.4f";
strings_["baltab_market_price_8dp"] = "Market: $%.8f";
strings_["baltab_pct_of_total_zaddr"] = "%.0f%% of total · %d Z-addr";
strings_["baltab_t_addresses_count"] = "%d T-addresses";
strings_["baltab_pct_change_24h"] = "%s%.1f%% 24h";
strings_["baltab_total_balance"] = "Total Balance";
strings_["baltab_shielded"] = "Shielded";
strings_["baltab_transparent"] = "Transparent";
strings_["baltab_market"] = "Market";
// settings_window.cpp
strings_["swin_invalid_suffix"] = " (invalid)";
strings_["swin_theme_list_refreshed"] = "Theme list refreshed";
strings_["swin_connection_successful"] = "Connection successful!\ndragonxd version: ";
strings_["swin_connection_failed"] = "Connection failed: ";
strings_["swin_rpc_client_not_initialized"] = "RPC client not initialized";
strings_["swin_rescan_started_from_block"] = "Rescan started from block ";
strings_["swin_rescan_to"] = " to ";
strings_["swin_rescan_failed"] = "Rescan failed: ";
strings_["swin_ztx_history_cleared"] = "Z-transaction history cleared";
strings_["swin_no_history_file_found"] = "No history file found";
strings_["swin_settings_saved"] = "Settings saved";
// settings_page.cpp / explorer_tab.cpp / block_info_dialog.cpp
strings_["grpa_tab_appearance"] = "Appearance";
strings_["grpa_tab_wallet"] = "Wallet";
strings_["grpa_tab_backup_data"] = "Backup & Data";
strings_["grpa_tab_node_security"] = "Node & Security";
strings_["grpa_tab_explorer"] = "Explorer";
strings_["grpa_tab_chat"] = "Chat";
strings_["grpa_tab_about"] = "About";
strings_["grpa_enter_private_key_to_import"] = "Enter a private key to import.";
strings_["grpa_invalid_suffix"] = " (invalid)";
strings_["grpa_seed_demo_chat"] = "Seed demo chat";
strings_["grpa_dbg_addrman"] = "Peer address tracking and management";
strings_["grpa_dbg_alert"] = "Alert system messages";
strings_["grpa_dbg_bench"] = "Benchmark timings for operations";
strings_["grpa_dbg_coindb"] = "Coin database read/write operations";
strings_["grpa_dbg_db"] = "Berkeley DB operations";
strings_["grpa_dbg_estimatefee"] = "Fee estimation algorithm";
strings_["grpa_dbg_http"] = "HTTP RPC server activity";
strings_["grpa_dbg_libevent"] = "Libevent networking library";
strings_["grpa_dbg_lock"] = "Lock contention debugging";
strings_["grpa_dbg_mempool"] = "Transaction memory pool activity";
strings_["grpa_dbg_net"] = "Network connections and messages";
strings_["grpa_dbg_paymentdisclosure"] = "Payment disclosure protocol";
strings_["grpa_dbg_pow"] = "Proof-of-work mining activity";
strings_["grpa_dbg_proxy"] = "SOCKS5 proxy connections";
strings_["grpa_dbg_prune"] = "Block pruning operations";
strings_["grpa_dbg_rand"] = "Random number generation";
strings_["grpa_dbg_reindex"] = "Blockchain reindexing progress";
strings_["grpa_dbg_rpc"] = "RPC command processing";
strings_["grpa_dbg_selectcoins"] = "Coin selection for transactions";
strings_["grpa_dbg_tor"] = "Tor integration and circuit info";
strings_["grpa_dbg_zmq"] = "ZeroMQ notification system";
strings_["grpa_dbg_zrpc"] = "Shielded (z-addr) RPC operations";
strings_["grpa_sec_ago"] = "%lld sec ago";
strings_["grpa_min_ago"] = "%lld min ago";
strings_["grpa_hr_ago"] = "%lld hr ago";
strings_["grpa_days_ago"] = "%lld days ago";
strings_["grpa_showing_first_100_of"] = "... showing first 100 of %d";
strings_["grpa_error_prefix"] = "Error: ";
strings_["grpa_invalid_response_from_daemon"] = "Invalid response from daemon";
strings_["grpa_current_block_paren"] = "(Current: %d)";
strings_["grpa_unexpected_getblockhash_result"] = "unexpected getblockhash result";
// receive / chat / send / transaction_details
strings_["grpb_new_badge_suffix"] = " [NEW]";
strings_["grpb_tooltip_address_balance"] = "%s\nBalance: %.8f %s%s";
strings_["grpb_selected_suffix"] = "\n(selected)";
strings_["grpb_preview_msg_payment_through"] = "Did the payment go through? 🙂";
strings_["grpb_preview_msg_yep_confirmed"] = "Yep — just confirmed ✅";
strings_["grpb_preview_msg_sending_rest"] = "Sending the rest now 👍";
strings_["grpb_max"] = "Max";
strings_["grpb_undo_clear"] = "Undo Clear";
strings_["grpb_copy"] = "Copy";
// bootstrap / mining_* / key_export
strings_["grpc_bootstrap_not_initialized"] = "Bootstrap not initialized";
strings_["grpc_bootstrap_failed"] = "Bootstrap failed";
strings_["grpc_na"] = "N/A";
strings_["grpc_key_not_available"] = "Key not available for this address";
strings_["grpc_benchmark_takes_secs"] = "Benchmark takes ~%ds and interrupts mining. Click again to start.";
strings_["grpc_hashrate_fee"] = "%s %s%% fee";
strings_["grpc_benchmark_inconclusive"] = "Benchmark inconclusive: no hashrate samples were recorded. Check the pool connection and try again.";
// misc pre-existing missing keys
strings_["copied_to_clipboard"] = "Copied to clipboard";
}
const char* I18n::translate(const char* key) const
+72 -15
View File
@@ -867,6 +867,70 @@ int Platform::getSystemIdleSeconds()
// GPU utilization detection
// ============================================================================
std::string Platform::runHiddenCapture(const std::string& cmdLine, bool mergeStderr, int* exitCode)
{
if (exitCode) *exitCode = -1;
#ifdef _WIN32
SECURITY_ATTRIBUTES sa;
ZeroMemory(&sa, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE hRead = NULL, hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return {};
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); // parent's read end stays private
HANDLE hNul = INVALID_HANDLE_VALUE;
if (!mergeStderr) {
hNul = CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = hWrite;
si.hStdError = mergeStderr ? hWrite : hNul;
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
std::string cl = cmdLine; // CreateProcessA may modify lpCommandLine → needs a mutable buffer
std::string out;
if (CreateProcessA(NULL, cl.empty() ? NULL : &cl[0], NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
CloseHandle(hWrite); hWrite = NULL; // close our copy so ReadFile hits EOF when the child exits
if (hNul != INVALID_HANDLE_VALUE) { CloseHandle(hNul); hNul = INVALID_HANDLE_VALUE; }
char buf[4096];
DWORD n = 0;
while (ReadFile(hRead, buf, sizeof(buf), &n, NULL) && n > 0) out.append(buf, n);
WaitForSingleObject(pi.hProcess, INFINITE);
if (exitCode) {
DWORD code = 0;
if (GetExitCodeProcess(pi.hProcess, &code)) *exitCode = static_cast<int>(code);
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
if (hWrite != NULL) CloseHandle(hWrite);
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
CloseHandle(hRead);
return out;
#else
const std::string full = cmdLine + (mergeStderr ? " 2>&1" : " 2>/dev/null");
std::string out;
FILE* f = popen(full.c_str(), "r");
if (!f) return out;
char buf[512];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) out.append(buf, n);
const int st = pclose(f);
if (exitCode) *exitCode = st; // raw status (matches prior pclose-based rc checks)
return out;
#endif
}
int Platform::getGpuUtilization()
{
#ifdef _WIN32
@@ -877,23 +941,16 @@ int Platform::getGpuUtilization()
static bool s_has_nvidia = false;
if (!s_tried_nvidia) {
s_tried_nvidia = true;
FILE* f = _popen("where nvidia-smi 2>nul", "r");
if (f) {
char buf[256];
s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr);
_pclose(f);
}
// Windowless (runHiddenCapture) so GPU-aware idle detection never flashes a cmd.exe console.
const std::string w = runHiddenCapture("where nvidia-smi");
s_has_nvidia = (w.find_first_not_of(" \t\r\n") != std::string::npos);
}
if (s_has_nvidia) {
FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r");
if (f) {
char buf[64];
int util = -1;
if (fgets(buf, sizeof(buf), f)) {
util = atoi(buf);
if (util < 0 || util > 100) util = -1;
}
_pclose(f);
const std::string o = runHiddenCapture(
"nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits");
if (!o.empty()) {
int util = atoi(o.c_str());
if (util < 0 || util > 100) util = -1;
return util;
}
}
+9
View File
@@ -179,6 +179,15 @@ public:
* @return GPU busy percent, or -1 if unavailable.
*/
static int getGpuUtilization();
// Run a command line and capture its stdout WITHOUT ever popping a console window: Windows uses
// CreateProcess + CREATE_NO_WINDOW (a plain popen()/_popen() flashes a cmd.exe console), POSIX uses
// popen(). Use this instead of _popen for anything run while the GUI is up. `mergeStderr` folds the
// child's stderr into the result (like "2>&1"); otherwise stderr is discarded. `exitCode`, if given,
// receives the child's exit status (raw pclose() status on POSIX, GetExitCodeProcess on Windows; -1
// if the process could not be launched).
static std::string runHiddenCapture(const std::string& cmdLine, bool mergeStderr = false,
int* exitCode = nullptr);
};
/**
+28 -8
View File
@@ -1417,31 +1417,51 @@ void testNetworkRefreshRpcCollectors()
});
coreRpc.addResponse("getblockchaininfo", json{
{"blocks", 150},
{"headers", 155},
{"headers", 150},
{"bestblockhash", "core-best-150"},
{"verificationprogress", 0.80},
{"longestchain", 160},
{"verificationprogress", 1.0},
{"longestchain", 150}, // caught up → balance scan runs
{"notarized", 145}
});
auto core = Refresh::collectCoreRefreshResult(coreRpc);
// getblockchaininfo is issued FIRST — sync detection gates (and must not be delayed behind) the
// O(mapWallet) balance scan. When caught up, the balance scan follows: display (minconf=0) +
// spendable (minconf=1).
EXPECT_TRUE(coreRpc.methodNames() == std::vector<std::string>({
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo" // display (minconf=0) + spendable (minconf=1)
"getblockchaininfo", "z_gettotalbalance", "z_gettotalbalance"
}));
EXPECT_EQ(coreRpc.calls[0].params, json::array({0}));
EXPECT_EQ(coreRpc.calls[1].params, json::array({1}));
EXPECT_EQ(coreRpc.calls[1].params, json::array({0}));
EXPECT_EQ(coreRpc.calls[2].params, json::array({1}));
EXPECT_TRUE(core.balanceOk);
EXPECT_TRUE(core.blockchainOk);
EXPECT_NEAR(*core.totalBalance, 4.25, 0.00000001);
EXPECT_EQ(*core.blocks, 150);
EXPECT_EQ(*core.bestBlockHash, std::string("core-best-150"));
EXPECT_EQ(*core.longestChain, 160);
EXPECT_EQ(*core.longestChain, 150);
// When getblockchaininfo shows the node is behind (blocks < longestchain - 2), the balance scan is
// skipped this cycle regardless of includeBalance — so sync-state (and hence kSyncProfile) updates
// promptly instead of after the multi-second scan. Only getblockchaininfo is issued.
MockRefreshRpc coreBehindRpc;
coreBehindRpc.addResponse("z_gettotalbalance", json{{"total", "4.25000000"}});
coreBehindRpc.addResponse("getblockchaininfo", json{
{"blocks", 150}, {"headers", 160}, {"longestchain", 160}
});
auto coreBehind = Refresh::collectCoreRefreshResult(coreBehindRpc);
EXPECT_TRUE(coreBehindRpc.methodNames() == std::vector<std::string>({"getblockchaininfo"}));
EXPECT_FALSE(coreBehind.balanceOk);
EXPECT_TRUE(coreBehind.blockchainOk);
EXPECT_EQ(*coreBehind.blocks, 150);
EXPECT_EQ(*coreBehind.longestChain, 160);
MockRefreshRpc coreFallbackRpc;
coreFallbackRpc.addFailure("z_gettotalbalance", "wallet warming up");
coreFallbackRpc.addResponse("getblockchaininfo", json{{"blocks", 8}, {"headers", 9}});
auto partialCore = Refresh::collectCoreRefreshResult(coreFallbackRpc);
// No longestchain in the response → not classified as "behind" → balance is still attempted (here it
// fails). getblockchaininfo is still issued first.
EXPECT_TRUE(coreFallbackRpc.methodNames() == std::vector<std::string>({
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo"
"getblockchaininfo", "z_gettotalbalance", "z_gettotalbalance"
}));
EXPECT_FALSE(partialCore.balanceOk);
EXPECT_TRUE(partialCore.blockchainOk);