54 Commits

Author SHA1 Message Date
f6024557d5 refactor(updater): drop dead RenderReleaseList picker; keep shared ReleaseRow
The miner and node updater dialogs were rebuilt around their own two-pane
tactile-Material version lists, which superseded release_list_view.h's
RenderReleaseList() picker. That function had zero call sites — both dialogs
include the header only for the shared ReleaseRow struct. Remove the dead
function (and the material/i18n/imgui includes it alone needed), leaving just
the ReleaseRow model, and correct CLAUDE.md's stale "shared picker" description
to reflect that the two-pane list is the browse-all UI and only the row model
is shared.

No behavior change: build + ctest + hygiene clean; grep confirms no remaining
RenderReleaseList references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:51:02 -05:00
d82821ba06 feat(settings): redesign the migrate-to-seed modal onto reference design
Migrate App::renderSeedMigrationDialog onto the settings-modal reference design.
This is the most fund-critical dialog in the app (create isolated seed wallet ->
z_mergetoaddress sweep -> confirm -> adopt/rescan), already fully i18n'd and
dp-scaled, so the change is purely the presentation shell:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 580,
  idSuffix "migrate"}) replacing the legacy positional overload
- all 26 StyledButton -> TactileButton (identical signature; height-0 auto-sizing)
- remove all 13 footer Separator dividers
- ShowSeed seed-phrase warning -> material::DialogWarningHeader (red text -> amber
  ⚠ header, matching the export-key reference dialog)

The fund pyramid is byte-for-byte unchanged: the busy/dismiss veto, the wipeSeed
(sodium_memzero) + close lambdas and the post-EndOverlayDialog seed scrub, all
seed_migration_step_ transitions, begin CreateSeedWallet/SweepToSeedWallet/
AdoptSeedWallet, refreshSeedMigrationBalance, pollSweepStatus, the snprintf
balance/confs/remaining formatting, both confirmation checkboxes and their gates,
RenderSeedWordGrid, copySecretToClipboard, writeFileAtomically, and the Discard
remove_all + settings cleanup.

Verified: build + ctest + hygiene clean; Spacing() count unchanged (28->28,
proving no collateral deletion); StyledButtons elsewhere in app.cpp untouched
(the swap was range-bounded to this function); a 3-lens adversarial review
(fund-path-untouched / secret-hygiene / widget-visual) returned zero findings,
with the fund-path lens proving the logic diffs byte-for-byte identical to HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:55:01 -05:00
8290e96ec4 test(sweep): add modal-pin-setup/-change/-remove capture surfaces
Register the three redesigned PIN dialogs as sweep surfaces for visual review.
Each setup only sets the show flag (never fires the async vault store/verify); the
teardown clears the status and zeroes the PIN/passphrase buffers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:29:02 -05:00
46f7da4fac feat(settings): redesign the PIN setup/change/remove modals onto reference design
Migrate the three PIN dialogs in App::renderPinDialogs (Set PIN / Change PIN /
Remove PIN) onto the settings-modal reference design — presentation-only, plus a
deliberate secret-hygiene addition:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 420/420/400,
  idSuffix pinsetup/pinchange/pinremove}) replacing the legacy positional overload
- drop each dialog's redundant post-description Separator
- every rendered-chrome string -> TR(): titles/buttons reuse settings_set_pin /
  settings_change_pin / settings_remove_pin, the Set-PIN confirm reuses
  wiz_pin_confirm; 7 new pin_* keys added to the English source + all 8
  res/lang/*.json additively; ImGui::Text -> TextUnformatted for labels
- HARDENING: wipe the passphrase/PIN input buffers on BlurFloat dismiss
  (X/Esc/outside-click) for all three dialogs. Set PIN's wallet-passphrase buffer
  (pin_passphrase_buf_) was previously left resident on dismiss; it is now zeroed.

The vault/RPC path is byte-for-byte unchanged: the submit-time memsets, the
walletpassphrase/walletlock RPCs, vault_->store/changePin/retrieve/removeVault,
SecureVault::secureZero, and setPinEnabled/save are untouched. The pin_status_ and
toast strings inside the worker callbacks are left in English (a separate
service-layer i18n pass), to avoid editing the secret path.

Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format
specifiers; a 3-lens adversarial review (secret-hygiene / presentation-only /
ui-i18n) returned zero findings, with the presentation lens proving the vault path
diffs byte-for-byte identical against HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:29:02 -05:00
5bd07b0505 test(sweep): add modal-decrypt capture surface
Register the redesigned "Remove Wallet Encryption" dialog as a sweep surface so
its passphrase-entry phase is captured for visual review. The setup resets the
WalletSecurityWorkflow (keeping it in PassphraseEntry) and never fires the async
unlock/export/restart pyramid; the teardown resets and zeroes decrypt_pass_buf_.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:02:11 -05:00
90df19b8b6 feat(settings): redesign the remove-encryption (decrypt) modal onto reference design
Migrate App::renderDecryptWalletDialog ("Remove Wallet Encryption") onto the
settings-modal reference design — presentation-only for a fund-critical dialog,
plus one deliberate secret-hygiene addition:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 480,
  idSuffix "decrypt"}) replacing the legacy positional overload
- Phase 0 amber TextWrapped warning -> material::DialogWarningHeader(decrypt_warning)
- drop the redundant Phase 0 Separator
- every rendered-chrome string across all 4 phases (PassphraseEntry / Working /
  Success / Error) -> TR() (14 new decrypt_*/try_again keys added to the English
  source + all 8 res/lang/*.json additively; the button reuses
  settings_remove_encryption, the label reuses change_pass_current, cancel/close reused)
- HARDENING: wipe decrypt_pass_buf_ on BlurFloat dismiss (X/Esc/outside-click),
  mirroring the change-passphrase fix; canClose() gates pOpen so this cannot fire
  during the Working phase

The fund-moving async worker pyramid (unlock -> export -> stop -> backup ->
restart -> re-import, vault/PIN cleanup) is byte-for-byte unchanged; the three
in-worker toast notifications and the "Incorrect passphrase" status are left in
English to avoid touching the fund path (a separate service-layer i18n pass).

Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format
specifiers and the wallet.dat.encrypted.bak literal preserved; a 3-lens adversarial
review (secret-hygiene / fund-path-untouched / ui-i18n) returned zero findings, with
the fund-path lens proving the async pyramid diffs IDENTICAL against HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:02:11 -05:00
5985f05fb2 test(sweep): add modal-encrypt & modal-change-passphrase capture surfaces
The redesigned settings-tab Encrypt Wallet and Change Passphrase dialogs had no
sweep surfaces, so the full UI sweep only captured the first-run wizard-encrypt /
wizard-pin variants — the settings modals were never rendered for visual review.
Register modal-encrypt (redesigned passphrase-entry phase) and
modal-change-passphrase, each with a defensive teardown that resets the phase and
zeroes the passphrase buffers. Neither setup path fires the async encrypt/change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:31:31 -05:00
60ee73bf1d fix(settings): wipe change-passphrase buffers on dialog dismiss
The Change Passphrase dialog only zeroed change_old/new/confirm_pass_buf_ on
the success path (inside changePassphrase); dismissing via X / Esc / outside-
click left the typed passphrases resident in the input buffers. Add a same-
frame teardown mirroring the encrypt dialog's X-button cleanup: when the
BlurFloat overlay clears show_change_passphrase_ mid-frame, memset the three
buffers. The failure path still keeps them intact for retry, and the async
change already copies the passphrases into worker-owned strings, so wiping the
UI buffers cannot disturb an in-flight change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:17:34 -05:00
625df8abe6 feat(settings): redesign encrypt & change-passphrase modals onto reference design
Migrate the Encrypt Wallet (3-phase) and Change Passphrase dialogs in
renderEncryptWalletDialog onto the settings-modal reference design —
presentation-only, no change to the crypto/state-machine/secret-wipe logic:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth, idSuffix})
  replacing the legacy positional overload (same visual style, dp-scaled width)
- amber TextWrapped warning -> material::DialogWarningHeader(wiz_encrypt_warning)
- drop redundant Separator dividers
- every hardcoded English string -> TR() (13 new enc_*/change_pass_* keys added to
  the English source + all 8 res/lang/*.json additively; CJK subset rebuilt)

The strength-meter math, 3-phase transitions, "cannot close while encrypting"
guard, memset/SecureVault buffer wipes, and the encrypt/changePassphrase/vault
async paths are byte-for-byte unchanged. Verified: build + ctest + hygiene clean,
wiz_strength %s format-string safe across all languages, and a 3-lens adversarial
review (secret-hygiene / presentation-only / ui-i18n) returned zero findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:13:44 -05:00
5dd68dc9bd feat(settings): migrate Seed phrase backup modal to the reference design
Phase 2 (second security modal), presentation-only + the review's one nit.

Struct-form BlurFloat overlay (was the legacy positional overload); the phrase
state's plain red warning -> material::DialogWarningHeader; StyledButtons ->
TactileButtons with the Copy/Save/Close row and shared Close centered via
PlaceOverlayDialogActions (the existing helper) and their separators dropped;
success status now wraps. Already fully i18n'd.

UNCHANGED (verified by the adversarial review): the closeAndWipe sodium_memzero of
the 24-word phrase on every close path (Begin-false early return, outside-click/Esc
teardown, phrase Close, shared Close), the async exportSeedPhrase fetch + its
show_seed_backup_ callback guard, the atomic restricted-perms file-write +
sodium_memzero of its content buffer, and copySecretToClipboard / RenderSeedWordGrid.
No seed leak; Begin/EndOverlayDialog balanced on every return path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:28:25 -05:00
8a30578872 feat(settings): migrate Export Private Key modal + fix outside-click secret leak
Phase 2 (first security modal), presentation-only + the review's one finding.

Migration: struct-form BlurFloat overlay (was legacy positional); full i18n
(reusing export_private_key / key_export_private_warning / key_export_private_key /
select_address / key_export_fetching / export / copy / close, plus one new
key_export_failed); DialogWarningHeader; centered DialogActionFooter (Export +
Close) + a Copy TactileButton; async UX (export_in_progress_ spinner, export_error_
line). The exported key stays in the char[256] buffer shown via a read-only
InputTextMultiline (deliberately NOT AddressCopyField, which would copy the secret
into a lingering std::string). exportPrivateKey is untouched.

Security fix (found by the adversarial review): the 0a "outside-click/Esc wipe" via
the Begin-returns-false early-return was UNREACHABLE — BeginOverlayDialog clears
show_export_key_ mid-frame but still returns true, so an outside-click/Esc dismiss
left the exported spending key un-wiped in export_result_. Add the in-body
`if (!show_export_key_)` teardown before EndOverlayDialog (the proven import-dialog
pattern), which is the real wipe point for every dismiss path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:47:11 -05:00
63d5c817aa chore(sweep): add modal-bootstrap surface for the standalone bootstrap dialog
The settings-triggered BootstrapDownloadDialog wasn't captured (only the wizard's
bootstrap page was). Add BootstrapDownloadDialog::hide() + a modal-bootstrap sweep
surface (opens the confirm screen) so the migrated dialog is verifiable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:37:26 -05:00
3662df550c feat(settings): Bootstrap modal — DialogWarningHeader + centered confirm buttons
Phase 1 (last low-risk modal). Replace the hand-rolled amber warning child-card
with the standard material::DialogWarningHeader (⚠ + Warning color, wrapping),
and center the Download / Mirror / Cancel button row. Already struct-form,
TactileButton, and i18n'd. Completes Phase 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:17:48 -05:00
067c96c425 feat(settings): Wallets modal — TactileButton + drop the footer divider
Phase 1. Swap the 5 StyledButton actions (Open / Create / Scan folder / Reveal /
Close) to TactileButton, and remove the ImGui::Separator above the footer (the
reference has no divider). Already struct-form + fully i18n'd. Deferred: R6
glass-panel grouping of the scanned-folders section (it interacts with the
dialog's pre-computed cardHeight — a separate, more careful change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:16:25 -05:00
a2c4a8df73 fix(material): wrap DialogWarningHeader so long warnings don't clip
Export All Keys' long DANGER warning was clipped at the card edge because the
header rendered a single unwrapped line. Add PushTextWrapPos so a long warning
flows to a second line; short one-line warnings (import/backup/settings) fit and
are unaffected. Surfaced by the Windows sweep on modal-export-all-keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:13:54 -05:00
822891d4ef feat(settings): migrate Export All Keys modal to the reference design
Phase 1. DialogWarningHeader care header (was a hand-rolled red icon+text);
centered DialogActionFooter (Export + Close) replacing the StyledButtons + two
separators; on full success the saved path shows in a copy field with a green
check, partial/error render inline; export progress uses LoadingDots. Full i18n:
the previously-hardcoded notification + status strings now use TR (8 new
export_keys_* keys in en + 8 langs). Added hide() + a modal-export-all-keys sweep
surface. Also fixes a latent Begin/EndOverlayDialog imbalance on the 0-address
early return. No change to the export RPC logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:43:53 -05:00
22638b094c feat(settings): migrate Backup Wallet modal to the reference design
Phase 1. Struct-form BlurFloat overlay (was the legacy positional overload);
DialogWarningHeader care header; centered DialogActionFooter (Create Backup +
Close) replacing the StyledButtons + separator; full i18n — title/destination/
button reuse existing backup_* keys, plus two new keys (backup_warn one-line care
line, backup_overwrite_confirm) added in en + 8 langs. No change to backupWallet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:37:58 -05:00
bf81ec1702 feat(settings): migrate Export-Transactions-CSV modal to the reference design
Phase 1 (first modal). Bring the CSV export dialog onto the key-import reference:
- Struct-form OverlayDialogSpec / BlurFloat (was the legacy positional overload).
- material::DialogActionFooter (centered TactileButton pair) replacing the
  StyledButtons + the two ImGui::Separators.
- Full i18n: TR("close") and the success/error status now via TR (were hardcoded
  English); on success the saved path shows in a widgets::AddressCopyField with a
  green check.
- Add ExportTransactionsDialog::hide() + a modal-export-transactions sweep surface
  so the dialog is captured (it wasn't before).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:22:41 -05:00
56cd9b6273 fix(settings): make the rescan slider visible on light themes
The scan-height slider drew its track, % label, and thumb in pure white, which
vanished on light skins because the glass sub-section goes opaque white there
(only the numeric input showed). Switch those to OnSurface-based colors so they
adapt: near-white on dark (unchanged look) and dark on light (now visible).
Surfaced by the full Windows UI sweep on the marble/light skins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:04:58 -05:00
8b9cd5bf80 feat(material): extract DialogActionFooter + GlassSectionScope helpers
Phase 0b of the settings-modal redesign — pull the reference dialog's two
repeated patterns into shared helpers so the upcoming migrations are mechanical
and identical:

- material::DialogActionFooter(primary, enabled, close, &outPrimary, &outClose)
  — the centered fixed-width primary+Close TactileButton pair with no divider.
- material::GlassSectionScope — an RAII auto-sized glass sub-section using a LOCAL
  ImDrawListSplitter (combo-safe, unlike GlassCardScope's shared ChannelsSplit) with
  the reference's 12/10 padding and 8dp rounding; interiorWidth() for full-width kids.

Dogfooded by refactoring renderImportKeyDialog's footer and scan-height panel onto
them — same padding/spec/centering, so the render is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:49:04 -05:00
2b191cea34 fix(settings): securely wipe the exported private key (no heap-lingering secret)
The Export Private Key dialog held the exported spending key in a plain
std::string cleared only with .clear() — never sodium_memzero'd — and on
outside-click/Esc the overlay's early return skipped even that, so the secret
lingered in freed heap. Hold it in a fixed char[256] and sodium_memzero on every
close path (Close button, outside-click/Esc early return, address change). Guard
the async export callback so it can't populate the buffer after the dialog closed.

Standalone security fix ahead of the settings-modal redesign (Phase 0a); the
dialog's full restyle is Phase 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:44:25 -05:00
6ecbf835ec refactor(settings): unify the viewing-key care header with the private-key one
Settle the key-import reference before propagating its design to the other
settings modals: the viewing-key import now uses the same DialogWarningHeader
(⚠ warning icon + Warning color) as the private-key import, instead of the milder
grey eye + grey note. The viewing-key note is longer, so that mode gets a wider
card (760 vs 640) to keep it on one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:37:51 -05:00
a13190a6df refactor(settings): import dialog layout polish
Several layout refinements to the Import Private/Viewing Key dialog:
- Widen the dialog to 640px so the one-line warning under the heading no longer wraps.
- Vertically center DialogWarningHeader's label with the (taller) warning icon
  instead of top-aligning it — a consistent fix for every warning header.
- Add a small gap between the key input's text and the flush eye toggle.
- Center + slightly enlarge the Paste / Clear buttons.
- Center the Import/Sweep + Close action buttons.
- Scan-height control: put the numeric input first with the slider below it, and
  drop the redundant "0 = rescan from the start" hint (0 is the field default; the
  transparent-key note is kept).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:59:53 -05:00
ffc24ee99b refactor(settings): drop the divider above the import dialog's action buttons
Remove the separator between the content and the Import/Sweep + Close buttons;
the spacing alone separates them cleanly enough.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:50:05 -05:00
3274c72a58 refactor(settings): drop the redundant "Key" label above the import field
The "Import Private Key" / "Import Viewing Key" dialog heading already gives the
context, so the "Key" / "Viewing key" label above the input was redundant. Remove
it; the field is the dialog's primary input and reads fine without it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:47:59 -05:00
b7b32bfde8 fix(settings): stop paste-preview hover flicker from layout shift
Hovering Paste reflected the clipboard's classification in the type-indicator
ROW, which appeared/disappeared with the hover. That grew/shrank the centered
dialog, moving the Paste button out from under the cursor → hover ended → row
vanished → dialog shrank back → cursor over the button again → rapid flicker.

Keep the in-field preview + verification but make them pure draw-list (no layout
change): the ghost overlay is now coloured by how the clipboard would classify
(green ok / amber wrong-type / red unrecognized), and the type indicator reverts
to the actual field content only. The dialog height is now identical whether or
not Paste is hovered, so there's no shift and no flicker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:52:11 -05:00
0b47134112 feat(settings): labelled Paste/Clear + in-field clipboard preview
Restore text labels on the import dialog's Paste and Clear buttons (the reveal
eye stays a compact icon flush in the field), and replace the paste-hover tooltip
with an in-field preview like the send tab: while Paste is hovered and the Key
field is empty, the trimmed clipboard is drawn as a transparent ghost overlay
inside the field — masked with '*' unless the key is revealed — so the user can
verify it before pasting.

The type indicator now reflects the hovered clipboard (indKey/iRecognized/…),
while the Import/Sweep button guard and the scan panel keep using the actual
field content only, so a hover can never enable or trigger the action. Removes
the now-unused RenderPasteHoverPreview tooltip helper and the dead wrongType local.

Reviewed via an adversarial pass (overlay masking/security, the indicator-vs-action
classification split, button regressions); the one finding (dead wrongType) is fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:43:17 -05:00
828018de2b feat(settings): sweep a private key's funds to your own wallet
Add a "Sweep to my wallet (don't keep the key)" option to the Import Private
Key dialog (spending keys only). Rather than keeping the key, it imports the
key, then sends ALL its funds to a destination you control — a freshly
generated shielded address by default, or an existing wallet address you pick.

Flow (App::sweepPrivateKey): import the key (rescan; the stock node has no
address index, so its UTXOs/notes can't be enumerated otherwise) → confirm it
holds spendable funds → resolve the destination → z_sendmany balance-minus-fee
from the key's address to the destination, via the existing tested submitZSendMany
wrapper + async-operation tracker. The imported key is left in the wallet with an
empty balance (there is no remove-key RPC) — stated plainly in the UI caveat.

Design notes forced by this daemon (verified against external/dragonx):
- z_importkey returns null here, so the shielded source address is found by
  diffing z_listaddresses before/after import (aborts if the pre-snapshot fails,
  so a wrong address can never be picked). Transparent uses importprivkey's return.
- z_sendmany, NOT z_mergetoaddress: moves a single-UTXO transparent source (the
  common paper-wallet case) and fails loudly instead of silently leaving a
  remainder. Amount computed in integer satoshis for an exact fixed-decimal string.
- Clear errors for no-funds / unconfirmed / dust-below-fee / already-imported.
- The mode + destination are locked during a running sweep; a sweep that finishes
  while the dialog is closed still shows its Done/Error+txid on reopen; sweep UI
  can't bleed into the watch-only viewing dialog.

i18n: 8 new sweep_* keys (en + 8 langs); dynamic status strings are English,
matching the beginSweepToSeedWallet precedent.

Reviewed across three adversarial multi-agent rounds (fund-safety focus): round 1
caught the feature was broken for shielded keys (z_importkey null) and rejected
single-UTXO transparent sources; round 2 caught reopen/double-submit/precision
issues; round 3 verified the fixes. NOT yet exercised against a live daemon —
needs a small-amount mainnet test before it's trusted, like migrate-to-seed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:29:10 -05:00
2a96c41e07 feat(settings): polish the import-key dialog + paste verify preview
Design pass on the Import Private/Viewing Key dialog:

- Group the optional scan-from-height section (label, slider, field, hint) in a
  subtle glass panel via a draw-list splitter so it reads as one distinct,
  clearly-optional block.
- Replace the boxed reveal / Paste / Clear buttons with frameless Material icon
  buttons: the eye toggle sits flush at the Key field's right edge (rounded to
  match the input frame), Paste / Clear are faint icon pills with tooltips.
- Add a centered %-of-chain label to the slider (mirrors the send-tab amount bar).

New: hovering Paste peeks at the clipboard and verifies it before pasting —
a tooltip classifies the clipboard the same way the field indicator does
(✓ valid type for this dialog / ⚠ a key for the other button / ✗ not a key),
with a preview line masked with '*' unless the key is currently revealed (then a
truncated peek). Empty clipboard shows "Clipboard is empty".

i18n: added paste_clip_empty (en + 8 langs); the preview reuses the field
indicator's type / wrong-type keys.

Reviewed via three adversarial multi-agent passes (splitter/panel, icon buttons,
slider math, and the paste preview's correctness/security + i18n); the confirmed
findings (eye hover-pill rounding, unrecognized-state color) are addressed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:38:36 -05:00
c25da0d26d feat(settings): stack the rescan slider above the height field
Lay the optional scan-from-height control out vertically: the send-tab-style
slider spans the full row on top, with the numeric field full-width directly
beneath it (both were previously side by side). Reads more clearly and gives
the slider room; the unsynced fallback (field only) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:04:23 -05:00
0c55ed0fe4 feat(settings): send-tab-style slider for the rescan start height
The optional "scan from block height" control in the Import Private/Viewing
Key dialog now leads with a slider styled like the send-tab amount bar
(rounded track, accent fill, glass thumb) bounded by the chain tip, with the
numeric field beside it for exact entry and the current chain height
right-aligned above. When the node's height is unknown (state_.sync.blocks == 0,
e.g. not yet synced) the slider is omitted and the field spans the full row —
which also fixes the previous cramped 140px field that left the row half empty.

- New static helpers in app.cpp: formatIntWithCommas() and RenderScanHeightControl()
  (mirrors RenderAmountBar in send_tab.cpp). The height decimal string is the single
  source of truth shared by slider + field; the slider reserves/queries its
  InvisibleButton before drawing so a drag has no one-frame lag; the bar height is
  ImGui::GetFrameHeight() so it aligns with the numeric field.
- Import handler clamps the parsed start height to the chain tip.
- i18n: added import_scan_tip ("current height") in en + 8 langs.

Reviewed via an adversarial multi-agent pass (slider-math / state-sync /
dpi-layout / consistency); the one confirmed finding (bar/field height mismatch)
is fixed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:17:15 -05:00
1fc3e03e6f feat(settings): rename to "Import Private Key" + add scan height to it
Follow-up to the key/viewing-key button split:

- Rename the "Import Key…" button to "Import Private Key…" (settings_import_key
  value in en + all 8 langs, reusing the existing import_key_title wording) so it
  reads as the natural counterpart to "Import Viewing Key…". The dialog title was
  already "Import Private Key". The viewing-key wrong-type redirect hint now names
  the renamed button.

- Show the "scan from block height" field in the private-key (spending) dialog too,
  not just the viewing-key one. z_importkey accepts a start height, so a shielded
  spend import of a recent key needn't rescan the whole chain. Transparent WIF
  (importprivkey) has no such param, so once a transparent key is recognized the
  field is disabled, its buffer cleared, and a "Transparent keys always rescan
  fully" note replaces the hint. importPrivateKey already gates the start-height
  append on (viewing || shielded), so transparent imports never receive it.

- i18n: shared scan keys renamed import_viewkey_scan_{label,hint} ->
  import_scan_{label,hint}; added import_scan_transparent (en + 8 langs).

Reviewed via an adversarial multi-agent pass (rpc-routing / i18n / ui-logic /
consistency); only two nits surfaced (stale disabled-field value + a stale
comment), both fixed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:38:40 -05:00
f428292ea4 feat(settings): split import into separate key + viewing-key buttons
Backup & Data now has two distinct import actions instead of one
auto-detecting dialog:

- "Import Key" imports a spending key (transparent WIF or shielded
  z-spending key) with the strong "grants access to funds" warning.
- "Import Viewing Key" imports a watch-only shielded viewing key
  (zxviews…) with a milder eye/watch-only note and an optional
  "scan from block height" field — z_importviewingkey accepts a start
  height, so watching a recent address needn't rescan the whole chain.

The shared Material dialog (renderImportKeyDialog) branches on
import_view_mode_: title, warning tone, field label, the live type
indicator, and the recognition guard. A key valid for the *other*
button (e.g. a spending key pasted into the viewing-key dialog) now
shows a redirect hint rather than a generic "unrecognized" error.

importPrivateKey gains a startHeight arg, appended to the shielded
import RPCs (z_importviewingkey / z_importkey) and ignored for
transparent WIF (importprivkey has no start-height param). The
scan-height buffer is wiped on open/close alongside the key buffer.

i18n: 9 new keys (button label + tooltip, viewing title/note/field,
scan label/hint, two wrong-type redirect hints) with translations for
all 8 languages; CJK subset rebuilt (+1 glyph). A modal-import-viewkey
sweep surface is added for the viewing-mode dialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:53:35 -05:00
46f3001360 feat(settings): redesign the Import Key dialog
Rework the full-node Import Key dialog into a safer, Material-consistent, more
capable flow:

- Security: the key is masked by default (Password) with a reveal (eye) toggle,
  a DialogWarningHeader "only import a key you own" note, and the buffer is
  wiped with sodium_memzero on close and on successful import.
- Progress: import triggers a blocking full-chain rescan; the dialog now shows
  "Importing & rescanning — this can take several minutes" with LoadingDots and
  disables Import while it runs (no double-submit). Offline shows a "connect a
  running node" note and disables Import up front.
- Viewing keys: the classifier gains isViewingKey / isRecognizedImportKey;
  importPrivateKey auto-detects a shielded viewing key (zxviews…) and routes to
  z_importviewingkey (watch-only) vs z_importkey / importprivkey. The live type
  indicator shows Transparent / Shielded spending / Shielded viewing (watch-only).
- Result: on success the imported address is captured from the RPC result and
  shown via AddressCopyField.
- Material restyle (OverlayDialogSpec/BlurFloat + TactileButton + Esc) and full
  i18n (12 new keys x 8 languages; CJK subset rebuilt).

Verified: rendered on dark + light skins; a seeded zxviews… key shows the masked
field + "Shielded viewing key (watch-only)" indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:58:36 -05:00
8293f02e36 fix(console): RPC modal — pointer cursor + Material restyle
Two issues from the audit:

- Cursor: hovering the modal showed the console's text-select cursor. The
  output's mouse-interaction guard only suppressed itself under ImGui popups,
  but this modal is a BeginOverlayDialog (not a popup), so the console kept
  setting the text cursor underneath. Suppress output interaction while the
  reference modal is open (!show_commands_popup_).

- Design: the two panes used hard 1px child borders and floated unpolished.
  Restyle to match the portfolio/wallets modals — each pane now sits on a soft
  DrawGlassPanel surface (borderless child + inner padding), category headers
  are subtle accent labels (no heavy filled bars), and the selected/hovered row
  gets a rounded Material fill instead of the sharp Selectable highlight. The
  glass panes are theme-adaptive (light on light skins), so the command-name
  blue and the mono example green are floored to readable contrast (FloorLight)
  and the detail heading uses theme Primary().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:11:54 -05:00
b34069f57c fix(console): don't silently drop a filled param after an empty optional
The builder's "omit trailing optionals" loop stopped at the FIRST empty
optional, which discarded any later field the user had filled — e.g. setban
with bantime empty but absolute=true built `setban "ip" "add"`, dropping the
typed `true`, and (worse) left Insert & run enabled so it ran a command that
ignored the input.

Include fields up to the last one that is filled or required; only genuinely
trailing empty optionals are dropped. A gap (an empty field before a later
filled one) keeps its placeholder and marks the command incomplete, so Insert
& run disables and nothing typed is lost. Found by an adversarial pass over all
82 param templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:38:25 -05:00
4416e01f9c feat(console): parameter builder in the command explorer (phase 3)
The detail pane's Parameters section is now an editable form: each parameter is
a type-hinted input field (string / number / json, optional marked), and a live
"Builds" preview shows the command being assembled. String values are
auto-quoted; a required field left empty keeps its placeholder and disables
"Insert & run"; trailing empty optionals are omitted.

Insert and Insert & run now use the assembled command (not just the template),
so Insert & run works for parameterized commands once the required fields are
filled — destructive commands still confirm first. Param fields reset when the
selected command changes or the modal opens. Keyboard nav (Up/Down/Enter) is
scoped to the search box so typing in a field doesn't move the selection.

A best-effort parseParamSpecs turns the human-readable param templates into
fields; JSON-array params (z_sendmany, createrawtransaction) surface as a single
"json" field the user pastes into. Adds the console_ref_builds label + 8
translations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:32:35 -05:00
44a17f3ad0 feat(console): enrich the remaining RPC commands for the explorer (phase 2)
Add plain-language details, a concrete example, and intent keywords to the
50 commands that Phase 1 left with only the terse description — so every entry
in the command explorer now explains itself and is searchable by task, not just
by name. Destructive flags are preserved.

Content was authored per-category and reviewed for technical accuracy (correct
RPC semantics, valid console example syntax, realistic DragonX placeholders).
Data-only change; command docs stay English (technical), consistent with the
existing policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:16:55 -05:00
c9e9c9f979 feat(console): redesign the RPC reference as a novice-friendly command explorer
Replace the flat, terse command-reference table with a guided two-pane
explorer built for users who don't know the RPC commands:

- Search by name OR task: keyword synonyms let "balance" find getbalance,
  "send money" find sendtoaddress; results are ranked (name-prefix > name >
  keyword > desc).
- Two panes (mirrors the portfolio editor): a category-grouped master list
  (mono names, a warning dot on consequential commands) + a rich detail pane
  with a plain-language explanation, a per-parameter type breakdown (string /
  number / json, optional dimmed), and a concrete example.
- Insert into console (fills the input with a template to review + run) and,
  for no-parameter commands, Insert & run — with a confirm for destructive
  ones. Deferred via pending_submit_ so the modal needs no executor.
- Keyboard: type to filter, Up/Down to move, Enter to insert; auto-focus and
  auto-select the top result.

Data model: ConsoleCommandEntry gains details/example/keywords/destructive
(C++17 defaults, so the ~60 un-enriched entries are untouched); ~20 common
commands enriched and 18 consequential ones flagged. Command docs stay English
(technical); the UI chrome + 7 category names are translated into all 8
languages (CJK subset rebuilt, +3 glyphs).

Verified: two-pane renders on dark + light skins and at font_scale 1.5; detail
pane shows explanation/params/example; danger dots + safety badge on
destructive commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:07:49 -05:00
ab4dd370c0 fix(console): RPC command-reference modal audit fixes
- Clicking a command now actually closes the modal. It called
  ImGui::CloseCurrentPopup(), a no-op here (BeginOverlayDialog is Begin/
  BeginChild, not an ImGui popup), so the picker stayed open covering the input
  and the insert looked like it did nothing. Set show_commands_popup_=false.
- Search filter is now a member cleared when the modal opens, so it isn't stale
  on reopen after an outside-click / command-click dismiss (was reset only by
  the Close button).
- Esc dismisses the modal (this overlay has no built-in Esc handling), and the
  search box auto-focuses on open so the user can type immediately.
- Scale the 2-vs-3 column breakpoint (cmd-min-width) by dpiScale() — it was a
  logical-px threshold compared against a physical-px available width.
- Give each category's table a unique ImGui id (##cmdsN) instead of sharing
  "##cmds", and translate the 7 category names (Control/Network/... — command
  descriptions stay English) into all 8 languages; CJK subset rebuilt (+2).

(Investigated but rejected: the command-name blue was flagged as low-contrast
on light skins, but the modal card is a dark glass panel on every theme, so the
original color is correct — verified by rendering marble/light/color-pop-light.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:25:51 -05:00
675d434958 fix(console): mop up audit minors — timestamp, filter count, new-line count
- D2: rpcTraceTimestamp used std::localtime (process-wide static tm) guarded by
  a private mutex that can't stop another thread's localtime from clobbering the
  shared buffer. Use localtime_r / localtime_s into a local tm (the codebase
  pattern) and drop the now-useless mutex. Runs on RPC worker threads.
- C1: the toolbar's "<N> matches" label read filter_match_count_ before
  renderOutput recomputed it, so it lagged one frame. Compute the visible/
  filtered set once at the top of render() (before the toolbar) and reuse it in
  renderOutput, so the count and the output share one consistent computation.
- C4: the "N new lines" indicator counted the raw drain count, inflating it with
  lines the active filter hides. Count only newly-added lines that pass the
  current filter, so the badge matches what the user sees on jumping to bottom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 18:10:27 -05:00
e8055888a5 feat(console): persist the output zoom level across launches
The console zoom (s_console_zoom) was a plain static reset to 1.0 each launch,
unlike the sibling toggles. Wire it through Settings (console_zoom float),
restored into the static at startup in the App ctor and written back via the
existing post-render diff-and-save. Loading clamps to [0.25, 4.0] (also
catching NaN) so a corrupt value can't feed the scanline divisor.

Verified: seeding console_zoom=1.5 survives a relaunch and the console text
renders larger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:54:47 -05:00
48eceb6593 fix(console): don't coerce quoted or malformed numeric args to JSON numbers
BuildConsoleRpcCall pushed any bare token that "looked numeric" as a JSON
number, and lost the tokenizer's quoting decision — so a genuinely-string
argument that happened to be all digits (e.g. a label) was sent with the wrong
type. std::stoll also silently truncated "1e999" to 1 (stops at the first
non-digit) and std::stod could yield inf/subnormal.

- ParseConsoleCommandArgsTagged returns {text, quoted}; quoted tokens are sent
  verbatim as JSON strings, never coerced. (ParseConsoleCommandArgs kept as a
  text-only wrapper so existing callers/tests are unchanged.)
- Bare tokens become a number only when the WHOLE token parses to a finite
  int/double; otherwise they're sent as a string.

Adds test coverage for quoted-numeric-string, bare-number, 1e999, and 123abc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:49:02 -05:00
4f71c18884 fix(console): audit fixes — HiDPI, fold UX, stop-confirm, RPC-trace UAF
From the console-tab audit:
- HiDPI: the "New output" pill (140x24 + margin/rounding/border) and the JSON
  indent-guide thickness were hand-drawn in raw px; scale them by dpiScale().
- Fold triangle: size it from the DPI/density-scaled gutter width (not the
  zoomed font) and center it in the gutter band so glyph and clickable cell
  stay aligned; only draw/handle it in the unfiltered view (folding_active_) —
  in a filtered flat view a click silently flipped the collapsed flag with no
  visible effect and the glyph disagreed with the rendered block.
- Selection: ignore left-clicks in the gutter (< output_origin_.x) so toggling
  a fold no longer clears the user's active text selection.
- 'stop' confirmation: promote the function-local static to a member and reset
  it in clear(), so a toolbar/context-menu clear between the two 'stop's can't
  leave a stale arm that skips the shutdown warning.
- RPC-trace callback: dereference the console pointer under the mutex (not
  after releasing it) so ~ConsoleTab can't destroy the object mid-call —
  closing a latent use-after-free on shutdown (fires on RPC worker threads).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:45:51 -05:00
232b81f8ee feat(settings): surface the console color toggles as checkboxes
Add "Color accents" and "Text colors" checkboxes under the effects section
(both layout variants) so the console output display prefs are discoverable
outside the console toolbar. They mirror the toolbar buttons — bound live to
the ConsoleTab statics and written through to settings on change.

Unlike scanline/theme-effects they carry no GPU cost, so a small helper closes
the effects row's BeginDisabled(low_spec), draws them on their own always-
enabled row, and reopens it for the acrylic sliders (net-balanced). Adds
console_accents / console_text_colors labels + 8 translations; the tooltips
reuse the existing toggle keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:40:25 -05:00
c40c252c9a feat(console): persist accent toggle + add monochrome-text toggle
Persist the two console output display preferences and add a second toolbar
toggle for monochrome text.

- Persistence: new Settings fields console_line_accents / console_text_color
  (both default true, matching the ConsoleTab statics so an upgrade re-save
  can't flip visible behavior). Restored into the statics at startup in the App
  constructor, and saved from a diff-and-save after the console renders (only
  when a value actually changed — save() is disk I/O). The startup restore also
  fixes a pre-existing bug: scanline was only synced from settings when the
  Settings page or first-run wizard ran, so an existing wallet ignored a saved
  scanline preference on launch.
- Monochrome text: a new toolbar toggle (FORMAT_COLOR_TEXT, dimmed when off)
  next to the accent toggle. channelTextColor() early-returns the neutral
  COLOR_RESULT for every channel (and JSON syntax role) when off, leaving the
  left accent bars independent. COLOR_RESULT is contrast-floored per theme, so
  text stays readable on light and dark terminals.

Adds console_toggle_text_color to the English source + all 8 translations.
Verified: both toggles independent, persistence round-trips across restart,
monochrome readable on light + dark skins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:26:44 -05:00
d156dddcd0 feat(console): toolbar toggle to hide per-line color accent bars
Adds a paint-bucket toggle button to the console toolbar (in the appearance
cluster next to zoom) that shows/hides the per-line left color accent bars for
a cleaner monochrome gutter. The icon reflects state (FORMAT_COLOR_FILL when on,
a dimmed FORMAT_COLOR_RESET when off) with a tooltip. Session-only, like the
console zoom factor; default on.

Adds console_toggle_accents to the English source and all eight translations
(no new CJK glyphs — the subset font already covers them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:36:51 -05:00
0e1957c5f9 feat(market): explain why the portfolio Save button is disabled
Hovering the disabled Save button now shows which requirement is missing
(name / at least one address / a manual price above 0), mirroring the
send-tab disabled-submit tooltip idiom (IsItemHovered(AllowWhenDisabled)).
The hint only appears for a validation gap — not the self-evident "nothing
changed" case.

Adds portfolio_save_need_{name,address,price} to the English source and all
eight translations; rebuilds the CJK subset font for the new JA/KO/ZH glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:22:56 -05:00
3f95765dcc fix(balance): unique IDs + scope filter for the Add-to-portfolio menu
The address context menu's "Add to portfolio" submenu built each item with
MenuItem(label), whose ImGui ID is derived from the label — so two groups with
the same label (or two empty labels) collided, routing a click to the wrong
group, and a blank label rendered as an invisible row.

- PushID(pi) per item gives a stable unique ID regardless of label, and an
  empty label now shows the "new entry" placeholder.
- Filter the list to the active wallet's groups (or legacy/unscoped), matching
  the Market summary and editor, so an address can't be added to a different
  wallet's group. The "no entries" placeholder now also covers the case where
  every group is filtered out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:11:35 -05:00
62c92cc862 fix(market): scope the portfolio editor to the active wallet
The Market summary filters custom groups by wallet scope, but the editor did
not: its master list showed and edited *every* wallet's groups, and "Manage…"
opened raw storage index 0 — which could belong to a different wallet.
Separately, a group could be persisted with an empty scope (a placeholder
added before the wallet identity resolved, then filled in and saved), which the
filter treats as "legacy" and shows in *every* wallet's list.

- The editor master list now filters to the active wallet's groups (plus
  legacy/unscoped), mirroring the summary. Storage indices are preserved so
  selection/delete still target the right entry.
- "Manage…" opens the first in-scope group (or the empty state), never index 0.
- The selection clamp resets to the first visible group if the current one is
  out of range or out of scope.
- On commit, a group with no scope is claimed for the active wallet when an
  identity is available, so it can't linger in the global bucket.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:28 -05:00
3aeb847657 fix(market): make portfolio-editor dismiss gestures consistently auto-save
The Close button auto-committed the current group, but Escape, outside-click,
switching groups, adding a group, and deleting another group all *discarded*
uncommitted edits — so whether your work was kept depended on how you happened
to leave, and the prominent unsaved-dot / Save / Revert affordances implied a
save model the exits didn't honor.

Align every dismiss/switch gesture with the Close button's intent (auto-save):
- Escape and outside-click now commit before closing (outside-click is caught
  right after BeginOverlayDialog, which clears open mid-frame).
- Switching to another group, "Add entry", and deleting a *different* group
  commit the current group first. Deleting the selected group still discards
  its own working state.

Save remains "persist now", Revert "undo to last saved", and the dot marks an
in-progress group. Ordering is safe against the placeholder GC: that GC runs
earlier in the frame (mktDrawPortfolio, before RenderPortfolioEditor) and only
while the editor is closed, so it never shifts entry indices under a commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:03:17 -05:00
2a9d32e78b fix(market): close portfolio-editor gaps found in re-audit
Adversarial re-audit of the group editor surfaced follow-ups to the prior
hardening pass:

- Dirty-check regression: the empty-currency->"USD" coercion was applied on
  save but not in the dirty comparison, so clearing the currency field left a
  group perpetually "dirty" (unsaved dot + Revert/Save never settling, a
  redundant persist on every Close). Both sides now share pfEditLabel() /
  pfEditCurrency() so they can't disagree — this also trims the label, so a
  trailing space no longer wedges the dirty state.
- Whitespace-only labels ("   ") passed validation and persisted as an
  invisible group; the trimmed label is now required by pfWorkingValid.
- "Add entry" persists an empty placeholder up front; abandoning it (Close/
  Esc/outside-click without adding an address) left a phantom "$0 · 0" group.
  The summary now prunes address-less groups while the editor is closed.
- Manual price accepted inf/NaN (neither is < 0); sanitize with std::isfinite.
- Escape closed the whole editor even when it was only meant to dismiss the
  open custom-color popup, discarding edits; guard on !IsPopupOpen.
- HiDPI: scale the Z/T chip pill's vertical padding and the balance-breakdown
  baseline nudge that the prior pass missed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:51:32 -05:00
97a54f0e0c fix(market): harden portfolio group editor + HiDPI scaling
Portfolio audit fixes:
- Guard the BTC value line behind price_btc > 0 so an empty BTC price no
  longer renders a misleading "≈ 0.00000000 BTC".
- Validate the working group before commit/save (pfWorkingValid: needs a
  label, at least one address, and a positive manual price when the manual
  price basis is selected); the Save button disables to match, so a
  half-filled editor no longer persists a broken group on close/switch.
- Default an empty manual currency to "USD" instead of an empty string.
- Reload the editor after clamping a stale selection index so it can't
  point past the end of the group list.
- Scale hand-drawn geometry by dpiScale(): address-row rounding, chip
  y-offset, preview border, chart toggle buttons, and the shielded/
  transparent ratio-bar corner radii now render correctly on HiDPI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:14:45 -05:00
82da4af857 feat(market): per-pair candle cache — instant switch-back, no re-fetch
The per-exchange chart used a single buffer keyed by the loaded pair, so
bouncing between venues (Ourbit <-> NonKYC) re-fetched every switch (loading
spinner each time). Add a per-pair cache: each venue's fetched candles (OHLC +
derived close series) are kept keyed by "<identifier>:<BASE>/<QUOTE>". Switching
back to a recently-viewed pair loads from the cache instantly — no fetch, no
spinner — while entries older than 30 min re-fetch to stay fresh. Bounded to 16
entries (irrelevant for DRGX's 2 venues, but safe if it lists more). Cache is
main-thread-only (populated by the worker's completion MainCb), so no races.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:48:53 -05:00
38 changed files with 3405 additions and 975 deletions

View File

@@ -79,13 +79,13 @@ The detailed milestone plan and design history (the v2 plan, backend artifact/AB
## Miner updater (xmrig) ## Miner updater (xmrig)
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. A **"Browse all releases…"** button (the `/releases` list, newest first, pre-releases included) lets users pin an older or pre-release build — same verify/install path via `startInstallRelease()`; the picker UI is shared with the daemon updater (`ui/windows/release_list_view.h`). The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. The dialog is a two-pane version picker (every `/releases` entry on the left, newest first, pre-releases included) so users can pin an older or pre-release build — same verify/install path via `startInstallRelease()`. It shares only the `ReleaseRow` row model with the daemon updater (`ui/windows/release_list_view.h`); each dialog renders its own tactile Material list.
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding). **Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
## Daemon updater (dragonxd) ## Daemon updater (dragonxd)
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). A **"Browse all releases…"** button (shared `release_list_view.h` picker) lets users pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). The dialog is a two-pane version picker (every `/releases` entry on the left) so users can pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. It shares only the `ReleaseRow` row model (`ui/windows/release_list_view.h`) with the miner updater; each renders its own tactile Material list.
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`). **Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).

Binary file not shown.

View File

@@ -62,6 +62,7 @@
"backup_data": "SICHERUNG & DATEN", "backup_data": "SICHERUNG & DATEN",
"backup_description": "Erstellen Sie eine Sicherung Ihrer wallet.dat-Datei. Diese Datei enthält alle Ihre privaten Schlüssel und den Transaktionsverlauf. Bewahren Sie die Sicherung an einem sicheren Ort auf.", "backup_description": "Erstellen Sie eine Sicherung Ihrer wallet.dat-Datei. Diese Datei enthält alle Ihre privaten Schlüssel und den Transaktionsverlauf. Bewahren Sie die Sicherung an einem sicheren Ort auf.",
"backup_destination": "Sicherungsziel:", "backup_destination": "Sicherungsziel:",
"backup_overwrite_confirm": "Dort existiert bereits eine Datei — erneut speichern, um sie zu überschreiben.",
"backup_source": "Quelle: %s", "backup_source": "Quelle: %s",
"backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher", "backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher",
"backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten", "backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten",
@@ -70,6 +71,7 @@
"backup_title": "Wallet sichern", "backup_title": "Wallet sichern",
"backup_wallet": "Wallet sichern...", "backup_wallet": "Wallet sichern...",
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden", "backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
"backup_warn": "Diese Datei enthält alle Ihre privaten Schlüssel — bewahren Sie sie sicher auf.",
"balance": "Guthaben", "balance": "Guthaben",
"balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...", "balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...",
"balance_layout": "Guthaben-Layout", "balance_layout": "Guthaben-Layout",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat ist geschützt)", "bootstrap_wallet_protected": "(wallet.dat ist geschützt)",
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.", "bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"change_pass_confirm": "Neue bestätigen:",
"change_pass_current": "Aktuelle Passphrase:",
"change_pass_new": "Neue Passphrase:",
"change_pass_title": "Passphrase ändern",
"characters": "Zeichen", "characters": "Zeichen",
"chat": "Chat", "chat": "Chat",
"chat_cancel": "Abbrechen", "chat_cancel": "Abbrechen",
@@ -180,10 +186,18 @@
"connected_peers": "Verbundene Peers", "connected_peers": "Verbundene Peers",
"connecting": "Verbinde...", "connecting": "Verbinde...",
"console": "Konsole", "console": "Konsole",
"console_accents": "Farbakzente",
"console_app": "App", "console_app": "App",
"console_auto_scroll": "Automatisch scrollen", "console_auto_scroll": "Automatisch scrollen",
"console_available_commands": "Verfügbare Befehle:", "console_available_commands": "Verfügbare Befehle:",
"console_capturing_output": "Erfasse Daemon-Ausgabe...", "console_capturing_output": "Erfasse Daemon-Ausgabe...",
"console_cat_blockchain": "Blockchain",
"console_cat_control": "Steuerung",
"console_cat_mining": "Mining",
"console_cat_network": "Netzwerk",
"console_cat_raw_transactions": "Rohtransaktionen",
"console_cat_utility": "Dienstprogramme",
"console_cat_wallet": "Wallet",
"console_clear": "Leeren", "console_clear": "Leeren",
"console_clear_console": "Konsole leeren", "console_clear_console": "Konsole leeren",
"console_cleared": "Konsole geleert", "console_cleared": "Konsole geleert",
@@ -221,6 +235,20 @@
"console_no_daemon": "Kein Daemon", "console_no_daemon": "Kein Daemon",
"console_not_connected": "Fehler: Nicht mit Daemon verbunden", "console_not_connected": "Fehler: Nicht mit Daemon verbunden",
"console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.", "console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.",
"console_ref_builds": "Ergibt",
"console_ref_cancel": "Abbrechen",
"console_ref_destructive": "Folgenreich",
"console_ref_example": "Beispiel",
"console_ref_insert": "In Konsole einfügen",
"console_ref_insert_run": "Einfügen & ausführen",
"console_ref_no_match": "Keine Befehle gefunden.",
"console_ref_no_params": "Erwartet keine Parameter.",
"console_ref_optional": "optional",
"console_ref_parameters": "Parameter",
"console_ref_run": "Ausführen",
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
"console_ref_search_hint": "Nach Name oder Aufgabe suchen…",
"console_ref_select_hint": "Wählen Sie einen Befehl, um zu sehen, was er tut.",
"console_rpc_reference": "RPC-Befehlsreferenz", "console_rpc_reference": "RPC-Befehlsreferenz",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Konsolen-Scanline", "console_scanline": "Konsolen-Scanline",
@@ -240,6 +268,9 @@
"console_status_stopping": "Stoppt", "console_status_stopping": "Stoppt",
"console_status_unknown": "Unbekannt", "console_status_unknown": "Unbekannt",
"console_tab_completion": "Tab zur Vervollständigung", "console_tab_completion": "Tab zur Vervollständigung",
"console_text_colors": "Textfarben",
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
"console_toggle_text_color": "Textfarben der Zeilen umschalten",
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle", "console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
"console_welcome": "Willkommen bei ObsidianDragon Konsole", "console_welcome": "Willkommen bei ObsidianDragon Konsole",
"console_zoom_in": "Vergrößern", "console_zoom_in": "Vergrößern",
@@ -302,6 +333,19 @@
"date": "Datum", "date": "Datum",
"date_label": "Datum:", "date_label": "Datum:",
"debug_logging": "FEHLERPROTOKOLLIERUNG", "debug_logging": "FEHLERPROTOKOLLIERUNG",
"decrypt_desc": "Die Wallet wird exportiert, der Daemon mit einer frischen, unverschlüsselten Wallet neu gestartet und alle Schlüssel werden erneut importiert. Je nach Wallet-Größe kann dies einige Minuten dauern.",
"decrypt_error_title": "Entschlüsselung fehlgeschlagen",
"decrypt_step_backup": "Verschlüsselte Wallet wird gesichert",
"decrypt_step_export": "Wallet-Schlüssel werden exportiert",
"decrypt_step_restart": "Daemon wird neu gestartet",
"decrypt_step_stop": "Daemon wird gestoppt",
"decrypt_step_unlock": "Wallet wird entsperrt",
"decrypt_success_desc": "Ihre Wallet ist jetzt unverschlüsselt. Eine Sicherung der verschlüsselten Wallet wurde als wallet.dat.encrypted.bak in Ihrem Datenverzeichnis gespeichert.",
"decrypt_success_title": "Wallet erfolgreich entschlüsselt!",
"decrypt_title": "Wallet-Verschlüsselung entfernen",
"decrypt_wait_general": "Bitte warten. Der Daemon exportiert Schlüssel, startet neu und importiert erneut. Dies kann einige Minuten dauern.",
"decrypt_wait_restart": "Warten, bis der Daemon vollständig gestartet ist...",
"decrypt_warning": "Dadurch wird die Verschlüsselung Ihrer Wallet entfernt. Ihre privaten Schlüssel werden ungeschützt auf der Festplatte gespeichert.",
"delete": "Löschen", "delete": "Löschen",
"delete_blockchain": "Blockchain löschen", "delete_blockchain": "Blockchain löschen",
"delete_blockchain_confirm": "Löschen & Neu synchronisieren", "delete_blockchain_confirm": "Löschen & Neu synchronisieren",
@@ -314,6 +358,15 @@
"download_bootstrap": "Bootstrap herunterladen", "download_bootstrap": "Bootstrap herunterladen",
"dragonx_green": "DragonX (Grün)", "dragonx_green": "DragonX (Grün)",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"enc_confirm": "Bestätigen:",
"enc_desc": "Die Verschlüsselung Ihrer Wallet schützt Ihre privaten Schlüssel mit einer Passphrase. Nach der Verschlüsselung wird der Daemon neu gestartet.",
"enc_encrypting": "Wallet wird verschlüsselt...",
"enc_pin_desc": "Mit einer 4-8-stelligen PIN entsperren Sie Ihre Wallet, ohne jedes Mal die vollständige Passphrase einzugeben.",
"enc_pin_set_ok": "PIN erfolgreich festgelegt",
"enc_pin_skipped": "PIN übersprungen. Sie können später in den Einstellungen eine festlegen.",
"enc_pin_vault_fail": "PIN-Tresor konnte nicht erstellt werden",
"enc_success": "Wallet erfolgreich verschlüsselt!",
"enc_wait": "Bitte warten, schließen Sie die Anwendung nicht.",
"error": "Fehler", "error": "Fehler",
"error_format": "Fehler: %s", "error_format": "Fehler: %s",
"est_time_to_block": "Gesch. Zeit bis Block", "est_time_to_block": "Gesch. Zeit bis Block",
@@ -346,10 +399,18 @@
"export_keys_danger": "ACHTUNG: Dies exportiert ALLE privaten Schlüssel aus Ihrer Wallet! Jeder mit Zugriff auf diese Datei kann Ihre Gelder stehlen. Sicher aufbewahren und nach Gebrauch löschen.", "export_keys_danger": "ACHTUNG: Dies exportiert ALLE privaten Schlüssel aus Ihrer Wallet! Jeder mit Zugriff auf diese Datei kann Ihre Gelder stehlen. Sicher aufbewahren und nach Gebrauch löschen.",
"export_keys_include_t": "T-Adressen einschließen (transparent)", "export_keys_include_t": "T-Adressen einschließen (transparent)",
"export_keys_include_z": "Z-Adressen einschließen (abgeschirmt)", "export_keys_include_z": "Z-Adressen einschließen (abgeschirmt)",
"export_keys_none_addrs": "Keine Adressen zum Exportieren",
"export_keys_none_result": "Keine Schlüssel exportiert (0 von %d) — entsperren Sie die Wallet und versuchen Sie es erneut.",
"export_keys_none_toast": "Es konnten keine Schlüssel exportiert werden — ist die Wallet entsperrt?",
"export_keys_not_connected": "Nicht mit dem Daemon verbunden",
"export_keys_options": "Export-Optionen:", "export_keys_options": "Export-Optionen:",
"export_keys_partial": "%d von %d Schlüsseln exportiert — unvollständig (einige hatten keinen Ausgabeschlüssel oder die Wallet ist gesperrt).",
"export_keys_partial_toast": "Teilexport: %d von %d Schlüsseln",
"export_keys_progress": "Exportiere %d/%d...", "export_keys_progress": "Exportiere %d/%d...",
"export_keys_select_type": "Wählen Sie mindestens einen Adresstyp",
"export_keys_success": "Schlüssel erfolgreich exportiert", "export_keys_success": "Schlüssel erfolgreich exportiert",
"export_keys_title": "Alle privaten Schlüssel exportieren", "export_keys_title": "Alle privaten Schlüssel exportieren",
"export_keys_write_fail": "Schlüsseldatei konnte nicht geschrieben werden.",
"export_private_key": "Privaten Schlüssel exportieren", "export_private_key": "Privaten Schlüssel exportieren",
"export_tx_count": "%zu Transaktionen als CSV exportieren.", "export_tx_count": "%zu Transaktionen als CSV exportieren.",
"export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden", "export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden",
@@ -391,21 +452,42 @@
"history": "Verlauf", "history": "Verlauf",
"immature_type": "Unreif", "immature_type": "Unreif",
"import": "Importieren", "import": "Importieren",
"import_key_address": "Adresse:",
"import_key_btn": "Schlüssel importieren", "import_key_btn": "Schlüssel importieren",
"import_key_done": "Importiert. Wallet wird neu gescannt.",
"import_key_field": "Schlüssel",
"import_key_formats": "Unterstützte Schlüsselformate:", "import_key_formats": "Unterstützte Schlüsselformate:",
"import_key_full_rescan": "(0 = vollständiger Rescan)", "import_key_full_rescan": "(0 = vollständiger Rescan)",
"import_key_import": "Importieren",
"import_key_label": "Privater Schlüssel:", "import_key_label": "Privater Schlüssel:",
"import_key_need_node": "Verbinden Sie einen laufenden Node, um einen Schlüssel zu importieren.",
"import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden", "import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden",
"import_key_progress": "Importiere %d/%d...", "import_key_progress": "Importiere %d/%d...",
"import_key_rescan": "Blockchain nach Import neu scannen", "import_key_rescan": "Blockchain nach Import neu scannen",
"import_key_rescanning": "Import & erneuter Scan — das kann einige Minuten dauern",
"import_key_reveal_tip": "Schlüssel ein-/ausblenden",
"import_key_start_height": "Starthöhe:", "import_key_start_height": "Starthöhe:",
"import_key_success": "Schlüssel erfolgreich importiert", "import_key_success": "Schlüssel erfolgreich importiert",
"import_key_t_format": "T-Adresse WIF private Schlüssel", "import_key_t_format": "T-Adresse WIF private Schlüssel",
"import_key_title": "Privaten Schlüssel importieren", "import_key_title": "Privaten Schlüssel importieren",
"import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.", "import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.",
"import_key_type_tkey": "Transparenter privater Schlüssel",
"import_key_type_unknown": "Unbekanntes Schlüsselformat",
"import_key_type_zspend": "Abgeschirmter Ausgabeschlüssel",
"import_key_type_zview": "Abgeschirmter Anzeigeschlüssel (nur Lesen)",
"import_key_warn": "Importieren Sie nur einen Schlüssel, der Ihnen gehört — er gewährt Zugriff auf dessen Guthaben.",
"import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.", "import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.",
"import_key_wrong_type": "Das sieht nach einem Anzeigeschlüssel aus. Verwenden Sie stattdessen \"Anzeigeschlüssel importieren\".",
"import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)", "import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
"import_private_key": "Privaten Schlüssel importieren...", "import_private_key": "Privaten Schlüssel importieren...",
"import_scan_hint": "0 = von Anfang an neu scannen",
"import_scan_label": "Ab Blockhöhe scannen (optional)",
"import_scan_tip": "aktuelle Höhe",
"import_scan_transparent": "Transparente Schlüssel scannen immer vollständig neu",
"import_viewkey_field": "Anzeigeschlüssel",
"import_viewkey_note": "Nur Lesen: Ein Anzeigeschlüssel zeigt Guthaben und Transaktionen einer Adresse, kann deren Mittel aber nicht ausgeben.",
"import_viewkey_title": "Anzeigeschlüssel importieren",
"import_viewkey_wrong_type": "Das sieht nach einem Ausgabeschlüssel aus. Verwenden Sie stattdessen \"Privaten Schlüssel importieren\".",
"incorrect_passphrase": "Falsches Passwort", "incorrect_passphrase": "Falsches Passwort",
"incorrect_pin": "Falsche PIN", "incorrect_pin": "Falsche PIN",
"insufficient_funds": "Unzureichendes Guthaben für diesen Betrag plus Gebühr.", "insufficient_funds": "Unzureichendes Guthaben für diesen Betrag plus Gebühr.",
@@ -414,6 +496,7 @@
"keep": "Behalten", "keep": "Behalten",
"keep_daemon": "Daemon weiterlaufen lassen", "keep_daemon": "Daemon weiterlaufen lassen",
"key_export_click_retrieve": "Klicken Sie, um den Schlüssel aus Ihrer Wallet abzurufen", "key_export_click_retrieve": "Klicken Sie, um den Schlüssel aus Ihrer Wallet abzurufen",
"key_export_failed": "Schlüssel konnte nicht exportiert werden — entsperren Sie die Wallet (falls verschlüsselt) und versuchen Sie es erneut.",
"key_export_fetching": "Schlüssel wird aus Wallet abgerufen...", "key_export_fetching": "Schlüssel wird aus Wallet abgerufen...",
"key_export_private_key": "Privater Schlüssel:", "key_export_private_key": "Privater Schlüssel:",
"key_export_private_warning": "Halten Sie diesen Schlüssel GEHEIM! Jeder mit diesem Schlüssel kann Ihre Gelder ausgeben. Teilen Sie ihn niemals online oder mit nicht vertrauenswürdigen Parteien.", "key_export_private_warning": "Halten Sie diesen Schlüssel GEHEIM! Jeder mit diesem Schlüssel kann Ihre Gelder ausgeben. Teilen Sie ihn niemals online oder mit nicht vertrauenswürdigen Parteien.",
@@ -739,6 +822,7 @@
"output_filename": "Ausgabedateiname:", "output_filename": "Ausgabedateiname:",
"overview": "Übersicht", "overview": "Übersicht",
"paste": "Einfügen", "paste": "Einfügen",
"paste_clip_empty": "Zwischenablage ist leer",
"paste_from_clipboard": "Aus Zwischenablage einfügen", "paste_from_clipboard": "Aus Zwischenablage einfügen",
"pay_from": "Zahlen von", "pay_from": "Zahlen von",
"payment_request": "ZAHLUNGSANFRAGE", "payment_request": "ZAHLUNGSANFRAGE",
@@ -790,7 +874,14 @@
"peers_upper": "PEERS", "peers_upper": "PEERS",
"peers_version": "Version", "peers_version": "Version",
"pending": "Ausstehend", "pending": "Ausstehend",
"pin_change_desc": "Ändern Sie Ihre Entsperr-PIN. Sie benötigen Ihre aktuelle PIN und eine neue PIN.",
"pin_confirm_new_label": "Neue PIN bestätigen:",
"pin_current_label": "Aktuelle PIN:",
"pin_new_label": "Neue PIN (4-8 Ziffern):",
"pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.", "pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.",
"pin_remove_desc": "Geben Sie Ihre aktuelle PIN ein, um das Entfernen zu bestätigen. Sie benötigen dann Ihre vollständige Passphrase zum Entsperren.",
"pin_setup_desc": "Legen Sie eine 4-8-stellige PIN für die schnelle Wallet-Entsperrung fest. Ihre Wallet-Passphrase wird mit dieser PIN verschlüsselt und lokal gespeichert.",
"pin_wallet_passphrase": "Wallet-Passphrase:",
"ping": "Ping", "ping": "Ping",
"portfolio_add_entry": "Eintrag hinzufügen", "portfolio_add_entry": "Eintrag hinzufügen",
"portfolio_add_to": "Zum Portfolio hinzufügen", "portfolio_add_to": "Zum Portfolio hinzufügen",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "Aus Portfolio entfernen", "portfolio_remove_from": "Aus Portfolio entfernen",
"portfolio_revert": "Zurücksetzen", "portfolio_revert": "Zurücksetzen",
"portfolio_save": "Speichern", "portfolio_save": "Speichern",
"portfolio_save_need_address": "Fügen Sie mindestens eine Adresse hinzu, um zu speichern.",
"portfolio_save_need_name": "Geben Sie einen Namen ein, um diese Gruppe zu speichern.",
"portfolio_save_need_price": "Geben Sie einen manuellen Preis über 0 ein, um zu speichern.",
"portfolio_search": "Adressen durchsuchen…", "portfolio_search": "Adressen durchsuchen…",
"portfolio_search_icons": "Symbole suchen…", "portfolio_search_icons": "Symbole suchen…",
"portfolio_select_all": "Alle", "portfolio_select_all": "Alle",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "Hintergrund-Verlauf", "settings_gradient_bg": "Hintergrund-Verlauf",
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen", "settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
"settings_idle_after": "nach", "settings_idle_after": "nach",
"settings_import_key": "Schlü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_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
"settings_lock_now": "Jetzt sperren", "settings_lock_now": "Jetzt sperren",
"settings_locked": "Gesperrt", "settings_locked": "Gesperrt",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "Transaktion wird übermittelt...", "submitting_transaction": "Transaktion wird übermittelt...",
"success": "Erfolg", "success": "Erfolg",
"summary": "Zusammenfassung", "summary": "Zusammenfassung",
"sweep_button": "Fegen",
"sweep_caveat": "Importiert den Schlüssel, um eine Transaktion zu signieren, die alle Mittel an Ihre Adresse verschiebt. Der Schlüssel bleibt mit leerem Guthaben in Ihrer Wallet.",
"sweep_dest_label": "Gefegte Mittel senden an",
"sweep_dest_new": "Neue abgeschirmte Adresse (empfohlen)",
"sweep_done": "Fertig — Mittel an Ihre Adresse gefegt.",
"sweep_to": "Gefegt an:",
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
"sweep_tx": "Transaktion:",
"syncing": "Synchronisiere...", "syncing": "Synchronisiere...",
"t_address": "T-Adresse", "t_address": "T-Adresse",
"t_addresses": "T-Adressen", "t_addresses": "T-Adressen",
@@ -1186,6 +1289,7 @@
"transfer_to": "Überweisen an:", "transfer_to": "Überweisen an:",
"transparent": "Transparent", "transparent": "Transparent",
"transparent_address": "Transparente Adresse", "transparent_address": "Transparente Adresse",
"try_again": "Erneut versuchen",
"tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer", "tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer",
"tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten", "tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten",
"tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren", "tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "Alle Texte und UI skalieren (1.0x = Standard, bis 1.5x).", "tt_font_scale": "Alle Texte und UI skalieren (1.0x = Standard, bis 1.5x).",
"tt_idle_delay": "Wie lange vor dem Start des Minings gewartet werden soll", "tt_idle_delay": "Wie lange vor dem Start des Minings gewartet werden soll",
"tt_import_key": "Einen privaten Schlüssel (zkey oder tkey) in diese Wallet importieren", "tt_import_key": "Einen privaten Schlüssel (zkey oder tkey) in diese Wallet importieren",
"tt_import_viewkey": "Einen abgeschirmten Anzeigeschlüssel importieren, um eine Adresse anzusehen (nur Lesen)",
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt", "tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
"tt_language": "Schnittstellensprache der Wallet-UI", "tt_language": "Schnittstellensprache der Wallet-UI",
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts", "tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",

View File

@@ -62,6 +62,7 @@
"backup_data": "RESPALDO Y DATOS", "backup_data": "RESPALDO Y DATOS",
"backup_description": "Crea un respaldo de tu archivo wallet.dat. Este archivo contiene todas tus claves privadas e historial de transacciones. Guarda el respaldo en un lugar seguro.", "backup_description": "Crea un respaldo de tu archivo wallet.dat. Este archivo contiene todas tus claves privadas e historial de transacciones. Guarda el respaldo en un lugar seguro.",
"backup_destination": "Destino del respaldo:", "backup_destination": "Destino del respaldo:",
"backup_overwrite_confirm": "Ya existe un archivo ahí: guarda de nuevo para sobrescribirlo.",
"backup_source": "Origen: %s", "backup_source": "Origen: %s",
"backup_tip_external": "Guarda respaldos en unidades externas o almacenamiento en la nube", "backup_tip_external": "Guarda respaldos en unidades externas o almacenamiento en la nube",
"backup_tip_multiple": "Crea múltiples respaldos en diferentes ubicaciones", "backup_tip_multiple": "Crea múltiples respaldos en diferentes ubicaciones",
@@ -70,6 +71,7 @@
"backup_title": "Respaldar Cartera", "backup_title": "Respaldar Cartera",
"backup_wallet": "Respaldar Cartera...", "backup_wallet": "Respaldar Cartera...",
"backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada", "backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
"backup_warn": "Este archivo contiene todas tus claves privadas: guárdalo en un lugar seguro.",
"balance": "Saldo", "balance": "Saldo",
"balance_history_collecting": "Historial de saldo — recopilando datos...", "balance_history_collecting": "Historial de saldo — recopilando datos...",
"balance_layout": "Diseño de Saldo", "balance_layout": "Diseño de Saldo",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat está protegido)", "bootstrap_wallet_protected": "(wallet.dat está protegido)",
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.", "bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
"cancel": "Cancelar", "cancel": "Cancelar",
"change_pass_confirm": "Confirmar nueva:",
"change_pass_current": "Frase de contraseña actual:",
"change_pass_new": "Nueva frase de contraseña:",
"change_pass_title": "Cambiar frase de contraseña",
"characters": "caracteres", "characters": "caracteres",
"chat": "Chat", "chat": "Chat",
"chat_cancel": "Cancelar", "chat_cancel": "Cancelar",
@@ -180,10 +186,18 @@
"connected_peers": "Nodos Conectados", "connected_peers": "Nodos Conectados",
"connecting": "Conectando...", "connecting": "Conectando...",
"console": "Consola", "console": "Consola",
"console_accents": "Acentos de color",
"console_app": "App", "console_app": "App",
"console_auto_scroll": "Auto-desplazamiento", "console_auto_scroll": "Auto-desplazamiento",
"console_available_commands": "Comandos disponibles:", "console_available_commands": "Comandos disponibles:",
"console_capturing_output": "Capturando salida del daemon...", "console_capturing_output": "Capturando salida del daemon...",
"console_cat_blockchain": "Blockchain",
"console_cat_control": "Control",
"console_cat_mining": "Minería",
"console_cat_network": "Red",
"console_cat_raw_transactions": "Transacciones sin procesar",
"console_cat_utility": "Utilidades",
"console_cat_wallet": "Cartera",
"console_clear": "Limpiar", "console_clear": "Limpiar",
"console_clear_console": "Limpiar Consola", "console_clear_console": "Limpiar Consola",
"console_cleared": "Consola limpiada", "console_cleared": "Consola limpiada",
@@ -221,6 +235,20 @@
"console_no_daemon": "Sin daemon", "console_no_daemon": "Sin daemon",
"console_not_connected": "Error: No conectado al daemon", "console_not_connected": "Error: No conectado al daemon",
"console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.", "console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.",
"console_ref_builds": "Genera",
"console_ref_cancel": "Cancelar",
"console_ref_destructive": "Delicado",
"console_ref_example": "Ejemplo",
"console_ref_insert": "Insertar en la consola",
"console_ref_insert_run": "Insertar y ejecutar",
"console_ref_no_match": "Ningún comando coincide.",
"console_ref_no_params": "No requiere parámetros.",
"console_ref_optional": "opcional",
"console_ref_parameters": "Parámetros",
"console_ref_run": "Ejecutar",
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
"console_ref_search_hint": "Buscar por nombre o tarea…",
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
"console_rpc_reference": "Referencia de Comandos RPC", "console_rpc_reference": "Referencia de Comandos RPC",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Líneas de consola", "console_scanline": "Líneas de consola",
@@ -240,6 +268,9 @@
"console_status_stopping": "Deteniendo", "console_status_stopping": "Deteniendo",
"console_status_unknown": "Desconocido", "console_status_unknown": "Desconocido",
"console_tab_completion": "Tab para completar", "console_tab_completion": "Tab para completar",
"console_text_colors": "Colores de texto",
"console_toggle_accents": "Alternar acentos de color de línea",
"console_toggle_text_color": "Alternar colores de texto de línea",
"console_type_help": "Escribe 'help' para ver los comandos disponibles", "console_type_help": "Escribe 'help' para ver los comandos disponibles",
"console_welcome": "Bienvenido a la Consola de ObsidianDragon", "console_welcome": "Bienvenido a la Consola de ObsidianDragon",
"console_zoom_in": "Acercar", "console_zoom_in": "Acercar",
@@ -302,6 +333,19 @@
"date": "Fecha", "date": "Fecha",
"date_label": "Fecha:", "date_label": "Fecha:",
"debug_logging": "REGISTRO DE DEPURACIÓN", "debug_logging": "REGISTRO DE DEPURACIÓN",
"decrypt_desc": "Se exportará el monedero, se reiniciará el daemon con un monedero nuevo sin cifrar y se reimportarán todas las claves. Esto puede tardar varios minutos según el tamaño del monedero.",
"decrypt_error_title": "Error al descifrar",
"decrypt_step_backup": "Respaldando el monedero cifrado",
"decrypt_step_export": "Exportando las claves del monedero",
"decrypt_step_restart": "Reiniciando el daemon",
"decrypt_step_stop": "Deteniendo el daemon",
"decrypt_step_unlock": "Desbloqueando el monedero",
"decrypt_success_desc": "Tu monedero ahora está sin cifrar. Se guardó una copia de seguridad del monedero cifrado como wallet.dat.encrypted.bak en tu directorio de datos.",
"decrypt_success_title": "¡Monedero descifrado correctamente!",
"decrypt_title": "Quitar el cifrado del monedero",
"decrypt_wait_general": "Espera. El daemon está exportando claves, reiniciando y reimportando. Esto puede tardar varios minutos.",
"decrypt_wait_restart": "Esperando a que el daemon termine de iniciarse...",
"decrypt_warning": "Esto quitará el cifrado de tu monedero. Tus claves privadas se almacenarán sin protección en el disco.",
"delete": "Eliminar", "delete": "Eliminar",
"delete_blockchain": "Eliminar Blockchain", "delete_blockchain": "Eliminar Blockchain",
"delete_blockchain_confirm": "Eliminar y Resincronizar", "delete_blockchain_confirm": "Eliminar y Resincronizar",
@@ -314,6 +358,15 @@
"download_bootstrap": "Descargar Bootstrap", "download_bootstrap": "Descargar Bootstrap",
"dragonx_green": "DragonX (Verde)", "dragonx_green": "DragonX (Verde)",
"edit": "Editar", "edit": "Editar",
"enc_confirm": "Confirmar:",
"enc_desc": "Cifrar tu monedero protege tus claves privadas con una frase de contraseña. Tras el cifrado, el daemon se reiniciará.",
"enc_encrypting": "Cifrando el monedero...",
"enc_pin_desc": "Un PIN de 4-8 dígitos te permite desbloquear tu monedero sin escribir la frase de contraseña completa cada vez.",
"enc_pin_set_ok": "PIN configurado correctamente",
"enc_pin_skipped": "PIN omitido. Puedes configurar uno más tarde en Ajustes.",
"enc_pin_vault_fail": "No se pudo crear la bóveda del PIN",
"enc_success": "¡Monedero cifrado correctamente!",
"enc_wait": "Espera, no cierres la aplicación.",
"error": "Error", "error": "Error",
"error_format": "Error: %s", "error_format": "Error: %s",
"est_time_to_block": "Tiempo Est. al Bloque", "est_time_to_block": "Tiempo Est. al Bloque",
@@ -346,10 +399,18 @@
"export_keys_danger": "PELIGRO: ¡Esto exportará TODAS las claves privadas de tu cartera! Cualquiera con acceso a este archivo puede robar tus fondos. Guárdalo de forma segura y elimínalo después de usar.", "export_keys_danger": "PELIGRO: ¡Esto exportará TODAS las claves privadas de tu cartera! Cualquiera con acceso a este archivo puede robar tus fondos. Guárdalo de forma segura y elimínalo después de usar.",
"export_keys_include_t": "Incluir direcciones T (transparentes)", "export_keys_include_t": "Incluir direcciones T (transparentes)",
"export_keys_include_z": "Incluir direcciones Z (protegidas)", "export_keys_include_z": "Incluir direcciones Z (protegidas)",
"export_keys_none_addrs": "No hay direcciones para exportar",
"export_keys_none_result": "No se exportaron claves (0 de %d): desbloquea el monedero e inténtalo de nuevo.",
"export_keys_none_toast": "No se pudieron exportar claves: ¿está desbloqueado el monedero?",
"export_keys_not_connected": "Sin conexión con el daemon",
"export_keys_options": "Opciones de exportación:", "export_keys_options": "Opciones de exportación:",
"export_keys_partial": "Se exportaron %d de %d claves: incompleto (algunas no tenían clave de gasto o el monedero está bloqueado).",
"export_keys_partial_toast": "Exportación parcial: %d de %d claves",
"export_keys_progress": "Exportando %d/%d...", "export_keys_progress": "Exportando %d/%d...",
"export_keys_select_type": "Selecciona al menos un tipo de dirección",
"export_keys_success": "Claves exportadas exitosamente", "export_keys_success": "Claves exportadas exitosamente",
"export_keys_title": "Exportar Todas las Claves Privadas", "export_keys_title": "Exportar Todas las Claves Privadas",
"export_keys_write_fail": "No se pudo escribir el archivo de claves.",
"export_private_key": "Exportar Clave Privada", "export_private_key": "Exportar Clave Privada",
"export_tx_count": "Exportar %zu transacciones a archivo CSV.", "export_tx_count": "Exportar %zu transacciones a archivo CSV.",
"export_tx_file_fail": "Error al crear archivo CSV", "export_tx_file_fail": "Error al crear archivo CSV",
@@ -391,21 +452,42 @@
"history": "Historial", "history": "Historial",
"immature_type": "Inmaduro", "immature_type": "Inmaduro",
"import": "Importar", "import": "Importar",
"import_key_address": "Dirección:",
"import_key_btn": "Importar Clave(s)", "import_key_btn": "Importar Clave(s)",
"import_key_done": "Importada. El monedero está reescaneando.",
"import_key_field": "Clave",
"import_key_formats": "Formatos de clave soportados:", "import_key_formats": "Formatos de clave soportados:",
"import_key_full_rescan": "(0 = re-escaneo completo)", "import_key_full_rescan": "(0 = re-escaneo completo)",
"import_key_import": "Importar",
"import_key_label": "Clave(s) Privada(s):", "import_key_label": "Clave(s) Privada(s):",
"import_key_need_node": "Conecta un nodo en ejecución para importar una clave.",
"import_key_no_valid": "No se encontraron claves válidas en la entrada", "import_key_no_valid": "No se encontraron claves válidas en la entrada",
"import_key_progress": "Importando %d/%d...", "import_key_progress": "Importando %d/%d...",
"import_key_rescan": "Re-escanear blockchain después de importar", "import_key_rescan": "Re-escanear blockchain después de importar",
"import_key_rescanning": "Importando y reescaneando: puede tardar varios minutos",
"import_key_reveal_tip": "Mostrar/ocultar la clave",
"import_key_start_height": "Altura inicial:", "import_key_start_height": "Altura inicial:",
"import_key_success": "Claves importadas exitosamente", "import_key_success": "Claves importadas exitosamente",
"import_key_t_format": "Claves privadas WIF de direcciones T", "import_key_t_format": "Claves privadas WIF de direcciones T",
"import_key_title": "Importar Clave Privada", "import_key_title": "Importar Clave Privada",
"import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.", "import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.",
"import_key_type_tkey": "Clave privada transparente",
"import_key_type_unknown": "Formato de clave no reconocido",
"import_key_type_zspend": "Clave de gasto blindada",
"import_key_type_zview": "Clave de visualización blindada (solo lectura)",
"import_key_warn": "Importa solo una clave que te pertenezca: da acceso a sus fondos.",
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.", "import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
"import_key_wrong_type": "Esto parece una clave de visualización. Usa \"Importar clave de visualización\" en su lugar.",
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)", "import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
"import_private_key": "Importar Clave Privada...", "import_private_key": "Importar Clave Privada...",
"import_scan_hint": "0 = volver a escanear desde el inicio",
"import_scan_label": "Escanear desde la altura de bloque (opcional)",
"import_scan_tip": "altura actual",
"import_scan_transparent": "Las claves transparentes siempre se reescanean por completo",
"import_viewkey_field": "Clave de visualización",
"import_viewkey_note": "Solo lectura: una clave de visualización revela el saldo y las transacciones de una dirección, pero no puede gastar sus fondos.",
"import_viewkey_title": "Importar clave de visualización",
"import_viewkey_wrong_type": "Esto parece una clave de gasto. Usa \"Importar Clave Privada\" en su lugar.",
"incorrect_passphrase": "Contraseña incorrecta", "incorrect_passphrase": "Contraseña incorrecta",
"incorrect_pin": "PIN incorrecto", "incorrect_pin": "PIN incorrecto",
"insufficient_funds": "Fondos insuficientes para este monto más la comisión.", "insufficient_funds": "Fondos insuficientes para este monto más la comisión.",
@@ -414,6 +496,7 @@
"keep": "Mantener", "keep": "Mantener",
"keep_daemon": "Mantener daemon activo", "keep_daemon": "Mantener daemon activo",
"key_export_click_retrieve": "Haga clic para recuperar la clave de su billetera", "key_export_click_retrieve": "Haga clic para recuperar la clave de su billetera",
"key_export_failed": "No se pudo exportar la clave: desbloquea el monedero (si está cifrado) e inténtalo de nuevo.",
"key_export_fetching": "Obteniendo clave de la cartera...", "key_export_fetching": "Obteniendo clave de la cartera...",
"key_export_private_key": "Clave Privada:", "key_export_private_key": "Clave Privada:",
"key_export_private_warning": "¡Mantén esta clave en SECRETO! Cualquiera con esta clave puede gastar tus fondos. Nunca la compartas en línea ni con personas no confiables.", "key_export_private_warning": "¡Mantén esta clave en SECRETO! Cualquiera con esta clave puede gastar tus fondos. Nunca la compartas en línea ni con personas no confiables.",
@@ -739,6 +822,7 @@
"output_filename": "Nombre del archivo:", "output_filename": "Nombre del archivo:",
"overview": "Resumen", "overview": "Resumen",
"paste": "Pegar", "paste": "Pegar",
"paste_clip_empty": "El portapapeles está vacío",
"paste_from_clipboard": "Pegar del Portapapeles", "paste_from_clipboard": "Pegar del Portapapeles",
"pay_from": "Pagar desde", "pay_from": "Pagar desde",
"payment_request": "SOLICITUD DE PAGO", "payment_request": "SOLICITUD DE PAGO",
@@ -790,7 +874,14 @@
"peers_upper": "NODOS", "peers_upper": "NODOS",
"peers_version": "Versión", "peers_version": "Versión",
"pending": "Pendiente", "pending": "Pendiente",
"pin_change_desc": "Cambia tu PIN de desbloqueo. Necesitas tu PIN actual y un nuevo PIN.",
"pin_confirm_new_label": "Confirmar nuevo PIN:",
"pin_current_label": "PIN actual:",
"pin_new_label": "Nuevo PIN (4-8 dígitos):",
"pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.", "pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.",
"pin_remove_desc": "Introduce tu PIN actual para confirmar la eliminación. Necesitarás tu frase de contraseña completa para desbloquear.",
"pin_setup_desc": "Establece un PIN de 4-8 dígitos para desbloquear el monedero rápidamente. Tu frase de contraseña se cifrará con este PIN y se almacenará localmente.",
"pin_wallet_passphrase": "Frase de contraseña del monedero:",
"ping": "Ping", "ping": "Ping",
"portfolio_add_entry": "Añadir entrada", "portfolio_add_entry": "Añadir entrada",
"portfolio_add_to": "Agregar a la cartera de inversiones", "portfolio_add_to": "Agregar a la cartera de inversiones",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "Quitar del portafolio", "portfolio_remove_from": "Quitar del portafolio",
"portfolio_revert": "Revertir", "portfolio_revert": "Revertir",
"portfolio_save": "Guardar", "portfolio_save": "Guardar",
"portfolio_save_need_address": "Añade al menos una dirección para guardar.",
"portfolio_save_need_name": "Introduce un nombre para guardar este grupo.",
"portfolio_save_need_price": "Introduce un precio manual mayor que 0 para guardar.",
"portfolio_search": "Buscar direcciones…", "portfolio_search": "Buscar direcciones…",
"portfolio_search_icons": "Buscar iconos…", "portfolio_search_icons": "Buscar iconos…",
"portfolio_select_all": "Todos", "portfolio_select_all": "Todos",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "Fondo degradado", "settings_gradient_bg": "Fondo degradado",
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves", "settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
"settings_idle_after": "después de", "settings_idle_after": "después de",
"settings_import_key": "Importar clave...", "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_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
"settings_lock_now": "Bloquear ahora", "settings_lock_now": "Bloquear ahora",
"settings_locked": "Bloqueado", "settings_locked": "Bloqueado",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "Enviando transacción...", "submitting_transaction": "Enviando transacción...",
"success": "Éxito", "success": "Éxito",
"summary": "Resumen", "summary": "Resumen",
"sweep_button": "Barrer",
"sweep_caveat": "Importa la clave para firmar una transacción que mueve todos sus fondos a tu dirección. La clave permanece en tu monedero con saldo cero.",
"sweep_dest_label": "Enviar los fondos barridos a",
"sweep_dest_new": "Nueva dirección blindada (recomendado)",
"sweep_done": "Listo — fondos barridos a tu dirección.",
"sweep_to": "Barrido a:",
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
"sweep_tx": "Transacción:",
"syncing": "Sincronizando...", "syncing": "Sincronizando...",
"t_address": "Dirección T", "t_address": "Dirección T",
"t_addresses": "Direcciones T", "t_addresses": "Direcciones T",
@@ -1186,6 +1289,7 @@
"transfer_to": "Transferir a:", "transfer_to": "Transferir a:",
"transparent": "Transparente", "transparent": "Transparente",
"transparent_address": "Dirección Transparente", "transparent_address": "Dirección Transparente",
"try_again": "Reintentar",
"tt_addr_url": "URL base para ver direcciones en un explorador de bloques", "tt_addr_url": "URL base para ver direcciones en un explorador de bloques",
"tt_address_book": "Administrar direcciones guardadas para envío rápido", "tt_address_book": "Administrar direcciones guardadas para envío rápido",
"tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad", "tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "Escalar todo el texto y la interfaz (1.0x = predeterminado, hasta 1.5x).", "tt_font_scale": "Escalar todo el texto y la interfaz (1.0x = predeterminado, hasta 1.5x).",
"tt_idle_delay": "Cuánto tiempo esperar antes de empezar a minar", "tt_idle_delay": "Cuánto tiempo esperar antes de empezar a minar",
"tt_import_key": "Importar una clave privada (zkey o tkey) en esta billetera", "tt_import_key": "Importar una clave privada (zkey o tkey) en esta billetera",
"tt_import_viewkey": "Importar una clave de visualización blindada para observar una dirección (solo lectura)",
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración", "tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
"tt_language": "Idioma de la interfaz de la billetera", "tt_language": "Idioma de la interfaz de la billetera",
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance", "tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",

View File

@@ -62,6 +62,7 @@
"backup_data": "SAUVEGARDE & DONNÉES", "backup_data": "SAUVEGARDE & DONNÉES",
"backup_description": "Créez une sauvegarde de votre fichier wallet.dat. Ce fichier contient toutes vos clés privées et l'historique des transactions. Conservez la sauvegarde dans un endroit sûr.", "backup_description": "Créez une sauvegarde de votre fichier wallet.dat. Ce fichier contient toutes vos clés privées et l'historique des transactions. Conservez la sauvegarde dans un endroit sûr.",
"backup_destination": "Destination de sauvegarde :", "backup_destination": "Destination de sauvegarde :",
"backup_overwrite_confirm": "Un fichier existe déjà là — enregistrez à nouveau pour l'écraser.",
"backup_source": "Source : %s", "backup_source": "Source : %s",
"backup_tip_external": "Stockez les sauvegardes sur des disques externes ou un stockage cloud", "backup_tip_external": "Stockez les sauvegardes sur des disques externes ou un stockage cloud",
"backup_tip_multiple": "Créez plusieurs sauvegardes à différents endroits", "backup_tip_multiple": "Créez plusieurs sauvegardes à différents endroits",
@@ -70,6 +71,7 @@
"backup_title": "Sauvegarder le portefeuille", "backup_title": "Sauvegarder le portefeuille",
"backup_wallet": "Sauvegarder le portefeuille...", "backup_wallet": "Sauvegarder le portefeuille...",
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu", "backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
"backup_warn": "Ce fichier contient toutes vos clés privées — conservez-le en lieu sûr.",
"balance": "Solde", "balance": "Solde",
"balance_history_collecting": "Historique du solde — collecte des données...", "balance_history_collecting": "Historique du solde — collecte des données...",
"balance_layout": "Disposition du solde", "balance_layout": "Disposition du solde",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat est protégé)", "bootstrap_wallet_protected": "(wallet.dat est protégé)",
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.", "bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
"cancel": "Annuler", "cancel": "Annuler",
"change_pass_confirm": "Confirmer la nouvelle :",
"change_pass_current": "Phrase secrète actuelle :",
"change_pass_new": "Nouvelle phrase secrète :",
"change_pass_title": "Changer la phrase secrète",
"characters": "caractères", "characters": "caractères",
"chat": "Discussion", "chat": "Discussion",
"chat_cancel": "Annuler", "chat_cancel": "Annuler",
@@ -180,10 +186,18 @@
"connected_peers": "Pairs connectés", "connected_peers": "Pairs connectés",
"connecting": "Connexion...", "connecting": "Connexion...",
"console": "Console", "console": "Console",
"console_accents": "Accents de couleur",
"console_app": "App", "console_app": "App",
"console_auto_scroll": "Défilement auto", "console_auto_scroll": "Défilement auto",
"console_available_commands": "Commandes disponibles :", "console_available_commands": "Commandes disponibles :",
"console_capturing_output": "Capture de la sortie du daemon...", "console_capturing_output": "Capture de la sortie du daemon...",
"console_cat_blockchain": "Blockchain",
"console_cat_control": "Contrôle",
"console_cat_mining": "Minage",
"console_cat_network": "Réseau",
"console_cat_raw_transactions": "Transactions brutes",
"console_cat_utility": "Utilitaires",
"console_cat_wallet": "Portefeuille",
"console_clear": "Effacer", "console_clear": "Effacer",
"console_clear_console": "Effacer la console", "console_clear_console": "Effacer la console",
"console_cleared": "Console effacée", "console_cleared": "Console effacée",
@@ -221,6 +235,20 @@
"console_no_daemon": "Pas de daemon", "console_no_daemon": "Pas de daemon",
"console_not_connected": "Erreur : Non connecté au daemon", "console_not_connected": "Erreur : Non connecté au daemon",
"console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.", "console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.",
"console_ref_builds": "Génère",
"console_ref_cancel": "Annuler",
"console_ref_destructive": "Sensible",
"console_ref_example": "Exemple",
"console_ref_insert": "Insérer dans la console",
"console_ref_insert_run": "Insérer et exécuter",
"console_ref_no_match": "Aucune commande ne correspond.",
"console_ref_no_params": "Ne prend aucun paramètre.",
"console_ref_optional": "optionnel",
"console_ref_parameters": "Paramètres",
"console_ref_run": "Exécuter",
"console_ref_run_confirm": "Exécuter %s maintenant ? C'est une commande à conséquences.",
"console_ref_search_hint": "Rechercher par nom ou tâche…",
"console_ref_select_hint": "Sélectionnez une commande pour voir ce qu'elle fait.",
"console_rpc_reference": "Référence des commandes RPC", "console_rpc_reference": "Référence des commandes RPC",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Scanline de la console", "console_scanline": "Scanline de la console",
@@ -240,6 +268,9 @@
"console_status_stopping": "Arrêt", "console_status_stopping": "Arrêt",
"console_status_unknown": "Inconnu", "console_status_unknown": "Inconnu",
"console_tab_completion": "Tab pour compléter", "console_tab_completion": "Tab pour compléter",
"console_text_colors": "Couleurs du texte",
"console_toggle_accents": "Basculer les accents de couleur des lignes",
"console_toggle_text_color": "Basculer les couleurs du texte des lignes",
"console_type_help": "Tapez 'help' pour les commandes disponibles", "console_type_help": "Tapez 'help' pour les commandes disponibles",
"console_welcome": "Bienvenue dans la console ObsidianDragon", "console_welcome": "Bienvenue dans la console ObsidianDragon",
"console_zoom_in": "Agrandir", "console_zoom_in": "Agrandir",
@@ -302,6 +333,19 @@
"date": "Date", "date": "Date",
"date_label": "Date :", "date_label": "Date :",
"debug_logging": "JOURNALISATION DE DÉBOGAGE", "debug_logging": "JOURNALISATION DE DÉBOGAGE",
"decrypt_desc": "Le portefeuille sera exporté, le daemon redémarré avec un nouveau portefeuille non chiffré, et toutes les clés réimportées. Cela peut prendre plusieurs minutes selon la taille du portefeuille.",
"decrypt_error_title": "Échec du déchiffrement",
"decrypt_step_backup": "Sauvegarde du portefeuille chiffré",
"decrypt_step_export": "Exportation des clés du portefeuille",
"decrypt_step_restart": "Redémarrage du daemon",
"decrypt_step_stop": "Arrêt du daemon",
"decrypt_step_unlock": "Déverrouillage du portefeuille",
"decrypt_success_desc": "Votre portefeuille est maintenant non chiffré. Une sauvegarde du portefeuille chiffré a été enregistrée sous wallet.dat.encrypted.bak dans votre répertoire de données.",
"decrypt_success_title": "Portefeuille déchiffré avec succès !",
"decrypt_title": "Supprimer le chiffrement du portefeuille",
"decrypt_wait_general": "Veuillez patienter. Le daemon exporte les clés, redémarre et réimporte. Cela peut prendre plusieurs minutes.",
"decrypt_wait_restart": "En attente du démarrage complet du daemon...",
"decrypt_warning": "Cela supprimera le chiffrement de votre portefeuille. Vos clés privées seront stockées sans protection sur le disque.",
"delete": "Supprimer", "delete": "Supprimer",
"delete_blockchain": "Supprimer Blockchain", "delete_blockchain": "Supprimer Blockchain",
"delete_blockchain_confirm": "Supprimer & Resynchroniser", "delete_blockchain_confirm": "Supprimer & Resynchroniser",
@@ -314,6 +358,15 @@
"download_bootstrap": "Télécharger Bootstrap", "download_bootstrap": "Télécharger Bootstrap",
"dragonx_green": "DragonX (Vert)", "dragonx_green": "DragonX (Vert)",
"edit": "Modifier", "edit": "Modifier",
"enc_confirm": "Confirmer :",
"enc_desc": "Chiffrer votre portefeuille protège vos clés privées avec une phrase secrète. Après le chiffrement, le daemon redémarrera.",
"enc_encrypting": "Chiffrement du portefeuille...",
"enc_pin_desc": "Un code PIN de 4 à 8 chiffres vous permet de déverrouiller votre portefeuille sans saisir la phrase secrète complète à chaque fois.",
"enc_pin_set_ok": "Code PIN défini avec succès",
"enc_pin_skipped": "Code PIN ignoré. Vous pourrez en définir un plus tard dans les Paramètres.",
"enc_pin_vault_fail": "Échec de la création du coffre du code PIN",
"enc_success": "Portefeuille chiffré avec succès !",
"enc_wait": "Veuillez patienter, ne fermez pas l'application.",
"error": "Erreur", "error": "Erreur",
"error_format": "Erreur : %s", "error_format": "Erreur : %s",
"est_time_to_block": "Temps est. par bloc", "est_time_to_block": "Temps est. par bloc",
@@ -346,10 +399,18 @@
"export_keys_danger": "DANGER : Ceci exportera TOUTES les clés privées de votre portefeuille ! Toute personne ayant accès à ce fichier peut voler vos fonds. Conservez-le en sécurité et supprimez-le après utilisation.", "export_keys_danger": "DANGER : Ceci exportera TOUTES les clés privées de votre portefeuille ! Toute personne ayant accès à ce fichier peut voler vos fonds. Conservez-le en sécurité et supprimez-le après utilisation.",
"export_keys_include_t": "Inclure les adresses T (transparentes)", "export_keys_include_t": "Inclure les adresses T (transparentes)",
"export_keys_include_z": "Inclure les adresses Z (blindées)", "export_keys_include_z": "Inclure les adresses Z (blindées)",
"export_keys_none_addrs": "Aucune adresse à exporter",
"export_keys_none_result": "Aucune clé exportée (0 sur %d) — déverrouillez le portefeuille et réessayez.",
"export_keys_none_toast": "Aucune clé n'a pu être exportée — le portefeuille est-il déverrouillé ?",
"export_keys_not_connected": "Non connecté au daemon",
"export_keys_options": "Options d'exportation :", "export_keys_options": "Options d'exportation :",
"export_keys_partial": "%d clés sur %d exportées — incomplet (certaines sans clé de dépense, ou le portefeuille est verrouillé).",
"export_keys_partial_toast": "Export partiel : %d clés sur %d",
"export_keys_progress": "Exportation %d/%d...", "export_keys_progress": "Exportation %d/%d...",
"export_keys_select_type": "Sélectionnez au moins un type d'adresse",
"export_keys_success": "Clés exportées avec succès", "export_keys_success": "Clés exportées avec succès",
"export_keys_title": "Exporter toutes les clés privées", "export_keys_title": "Exporter toutes les clés privées",
"export_keys_write_fail": "Échec de l'écriture du fichier de clés.",
"export_private_key": "Exporter la clé privée", "export_private_key": "Exporter la clé privée",
"export_tx_count": "Exporter %zu transactions en fichier CSV.", "export_tx_count": "Exporter %zu transactions en fichier CSV.",
"export_tx_file_fail": "Impossible de créer le fichier CSV", "export_tx_file_fail": "Impossible de créer le fichier CSV",
@@ -391,21 +452,42 @@
"history": "Historique", "history": "Historique",
"immature_type": "Immature", "immature_type": "Immature",
"import": "Importer", "import": "Importer",
"import_key_address": "Adresse :",
"import_key_btn": "Importer clé(s)", "import_key_btn": "Importer clé(s)",
"import_key_done": "Importée. Le portefeuille réanalyse.",
"import_key_field": "Clé",
"import_key_formats": "Formats de clés pris en charge :", "import_key_formats": "Formats de clés pris en charge :",
"import_key_full_rescan": "(0 = rescan complet)", "import_key_full_rescan": "(0 = rescan complet)",
"import_key_import": "Importer",
"import_key_label": "Clé(s) privée(s) :", "import_key_label": "Clé(s) privée(s) :",
"import_key_need_node": "Connectez un nœud en cours d'exécution pour importer une clé.",
"import_key_no_valid": "Aucune clé valide trouvée dans l'entrée", "import_key_no_valid": "Aucune clé valide trouvée dans l'entrée",
"import_key_progress": "Importation %d/%d...", "import_key_progress": "Importation %d/%d...",
"import_key_rescan": "Re-scanner la blockchain après l'importation", "import_key_rescan": "Re-scanner la blockchain après l'importation",
"import_key_rescanning": "Import et réanalyse — cela peut prendre plusieurs minutes",
"import_key_reveal_tip": "Afficher/masquer la clé",
"import_key_start_height": "Hauteur de départ :", "import_key_start_height": "Hauteur de départ :",
"import_key_success": "Clés importées avec succès", "import_key_success": "Clés importées avec succès",
"import_key_t_format": "Clés privées WIF d'adresses T", "import_key_t_format": "Clés privées WIF d'adresses T",
"import_key_title": "Importer une clé privée", "import_key_title": "Importer une clé privée",
"import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.", "import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.",
"import_key_type_tkey": "Clé privée transparente",
"import_key_type_unknown": "Format de clé non reconnu",
"import_key_type_zspend": "Clé de dépense blindée",
"import_key_type_zview": "Clé de visualisation blindée (lecture seule)",
"import_key_warn": "N'importez qu'une clé qui vous appartient — elle donne accès à ses fonds.",
"import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.", "import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.",
"import_key_wrong_type": "Ceci ressemble à une clé de visualisation. Utilisez plutôt « Importer la clé de visualisation ».",
"import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)", "import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
"import_private_key": "Importer une clé privée...", "import_private_key": "Importer une clé privée...",
"import_scan_hint": "0 = réanalyser depuis le début",
"import_scan_label": "Analyser à partir de la hauteur de bloc (facultatif)",
"import_scan_tip": "hauteur actuelle",
"import_scan_transparent": "Les clés transparentes réanalysent toujours entièrement",
"import_viewkey_field": "Clé de visualisation",
"import_viewkey_note": "Lecture seule : une clé de visualisation révèle le solde et les transactions d'une adresse, mais ne peut pas dépenser ses fonds.",
"import_viewkey_title": "Importer la clé de visualisation",
"import_viewkey_wrong_type": "Ceci ressemble à une clé de dépense. Utilisez plutôt « Importer une clé privée ».",
"incorrect_passphrase": "Mot de passe incorrect", "incorrect_passphrase": "Mot de passe incorrect",
"incorrect_pin": "PIN incorrect", "incorrect_pin": "PIN incorrect",
"insufficient_funds": "Fonds insuffisants pour ce montant plus les frais.", "insufficient_funds": "Fonds insuffisants pour ce montant plus les frais.",
@@ -414,6 +496,7 @@
"keep": "Conserver", "keep": "Conserver",
"keep_daemon": "Garder le daemon en marche", "keep_daemon": "Garder le daemon en marche",
"key_export_click_retrieve": "Cliquez pour récupérer la clé de votre portefeuille", "key_export_click_retrieve": "Cliquez pour récupérer la clé de votre portefeuille",
"key_export_failed": "Impossible d'exporter la clé — déverrouillez le portefeuille (s'il est chiffré) et réessayez.",
"key_export_fetching": "Récupération de la clé depuis le portefeuille...", "key_export_fetching": "Récupération de la clé depuis le portefeuille...",
"key_export_private_key": "Clé privée :", "key_export_private_key": "Clé privée :",
"key_export_private_warning": "Gardez cette clé SECRÈTE ! Toute personne possédant cette clé peut dépenser vos fonds. Ne la partagez jamais en ligne ou avec des tiers non fiables.", "key_export_private_warning": "Gardez cette clé SECRÈTE ! Toute personne possédant cette clé peut dépenser vos fonds. Ne la partagez jamais en ligne ou avec des tiers non fiables.",
@@ -739,6 +822,7 @@
"output_filename": "Nom du fichier de sortie :", "output_filename": "Nom du fichier de sortie :",
"overview": "Aperçu", "overview": "Aperçu",
"paste": "Coller", "paste": "Coller",
"paste_clip_empty": "Le presse-papiers est vide",
"paste_from_clipboard": "Coller depuis le presse-papiers", "paste_from_clipboard": "Coller depuis le presse-papiers",
"pay_from": "Payer depuis", "pay_from": "Payer depuis",
"payment_request": "DEMANDE DE PAIEMENT", "payment_request": "DEMANDE DE PAIEMENT",
@@ -790,7 +874,14 @@
"peers_upper": "PAIRS", "peers_upper": "PAIRS",
"peers_version": "Version", "peers_version": "Version",
"pending": "En attente", "pending": "En attente",
"pin_change_desc": "Modifiez votre code PIN de déverrouillage. Vous avez besoin de votre code PIN actuel et d'un nouveau code PIN.",
"pin_confirm_new_label": "Confirmer le nouveau code PIN :",
"pin_current_label": "Code PIN actuel :",
"pin_new_label": "Nouveau code PIN (4 à 8 chiffres) :",
"pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.", "pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.",
"pin_remove_desc": "Saisissez votre code PIN actuel pour confirmer la suppression. Vous devrez utiliser votre phrase secrète complète pour déverrouiller.",
"pin_setup_desc": "Définissez un code PIN de 4 à 8 chiffres pour déverrouiller rapidement votre portefeuille. Votre phrase secrète sera chiffrée avec ce code PIN et stockée localement.",
"pin_wallet_passphrase": "Phrase secrète du portefeuille :",
"ping": "Ping", "ping": "Ping",
"portfolio_add_entry": "Ajouter une entrée", "portfolio_add_entry": "Ajouter une entrée",
"portfolio_add_to": "Ajouter au portefeuille", "portfolio_add_to": "Ajouter au portefeuille",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "Retirer du portefeuille d'actifs", "portfolio_remove_from": "Retirer du portefeuille d'actifs",
"portfolio_revert": "Annuler", "portfolio_revert": "Annuler",
"portfolio_save": "Enregistrer", "portfolio_save": "Enregistrer",
"portfolio_save_need_address": "Ajoutez au moins une adresse pour enregistrer.",
"portfolio_save_need_name": "Saisissez un nom pour enregistrer ce groupe.",
"portfolio_save_need_price": "Saisissez un prix manuel supérieur à 0 pour enregistrer.",
"portfolio_search": "Rechercher des adresses\\xE2\\x80\\xA6", "portfolio_search": "Rechercher des adresses\\xE2\\x80\\xA6",
"portfolio_search_icons": "Rechercher des icônes\\xE2\\x80\\xA6", "portfolio_search_icons": "Rechercher des icônes\\xE2\\x80\\xA6",
"portfolio_select_all": "Tout", "portfolio_select_all": "Tout",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "Fond dégradé", "settings_gradient_bg": "Fond dégradé",
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses", "settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
"settings_idle_after": "après", "settings_idle_after": "après",
"settings_import_key": "Importer la clé...", "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_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
"settings_lock_now": "Verrouiller maintenant", "settings_lock_now": "Verrouiller maintenant",
"settings_locked": "Verrouillé", "settings_locked": "Verrouillé",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "Soumission de la transaction...", "submitting_transaction": "Soumission de la transaction...",
"success": "Succès", "success": "Succès",
"summary": "Résumé", "summary": "Résumé",
"sweep_button": "Balayer",
"sweep_caveat": "Importe la clé pour signer une transaction qui déplace tous ses fonds vers votre adresse. La clé reste dans votre portefeuille avec un solde vide.",
"sweep_dest_label": "Envoyer les fonds balayés vers",
"sweep_dest_new": "Nouvelle adresse blindée (recommandé)",
"sweep_done": "Terminé — fonds balayés vers votre adresse.",
"sweep_to": "Balayé vers :",
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
"sweep_tx": "Transaction :",
"syncing": "Synchronisation...", "syncing": "Synchronisation...",
"t_address": "Adresse T", "t_address": "Adresse T",
"t_addresses": "Adresses T", "t_addresses": "Adresses T",
@@ -1186,6 +1289,7 @@
"transfer_to": "Transférer à :", "transfer_to": "Transférer à :",
"transparent": "Transparent", "transparent": "Transparent",
"transparent_address": "Adresse transparente", "transparent_address": "Adresse transparente",
"try_again": "Réessayer",
"tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs", "tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs",
"tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide", "tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide",
"tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité", "tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "Mettre à l'échelle tout le texte et l'interface (1.0x = par défaut, jusqu'à 1.5x).", "tt_font_scale": "Mettre à l'échelle tout le texte et l'interface (1.0x = par défaut, jusqu'à 1.5x).",
"tt_idle_delay": "Combien de temps attendre avant de commencer le minage", "tt_idle_delay": "Combien de temps attendre avant de commencer le minage",
"tt_import_key": "Importer une clé privée (zkey ou tkey) dans ce portefeuille", "tt_import_key": "Importer une clé privée (zkey ou tkey) dans ce portefeuille",
"tt_import_viewkey": "Importer une clé de visualisation blindée pour observer une adresse (lecture seule)",
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration", "tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
"tt_language": "Langue de l'interface du portefeuille", "tt_language": "Langue de l'interface du portefeuille",
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance", "tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",

View File

@@ -62,6 +62,7 @@
"backup_data": "バックアップとデータ", "backup_data": "バックアップとデータ",
"backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。", "backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。",
"backup_destination": "バックアップ先:", "backup_destination": "バックアップ先:",
"backup_overwrite_confirm": "そこには既にファイルがあります。もう一度保存すると上書きされます。",
"backup_source": "ソース:%s", "backup_source": "ソース:%s",
"backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存", "backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存",
"backup_tip_multiple": "異なる場所に複数のバックアップを作成", "backup_tip_multiple": "異なる場所に複数のバックアップを作成",
@@ -70,6 +71,7 @@
"backup_title": "ウォレットのバックアップ", "backup_title": "ウォレットのバックアップ",
"backup_wallet": "ウォレットをバックアップ...", "backup_wallet": "ウォレットをバックアップ...",
"backup_wallet_not_found": "警告予想される場所にwallet.datが見つかりません", "backup_wallet_not_found": "警告予想される場所にwallet.datが見つかりません",
"backup_warn": "このファイルにはすべての秘密鍵が含まれています。安全な場所に保管してください。",
"balance": "残高", "balance": "残高",
"balance_history_collecting": "残高履歴 — データを収集中...", "balance_history_collecting": "残高履歴 — データを収集中...",
"balance_layout": "残高レイアウト", "balance_layout": "残高レイアウト",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat は保護されています)", "bootstrap_wallet_protected": "(wallet.dat は保護されています)",
"bootstrap_warning": "既存のブロックデータblocks、chainstate、notarizationsは削除され置き換えられます。wallet.dat は変更・削除されません。", "bootstrap_warning": "既存のブロックデータblocks、chainstate、notarizationsは削除され置き換えられます。wallet.dat は変更・削除されません。",
"cancel": "キャンセル", "cancel": "キャンセル",
"change_pass_confirm": "新しいパスフレーズ(確認):",
"change_pass_current": "現在のパスフレーズ:",
"change_pass_new": "新しいパスフレーズ:",
"change_pass_title": "パスフレーズを変更",
"characters": "文字", "characters": "文字",
"chat": "チャット", "chat": "チャット",
"chat_cancel": "キャンセル", "chat_cancel": "キャンセル",
@@ -180,10 +186,18 @@
"connected_peers": "接続中のピア", "connected_peers": "接続中のピア",
"connecting": "接続中...", "connecting": "接続中...",
"console": "コンソール", "console": "コンソール",
"console_accents": "カラーアクセント",
"console_app": "アプリ", "console_app": "アプリ",
"console_auto_scroll": "自動スクロール", "console_auto_scroll": "自動スクロール",
"console_available_commands": "利用可能なコマンド:", "console_available_commands": "利用可能なコマンド:",
"console_capturing_output": "デーモン出力をキャプチャ中...", "console_capturing_output": "デーモン出力をキャプチャ中...",
"console_cat_blockchain": "ブロックチェーン",
"console_cat_control": "制御",
"console_cat_mining": "マイニング",
"console_cat_network": "ネットワーク",
"console_cat_raw_transactions": "生トランザクション",
"console_cat_utility": "ユーティリティ",
"console_cat_wallet": "ウォレット",
"console_clear": "クリア", "console_clear": "クリア",
"console_clear_console": "コンソールをクリア", "console_clear_console": "コンソールをクリア",
"console_cleared": "コンソールをクリアしました", "console_cleared": "コンソールをクリアしました",
@@ -221,6 +235,20 @@
"console_no_daemon": "デーモンなし", "console_no_daemon": "デーモンなし",
"console_not_connected": "エラー:デーモンに接続されていません", "console_not_connected": "エラー:デーモンに接続されていません",
"console_quit_note": "ここでは 'quit''exit' は不要です — ウィンドウを閉じるだけで構いません。", "console_quit_note": "ここでは 'quit''exit' は不要です — ウィンドウを閉じるだけで構いません。",
"console_ref_builds": "生成",
"console_ref_cancel": "キャンセル",
"console_ref_destructive": "要注意",
"console_ref_example": "例",
"console_ref_insert": "コンソールに挿入",
"console_ref_insert_run": "挿入して実行",
"console_ref_no_match": "一致するコマンドはありません。",
"console_ref_no_params": "パラメーターは不要です。",
"console_ref_optional": "任意",
"console_ref_parameters": "パラメーター",
"console_ref_run": "実行",
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
"console_ref_search_hint": "名前または用途で検索…",
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
"console_rpc_reference": "RPCコマンドリファレンス", "console_rpc_reference": "RPCコマンドリファレンス",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "コンソールスキャンライン", "console_scanline": "コンソールスキャンライン",
@@ -240,6 +268,9 @@
"console_status_stopping": "停止中", "console_status_stopping": "停止中",
"console_status_unknown": "不明", "console_status_unknown": "不明",
"console_tab_completion": "Tabで補完", "console_tab_completion": "Tabで補完",
"console_text_colors": "テキスト色",
"console_toggle_accents": "行のカラーアクセントを切り替え",
"console_toggle_text_color": "行のテキスト色を切り替え",
"console_type_help": "'help'と入力して利用可能なコマンドを表示", "console_type_help": "'help'と入力して利用可能なコマンドを表示",
"console_welcome": "ObsidianDragonコンソールへようこそ", "console_welcome": "ObsidianDragonコンソールへようこそ",
"console_zoom_in": "拡大", "console_zoom_in": "拡大",
@@ -302,6 +333,19 @@
"date": "日付", "date": "日付",
"date_label": "日付:", "date_label": "日付:",
"debug_logging": "デバッグログ", "debug_logging": "デバッグログ",
"decrypt_desc": "ウォレットがエクスポートされ、デーモンが新しい暗号化されていないウォレットで再起動し、すべての鍵が再インポートされます。ウォレットのサイズによっては数分かかる場合があります。",
"decrypt_error_title": "復号に失敗しました",
"decrypt_step_backup": "暗号化されたウォレットをバックアップしています",
"decrypt_step_export": "ウォレットの鍵をエクスポートしています",
"decrypt_step_restart": "デーモンを再起動しています",
"decrypt_step_stop": "デーモンを停止しています",
"decrypt_step_unlock": "ウォレットのロックを解除しています",
"decrypt_success_desc": "ウォレットは暗号化されていない状態になりました。暗号化されたウォレットのバックアップが、データディレクトリに wallet.dat.encrypted.bak として保存されました。",
"decrypt_success_title": "ウォレットの復号に成功しました!",
"decrypt_title": "ウォレットの暗号化を解除",
"decrypt_wait_general": "お待ちください。デーモンが鍵をエクスポートし、再起動して再インポートしています。数分かかる場合があります。",
"decrypt_wait_restart": "デーモンの起動が完了するのを待っています...",
"decrypt_warning": "ウォレットの暗号化が解除されます。秘密鍵は保護されずにディスクに保存されます。",
"delete": "削除", "delete": "削除",
"delete_blockchain": "ブロックチェーンを削除", "delete_blockchain": "ブロックチェーンを削除",
"delete_blockchain_confirm": "削除して再同期", "delete_blockchain_confirm": "削除して再同期",
@@ -314,6 +358,15 @@
"download_bootstrap": "ブートストラップをダウンロード", "download_bootstrap": "ブートストラップをダウンロード",
"dragonx_green": "DragonXグリーン", "dragonx_green": "DragonXグリーン",
"edit": "編集", "edit": "編集",
"enc_confirm": "確認:",
"enc_desc": "ウォレットを暗号化すると、パスフレーズで秘密鍵が保護されます。暗号化後、デーモンが再起動します。",
"enc_encrypting": "ウォレットを暗号化しています...",
"enc_pin_desc": "4〜8桁のPINを使うと、毎回パスフレーズ全体を入力せずにウォレットのロックを解除できます。",
"enc_pin_set_ok": "PINを設定しました",
"enc_pin_skipped": "PINをスキップしました。後で設定で設定できます。",
"enc_pin_vault_fail": "PINボルトの作成に失敗しました",
"enc_success": "ウォレットの暗号化に成功しました!",
"enc_wait": "お待ちください。アプリケーションを閉じないでください。",
"error": "エラー", "error": "エラー",
"error_format": "エラー:%s", "error_format": "エラー:%s",
"est_time_to_block": "予測ブロック時間", "est_time_to_block": "予測ブロック時間",
@@ -346,10 +399,18 @@
"export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。", "export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。",
"export_keys_include_t": "Tアドレスを含める透明", "export_keys_include_t": "Tアドレスを含める透明",
"export_keys_include_z": "Zアドレスを含めるシールド", "export_keys_include_z": "Zアドレスを含めるシールド",
"export_keys_none_addrs": "エクスポートするアドレスがありません",
"export_keys_none_result": "鍵をエクスポートできませんでした(%d 件中 0 件)。ウォレットのロックを解除して再試行してください。",
"export_keys_none_toast": "鍵をエクスポートできませんでした。ウォレットはロック解除されていますか?",
"export_keys_not_connected": "デーモンに接続されていません",
"export_keys_options": "エクスポートオプション:", "export_keys_options": "エクスポートオプション:",
"export_keys_partial": "%d 件中 %d 件の鍵をエクスポート — 不完全(支払い鍵がない、またはウォレットがロックされています)。",
"export_keys_partial_toast": "部分エクスポート: %d 件中 %d 件",
"export_keys_progress": "エクスポート中 %d/%d...", "export_keys_progress": "エクスポート中 %d/%d...",
"export_keys_select_type": "アドレスの種類を少なくとも1つ選択してください",
"export_keys_success": "鍵のエクスポートに成功しました", "export_keys_success": "鍵のエクスポートに成功しました",
"export_keys_title": "すべての秘密鍵をエクスポート", "export_keys_title": "すべての秘密鍵をエクスポート",
"export_keys_write_fail": "鍵ファイルの書き込みに失敗しました。",
"export_private_key": "秘密鍵をエクスポート", "export_private_key": "秘密鍵をエクスポート",
"export_tx_count": "%zu件の取引をCSVファイルにエクスポート。", "export_tx_count": "%zu件の取引をCSVファイルにエクスポート。",
"export_tx_file_fail": "CSVファイルの作成に失敗しました", "export_tx_file_fail": "CSVファイルの作成に失敗しました",
@@ -391,21 +452,42 @@
"history": "履歴", "history": "履歴",
"immature_type": "未成熟", "immature_type": "未成熟",
"import": "インポート", "import": "インポート",
"import_key_address": "アドレス:",
"import_key_btn": "鍵をインポート", "import_key_btn": "鍵をインポート",
"import_key_done": "インポートしました。ウォレットを再スキャンしています。",
"import_key_field": "鍵",
"import_key_formats": "サポートされる鍵形式:", "import_key_formats": "サポートされる鍵形式:",
"import_key_full_rescan": "0 = 完全再スキャン)", "import_key_full_rescan": "0 = 完全再スキャン)",
"import_key_import": "インポート",
"import_key_label": "秘密鍵:", "import_key_label": "秘密鍵:",
"import_key_need_node": "鍵をインポートするには稼働中のノードに接続してください。",
"import_key_no_valid": "入力に有効な鍵が見つかりません", "import_key_no_valid": "入力に有効な鍵が見つかりません",
"import_key_progress": "インポート中 %d/%d...", "import_key_progress": "インポート中 %d/%d...",
"import_key_rescan": "インポート後にブロックチェーンを再スキャン", "import_key_rescan": "インポート後にブロックチェーンを再スキャン",
"import_key_rescanning": "インポートと再スキャン中 — 数分かかることがあります",
"import_key_reveal_tip": "鍵の表示/非表示",
"import_key_start_height": "開始高:", "import_key_start_height": "開始高:",
"import_key_success": "鍵のインポートに成功しました", "import_key_success": "鍵のインポートに成功しました",
"import_key_t_format": "TアドレスWIF秘密鍵", "import_key_t_format": "TアドレスWIF秘密鍵",
"import_key_title": "秘密鍵をインポート", "import_key_title": "秘密鍵をインポート",
"import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。", "import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。",
"import_key_type_tkey": "透明な秘密鍵",
"import_key_type_unknown": "認識できない鍵の形式",
"import_key_type_zspend": "シールド支払鍵",
"import_key_type_zview": "シールド閲覧鍵(監視のみ)",
"import_key_warn": "自分が所有する鍵のみをインポートしてください — その資金へのアクセスを許可します。",
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。", "import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
"import_key_wrong_type": "これは閲覧鍵のようです。代わりに「閲覧鍵をインポート」を使用してください。",
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)", "import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
"import_private_key": "秘密鍵をインポート...", "import_private_key": "秘密鍵をインポート...",
"import_scan_hint": "0 = 最初から再スキャン",
"import_scan_label": "スキャン開始ブロック高(任意)",
"import_scan_tip": "現在の高さ",
"import_scan_transparent": "透明鍵は常に全体を再スキャンします",
"import_viewkey_field": "閲覧鍵",
"import_viewkey_note": "監視のみ:閲覧鍵はアドレスの残高と取引を表示できますが、資金を送金することはできません。",
"import_viewkey_title": "閲覧鍵をインポート",
"import_viewkey_wrong_type": "これは支払い鍵のようです。代わりに「秘密鍵をインポート」を使用してください。",
"incorrect_passphrase": "パスフレーズが正しくありません", "incorrect_passphrase": "パスフレーズが正しくありません",
"incorrect_pin": "PINが正しくありません", "incorrect_pin": "PINが正しくありません",
"insufficient_funds": "この金額と手数料に対して残高が不足しています。", "insufficient_funds": "この金額と手数料に対して残高が不足しています。",
@@ -414,6 +496,7 @@
"keep": "保持", "keep": "保持",
"keep_daemon": "デーモンを実行し続ける", "keep_daemon": "デーモンを実行し続ける",
"key_export_click_retrieve": "クリックしてウォレットからキーを取得", "key_export_click_retrieve": "クリックしてウォレットからキーを取得",
"key_export_failed": "鍵をエクスポートできませんでした。ウォレットのロックを解除して(暗号化されている場合)再試行してください。",
"key_export_fetching": "ウォレットから鍵を取得中...", "key_export_fetching": "ウォレットから鍵を取得中...",
"key_export_private_key": "秘密鍵:", "key_export_private_key": "秘密鍵:",
"key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。", "key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。",
@@ -739,6 +822,7 @@
"output_filename": "出力ファイル名:", "output_filename": "出力ファイル名:",
"overview": "概要", "overview": "概要",
"paste": "貼り付け", "paste": "貼り付け",
"paste_clip_empty": "クリップボードは空です",
"paste_from_clipboard": "クリップボードから貼り付け", "paste_from_clipboard": "クリップボードから貼り付け",
"pay_from": "支払い元", "pay_from": "支払い元",
"payment_request": "支払い請求", "payment_request": "支払い請求",
@@ -790,7 +874,14 @@
"peers_upper": "ピア", "peers_upper": "ピア",
"peers_version": "バージョン", "peers_version": "バージョン",
"pending": "保留中", "pending": "保留中",
"pin_change_desc": "ロック解除PINを変更します。現在のPINと新しいPINが必要です。",
"pin_confirm_new_label": "新しいPINの確認:",
"pin_current_label": "現在のPIN:",
"pin_new_label": "新しいPIN4〜8桁:",
"pin_not_set": "PINが設定されていません。パスフレーズで解除してください。", "pin_not_set": "PINが設定されていません。パスフレーズで解除してください。",
"pin_remove_desc": "削除を確認するには現在のPINを入力してください。ロック解除には完全なパスフレーズが必要になります。",
"pin_setup_desc": "ウォレットをすばやくロック解除するための4〜8桁のPINを設定します。ウォレットのパスフレーズはこのPINで暗号化され、ローカルに保存されます。",
"pin_wallet_passphrase": "ウォレットのパスフレーズ:",
"ping": "Ping", "ping": "Ping",
"portfolio_add_entry": "エントリを追加", "portfolio_add_entry": "エントリを追加",
"portfolio_add_to": "ポートフォリオに追加", "portfolio_add_to": "ポートフォリオに追加",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "ポートフォリオから削除", "portfolio_remove_from": "ポートフォリオから削除",
"portfolio_revert": "元に戻す", "portfolio_revert": "元に戻す",
"portfolio_save": "保存", "portfolio_save": "保存",
"portfolio_save_need_address": "保存するにはアドレスを1つ以上追加してください。",
"portfolio_save_need_name": "このグループを保存するには名前を入力してください。",
"portfolio_save_need_price": "保存するには0より大きい手動価格を入力してください。",
"portfolio_search": "アドレスを検索…", "portfolio_search": "アドレスを検索…",
"portfolio_search_icons": "アイコンを検索…", "portfolio_search_icons": "アイコンを検索…",
"portfolio_select_all": "すべて", "portfolio_select_all": "すべて",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "グラデーション背景", "settings_gradient_bg": "グラデーション背景",
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換", "settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
"settings_idle_after": "経過後", "settings_idle_after": "経過後",
"settings_import_key": "鍵をインポート...", "settings_import_key": "秘密鍵をインポート...",
"settings_import_viewkey": "閲覧鍵をインポート...",
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です", "settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
"settings_lock_now": "今すぐロック", "settings_lock_now": "今すぐロック",
"settings_locked": "ロック済み", "settings_locked": "ロック済み",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "取引を送信中...", "submitting_transaction": "取引を送信中...",
"success": "成功", "success": "成功",
"summary": "概要", "summary": "概要",
"sweep_button": "集約",
"sweep_caveat": "鍵をインポートして、すべての資金をあなたのアドレスへ移す取引に署名します。鍵は残高ゼロでウォレットに残ります。",
"sweep_dest_label": "集約先",
"sweep_dest_new": "新しいシールドアドレス(推奨)",
"sweep_done": "完了 — 資金をあなたのアドレスに集約しました。",
"sweep_to": "集約先:",
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
"sweep_tx": "取引:",
"syncing": "同期中...", "syncing": "同期中...",
"t_address": "Tアドレス", "t_address": "Tアドレス",
"t_addresses": "Tアドレス", "t_addresses": "Tアドレス",
@@ -1186,6 +1289,7 @@
"transfer_to": "送金先:", "transfer_to": "送金先:",
"transparent": "透明", "transparent": "透明",
"transparent_address": "トランスペアレントアドレス", "transparent_address": "トランスペアレントアドレス",
"try_again": "再試行",
"tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL", "tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL",
"tt_address_book": "クイック送信用の保存済みアドレスを管理", "tt_address_book": "クイック送信用の保存済みアドレスを管理",
"tt_auto_lock": "この無操作時間後にウォレットをロック", "tt_auto_lock": "この無操作時間後にウォレットをロック",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "すべてのテキストと UI をスケーリング1.0x = デフォルト、最大 1.5x)。", "tt_font_scale": "すべてのテキストと UI をスケーリング1.0x = デフォルト、最大 1.5x)。",
"tt_idle_delay": "マイニング開始前の待機時間", "tt_idle_delay": "マイニング開始前の待機時間",
"tt_import_key": "このウォレットに秘密鍵zkey または tkeyをインポート", "tt_import_key": "このウォレットに秘密鍵zkey または tkeyをインポート",
"tt_import_viewkey": "シールド閲覧鍵をインポートしてアドレスを閲覧(読み取り専用)",
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します", "tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
"tt_language": "ウォレット UI のインターフェース言語", "tt_language": "ウォレット UI のインターフェース言語",
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え", "tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",

View File

@@ -62,6 +62,7 @@
"backup_data": "백업 및 데이터", "backup_data": "백업 및 데이터",
"backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.", "backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.",
"backup_destination": "백업 위치:", "backup_destination": "백업 위치:",
"backup_overwrite_confirm": "그곳에 이미 파일이 있습니다. 다시 저장하면 덮어씁니다.",
"backup_source": "소스: %s", "backup_source": "소스: %s",
"backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장", "backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장",
"backup_tip_multiple": "서로 다른 위치에 여러 백업 생성", "backup_tip_multiple": "서로 다른 위치에 여러 백업 생성",
@@ -70,6 +71,7 @@
"backup_title": "지갑 백업", "backup_title": "지갑 백업",
"backup_wallet": "지갑 백업...", "backup_wallet": "지갑 백업...",
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다", "backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
"backup_warn": "이 파일에는 모든 개인 키가 들어 있습니다. 안전한 곳에 보관하세요.",
"balance": "잔액", "balance": "잔액",
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...", "balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
"balance_layout": "잔액 레이아웃", "balance_layout": "잔액 레이아웃",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat 보호됨)", "bootstrap_wallet_protected": "(wallet.dat 보호됨)",
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.", "bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
"cancel": "취소", "cancel": "취소",
"change_pass_confirm": "새 암호 확인:",
"change_pass_current": "현재 암호:",
"change_pass_new": "새 암호:",
"change_pass_title": "암호 변경",
"characters": "문자", "characters": "문자",
"chat": "채팅", "chat": "채팅",
"chat_cancel": "취소", "chat_cancel": "취소",
@@ -180,10 +186,18 @@
"connected_peers": "연결된 피어", "connected_peers": "연결된 피어",
"connecting": "연결 중...", "connecting": "연결 중...",
"console": "콘솔", "console": "콘솔",
"console_accents": "색상 강조",
"console_app": "앱", "console_app": "앱",
"console_auto_scroll": "자동 스크롤", "console_auto_scroll": "자동 스크롤",
"console_available_commands": "사용 가능한 명령어:", "console_available_commands": "사용 가능한 명령어:",
"console_capturing_output": "데몬 출력 캡처 중...", "console_capturing_output": "데몬 출력 캡처 중...",
"console_cat_blockchain": "블록체인",
"console_cat_control": "제어",
"console_cat_mining": "채굴",
"console_cat_network": "네트워크",
"console_cat_raw_transactions": "원시 트랜잭션",
"console_cat_utility": "유틸리티",
"console_cat_wallet": "지갑",
"console_clear": "지우기", "console_clear": "지우기",
"console_clear_console": "콘솔 지우기", "console_clear_console": "콘솔 지우기",
"console_cleared": "콘솔이 지워졌습니다", "console_cleared": "콘솔이 지워졌습니다",
@@ -221,6 +235,20 @@
"console_no_daemon": "데몬 없음", "console_no_daemon": "데몬 없음",
"console_not_connected": "오류: 데몬에 연결되지 않았습니다", "console_not_connected": "오류: 데몬에 연결되지 않았습니다",
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.", "console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
"console_ref_builds": "생성",
"console_ref_cancel": "취소",
"console_ref_destructive": "주의",
"console_ref_example": "예시",
"console_ref_insert": "콘솔에 삽입",
"console_ref_insert_run": "삽입 후 실행",
"console_ref_no_match": "일치하는 명령이 없습니다.",
"console_ref_no_params": "매개변수가 없습니다.",
"console_ref_optional": "선택",
"console_ref_parameters": "매개변수",
"console_ref_run": "실행",
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
"console_ref_search_hint": "이름 또는 용도로 검색…",
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
"console_rpc_reference": "RPC 명령어 참조", "console_rpc_reference": "RPC 명령어 참조",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "콘솔 스캔라인", "console_scanline": "콘솔 스캔라인",
@@ -240,6 +268,9 @@
"console_status_stopping": "중지 중", "console_status_stopping": "중지 중",
"console_status_unknown": "알 수 없음", "console_status_unknown": "알 수 없음",
"console_tab_completion": "Tab으로 자동 완성", "console_tab_completion": "Tab으로 자동 완성",
"console_text_colors": "텍스트 색상",
"console_toggle_accents": "줄 색상 강조 전환",
"console_toggle_text_color": "줄 텍스트 색상 전환",
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기", "console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다", "console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
"console_zoom_in": "확대", "console_zoom_in": "확대",
@@ -302,6 +333,19 @@
"date": "날짜", "date": "날짜",
"date_label": "날짜:", "date_label": "날짜:",
"debug_logging": "디버그 로깅", "debug_logging": "디버그 로깅",
"decrypt_desc": "지갑을 내보내고 데몬을 새 암호화되지 않은 지갑으로 다시 시작한 후 모든 키를 다시 가져옵니다. 지갑 크기에 따라 몇 분 정도 걸릴 수 있습니다.",
"decrypt_error_title": "복호화 실패",
"decrypt_step_backup": "암호화된 지갑 백업 중",
"decrypt_step_export": "지갑 키 내보내는 중",
"decrypt_step_restart": "데몬 다시 시작 중",
"decrypt_step_stop": "데몬 중지 중",
"decrypt_step_unlock": "지갑 잠금 해제 중",
"decrypt_success_desc": "이제 지갑이 암호화되지 않았습니다. 암호화된 지갑의 백업이 데이터 디렉터리에 wallet.dat.encrypted.bak로 저장되었습니다.",
"decrypt_success_title": "지갑 복호화에 성공했습니다!",
"decrypt_title": "지갑 암호화 제거",
"decrypt_wait_general": "잠시 기다려 주세요. 데몬이 키를 내보내고 다시 시작한 후 다시 가져오고 있습니다. 몇 분 정도 걸릴 수 있습니다.",
"decrypt_wait_restart": "데몬이 완전히 시작될 때까지 기다리는 중...",
"decrypt_warning": "지갑의 암호화가 제거됩니다. 개인 키가 보호되지 않은 상태로 디스크에 저장됩니다.",
"delete": "삭제", "delete": "삭제",
"delete_blockchain": "블록체인 삭제", "delete_blockchain": "블록체인 삭제",
"delete_blockchain_confirm": "삭제 후 재동기화", "delete_blockchain_confirm": "삭제 후 재동기화",
@@ -314,6 +358,15 @@
"download_bootstrap": "부트스트랩 다운로드", "download_bootstrap": "부트스트랩 다운로드",
"dragonx_green": "DragonX(그린)", "dragonx_green": "DragonX(그린)",
"edit": "편집", "edit": "편집",
"enc_confirm": "확인:",
"enc_desc": "지갑을 암호화하면 암호로 개인 키를 보호합니다. 암호화 후 데몬이 다시 시작됩니다.",
"enc_encrypting": "지갑을 암호화하는 중...",
"enc_pin_desc": "4~8자리 PIN으로 매번 전체 암호를 입력하지 않고도 지갑 잠금을 해제할 수 있습니다.",
"enc_pin_set_ok": "PIN이 설정되었습니다",
"enc_pin_skipped": "PIN을 건너뛰었습니다. 나중에 설정에서 만들 수 있습니다.",
"enc_pin_vault_fail": "PIN 보관소를 만들지 못했습니다",
"enc_success": "지갑이 성공적으로 암호화되었습니다!",
"enc_wait": "잠시 기다려 주세요. 애플리케이션을 닫지 마세요.",
"error": "오류", "error": "오류",
"error_format": "오류: %s", "error_format": "오류: %s",
"est_time_to_block": "예상 블록 시간", "est_time_to_block": "예상 블록 시간",
@@ -346,10 +399,18 @@
"export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.", "export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.",
"export_keys_include_t": "T 주소 포함 (투명)", "export_keys_include_t": "T 주소 포함 (투명)",
"export_keys_include_z": "Z 주소 포함 (차폐)", "export_keys_include_z": "Z 주소 포함 (차폐)",
"export_keys_none_addrs": "내보낼 주소가 없습니다",
"export_keys_none_result": "키를 내보내지 못했습니다(%d개 중 0개). 지갑 잠금을 해제하고 다시 시도하세요.",
"export_keys_none_toast": "키를 내보낼 수 없습니다. 지갑이 잠금 해제되어 있나요?",
"export_keys_not_connected": "데몬에 연결되어 있지 않습니다",
"export_keys_options": "내보내기 옵션:", "export_keys_options": "내보내기 옵션:",
"export_keys_partial": "%d개 중 %d개 키 내보냄 — 불완전(일부는 지출 키가 없거나 지갑이 잠겨 있음).",
"export_keys_partial_toast": "부분 내보내기: %d개 중 %d개 키",
"export_keys_progress": "내보내는 중 %d/%d...", "export_keys_progress": "내보내는 중 %d/%d...",
"export_keys_select_type": "주소 유형을 하나 이상 선택하세요",
"export_keys_success": "키 내보내기 성공", "export_keys_success": "키 내보내기 성공",
"export_keys_title": "모든 개인 키 내보내기", "export_keys_title": "모든 개인 키 내보내기",
"export_keys_write_fail": "키 파일을 쓰지 못했습니다.",
"export_private_key": "개인 키 내보내기", "export_private_key": "개인 키 내보내기",
"export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.", "export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.",
"export_tx_file_fail": "CSV 파일 생성 실패", "export_tx_file_fail": "CSV 파일 생성 실패",
@@ -391,21 +452,42 @@
"history": "내역", "history": "내역",
"immature_type": "미성숙", "immature_type": "미성숙",
"import": "가져오기", "import": "가져오기",
"import_key_address": "주소:",
"import_key_btn": "키 가져오기", "import_key_btn": "키 가져오기",
"import_key_done": "가져왔습니다. 지갑을 재검색 중입니다.",
"import_key_field": "키",
"import_key_formats": "지원되는 키 형식:", "import_key_formats": "지원되는 키 형식:",
"import_key_full_rescan": "(0 = 전체 재스캔)", "import_key_full_rescan": "(0 = 전체 재스캔)",
"import_key_import": "가져오기",
"import_key_label": "개인 키:", "import_key_label": "개인 키:",
"import_key_need_node": "키를 가져오려면 실행 중인 노드에 연결하세요.",
"import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다", "import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다",
"import_key_progress": "가져오는 중 %d/%d...", "import_key_progress": "가져오는 중 %d/%d...",
"import_key_rescan": "가져오기 후 블록체인 재스캔", "import_key_rescan": "가져오기 후 블록체인 재스캔",
"import_key_rescanning": "가져오기 및 재검색 중 — 몇 분 걸릴 수 있습니다",
"import_key_reveal_tip": "키 표시/숨기기",
"import_key_start_height": "시작 높이:", "import_key_start_height": "시작 높이:",
"import_key_success": "키 가져오기 성공", "import_key_success": "키 가져오기 성공",
"import_key_t_format": "T 주소 WIF 개인 키", "import_key_t_format": "T 주소 WIF 개인 키",
"import_key_title": "개인 키 가져오기", "import_key_title": "개인 키 가져오기",
"import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.", "import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.",
"import_key_type_tkey": "투명 개인 키",
"import_key_type_unknown": "알 수 없는 키 형식",
"import_key_type_zspend": "차폐 지출 키",
"import_key_type_zview": "차폐 조회 키 (읽기 전용)",
"import_key_warn": "본인이 소유한 키만 가져오세요 — 해당 자금에 접근할 수 있습니다.",
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.", "import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
"import_key_wrong_type": "조회 키인 것 같습니다. 대신 \"조회 키 가져오기\"를 사용하세요.",
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)", "import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
"import_private_key": "개인 키 가져오기...", "import_private_key": "개인 키 가져오기...",
"import_scan_hint": "0 = 처음부터 다시 스캔",
"import_scan_label": "스캔 시작 블록 높이(선택 사항)",
"import_scan_tip": "현재 높이",
"import_scan_transparent": "투명 키는 항상 전체를 다시 스캔합니다",
"import_viewkey_field": "조회 키",
"import_viewkey_note": "읽기 전용: 조회 키는 주소의 잔액과 거래를 보여주지만 자금을 사용할 수는 없습니다.",
"import_viewkey_title": "조회 키 가져오기",
"import_viewkey_wrong_type": "지출 키인 것 같습니다. 대신 \"개인 키 가져오기\"를 사용하세요.",
"incorrect_passphrase": "잘못된 암호", "incorrect_passphrase": "잘못된 암호",
"incorrect_pin": "잘못된 PIN", "incorrect_pin": "잘못된 PIN",
"insufficient_funds": "이 금액과 수수료를 위한 잔액이 부족합니다.", "insufficient_funds": "이 금액과 수수료를 위한 잔액이 부족합니다.",
@@ -414,6 +496,7 @@
"keep": "유지", "keep": "유지",
"keep_daemon": "데몬 계속 실행", "keep_daemon": "데몬 계속 실행",
"key_export_click_retrieve": "지갑에서 키를 가져오려면 클릭", "key_export_click_retrieve": "지갑에서 키를 가져오려면 클릭",
"key_export_failed": "키를 내보내지 못했습니다. 지갑 잠금을 해제하고(암호화된 경우) 다시 시도하세요.",
"key_export_fetching": "지갑에서 키를 가져오는 중...", "key_export_fetching": "지갑에서 키를 가져오는 중...",
"key_export_private_key": "개인 키:", "key_export_private_key": "개인 키:",
"key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.", "key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.",
@@ -739,6 +822,7 @@
"output_filename": "출력 파일명:", "output_filename": "출력 파일명:",
"overview": "개요", "overview": "개요",
"paste": "붙여넣기", "paste": "붙여넣기",
"paste_clip_empty": "클립보드가 비어 있습니다",
"paste_from_clipboard": "클립보드에서 붙여넣기", "paste_from_clipboard": "클립보드에서 붙여넣기",
"pay_from": "보낼 곳", "pay_from": "보낼 곳",
"payment_request": "결제 요청", "payment_request": "결제 요청",
@@ -790,7 +874,14 @@
"peers_upper": "피어", "peers_upper": "피어",
"peers_version": "버전", "peers_version": "버전",
"pending": "대기 중", "pending": "대기 중",
"pin_change_desc": "잠금 해제 PIN을 변경합니다. 현재 PIN과 새 PIN이 필요합니다.",
"pin_confirm_new_label": "새 PIN 확인:",
"pin_current_label": "현재 PIN:",
"pin_new_label": "새 PIN(4~8자리):",
"pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.", "pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.",
"pin_remove_desc": "삭제를 확인하려면 현재 PIN을 입력하세요. 잠금 해제에는 전체 암호가 필요합니다.",
"pin_setup_desc": "지갑을 빠르게 잠금 해제할 4~8자리 PIN을 설정하세요. 지갑 암호가 이 PIN으로 암호화되어 로컬에 저장됩니다.",
"pin_wallet_passphrase": "지갑 암호:",
"ping": "Ping", "ping": "Ping",
"portfolio_add_entry": "항목 추가", "portfolio_add_entry": "항목 추가",
"portfolio_add_to": "포트폴리오에 추가", "portfolio_add_to": "포트폴리오에 추가",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "포트폴리오에서 제거", "portfolio_remove_from": "포트폴리오에서 제거",
"portfolio_revert": "되돌리기", "portfolio_revert": "되돌리기",
"portfolio_save": "저장", "portfolio_save": "저장",
"portfolio_save_need_address": "저장하려면 주소를 하나 이상 추가하세요.",
"portfolio_save_need_name": "이 그룹을 저장하려면 이름을 입력하세요.",
"portfolio_save_need_price": "저장하려면 0보다 큰 수동 가격을 입력하세요.",
"portfolio_search": "주소 검색…", "portfolio_search": "주소 검색…",
"portfolio_search_icons": "아이콘 검색\\xE2\\x80\\xA6", "portfolio_search_icons": "아이콘 검색\\xE2\\x80\\xA6",
"portfolio_select_all": "전체", "portfolio_select_all": "전체",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "그라데이션 배경", "settings_gradient_bg": "그라데이션 배경",
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체", "settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
"settings_idle_after": "후", "settings_idle_after": "후",
"settings_import_key": "키 가져오기...", "settings_import_key": "개인 키 가져오기...",
"settings_import_viewkey": "조회 키 가져오기...",
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다", "settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
"settings_lock_now": "지금 잠금", "settings_lock_now": "지금 잠금",
"settings_locked": "잠김", "settings_locked": "잠김",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "거래 제출 중...", "submitting_transaction": "거래 제출 중...",
"success": "성공", "success": "성공",
"summary": "요약", "summary": "요약",
"sweep_button": "쓸어담기",
"sweep_caveat": "키를 가져와 모든 자금을 내 주소로 옮기는 거래에 서명합니다. 키는 잔액이 0인 상태로 지갑에 남습니다.",
"sweep_dest_label": "쓸어담은 자금을 보낼 주소",
"sweep_dest_new": "새 차폐 주소 (권장)",
"sweep_done": "완료 — 자금을 내 주소로 쓸어담았습니다.",
"sweep_to": "쓸어담은 주소:",
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
"sweep_tx": "거래:",
"syncing": "동기화 중...", "syncing": "동기화 중...",
"t_address": "T 주소", "t_address": "T 주소",
"t_addresses": "T 주소", "t_addresses": "T 주소",
@@ -1186,6 +1289,7 @@
"transfer_to": "이체 대상:", "transfer_to": "이체 대상:",
"transparent": "투명", "transparent": "투명",
"transparent_address": "투명 주소", "transparent_address": "투명 주소",
"try_again": "다시 시도",
"tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL", "tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL",
"tt_address_book": "빠른 전송을 위해 저장된 주소 관리", "tt_address_book": "빠른 전송을 위해 저장된 주소 관리",
"tt_auto_lock": "이 비활성 시간 후 지갑 잠금", "tt_auto_lock": "이 비활성 시간 후 지갑 잠금",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "모든 텍스트 및 UI 크기 조정 (1.0x = 기본, 최대 1.5x).", "tt_font_scale": "모든 텍스트 및 UI 크기 조정 (1.0x = 기본, 최대 1.5x).",
"tt_idle_delay": "채굴 시작 전 대기 시간", "tt_idle_delay": "채굴 시작 전 대기 시간",
"tt_import_key": "이 지갑에 개인키 (zkey 또는 tkey) 가져오기", "tt_import_key": "이 지갑에 개인키 (zkey 또는 tkey) 가져오기",
"tt_import_viewkey": "차폐 조회 키를 가져와 주소를 조회(읽기 전용)",
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다", "tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
"tt_language": "지갑 UI 인터페이스 언어", "tt_language": "지갑 UI 인터페이스 언어",
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환", "tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",

View File

@@ -62,6 +62,7 @@
"backup_data": "BACKUP & DADOS", "backup_data": "BACKUP & DADOS",
"backup_description": "Crie um backup do seu arquivo wallet.dat. Este arquivo contém todas as suas chaves privadas e histórico de transações. Guarde o backup em um local seguro.", "backup_description": "Crie um backup do seu arquivo wallet.dat. Este arquivo contém todas as suas chaves privadas e histórico de transações. Guarde o backup em um local seguro.",
"backup_destination": "Destino do backup:", "backup_destination": "Destino do backup:",
"backup_overwrite_confirm": "Já existe um arquivo ali — salve novamente para sobrescrevê-lo.",
"backup_source": "Origem: %s", "backup_source": "Origem: %s",
"backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem", "backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem",
"backup_tip_multiple": "Crie múltiplos backups em diferentes locais", "backup_tip_multiple": "Crie múltiplos backups em diferentes locais",
@@ -70,6 +71,7 @@
"backup_title": "Backup da Carteira", "backup_title": "Backup da Carteira",
"backup_wallet": "Fazer Backup da Carteira...", "backup_wallet": "Fazer Backup da Carteira...",
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado", "backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
"backup_warn": "Este arquivo contém todas as suas chaves privadas — guarde-o em local seguro.",
"balance": "Saldo", "balance": "Saldo",
"balance_history_collecting": "Histórico de saldo — coletando dados...", "balance_history_collecting": "Histórico de saldo — coletando dados...",
"balance_layout": "Layout do Saldo", "balance_layout": "Layout do Saldo",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat está protegido)", "bootstrap_wallet_protected": "(wallet.dat está protegido)",
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.", "bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
"cancel": "Cancelar", "cancel": "Cancelar",
"change_pass_confirm": "Confirmar nova:",
"change_pass_current": "Senha atual:",
"change_pass_new": "Nova senha:",
"change_pass_title": "Alterar senha",
"characters": "caracteres", "characters": "caracteres",
"chat": "Chat", "chat": "Chat",
"chat_cancel": "Cancelar", "chat_cancel": "Cancelar",
@@ -180,10 +186,18 @@
"connected_peers": "Pares Conectados", "connected_peers": "Pares Conectados",
"connecting": "Conectando...", "connecting": "Conectando...",
"console": "Console", "console": "Console",
"console_accents": "Destaques de cor",
"console_app": "App", "console_app": "App",
"console_auto_scroll": "Rolagem automática", "console_auto_scroll": "Rolagem automática",
"console_available_commands": "Comandos disponíveis:", "console_available_commands": "Comandos disponíveis:",
"console_capturing_output": "Capturando saída do daemon...", "console_capturing_output": "Capturando saída do daemon...",
"console_cat_blockchain": "Blockchain",
"console_cat_control": "Controle",
"console_cat_mining": "Mineração",
"console_cat_network": "Rede",
"console_cat_raw_transactions": "Transações brutas",
"console_cat_utility": "Utilitários",
"console_cat_wallet": "Carteira",
"console_clear": "Limpar", "console_clear": "Limpar",
"console_clear_console": "Limpar Console", "console_clear_console": "Limpar Console",
"console_cleared": "Console limpo", "console_cleared": "Console limpo",
@@ -221,6 +235,20 @@
"console_no_daemon": "Sem daemon", "console_no_daemon": "Sem daemon",
"console_not_connected": "Erro: Não conectado ao daemon", "console_not_connected": "Erro: Não conectado ao daemon",
"console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.", "console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.",
"console_ref_builds": "Gera",
"console_ref_cancel": "Cancelar",
"console_ref_destructive": "Sensível",
"console_ref_example": "Exemplo",
"console_ref_insert": "Inserir no console",
"console_ref_insert_run": "Inserir e executar",
"console_ref_no_match": "Nenhum comando corresponde.",
"console_ref_no_params": "Não requer parâmetros.",
"console_ref_optional": "opcional",
"console_ref_parameters": "Parâmetros",
"console_ref_run": "Executar",
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
"console_ref_select_hint": "Selecione um comando para ver o que ele faz.",
"console_rpc_reference": "Referência de Comandos RPC", "console_rpc_reference": "Referência de Comandos RPC",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Scanline do console", "console_scanline": "Scanline do console",
@@ -240,6 +268,9 @@
"console_status_stopping": "Parando", "console_status_stopping": "Parando",
"console_status_unknown": "Desconhecido", "console_status_unknown": "Desconhecido",
"console_tab_completion": "Tab para completar", "console_tab_completion": "Tab para completar",
"console_text_colors": "Cores do texto",
"console_toggle_accents": "Alternar destaques de cor das linhas",
"console_toggle_text_color": "Alternar cores do texto das linhas",
"console_type_help": "Digite 'help' para comandos disponíveis", "console_type_help": "Digite 'help' para comandos disponíveis",
"console_welcome": "Bem-vindo ao Console ObsidianDragon", "console_welcome": "Bem-vindo ao Console ObsidianDragon",
"console_zoom_in": "Aumentar zoom", "console_zoom_in": "Aumentar zoom",
@@ -302,6 +333,19 @@
"date": "Data", "date": "Data",
"date_label": "Data:", "date_label": "Data:",
"debug_logging": "REGISTRO DE DEPURAÇÃO", "debug_logging": "REGISTRO DE DEPURAÇÃO",
"decrypt_desc": "A carteira será exportada, o daemon reiniciado com uma nova carteira sem criptografia e todas as chaves reimportadas. Isso pode levar alguns minutos, dependendo do tamanho da carteira.",
"decrypt_error_title": "Falha na descriptografia",
"decrypt_step_backup": "Fazendo backup da carteira criptografada",
"decrypt_step_export": "Exportando as chaves da carteira",
"decrypt_step_restart": "Reiniciando o daemon",
"decrypt_step_stop": "Parando o daemon",
"decrypt_step_unlock": "Desbloqueando a carteira",
"decrypt_success_desc": "Sua carteira agora está sem criptografia. Um backup da carteira criptografada foi salvo como wallet.dat.encrypted.bak no seu diretório de dados.",
"decrypt_success_title": "Carteira descriptografada com sucesso!",
"decrypt_title": "Remover criptografia da carteira",
"decrypt_wait_general": "Aguarde. O daemon está exportando chaves, reiniciando e reimportando. Isso pode levar alguns minutos.",
"decrypt_wait_restart": "Aguardando o daemon terminar de iniciar...",
"decrypt_warning": "Isso removerá a criptografia da sua carteira. Suas chaves privadas serão armazenadas sem proteção no disco.",
"delete": "Excluir", "delete": "Excluir",
"delete_blockchain": "Excluir Blockchain", "delete_blockchain": "Excluir Blockchain",
"delete_blockchain_confirm": "Excluir e Ressincronizar", "delete_blockchain_confirm": "Excluir e Ressincronizar",
@@ -314,6 +358,15 @@
"download_bootstrap": "Baixar Bootstrap", "download_bootstrap": "Baixar Bootstrap",
"dragonx_green": "DragonX (Verde)", "dragonx_green": "DragonX (Verde)",
"edit": "Editar", "edit": "Editar",
"enc_confirm": "Confirmar:",
"enc_desc": "Criptografar sua carteira protege suas chaves privadas com uma senha. Após a criptografia, o daemon será reiniciado.",
"enc_encrypting": "Criptografando a carteira...",
"enc_pin_desc": "Um PIN de 4 a 8 dígitos permite desbloquear sua carteira sem digitar a senha completa toda vez.",
"enc_pin_set_ok": "PIN definido com sucesso",
"enc_pin_skipped": "PIN ignorado. Você pode definir um depois em Configurações.",
"enc_pin_vault_fail": "Falha ao criar o cofre do PIN",
"enc_success": "Carteira criptografada com sucesso!",
"enc_wait": "Aguarde, não feche o aplicativo.",
"error": "Erro", "error": "Erro",
"error_format": "Erro: %s", "error_format": "Erro: %s",
"est_time_to_block": "Tempo Est. por Bloco", "est_time_to_block": "Tempo Est. por Bloco",
@@ -346,10 +399,18 @@
"export_keys_danger": "PERIGO: Isto exportará TODAS as chaves privadas da sua carteira! Qualquer pessoa com acesso a este arquivo pode roubar seus fundos. Guarde com segurança e exclua após o uso.", "export_keys_danger": "PERIGO: Isto exportará TODAS as chaves privadas da sua carteira! Qualquer pessoa com acesso a este arquivo pode roubar seus fundos. Guarde com segurança e exclua após o uso.",
"export_keys_include_t": "Incluir endereços T (transparentes)", "export_keys_include_t": "Incluir endereços T (transparentes)",
"export_keys_include_z": "Incluir endereços Z (blindados)", "export_keys_include_z": "Incluir endereços Z (blindados)",
"export_keys_none_addrs": "Nenhum endereço para exportar",
"export_keys_none_result": "Nenhuma chave exportada (0 de %d) — desbloqueie a carteira e tente novamente.",
"export_keys_none_toast": "Não foi possível exportar chaves — a carteira está desbloqueada?",
"export_keys_not_connected": "Não conectado ao daemon",
"export_keys_options": "Opções de exportação:", "export_keys_options": "Opções de exportação:",
"export_keys_partial": "Exportadas %d de %d chaves — incompleto (algumas sem chave de gasto, ou a carteira está bloqueada).",
"export_keys_partial_toast": "Exportação parcial: %d de %d chaves",
"export_keys_progress": "Exportando %d/%d...", "export_keys_progress": "Exportando %d/%d...",
"export_keys_select_type": "Selecione ao menos um tipo de endereço",
"export_keys_success": "Chaves exportadas com sucesso", "export_keys_success": "Chaves exportadas com sucesso",
"export_keys_title": "Exportar Todas as Chaves Privadas", "export_keys_title": "Exportar Todas as Chaves Privadas",
"export_keys_write_fail": "Falha ao gravar o arquivo de chaves.",
"export_private_key": "Exportar Chave Privada", "export_private_key": "Exportar Chave Privada",
"export_tx_count": "Exportar %zu transações para arquivo CSV.", "export_tx_count": "Exportar %zu transações para arquivo CSV.",
"export_tx_file_fail": "Falha ao criar arquivo CSV", "export_tx_file_fail": "Falha ao criar arquivo CSV",
@@ -391,21 +452,42 @@
"history": "Histórico", "history": "Histórico",
"immature_type": "Imaturo", "immature_type": "Imaturo",
"import": "Importar", "import": "Importar",
"import_key_address": "Endereço:",
"import_key_btn": "Importar Chave(s)", "import_key_btn": "Importar Chave(s)",
"import_key_done": "Importada. A carteira está reescaneando.",
"import_key_field": "Chave",
"import_key_formats": "Formatos de chave suportados:", "import_key_formats": "Formatos de chave suportados:",
"import_key_full_rescan": "(0 = rescan completo)", "import_key_full_rescan": "(0 = rescan completo)",
"import_key_import": "Importar",
"import_key_label": "Chave(s) Privada(s):", "import_key_label": "Chave(s) Privada(s):",
"import_key_need_node": "Conecte um nó em execução para importar uma chave.",
"import_key_no_valid": "Nenhuma chave válida encontrada na entrada", "import_key_no_valid": "Nenhuma chave válida encontrada na entrada",
"import_key_progress": "Importando %d/%d...", "import_key_progress": "Importando %d/%d...",
"import_key_rescan": "Reescanear blockchain após importação", "import_key_rescan": "Reescanear blockchain após importação",
"import_key_rescanning": "Importando e reescaneando — pode levar vários minutos",
"import_key_reveal_tip": "Mostrar/ocultar a chave",
"import_key_start_height": "Altura inicial:", "import_key_start_height": "Altura inicial:",
"import_key_success": "Chaves importadas com sucesso", "import_key_success": "Chaves importadas com sucesso",
"import_key_t_format": "Chaves privadas WIF de endereços T", "import_key_t_format": "Chaves privadas WIF de endereços T",
"import_key_title": "Importar Chave Privada", "import_key_title": "Importar Chave Privada",
"import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.", "import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.",
"import_key_type_tkey": "Chave privada transparente",
"import_key_type_unknown": "Formato de chave não reconhecido",
"import_key_type_zspend": "Chave de gasto blindada",
"import_key_type_zview": "Chave de visualização blindada (somente leitura)",
"import_key_warn": "Importe apenas uma chave que você possui — ela concede acesso aos seus fundos.",
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.", "import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
"import_key_wrong_type": "Isto parece uma chave de visualização. Use \"Importar chave de visualização\".",
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)", "import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
"import_private_key": "Importar Chave Privada...", "import_private_key": "Importar Chave Privada...",
"import_scan_hint": "0 = reescanear desde o início",
"import_scan_label": "Escanear a partir da altura do bloco (opcional)",
"import_scan_tip": "altura atual",
"import_scan_transparent": "Chaves transparentes sempre reescaneiam totalmente",
"import_viewkey_field": "Chave de visualização",
"import_viewkey_note": "Somente leitura: uma chave de visualização revela o saldo e as transações de um endereço, mas não pode gastar seus fundos.",
"import_viewkey_title": "Importar chave de visualização",
"import_viewkey_wrong_type": "Isto parece uma chave de gasto. Use \"Importar Chave Privada\".",
"incorrect_passphrase": "Senha incorreta", "incorrect_passphrase": "Senha incorreta",
"incorrect_pin": "PIN incorreto", "incorrect_pin": "PIN incorreto",
"insufficient_funds": "Fundos insuficientes para este valor mais taxa.", "insufficient_funds": "Fundos insuficientes para este valor mais taxa.",
@@ -414,6 +496,7 @@
"keep": "Manter", "keep": "Manter",
"keep_daemon": "Manter daemon em execução", "keep_daemon": "Manter daemon em execução",
"key_export_click_retrieve": "Clique para recuperar a chave da sua carteira", "key_export_click_retrieve": "Clique para recuperar a chave da sua carteira",
"key_export_failed": "Não foi possível exportar a chave — desbloqueie a carteira (se estiver criptografada) e tente novamente.",
"key_export_fetching": "Buscando chave da carteira...", "key_export_fetching": "Buscando chave da carteira...",
"key_export_private_key": "Chave Privada:", "key_export_private_key": "Chave Privada:",
"key_export_private_warning": "Mantenha esta chave em SEGREDO! Qualquer pessoa com esta chave pode gastar seus fundos. Nunca a compartilhe online ou com terceiros não confiáveis.", "key_export_private_warning": "Mantenha esta chave em SEGREDO! Qualquer pessoa com esta chave pode gastar seus fundos. Nunca a compartilhe online ou com terceiros não confiáveis.",
@@ -739,6 +822,7 @@
"output_filename": "Nome do arquivo de saída:", "output_filename": "Nome do arquivo de saída:",
"overview": "Resumo", "overview": "Resumo",
"paste": "Colar", "paste": "Colar",
"paste_clip_empty": "A área de transferência está vazia",
"paste_from_clipboard": "Colar da Área de Transferência", "paste_from_clipboard": "Colar da Área de Transferência",
"pay_from": "Pagar de", "pay_from": "Pagar de",
"payment_request": "SOLICITAÇÃO DE PAGAMENTO", "payment_request": "SOLICITAÇÃO DE PAGAMENTO",
@@ -790,7 +874,14 @@
"peers_upper": "PARES", "peers_upper": "PARES",
"peers_version": "Versão", "peers_version": "Versão",
"pending": "Pendente", "pending": "Pendente",
"pin_change_desc": "Altere seu PIN de desbloqueio. Você precisa do seu PIN atual e de um novo PIN.",
"pin_confirm_new_label": "Confirmar novo PIN:",
"pin_current_label": "PIN atual:",
"pin_new_label": "Novo PIN (4 a 8 dígitos):",
"pin_not_set": "PIN não definido. Use a senha para desbloquear.", "pin_not_set": "PIN não definido. Use a senha para desbloquear.",
"pin_remove_desc": "Digite seu PIN atual para confirmar a remoção. Você precisará usar sua senha completa para desbloquear.",
"pin_setup_desc": "Defina um PIN de 4 a 8 dígitos para desbloquear a carteira rapidamente. Sua senha da carteira será criptografada com este PIN e armazenada localmente.",
"pin_wallet_passphrase": "Senha da carteira:",
"ping": "Ping", "ping": "Ping",
"portfolio_add_entry": "Adicionar entrada", "portfolio_add_entry": "Adicionar entrada",
"portfolio_add_to": "Adicionar ao portfólio", "portfolio_add_to": "Adicionar ao portfólio",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "Remover do portfólio", "portfolio_remove_from": "Remover do portfólio",
"portfolio_revert": "Reverter", "portfolio_revert": "Reverter",
"portfolio_save": "Salvar", "portfolio_save": "Salvar",
"portfolio_save_need_address": "Adicione pelo menos um endereço para salvar.",
"portfolio_save_need_name": "Insira um nome para salvar este grupo.",
"portfolio_save_need_price": "Insira um preço manual acima de 0 para salvar.",
"portfolio_search": "Pesquisar endereços…", "portfolio_search": "Pesquisar endereços…",
"portfolio_search_icons": "Pesquisar ícones\\xE2\\x80\\xA6", "portfolio_search_icons": "Pesquisar ícones\\xE2\\x80\\xA6",
"portfolio_select_all": "Todos", "portfolio_select_all": "Todos",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "Fundo gradiente", "settings_gradient_bg": "Fundo gradiente",
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves", "settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
"settings_idle_after": "após", "settings_idle_after": "após",
"settings_import_key": "Importar chave...", "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_language_note": "Nota: Alguns textos requerem reinício para atualizar",
"settings_lock_now": "Bloquear agora", "settings_lock_now": "Bloquear agora",
"settings_locked": "Bloqueado", "settings_locked": "Bloqueado",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "Enviando transação...", "submitting_transaction": "Enviando transação...",
"success": "Sucesso", "success": "Sucesso",
"summary": "Resumo", "summary": "Resumo",
"sweep_button": "Varrer",
"sweep_caveat": "Importa a chave para assinar uma transação que move todos os seus fundos para o seu endereço. A chave permanece na carteira com saldo zero.",
"sweep_dest_label": "Enviar fundos varridos para",
"sweep_dest_new": "Novo endereço blindado (recomendado)",
"sweep_done": "Concluído — fundos varridos para o seu endereço.",
"sweep_to": "Varrido para:",
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
"sweep_tx": "Transação:",
"syncing": "Sincronizando...", "syncing": "Sincronizando...",
"t_address": "Endereço T", "t_address": "Endereço T",
"t_addresses": "Endereços T", "t_addresses": "Endereços T",
@@ -1186,6 +1289,7 @@
"transfer_to": "Transferir para:", "transfer_to": "Transferir para:",
"transparent": "Transparente", "transparent": "Transparente",
"transparent_address": "Endereço Transparente", "transparent_address": "Endereço Transparente",
"try_again": "Tentar novamente",
"tt_addr_url": "URL base para visualizar endereços em um explorador de blocos", "tt_addr_url": "URL base para visualizar endereços em um explorador de blocos",
"tt_address_book": "Gerenciar endereços salvos para envio rápido", "tt_address_book": "Gerenciar endereços salvos para envio rápido",
"tt_auto_lock": "Bloquear carteira após este tempo de inatividade", "tt_auto_lock": "Bloquear carteira após este tempo de inatividade",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "Escalar todo o texto e interface (1.0x = padrão, até 1.5x).", "tt_font_scale": "Escalar todo o texto e interface (1.0x = padrão, até 1.5x).",
"tt_idle_delay": "Quanto tempo esperar antes de iniciar a mineração", "tt_idle_delay": "Quanto tempo esperar antes de iniciar a mineração",
"tt_import_key": "Importar uma chave privada (zkey ou tkey) nesta carteira", "tt_import_key": "Importar uma chave privada (zkey ou tkey) nesta carteira",
"tt_import_viewkey": "Importar uma chave de visualização blindada para observar um endereço (somente leitura)",
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração", "tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
"tt_language": "Idioma da interface da carteira", "tt_language": "Idioma da interface da carteira",
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo", "tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",

View File

@@ -62,6 +62,7 @@
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ", "backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
"backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.", "backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.",
"backup_destination": "Место сохранения:", "backup_destination": "Место сохранения:",
"backup_overwrite_confirm": "Там уже есть файл — сохраните ещё раз, чтобы перезаписать его.",
"backup_source": "Источник: %s", "backup_source": "Источник: %s",
"backup_tip_external": "Храните резервные копии на внешних дисках или в облаке", "backup_tip_external": "Храните резервные копии на внешних дисках или в облаке",
"backup_tip_multiple": "Создавайте несколько копий в разных местах", "backup_tip_multiple": "Создавайте несколько копий в разных местах",
@@ -70,6 +71,7 @@
"backup_title": "Резервное копирование кошелька", "backup_title": "Резервное копирование кошелька",
"backup_wallet": "Создать резервную копию...", "backup_wallet": "Создать резервную копию...",
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении", "backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
"backup_warn": "Этот файл содержит все ваши приватные ключи — храните его в надёжном месте.",
"balance": "Баланс", "balance": "Баланс",
"balance_history_collecting": "История баланса — сбор данных...", "balance_history_collecting": "История баланса — сбор данных...",
"balance_layout": "Макет баланса", "balance_layout": "Макет баланса",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat защищён)", "bootstrap_wallet_protected": "(wallet.dat защищён)",
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.", "bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
"cancel": "Отмена", "cancel": "Отмена",
"change_pass_confirm": "Подтвердите новый:",
"change_pass_current": "Текущий пароль:",
"change_pass_new": "Новый пароль:",
"change_pass_title": "Сменить пароль",
"characters": "символов", "characters": "символов",
"chat": "Чат", "chat": "Чат",
"chat_cancel": "Отмена", "chat_cancel": "Отмена",
@@ -180,10 +186,18 @@
"connected_peers": "Подключённые узлы", "connected_peers": "Подключённые узлы",
"connecting": "Подключение...", "connecting": "Подключение...",
"console": "Консоль", "console": "Консоль",
"console_accents": "Цветовые акценты",
"console_app": "Прил.", "console_app": "Прил.",
"console_auto_scroll": "Авто-прокрутка", "console_auto_scroll": "Авто-прокрутка",
"console_available_commands": "Доступные команды:", "console_available_commands": "Доступные команды:",
"console_capturing_output": "Захват вывода daemon...", "console_capturing_output": "Захват вывода daemon...",
"console_cat_blockchain": "Блокчейн",
"console_cat_control": "Управление",
"console_cat_mining": "Майнинг",
"console_cat_network": "Сеть",
"console_cat_raw_transactions": "Сырые транзакции",
"console_cat_utility": "Утилиты",
"console_cat_wallet": "Кошелёк",
"console_clear": "Очистить", "console_clear": "Очистить",
"console_clear_console": "Очистить консоль", "console_clear_console": "Очистить консоль",
"console_cleared": "Консоль очищена", "console_cleared": "Консоль очищена",
@@ -221,6 +235,20 @@
"console_no_daemon": "Нет daemon", "console_no_daemon": "Нет daemon",
"console_not_connected": "Ошибка: Не подключено к daemon", "console_not_connected": "Ошибка: Не подключено к daemon",
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.", "console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
"console_ref_builds": "Формирует",
"console_ref_cancel": "Отмена",
"console_ref_destructive": "Осторожно",
"console_ref_example": "Пример",
"console_ref_insert": "Вставить в консоль",
"console_ref_insert_run": "Вставить и выполнить",
"console_ref_no_match": "Нет подходящих команд.",
"console_ref_no_params": "Не требует параметров.",
"console_ref_optional": "необязательно",
"console_ref_parameters": "Параметры",
"console_ref_run": "Выполнить",
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
"console_ref_search_hint": "Поиск по названию или задаче…",
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
"console_rpc_reference": "Справочник RPC-команд", "console_rpc_reference": "Справочник RPC-команд",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "Скан-линия консоли", "console_scanline": "Скан-линия консоли",
@@ -240,6 +268,9 @@
"console_status_stopping": "Остановка", "console_status_stopping": "Остановка",
"console_status_unknown": "Неизвестно", "console_status_unknown": "Неизвестно",
"console_tab_completion": "Tab для дополнения", "console_tab_completion": "Tab для дополнения",
"console_text_colors": "Цвета текста",
"console_toggle_accents": "Переключить цветовые акценты строк",
"console_toggle_text_color": "Переключить цвета текста строк",
"console_type_help": "Введите 'help' для списка команд", "console_type_help": "Введите 'help' для списка команд",
"console_welcome": "Добро пожаловать в консоль ObsidianDragon", "console_welcome": "Добро пожаловать в консоль ObsidianDragon",
"console_zoom_in": "Увеличить", "console_zoom_in": "Увеличить",
@@ -302,6 +333,19 @@
"date": "Дата", "date": "Дата",
"date_label": "Дата:", "date_label": "Дата:",
"debug_logging": "ЖУРНАЛ ОТЛАДКИ", "debug_logging": "ЖУРНАЛ ОТЛАДКИ",
"decrypt_desc": "Кошелёк будет экспортирован, демон перезапущен с новым незашифрованным кошельком, и все ключи будут повторно импортированы. Это может занять несколько минут в зависимости от размера кошелька.",
"decrypt_error_title": "Не удалось расшифровать",
"decrypt_step_backup": "Резервное копирование зашифрованного кошелька",
"decrypt_step_export": "Экспорт ключей кошелька",
"decrypt_step_restart": "Перезапуск демона",
"decrypt_step_stop": "Остановка демона",
"decrypt_step_unlock": "Разблокировка кошелька",
"decrypt_success_desc": "Ваш кошелёк теперь не зашифрован. Резервная копия зашифрованного кошелька сохранена как wallet.dat.encrypted.bak в вашем каталоге данных.",
"decrypt_success_title": "Кошелёк успешно расшифрован!",
"decrypt_title": "Удалить шифрование кошелька",
"decrypt_wait_general": "Пожалуйста, подождите. Демон экспортирует ключи, перезапускается и повторно импортирует. Это может занять несколько минут.",
"decrypt_wait_restart": "Ожидание полного запуска демона...",
"decrypt_warning": "Это удалит шифрование вашего кошелька. Ваши приватные ключи будут храниться на диске без защиты.",
"delete": "Удалить", "delete": "Удалить",
"delete_blockchain": "Удалить блокчейн", "delete_blockchain": "Удалить блокчейн",
"delete_blockchain_confirm": "Удалить и пересинхронизировать", "delete_blockchain_confirm": "Удалить и пересинхронизировать",
@@ -314,6 +358,15 @@
"download_bootstrap": "Скачать бутстрап", "download_bootstrap": "Скачать бутстрап",
"dragonx_green": "DragonX (Зелёная)", "dragonx_green": "DragonX (Зелёная)",
"edit": "Редактировать", "edit": "Редактировать",
"enc_confirm": "Подтвердите:",
"enc_desc": "Шифрование кошелька защищает ваши приватные ключи паролем. После шифрования демон перезапустится.",
"enc_encrypting": "Шифрование кошелька...",
"enc_pin_desc": "PIN-код из 48 цифр позволяет разблокировать кошелёк, не вводя полный пароль каждый раз.",
"enc_pin_set_ok": "PIN-код успешно установлен",
"enc_pin_skipped": "PIN-код пропущен. Вы можете задать его позже в настройках.",
"enc_pin_vault_fail": "Не удалось создать хранилище PIN-кода",
"enc_success": "Кошелёк успешно зашифрован!",
"enc_wait": "Пожалуйста, подождите, не закрывайте приложение.",
"error": "Ошибка", "error": "Ошибка",
"error_format": "Ошибка: %s", "error_format": "Ошибка: %s",
"est_time_to_block": "Расч. время до блока", "est_time_to_block": "Расч. время до блока",
@@ -346,10 +399,18 @@
"export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.", "export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.",
"export_keys_include_t": "Включить T-адреса (прозрачные)", "export_keys_include_t": "Включить T-адреса (прозрачные)",
"export_keys_include_z": "Включить Z-адреса (экранированные)", "export_keys_include_z": "Включить Z-адреса (экранированные)",
"export_keys_none_addrs": "Нет адресов для экспорта",
"export_keys_none_result": "Ключи не экспортированы (0 из %d) — разблокируйте кошелёк и повторите попытку.",
"export_keys_none_toast": "Не удалось экспортировать ключи — кошелёк разблокирован?",
"export_keys_not_connected": "Нет подключения к демону",
"export_keys_options": "Параметры экспорта:", "export_keys_options": "Параметры экспорта:",
"export_keys_partial": "Экспортировано %d из %d ключей — неполно (у некоторых нет ключа расходования или кошелёк заблокирован).",
"export_keys_partial_toast": "Частичный экспорт: %d из %d ключей",
"export_keys_progress": "Экспорт %d/%d...", "export_keys_progress": "Экспорт %d/%d...",
"export_keys_select_type": "Выберите хотя бы один тип адреса",
"export_keys_success": "Ключи успешно экспортированы", "export_keys_success": "Ключи успешно экспортированы",
"export_keys_title": "Экспорт всех приватных ключей", "export_keys_title": "Экспорт всех приватных ключей",
"export_keys_write_fail": "Не удалось записать файл ключей.",
"export_private_key": "Экспорт приватного ключа", "export_private_key": "Экспорт приватного ключа",
"export_tx_count": "Экспортировать %zu транзакций в файл CSV.", "export_tx_count": "Экспортировать %zu транзакций в файл CSV.",
"export_tx_file_fail": "Не удалось создать файл CSV", "export_tx_file_fail": "Не удалось создать файл CSV",
@@ -391,21 +452,42 @@
"history": "История", "history": "История",
"immature_type": "Незрелая", "immature_type": "Незрелая",
"import": "Импорт", "import": "Импорт",
"import_key_address": "Адрес:",
"import_key_btn": "Импорт ключей", "import_key_btn": "Импорт ключей",
"import_key_done": "Импортировано. Кошелёк выполняет повторное сканирование.",
"import_key_field": "Ключ",
"import_key_formats": "Поддерживаемые форматы ключей:", "import_key_formats": "Поддерживаемые форматы ключей:",
"import_key_full_rescan": "(0 = полное сканирование)", "import_key_full_rescan": "(0 = полное сканирование)",
"import_key_import": "Импорт",
"import_key_label": "Приватный ключ(и):", "import_key_label": "Приватный ключ(и):",
"import_key_need_node": "Подключите запущенный узел, чтобы импортировать ключ.",
"import_key_no_valid": "В введённых данных не найдено действительных ключей", "import_key_no_valid": "В введённых данных не найдено действительных ключей",
"import_key_progress": "Импорт %d/%d...", "import_key_progress": "Импорт %d/%d...",
"import_key_rescan": "Пересканировать блокчейн после импорта", "import_key_rescan": "Пересканировать блокчейн после импорта",
"import_key_rescanning": "Импорт и повторное сканирование — это может занять несколько минут",
"import_key_reveal_tip": "Показать/скрыть ключ",
"import_key_start_height": "Начальная высота:", "import_key_start_height": "Начальная высота:",
"import_key_success": "Ключи успешно импортированы", "import_key_success": "Ключи успешно импортированы",
"import_key_t_format": "Приватные ключи WIF для T-адресов", "import_key_t_format": "Приватные ключи WIF для T-адресов",
"import_key_title": "Импорт приватного ключа", "import_key_title": "Импорт приватного ключа",
"import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.", "import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.",
"import_key_type_tkey": "Прозрачный приватный ключ",
"import_key_type_unknown": "Нераспознанный формат ключа",
"import_key_type_zspend": "Экранированный ключ траты",
"import_key_type_zview": "Экранированный ключ просмотра (только чтение)",
"import_key_warn": "Импортируйте только свой ключ — он даёт доступ к его средствам.",
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.", "import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
"import_key_wrong_type": "Похоже, это ключ просмотра. Используйте «Импорт ключа просмотра».",
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)", "import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
"import_private_key": "Импорт приватного ключа...", "import_private_key": "Импорт приватного ключа...",
"import_scan_hint": "0 = повторное сканирование с начала",
"import_scan_label": "Сканировать с высоты блока (необязательно)",
"import_scan_tip": "текущая высота",
"import_scan_transparent": "Прозрачные ключи всегда пересканируются полностью",
"import_viewkey_field": "Ключ просмотра",
"import_viewkey_note": "Только чтение: ключ просмотра показывает баланс и транзакции адреса, но не может тратить его средства.",
"import_viewkey_title": "Импорт ключа просмотра",
"import_viewkey_wrong_type": "Похоже, это ключ расходования. Используйте «Импорт приватного ключа».",
"incorrect_passphrase": "Неверный пароль", "incorrect_passphrase": "Неверный пароль",
"incorrect_pin": "Неверный PIN", "incorrect_pin": "Неверный PIN",
"insufficient_funds": "Недостаточно средств для этой суммы плюс комиссия.", "insufficient_funds": "Недостаточно средств для этой суммы плюс комиссия.",
@@ -414,6 +496,7 @@
"keep": "Сохранить", "keep": "Сохранить",
"keep_daemon": "Оставить daemon работающим", "keep_daemon": "Оставить daemon работающим",
"key_export_click_retrieve": "Нажмите, чтобы получить ключ из вашего кошелька", "key_export_click_retrieve": "Нажмите, чтобы получить ключ из вашего кошелька",
"key_export_failed": "Не удалось экспортировать ключ — разблокируйте кошелёк (если зашифрован) и повторите попытку.",
"key_export_fetching": "Получение ключа из кошелька...", "key_export_fetching": "Получение ключа из кошелька...",
"key_export_private_key": "Приватный ключ:", "key_export_private_key": "Приватный ключ:",
"key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.", "key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.",
@@ -739,6 +822,7 @@
"output_filename": "Имя выходного файла:", "output_filename": "Имя выходного файла:",
"overview": "Обзор", "overview": "Обзор",
"paste": "Вставить", "paste": "Вставить",
"paste_clip_empty": "Буфер обмена пуст",
"paste_from_clipboard": "Вставить из буфера обмена", "paste_from_clipboard": "Вставить из буфера обмена",
"pay_from": "Оплатить с", "pay_from": "Оплатить с",
"payment_request": "ЗАПРОС НА ОПЛАТУ", "payment_request": "ЗАПРОС НА ОПЛАТУ",
@@ -790,7 +874,14 @@
"peers_upper": "УЗЛЫ", "peers_upper": "УЗЛЫ",
"peers_version": "Версия", "peers_version": "Версия",
"pending": "Ожидание", "pending": "Ожидание",
"pin_change_desc": "Измените PIN-код разблокировки. Вам понадобится текущий PIN-код и новый PIN-код.",
"pin_confirm_new_label": "Подтвердите новый PIN-код:",
"pin_current_label": "Текущий PIN-код:",
"pin_new_label": "Новый PIN-код (48 цифр):",
"pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.", "pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.",
"pin_remove_desc": "Введите текущий PIN-код для подтверждения удаления. Для разблокировки вам понадобится полный пароль.",
"pin_setup_desc": "Установите PIN-код из 48 цифр для быстрой разблокировки кошелька. Пароль вашего кошелька будет зашифрован этим PIN-кодом и сохранён локально.",
"pin_wallet_passphrase": "Пароль кошелька:",
"ping": "Пинг", "ping": "Пинг",
"portfolio_add_entry": "Добавить запись", "portfolio_add_entry": "Добавить запись",
"portfolio_add_to": "Добавить в портфель", "portfolio_add_to": "Добавить в портфель",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "Удалить из портфеля", "portfolio_remove_from": "Удалить из портфеля",
"portfolio_revert": "Отменить изменения", "portfolio_revert": "Отменить изменения",
"portfolio_save": "Сохранить", "portfolio_save": "Сохранить",
"portfolio_save_need_address": "Добавьте хотя бы один адрес, чтобы сохранить.",
"portfolio_save_need_name": "Введите имя, чтобы сохранить эту группу.",
"portfolio_save_need_price": "Введите ручную цену больше 0, чтобы сохранить.",
"portfolio_search": "Поиск адресов…", "portfolio_search": "Поиск адресов…",
"portfolio_search_icons": "Поиск иконок…", "portfolio_search_icons": "Поиск иконок…",
"portfolio_select_all": "Все", "portfolio_select_all": "Все",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "Градиент фона", "settings_gradient_bg": "Градиент фона",
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами", "settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
"settings_idle_after": "через", "settings_idle_after": "через",
"settings_import_key": "Импортировать ключ...", "settings_import_key": "Импорт приватного ключа...",
"settings_import_viewkey": "Импортировать ключ просмотра...",
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления", "settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
"settings_lock_now": "Заблокировать сейчас", "settings_lock_now": "Заблокировать сейчас",
"settings_locked": "Заблокирован", "settings_locked": "Заблокирован",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "Отправка транзакции...", "submitting_transaction": "Отправка транзакции...",
"success": "Успешно", "success": "Успешно",
"summary": "Итоги", "summary": "Итоги",
"sweep_button": "Перевести",
"sweep_caveat": "Импортирует ключ, чтобы подписать одну транзакцию, переводящую все его средства на ваш адрес. Ключ остаётся в кошельке с нулевым балансом.",
"sweep_dest_label": "Отправить переведённые средства на",
"sweep_dest_new": "Новый экранированный адрес (рекомендуется)",
"sweep_done": "Готово — средства переведены на ваш адрес.",
"sweep_to": "Переведено на:",
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
"sweep_tx": "Транзакция:",
"syncing": "Синхронизация...", "syncing": "Синхронизация...",
"t_address": "T-адрес", "t_address": "T-адрес",
"t_addresses": "T-адреса", "t_addresses": "T-адреса",
@@ -1186,6 +1289,7 @@
"transfer_to": "Перевести на:", "transfer_to": "Перевести на:",
"transparent": "Прозрачный", "transparent": "Прозрачный",
"transparent_address": "Прозрачный адрес", "transparent_address": "Прозрачный адрес",
"try_again": "Повторить",
"tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков", "tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков",
"tt_address_book": "Управление сохранёнными адресами для быстрой отправки", "tt_address_book": "Управление сохранёнными адресами для быстрой отправки",
"tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия", "tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "Масштабировать весь текст и интерфейс (1.0x = по умолчанию, до 1.5x).", "tt_font_scale": "Масштабировать весь текст и интерфейс (1.0x = по умолчанию, до 1.5x).",
"tt_idle_delay": "Сколько ждать перед началом майнинга", "tt_idle_delay": "Сколько ждать перед началом майнинга",
"tt_import_key": "Импортировать приватный ключ (zkey или tkey) в этот кошелёк", "tt_import_key": "Импортировать приватный ключ (zkey или tkey) в этот кошелёк",
"tt_import_viewkey": "Импортировать экранированный ключ просмотра для наблюдения за адресом (только чтение)",
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки", "tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
"tt_language": "Язык интерфейса кошелька", "tt_language": "Язык интерфейса кошелька",
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса", "tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",

View File

@@ -62,6 +62,7 @@
"backup_data": "备份与数据", "backup_data": "备份与数据",
"backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。", "backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。",
"backup_destination": "备份目标:", "backup_destination": "备份目标:",
"backup_overwrite_confirm": "该位置已存在文件,再次保存将覆盖它。",
"backup_source": "来源:%s", "backup_source": "来源:%s",
"backup_tip_external": "将备份存储在外部驱动器或云存储中", "backup_tip_external": "将备份存储在外部驱动器或云存储中",
"backup_tip_multiple": "在不同位置创建多个备份", "backup_tip_multiple": "在不同位置创建多个备份",
@@ -70,6 +71,7 @@
"backup_title": "备份钱包", "backup_title": "备份钱包",
"backup_wallet": "备份钱包...", "backup_wallet": "备份钱包...",
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat", "backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
"backup_warn": "此文件包含您的所有私钥,请妥善保管。",
"balance": "余额", "balance": "余额",
"balance_history_collecting": "余额历史——正在收集数据…", "balance_history_collecting": "余额历史——正在收集数据…",
"balance_layout": "余额布局", "balance_layout": "余额布局",
@@ -114,6 +116,10 @@
"bootstrap_wallet_protected": "(wallet.dat 已受保护)", "bootstrap_wallet_protected": "(wallet.dat 已受保护)",
"bootstrap_warning": "现有区块数据blocks、chainstate、notarizations将被删除并替换。您的 wallet.dat 不会被修改或删除。", "bootstrap_warning": "现有区块数据blocks、chainstate、notarizations将被删除并替换。您的 wallet.dat 不会被修改或删除。",
"cancel": "取消", "cancel": "取消",
"change_pass_confirm": "确认新密码:",
"change_pass_current": "当前密码短语:",
"change_pass_new": "新密码短语:",
"change_pass_title": "更改密码短语",
"characters": "字符", "characters": "字符",
"chat": "聊天", "chat": "聊天",
"chat_cancel": "取消", "chat_cancel": "取消",
@@ -180,10 +186,18 @@
"connected_peers": "已连接节点", "connected_peers": "已连接节点",
"connecting": "连接中...", "connecting": "连接中...",
"console": "控制台", "console": "控制台",
"console_accents": "颜色强调",
"console_app": "应用", "console_app": "应用",
"console_auto_scroll": "自动滚动", "console_auto_scroll": "自动滚动",
"console_available_commands": "可用命令:", "console_available_commands": "可用命令:",
"console_capturing_output": "正在捕获守护进程输出...", "console_capturing_output": "正在捕获守护进程输出...",
"console_cat_blockchain": "区块链",
"console_cat_control": "控制",
"console_cat_mining": "挖矿",
"console_cat_network": "网络",
"console_cat_raw_transactions": "原始交易",
"console_cat_utility": "实用工具",
"console_cat_wallet": "钱包",
"console_clear": "清除", "console_clear": "清除",
"console_clear_console": "清除控制台", "console_clear_console": "清除控制台",
"console_cleared": "控制台已清除", "console_cleared": "控制台已清除",
@@ -221,6 +235,20 @@
"console_no_daemon": "无守护进程", "console_no_daemon": "无守护进程",
"console_not_connected": "错误:未连接到守护进程", "console_not_connected": "错误:未连接到守护进程",
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。", "console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
"console_ref_builds": "生成",
"console_ref_cancel": "取消",
"console_ref_destructive": "谨慎",
"console_ref_example": "示例",
"console_ref_insert": "插入到控制台",
"console_ref_insert_run": "插入并运行",
"console_ref_no_match": "没有匹配的命令。",
"console_ref_no_params": "无需参数。",
"console_ref_optional": "可选",
"console_ref_parameters": "参数",
"console_ref_run": "运行",
"console_ref_run_confirm": "立即运行 %s这是一个有重大影响的命令。",
"console_ref_search_hint": "按名称或用途搜索…",
"console_ref_select_hint": "选择一个命令以查看其功能。",
"console_rpc_reference": "RPC 命令参考", "console_rpc_reference": "RPC 命令参考",
"console_rpc_trace": "RPC", "console_rpc_trace": "RPC",
"console_scanline": "控制台扫描线", "console_scanline": "控制台扫描线",
@@ -240,6 +268,9 @@
"console_status_stopping": "停止中", "console_status_stopping": "停止中",
"console_status_unknown": "未知", "console_status_unknown": "未知",
"console_tab_completion": "Tab 补全", "console_tab_completion": "Tab 补全",
"console_text_colors": "文本颜色",
"console_toggle_accents": "切换行颜色强调",
"console_toggle_text_color": "切换行文本颜色",
"console_type_help": "输入 'help' 查看可用命令", "console_type_help": "输入 'help' 查看可用命令",
"console_welcome": "欢迎使用 ObsidianDragon 控制台", "console_welcome": "欢迎使用 ObsidianDragon 控制台",
"console_zoom_in": "放大", "console_zoom_in": "放大",
@@ -302,6 +333,19 @@
"date": "日期", "date": "日期",
"date_label": "日期:", "date_label": "日期:",
"debug_logging": "调试日志", "debug_logging": "调试日志",
"decrypt_desc": "钱包将被导出,守护进程将使用全新的未加密钱包重新启动,并重新导入所有密钥。根据钱包大小,这可能需要几分钟。",
"decrypt_error_title": "解密失败",
"decrypt_step_backup": "正在备份加密钱包",
"decrypt_step_export": "正在导出钱包密钥",
"decrypt_step_restart": "正在重启守护进程",
"decrypt_step_stop": "正在停止守护进程",
"decrypt_step_unlock": "正在解锁钱包",
"decrypt_success_desc": "您的钱包现在未加密。加密钱包的备份已保存为 wallet.dat.encrypted.bak位于您的数据目录中。",
"decrypt_success_title": "钱包解密成功!",
"decrypt_title": "移除钱包加密",
"decrypt_wait_general": "请稍候。守护进程正在导出密钥、重启并重新导入。这可能需要几分钟。",
"decrypt_wait_restart": "正在等待守护进程完成启动...",
"decrypt_warning": "这将移除您钱包的加密。您的私钥将以未受保护的方式存储在磁盘上。",
"delete": "删除", "delete": "删除",
"delete_blockchain": "删除区块链", "delete_blockchain": "删除区块链",
"delete_blockchain_confirm": "删除并重新同步", "delete_blockchain_confirm": "删除并重新同步",
@@ -314,6 +358,15 @@
"download_bootstrap": "下载引导程序", "download_bootstrap": "下载引导程序",
"dragonx_green": "DragonX绿色", "dragonx_green": "DragonX绿色",
"edit": "编辑", "edit": "编辑",
"enc_confirm": "确认:",
"enc_desc": "加密钱包会用密码短语保护您的私钥。加密后,守护进程将重新启动。",
"enc_encrypting": "正在加密钱包...",
"enc_pin_desc": "4-8 位 PIN 码可让您无需每次输入完整密码短语即可解锁钱包。",
"enc_pin_set_ok": "PIN 码设置成功",
"enc_pin_skipped": "已跳过 PIN。您可以稍后在设置中设置。",
"enc_pin_vault_fail": "无法创建 PIN 保险库",
"enc_success": "钱包加密成功!",
"enc_wait": "请稍候,不要关闭应用程序。",
"error": "错误", "error": "错误",
"error_format": "错误:%s", "error_format": "错误:%s",
"est_time_to_block": "预计出块时间", "est_time_to_block": "预计出块时间",
@@ -346,10 +399,18 @@
"export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。", "export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。",
"export_keys_include_t": "包含 T 地址(透明)", "export_keys_include_t": "包含 T 地址(透明)",
"export_keys_include_z": "包含 Z 地址(屏蔽)", "export_keys_include_z": "包含 Z 地址(屏蔽)",
"export_keys_none_addrs": "没有可导出的地址",
"export_keys_none_result": "未导出任何密钥(%d 个中 0 个)——请解锁钱包后重试。",
"export_keys_none_toast": "无法导出密钥——钱包已解锁吗?",
"export_keys_not_connected": "未连接到守护进程",
"export_keys_options": "导出选项:", "export_keys_options": "导出选项:",
"export_keys_partial": "已导出 %d/%d 个密钥——不完整(部分没有花费密钥,或钱包已锁定)。",
"export_keys_partial_toast": "部分导出:%d/%d 个密钥",
"export_keys_progress": "正在导出 %d/%d...", "export_keys_progress": "正在导出 %d/%d...",
"export_keys_select_type": "请至少选择一种地址类型",
"export_keys_success": "密钥导出成功", "export_keys_success": "密钥导出成功",
"export_keys_title": "导出所有私钥", "export_keys_title": "导出所有私钥",
"export_keys_write_fail": "写入密钥文件失败。",
"export_private_key": "导出私钥", "export_private_key": "导出私钥",
"export_tx_count": "导出 %zu 笔交易到 CSV 文件。", "export_tx_count": "导出 %zu 笔交易到 CSV 文件。",
"export_tx_file_fail": "无法创建 CSV 文件", "export_tx_file_fail": "无法创建 CSV 文件",
@@ -391,21 +452,42 @@
"history": "历史", "history": "历史",
"immature_type": "未成熟", "immature_type": "未成熟",
"import": "导入", "import": "导入",
"import_key_address": "地址:",
"import_key_btn": "导入密钥", "import_key_btn": "导入密钥",
"import_key_done": "已导入。钱包正在重新扫描。",
"import_key_field": "密钥",
"import_key_formats": "支持的密钥格式:", "import_key_formats": "支持的密钥格式:",
"import_key_full_rescan": "0 = 完整重扫)", "import_key_full_rescan": "0 = 完整重扫)",
"import_key_import": "导入",
"import_key_label": "私钥:", "import_key_label": "私钥:",
"import_key_need_node": "请连接一个运行中的节点以导入密钥。",
"import_key_no_valid": "输入中未找到有效密钥", "import_key_no_valid": "输入中未找到有效密钥",
"import_key_progress": "正在导入 %d/%d...", "import_key_progress": "正在导入 %d/%d...",
"import_key_rescan": "导入后重新扫描区块链", "import_key_rescan": "导入后重新扫描区块链",
"import_key_rescanning": "正在导入并重新扫描——可能需要几分钟",
"import_key_reveal_tip": "显示/隐藏密钥",
"import_key_start_height": "起始高度:", "import_key_start_height": "起始高度:",
"import_key_success": "密钥导入成功", "import_key_success": "密钥导入成功",
"import_key_t_format": "T 地址 WIF 私钥", "import_key_t_format": "T 地址 WIF 私钥",
"import_key_title": "导入私钥", "import_key_title": "导入私钥",
"import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。", "import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。",
"import_key_type_tkey": "透明私钥",
"import_key_type_unknown": "无法识别的密钥格式",
"import_key_type_zspend": "屏蔽花费密钥",
"import_key_type_zview": "屏蔽查看密钥(仅查看)",
"import_key_warn": "只导入你自己拥有的密钥——它可访问该地址的资金。",
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。", "import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
"import_key_wrong_type": "这看起来像查看密钥。请改用\"导入查看密钥\"。",
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)", "import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
"import_private_key": "导入私钥...", "import_private_key": "导入私钥...",
"import_scan_hint": "0 = 从头重新扫描",
"import_scan_label": "从区块高度开始扫描(可选)",
"import_scan_tip": "当前高度",
"import_scan_transparent": "透明密钥始终完整重新扫描",
"import_viewkey_field": "查看密钥",
"import_viewkey_note": "仅查看:查看密钥可显示地址的余额和交易,但无法花费其资金。",
"import_viewkey_title": "导入查看密钥",
"import_viewkey_wrong_type": "这看起来像花费密钥。请改用\"导入私钥\"。",
"incorrect_passphrase": "密码错误", "incorrect_passphrase": "密码错误",
"incorrect_pin": "PIN 错误", "incorrect_pin": "PIN 错误",
"insufficient_funds": "余额不足以支付此金额加手续费。", "insufficient_funds": "余额不足以支付此金额加手续费。",
@@ -414,6 +496,7 @@
"keep": "保留", "keep": "保留",
"keep_daemon": "保持守护进程运行", "keep_daemon": "保持守护进程运行",
"key_export_click_retrieve": "点击从钱包中获取密钥", "key_export_click_retrieve": "点击从钱包中获取密钥",
"key_export_failed": "无法导出密钥——请解锁钱包(如已加密)后重试。",
"key_export_fetching": "正在从钱包获取密钥...", "key_export_fetching": "正在从钱包获取密钥...",
"key_export_private_key": "私钥:", "key_export_private_key": "私钥:",
"key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。", "key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。",
@@ -739,6 +822,7 @@
"output_filename": "输出文件名:", "output_filename": "输出文件名:",
"overview": "概览", "overview": "概览",
"paste": "粘贴", "paste": "粘贴",
"paste_clip_empty": "剪贴板为空",
"paste_from_clipboard": "从剪贴板粘贴", "paste_from_clipboard": "从剪贴板粘贴",
"pay_from": "付款来源", "pay_from": "付款来源",
"payment_request": "付款请求", "payment_request": "付款请求",
@@ -790,7 +874,14 @@
"peers_upper": "节点", "peers_upper": "节点",
"peers_version": "版本", "peers_version": "版本",
"pending": "待处理", "pending": "待处理",
"pin_change_desc": "更改您的解锁 PIN 码。您需要当前 PIN 码和新 PIN 码。",
"pin_confirm_new_label": "确认新 PIN 码:",
"pin_current_label": "当前 PIN 码:",
"pin_new_label": "新 PIN 码4-8 位):",
"pin_not_set": "未设置 PIN。使用密码解锁。", "pin_not_set": "未设置 PIN。使用密码解锁。",
"pin_remove_desc": "输入当前 PIN 码以确认移除。解锁时您需要使用完整的密码短语。",
"pin_setup_desc": "设置一个 4-8 位 PIN 码以快速解锁钱包。您的钱包密码短语将使用此 PIN 码加密并存储在本地。",
"pin_wallet_passphrase": "钱包密码短语:",
"ping": "延迟", "ping": "延迟",
"portfolio_add_entry": "添加条目", "portfolio_add_entry": "添加条目",
"portfolio_add_to": "添加到投资组合", "portfolio_add_to": "添加到投资组合",
@@ -829,6 +920,9 @@
"portfolio_remove_from": "从投资组合中移除", "portfolio_remove_from": "从投资组合中移除",
"portfolio_revert": "还原", "portfolio_revert": "还原",
"portfolio_save": "保存", "portfolio_save": "保存",
"portfolio_save_need_address": "添加至少一个地址以保存。",
"portfolio_save_need_name": "输入名称以保存此分组。",
"portfolio_save_need_price": "输入大于 0 的手动价格以保存。",
"portfolio_search": "搜索地址…", "portfolio_search": "搜索地址…",
"portfolio_search_icons": "搜索图标…", "portfolio_search_icons": "搜索图标…",
"portfolio_select_all": "全部", "portfolio_select_all": "全部",
@@ -1064,7 +1158,8 @@
"settings_gradient_bg": "渐变背景", "settings_gradient_bg": "渐变背景",
"settings_gradient_desc": "用平滑渐变替换纹理背景", "settings_gradient_desc": "用平滑渐变替换纹理背景",
"settings_idle_after": "之后", "settings_idle_after": "之后",
"settings_import_key": "导入钥...", "settings_import_key": "导入钥...",
"settings_import_viewkey": "导入查看密钥...",
"settings_language_note": "注意:部分文本需要重启才能更新", "settings_language_note": "注意:部分文本需要重启才能更新",
"settings_lock_now": "立即锁定", "settings_lock_now": "立即锁定",
"settings_locked": "已锁定", "settings_locked": "已锁定",
@@ -1151,6 +1246,14 @@
"submitting_transaction": "正在提交交易...", "submitting_transaction": "正在提交交易...",
"success": "成功", "success": "成功",
"summary": "摘要", "summary": "摘要",
"sweep_button": "归集",
"sweep_caveat": "导入密钥以签署一笔将其所有资金转移到您地址的交易。密钥将保留在钱包中,余额为零。",
"sweep_dest_label": "将归集资金发送到",
"sweep_dest_new": "新的屏蔽地址(推荐)",
"sweep_done": "完成 — 资金已归集到您的地址。",
"sweep_to": "归集到:",
"sweep_toggle": "归集到我的钱包(不保留密钥)",
"sweep_tx": "交易:",
"syncing": "同步中...", "syncing": "同步中...",
"t_address": "T 地址", "t_address": "T 地址",
"t_addresses": "T 地址", "t_addresses": "T 地址",
@@ -1186,6 +1289,7 @@
"transfer_to": "转账至:", "transfer_to": "转账至:",
"transparent": "透明", "transparent": "透明",
"transparent_address": "透明地址", "transparent_address": "透明地址",
"try_again": "重试",
"tt_addr_url": "在区块浏览器中查看地址的基础 URL", "tt_addr_url": "在区块浏览器中查看地址的基础 URL",
"tt_address_book": "管理已保存的地址以快速发送", "tt_address_book": "管理已保存的地址以快速发送",
"tt_auto_lock": "在此不活动时间后锁定钱包", "tt_auto_lock": "在此不活动时间后锁定钱包",
@@ -1212,6 +1316,7 @@
"tt_font_scale": "缩放所有文本和界面1.0x = 默认,最大 1.5x)。", "tt_font_scale": "缩放所有文本和界面1.0x = 默认,最大 1.5x)。",
"tt_idle_delay": "开始挖矿前等待多长时间", "tt_idle_delay": "开始挖矿前等待多长时间",
"tt_import_key": "将私钥zkey 或 tkey导入此钱包", "tt_import_key": "将私钥zkey 或 tkey导入此钱包",
"tt_import_viewkey": "导入屏蔽查看密钥以查看某个地址(只读)",
"tt_keep_daemon": "运行设置向导时守护进程仍会停止", "tt_keep_daemon": "运行设置向导时守护进程仍会停止",
"tt_language": "钱包界面语言", "tt_language": "钱包界面语言",
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局", "tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",

File diff suppressed because it is too large Load Diff

View File

@@ -300,7 +300,17 @@ public:
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback); void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export. // callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
void exportAllKeys(std::function<void(const std::string&, int, int)> callback); void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback); // callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
// returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
// startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF).
void importPrivateKey(const std::string& key, int startHeight,
std::function<void(bool, const std::string&, const std::string&)> callback);
// Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance fee) to a
// destination you own — a freshly generated shielded address when destMode == 0, else destExisting.
// Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker.
void sweepPrivateKey(const std::string& key, int startHeight, int destMode,
const std::string& destExisting);
// Wallet backup // Wallet backup
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback); void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
@@ -370,7 +380,8 @@ public:
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place) std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
// Dialog triggers (used by settings page to open modal dialogs) // Dialog triggers (used by settings page to open modal dialogs)
void showImportKeyDialog() { show_import_key_ = true; } void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
void showExportKeyDialog() { show_export_key_ = true; } void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; } void showBackupDialog() { show_backup_ = true; }
void showSeedBackupDialog() { show_seed_backup_ = true; } void showSeedBackupDialog() { show_seed_backup_ = true; }
@@ -687,6 +698,14 @@ private:
std::string exchange_chart_key_; // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none) std::string exchange_chart_key_; // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none)
bool exchange_chart_fetch_in_flight_ = false; bool exchange_chart_fetch_in_flight_ = false;
std::chrono::steady_clock::time_point exchange_chart_last_fetch_{}; std::chrono::steady_clock::time_point exchange_chart_last_fetch_{};
// Per-pair candle cache: switching back to a recently-viewed venue loads instantly (no re-fetch)
// instead of overwriting the single active buffer. Keyed like exchange_chart_key_.
struct ExchangeChartCache {
std::vector<std::pair<std::time_t, double>> closeIntraday, closeDaily;
std::vector<data::Candle> ohlcIntraday, ohlcDaily;
std::chrono::steady_clock::time_point fetchedAt{};
};
std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
util::AsyncTaskManager async_tasks_; util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
@@ -789,11 +808,28 @@ private:
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection) size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// Export/Import state // Export/Import state
std::string export_result_; char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
bool export_error_ = false; // last export returned no key (locked wallet / failure)
char import_key_input_[512] = {0}; char import_key_input_[512] = {0};
std::string export_address_; std::string export_address_;
std::string import_status_; std::string import_status_;
bool import_success_ = false; bool import_success_ = false;
bool import_key_reveal_ = false; // show the key in plaintext (default masked)
bool import_in_progress_ = false; // an import + rescan is running (disable/spinner)
std::string import_result_address_; // address imported on success (shown as a copy field)
bool import_view_mode_ = false; // dialog mode: false = spending key, true = viewing key
char import_key_scan_height_[16] = {0}; // optional rescan start height (shielded spend / viewing-key imports)
// --- Sweep: import a spending key then move all its funds to one of your own addresses, instead
// of keeping the key in the wallet (spending-key / non-view mode only). ---
bool import_sweep_mode_ = false; // sweep instead of a plain import
int sweep_dest_mode_ = 0; // destination: 0 = fresh shielded address, 1 = an existing one
char sweep_dest_pick_[128] = {0}; // chosen existing destination (sweep_dest_mode_ == 1)
enum class SweepStep { Idle, Running, Done, Error };
SweepStep sweep_step_ = SweepStep::Idle;
std::string sweep_status_; // progress / error text
std::string sweep_txid_; // sweep transaction id (on success)
std::string sweep_dest_shown_; // the destination address the funds were swept to
std::string backup_status_; std::string backup_status_;
bool backup_success_ = false; bool backup_success_ = false;

View File

@@ -1806,6 +1806,19 @@ void App::refreshExchangeChart()
if (key == exchange_chart_key_ && state_.market.exchange_chart_active && if (key == exchange_chart_key_ && state_.market.exchange_chart_active &&
std::chrono::steady_clock::now() - exchange_chart_last_fetch_ < std::chrono::minutes(30)) std::chrono::steady_clock::now() - exchange_chart_last_fetch_ < std::chrono::minutes(30))
return; return;
// Cached from a recent view of this pair -> load instantly, no re-fetch (switching back is snappy).
if (auto it = exchange_chart_cache_.find(key);
it != exchange_chart_cache_.end() &&
std::chrono::steady_clock::now() - it->second.fetchedAt < std::chrono::minutes(30)) {
state_.market.exchange_chart_intraday = it->second.closeIntraday;
state_.market.exchange_chart_daily = it->second.closeDaily;
state_.market.exchange_ohlc_intraday = it->second.ohlcIntraday;
state_.market.exchange_ohlc_daily = it->second.ohlcDaily;
state_.market.exchange_chart_active = true;
exchange_chart_key_ = key;
exchange_chart_last_fetch_ = it->second.fetchedAt;
return;
}
// Switching pairs: fall back to the aggregate until the new venue's candles arrive. // Switching pairs: fall back to the aggregate until the new venue's candles arrive.
if (key != exchange_chart_key_) state_.market.exchange_chart_active = false; if (key != exchange_chart_key_) state_.market.exchange_chart_active = false;
@@ -1824,18 +1837,25 @@ void App::refreshExchangeChart()
return [this, key, ohlcIntra = std::move(ohlcIntra), ohlcDaily = std::move(ohlcDaily)]() mutable { return [this, key, ohlcIntra = std::move(ohlcIntra), ohlcDaily = std::move(ohlcDaily)]() mutable {
exchange_chart_fetch_in_flight_ = false; exchange_chart_fetch_in_flight_ = false;
if (!ohlcDaily.empty() || !ohlcIntra.empty()) { if (!ohlcDaily.empty() || !ohlcIntra.empty()) {
// Derive close series (line + change%) from the OHLC (candles). // Build a cache entry (OHLC + derived close series for the line/change%), publish it to
std::vector<std::pair<std::time_t, double>> closeIntra, closeDaily; // the active buffer, and keep it so switching back to this pair later is instant.
closeIntra.reserve(ohlcIntra.size()); closeDaily.reserve(ohlcDaily.size()); ExchangeChartCache e;
for (const auto& c : ohlcIntra) closeIntra.emplace_back(c.time, c.close); e.ohlcIntraday = std::move(ohlcIntra);
for (const auto& c : ohlcDaily) closeDaily.emplace_back(c.time, c.close); e.ohlcDaily = std::move(ohlcDaily);
state_.market.exchange_chart_intraday = std::move(closeIntra); e.closeIntraday.reserve(e.ohlcIntraday.size());
state_.market.exchange_chart_daily = std::move(closeDaily); e.closeDaily.reserve(e.ohlcDaily.size());
state_.market.exchange_ohlc_intraday = std::move(ohlcIntra); for (const auto& c : e.ohlcIntraday) e.closeIntraday.emplace_back(c.time, c.close);
state_.market.exchange_ohlc_daily = std::move(ohlcDaily); for (const auto& c : e.ohlcDaily) e.closeDaily.emplace_back(c.time, c.close);
e.fetchedAt = std::chrono::steady_clock::now();
state_.market.exchange_chart_intraday = e.closeIntraday;
state_.market.exchange_chart_daily = e.closeDaily;
state_.market.exchange_ohlc_intraday = e.ohlcIntraday;
state_.market.exchange_ohlc_daily = e.ohlcDaily;
state_.market.exchange_chart_active = true; state_.market.exchange_chart_active = true;
exchange_chart_key_ = key; exchange_chart_key_ = key;
exchange_chart_last_fetch_ = std::chrono::steady_clock::now(); exchange_chart_last_fetch_ = e.fetchedAt;
if (exchange_chart_cache_.size() >= 16) exchange_chart_cache_.clear(); // bound it (many venues)
exchange_chart_cache_[key] = std::move(e);
} else { } else {
// Both ranges empty -> the venue's API failed/changed; keep the aggregate. // Both ranges empty -> the venue's API failed/changed; keep the aggregate.
state_.market.exchange_chart_active = false; state_.market.exchange_chart_active = false;
@@ -2901,10 +2921,11 @@ void App::exportAllKeys(std::function<void(const std::string&, int, int)> callba
} }
} }
void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, const std::string&)> callback) void App::importPrivateKey(const std::string& rawKey, int startHeight,
std::function<void(bool, const std::string&, const std::string&)> callback)
{ {
if (!state_.connected || !rpc_ || !worker_) { if (!state_.connected || !rpc_ || !worker_) {
if (callback) callback(false, "Not connected"); if (callback) callback(false, "Not connected", "");
return; return;
} }
@@ -2914,23 +2935,35 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin()); while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin());
while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back(); while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back();
// Reject anything that doesn't look like a Z/T private key before handing it to the daemon (the // Reject anything that isn't a recognized Z/T private key or shielded viewing key before handing
// dialog's indicator and this guard now share isRecognizedPrivateKey, so they can't disagree). // it to the daemon (the dialog's indicator and this guard share isRecognizedImportKey).
if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) { if (!services::WalletSecurityController::isRecognizedImportKey(key)) {
if (callback) callback(false, "Unrecognized private-key format."); if (callback) callback(false, "Unrecognized key format.", "");
return; return;
} }
const bool viewing = services::WalletSecurityController::isViewingKey(key);
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key) const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
== services::WalletSecurityController::KeyKind::Shielded; == services::WalletSecurityController::KeyKind::Shielded;
// Run on the worker thread — import requests a full rescan (rescan=true), so the // Run on the worker thread — import requests a full rescan (rescan=true), so the
// synchronous curl call can take many seconds; never block the UI thread on it. // synchronous curl call can take many seconds; never block the UI thread on it.
worker_->post([this, key, shielded, callback]() -> rpc::RPCWorker::MainCb { worker_->post([this, key, viewing, shielded, startHeight, callback]() -> rpc::RPCWorker::MainCb {
std::string err; std::string err, addr;
try { try {
rpc::RPCClient::TraceScope trace("Settings / Import private key"); rpc::RPCClient::TraceScope trace("Settings / Import key");
if (shielded) rpc_->call("z_importkey", {key, "yes"}); // rescan std::string method;
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan nlohmann::json params;
if (viewing) { method = "z_importviewingkey"; params = {key, "yes"}; } // watch-only
else if (shielded) { method = "z_importkey"; params = {key, "yes"}; }
else { method = "importprivkey"; params = {key, "", true}; }
// A start height (shielded RPCs only) rescans from that block instead of genesis.
if (startHeight > 0 && (viewing || shielded)) params.push_back(startHeight);
nlohmann::json r = rpc_->call(method, params);
// z_import* return {type,address}; importprivkey returns the t-address string.
if (r.is_object() && r.contains("address") && r["address"].is_string())
addr = r["address"].get<std::string>();
else if (r.is_string())
addr = r.get<std::string>();
} catch (const std::exception& e) { } catch (const std::exception& e) {
err = e.what(); err = e.what();
} catch (...) { } catch (...) {
@@ -2938,16 +2971,169 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
// below would never run, leaving a stuck "Importing…" spinner. // below would never run, leaving a stuck "Importing…" spinner.
err = "Import failed (unknown error)"; err = "Import failed (unknown error)";
} }
return [this, shielded, err, callback]() { return [this, err, addr, callback]() {
if (!err.empty()) { if (!err.empty()) {
if (callback) callback(false, err); if (callback) callback(false, err, "");
return; return;
} }
invalidateAddressValidationCache(); invalidateAddressValidationCache();
refreshAddresses(); refreshAddresses();
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage( if (callback) callback(true, "", addr);
shielded ? services::WalletSecurityController::KeyKind::Shielded };
: services::WalletSecurityController::KeyKind::Transparent)); });
}
// Sweep a spending key: import it (a full rescan populates its UTXOs/notes — the stock node has no
// address index, so there is no way to enumerate them without importing) then z_sendmany ALL of the
// key's funds (balance fee) to a destination the user already controls. The imported key is left in
// the wallet with an empty balance (there is no remove-key RPC) — the point is that the funds now sit
// on the user's own key. z_sendmany (not z_mergetoaddress): it moves a single-UTXO transparent source
// and fails loudly rather than silently leaving a remainder. Fund-moving — see the reviewed design.
void App::sweepPrivateKey(const std::string& rawKey, int startHeight, int destMode,
const std::string& destExisting)
{
if (!state_.connected || !rpc_ || !worker_) {
sweep_status_ = "Not connected to the daemon.";
sweep_step_ = SweepStep::Error;
return;
}
// Trim (manual entry doesn't go through the dialog's Paste trimmer).
std::string key(rawKey);
while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin());
while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back();
// Sweeping requires a SPENDING key (transparent WIF or shielded z-spending key) — a viewing key
// can't sign, and this must never accidentally route to z_importviewingkey.
if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) {
sweep_status_ = "Enter a spending key (a viewing key can't move funds).";
sweep_step_ = SweepStep::Error;
return;
}
sweep_step_ = SweepStep::Running;
sweep_status_ = "Importing key & rescanning…";
sweep_txid_.clear();
sweep_dest_shown_.clear();
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
== services::WalletSecurityController::KeyKind::Shielded;
const double fee = DRAGONX_DEFAULT_FEE;
worker_->post([this, key, startHeight, destMode, destExisting, shielded, fee]() -> rpc::RPCWorker::MainCb {
std::string err, dest, sourceAddr, amountStr;
double amount = 0.0;
try {
rpc::RPCClient::TraceScope trace("Settings / Sweep key");
// For a shielded key this daemon's z_importkey returns null (no address), so snapshot the
// z-address set before the import and diff it after to find the newly-controlled address.
std::vector<std::string> preZ;
bool preZok = false;
auto listZ = [&](std::vector<std::string>& out) {
out.clear();
auto la = rpc_->call("z_listaddresses");
if (la.is_array()) for (auto& a : la) if (a.is_string()) out.push_back(a.get<std::string>());
};
if (shielded) { try { listZ(preZ); preZok = true; } catch (...) { preZok = false; } }
// 1. Import the key (blocks until the rescan completes → its funds become spendable).
std::string method;
nlohmann::json params;
if (shielded) { method = "z_importkey"; params = {key, "yes"}; }
else { method = "importprivkey"; params = {key, "", true}; }
if (startHeight > 0 && shielded) params.push_back(startHeight);
nlohmann::json r = rpc_->call(method, params);
// 2. Determine the swept address. importprivkey returns the t-address string; z_importkey
// returns null, so diff the z-address list to find the one the key just added.
if (shielded) {
// A failed pre-snapshot would make the diff treat pre-existing addresses as "new" and
// could pick the WRONG source — refuse rather than risk moving another address's funds.
if (!preZok) throw std::runtime_error("Couldn't read the wallet's addresses to identify the swept key. Try again.");
std::vector<std::string> postZ;
listZ(postZ);
std::vector<std::string> added;
for (const auto& s : postZ)
if (std::find(preZ.begin(), preZ.end(), s) == preZ.end()) added.push_back(s);
if (added.size() == 1) sourceAddr = added[0];
else if (added.empty()) throw std::runtime_error("__ALREADY__"); // key already in wallet
else throw std::runtime_error("Could not identify the key's address after import.");
} else {
if (r.is_string()) sourceAddr = r.get<std::string>();
else if (r.is_object() && r.contains("address") && r["address"].is_string())
sourceAddr = r["address"].get<std::string>();
}
if (sourceAddr.empty()) throw std::runtime_error("Could not determine the key's address.");
// 3. Confirm the key holds spendable (confirmed) funds. z_getbalance covers t- and z-addrs.
auto balAt = [&](int minconf) -> double {
try {
nlohmann::json b = rpc_->call("z_getbalance", {sourceAddr, minconf});
return b.is_number() ? b.get<double>()
: b.is_string() ? std::stod(b.get<std::string>()) : 0.0;
} catch (...) { return 0.0; }
};
double bal = balAt(1);
if (bal <= 0.0) {
throw std::runtime_error(balAt(0) > 0.0 ? "__UNCONFIRMED__" : "__NOFUNDS__");
}
if (bal <= fee) throw std::runtime_error("__DUST__");
// 4. Resolve the destination — a fresh shielded address by default, else the picked one.
if (destMode == 0) dest = rpc_->call("z_getnewaddress").get<std::string>();
else dest = destExisting;
if (dest.empty()) throw std::runtime_error("No destination address for the sweep.");
// Send everything; the fee consumes the remainder (no change/residue). Compute in integer
// satoshis so the fixed-decimal amount is exact for all realistic balances (double bal-fee
// is exact below 2^52 sat ≈ 45M DRGX; above that the JSON-parsed balance is itself lossy —
// a swept key that large is implausible and would fail safe with funds left in place).
const long long feeSats = (long long)std::llround(fee * 100000000.0);
const long long amtSats = (long long)std::llround(bal * 100000000.0) - feeSats;
if (amtSats <= 0) throw std::runtime_error("__DUST__");
char amtBuf[32];
snprintf(amtBuf, sizeof(amtBuf), "%lld.%08lld",
amtSats / 100000000LL, amtSats % 100000000LL);
amountStr = amtBuf;
amount = (double)amtSats / 100000000.0; // for send bookkeeping / display only
} catch (const std::exception& e) {
err = e.what();
} catch (...) {
err = "Sweep failed (unknown error)";
}
return [this, err, sourceAddr, dest, amount, amountStr, fee]() {
invalidateAddressValidationCache();
refreshAddresses();
if (!err.empty()) {
if (err == "__NOFUNDS__") sweep_status_ = "This key holds no funds to sweep.";
else if (err == "__UNCONFIRMED__") sweep_status_ = "This key's funds are still unconfirmed — try again shortly.";
else if (err == "__ALREADY__") sweep_status_ = "This key is already in your wallet — use Send to move its funds.";
else if (err == "__DUST__") sweep_status_ = "This key's balance is too small to cover the network fee.";
else sweep_status_ = err;
sweep_step_ = SweepStep::Error;
return;
}
sweep_dest_shown_ = dest;
sweep_status_ = "Sweeping funds to your address…";
// Send all funds from the swept address to the destination through the tested z_sendmany
// wrapper (fixed-decimal amount + async-op tracking). z_sendmany needs every input in one
// transaction to cover the full balance, so it fails loudly rather than partial-sweeping.
nlohmann::json recipients = nlohmann::json::array();
nlohmann::json rcp;
rcp["address"] = dest;
rcp["amount"] = amountStr; // exact integer-satoshi fixed-decimal string
recipients.push_back(rcp);
submitZSendMany(sourceAddr, dest, amount, fee, "", recipients, "Settings / Sweep key",
/*markFeeGapRetry*/ false, [this](bool ok, const std::string& result) {
if (ok) {
sweep_txid_ = result;
sweep_status_.clear();
sweep_step_ = SweepStep::Done;
} else {
sweep_status_ = result.empty() ? "The sweep transaction failed." : result;
sweep_step_ = SweepStep::Error;
}
refreshBalance();
});
}; };
}); });
} }

View File

@@ -1125,35 +1125,33 @@ void App::renderEncryptWalletDialog() {
// Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup // Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
if (show_encrypt_dialog_) { if (show_encrypt_dialog_) {
const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
? "Quick-Unlock PIN" : "Encrypt Wallet"; ? TR("wiz_pin_title") : TR("settings_encrypt_wallet");
// Prevent closing via X button while encrypting // Prevent closing via X button while encrypting
bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting); bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr; bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;
if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) { OverlayDialogSpec ov;
ov.title = dlgTitle; ov.p_open = pOpen;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 480.0f; ov.idSuffix = "encrypt";
if (BeginOverlayDialog(ov)) {
// ---- Phase 1: Passphrase entry ---- // ---- Phase 1: Passphrase entry ----
if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) { if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1)); DialogWarningHeader(TR("wiz_encrypt_warning"));
ImGui::TextWrapped(ICON_MD_WARNING
" If you lose your passphrase, you lose access to your funds.");
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped("Encrypting your wallet protects your private keys " ImGui::TextWrapped("%s", TR("enc_desc"));
"with a passphrase. After encryption, the daemon will restart.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("Passphrase:"); ImGui::TextUnformatted(TR("wiz_passphrase"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_), ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_),
ImGuiInputTextFlags_Password); ImGuiInputTextFlags_Password);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("Confirm:"); ImGui::TextUnformatted(TR("enc_confirm"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_), ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_),
ImGuiInputTextFlags_Password); ImGuiInputTextFlags_Password);
@@ -1174,7 +1172,7 @@ void App::renderEncryptWalletDialog() {
} }
int classes = (int)hasDigit + (int)hasLower + (int)hasUpper + (int)hasSymbol; int classes = (int)hasDigit + (int)hasLower + (int)hasUpper + (int)hasSymbol;
const char* strengthLabel = "Weak"; const char* strengthLabel = TR("wiz_strength_weak");
ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f); ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
float strengthPct = 0.25f; float strengthPct = 0.25f;
int tier = 0; // 0=Weak, 1=Fair, 2=Good, 3=Strong int tier = 0; // 0=Weak, 1=Fair, 2=Good, 3=Strong
@@ -1183,9 +1181,9 @@ void App::renderEncryptWalletDialog() {
else if (len >= 8) tier = 1; else if (len >= 8) tier = 1;
// Downgrade one tier when only a single character class is used. // Downgrade one tier when only a single character class is used.
if (classes <= 1 && tier > 0) tier -= 1; if (classes <= 1 && tier > 0) tier -= 1;
if (tier == 3) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; } if (tier == 3) { strengthLabel = TR("wiz_strength_strong"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
else if (tier == 2) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; } else if (tier == 2) { strengthLabel = TR("wiz_strength_good"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
else if (tier == 1) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; } else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
float barW = ImGui::GetContentRegionAvail().x; float barW = ImGui::GetContentRegionAvail().x;
float barH = 4.0f; float barH = 4.0f;
@@ -1197,7 +1195,7 @@ void App::renderEncryptWalletDialog() {
dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH), dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH),
ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f); ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f);
ImGui::Dummy(ImVec2(barW, barH)); ImGui::Dummy(ImVec2(barW, barH));
ImGui::Text("Strength: %s", strengthLabel); ImGui::Text(TR("wiz_strength"), strengthLabel);
} }
if (!encrypt_status_.empty()) { if (!encrypt_status_.empty()) {
@@ -1210,7 +1208,7 @@ void App::renderEncryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!valid || encrypt_in_progress_); ImGui::BeginDisabled(!valid || encrypt_in_progress_);
if (ui::material::TactileButton("Encrypt Wallet", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("settings_encrypt_wallet"), ImVec2(btnW, 40))) {
std::string pass(encrypt_pass_buf_); std::string pass(encrypt_pass_buf_);
enc_dlg_saved_passphrase_ = pass; enc_dlg_saved_passphrase_ = pass;
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_)); memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
@@ -1222,7 +1220,7 @@ void App::renderEncryptWalletDialog() {
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_)); memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_)); memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
show_encrypt_dialog_ = false; show_encrypt_dialog_ = false;
@@ -1231,7 +1229,7 @@ void App::renderEncryptWalletDialog() {
// ---- Phase 2: Encrypting in progress ---- // ---- Phase 2: Encrypting in progress ----
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { } else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
const char* statusTitle = encrypt_in_progress_ const char* statusTitle = encrypt_in_progress_
? "Encrypting wallet..." : encrypt_status_.c_str(); ? TR("enc_encrypting") : encrypt_status_.c_str();
ImGui::Text("%s", statusTitle); ImGui::Text("%s", statusTitle);
ImGui::Spacing(); ImGui::Spacing();
@@ -1258,7 +1256,7 @@ void App::renderEncryptWalletDialog() {
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextColored(ImVec4(1,1,1,0.4f), "Please wait, do not close the application."); ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", TR("enc_wait"));
// Transition to PIN phase when encryption finishes successfully // Transition to PIN phase when encryption finishes successfully
if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
@@ -1268,23 +1266,20 @@ void App::renderEncryptWalletDialog() {
// ---- Phase 3: PIN setup (after successful encryption) ---- // ---- Phase 3: PIN setup (after successful encryption) ----
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) { } else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1));
ImGui::Text(ICON_MD_CHECK_CIRCLE " Wallet encrypted successfully!"); ImGui::Text(ICON_MD_CHECK_CIRCLE " %s", TR("enc_success"));
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped("A 4-8 digit PIN lets you unlock your wallet " ImGui::TextWrapped("%s", TR("enc_pin_desc"));
"without typing the full passphrase every time.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("PIN (4-8 digits):"); ImGui::TextUnformatted(TR("wiz_pin_label"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_), ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("Confirm PIN:"); ImGui::TextUnformatted(TR("wiz_pin_confirm"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_, ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_,
sizeof(enc_dlg_pin_confirm_buf_), sizeof(enc_dlg_pin_confirm_buf_),
@@ -1304,7 +1299,7 @@ void App::renderEncryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_); ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
if (ui::material::TactileButton("Set PIN", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
enc_dlg_pin_status_.clear(); enc_dlg_pin_status_.clear();
std::string savedPass = enc_dlg_saved_passphrase_; std::string savedPass = enc_dlg_saved_passphrase_;
@@ -1317,7 +1312,7 @@ void App::renderEncryptWalletDialog() {
settings_->setPinEnabled(true); settings_->setPinEnabled(true);
settings_->save(); settings_->save();
pin_in_progress_ = false; pin_in_progress_ = false;
ui::Notifications::instance().info("PIN set successfully"); ui::Notifications::instance().info(TR("enc_pin_set_ok"));
// Clean up // Clean up
if (!enc_dlg_saved_passphrase_.empty()) { if (!enc_dlg_saved_passphrase_.empty()) {
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0], util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
@@ -1328,20 +1323,20 @@ void App::renderEncryptWalletDialog() {
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_)); memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
show_encrypt_dialog_ = false; show_encrypt_dialog_ = false;
} else { } else {
enc_dlg_pin_status_ = "Failed to create PIN vault"; enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
enc_dlg_pin_status_ = "Failed to create PIN vault"; enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::TactileButton("Skip", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW, 40))) {
if (!enc_dlg_saved_passphrase_.empty()) { if (!enc_dlg_saved_passphrase_.empty()) {
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0], util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
enc_dlg_saved_passphrase_.size()); enc_dlg_saved_passphrase_.size());
@@ -1350,8 +1345,7 @@ void App::renderEncryptWalletDialog() {
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_)); memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_)); memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
show_encrypt_dialog_ = false; show_encrypt_dialog_ = false;
ui::Notifications::instance().info( ui::Notifications::instance().info(TR("enc_pin_skipped"));
"PIN skipped. You can set one later in Settings.");
} }
} }
EndOverlayDialog(); EndOverlayDialog();
@@ -1367,21 +1361,25 @@ void App::renderEncryptWalletDialog() {
// Change passphrase dialog // Change passphrase dialog
if (show_change_passphrase_) { if (show_change_passphrase_) {
if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) { OverlayDialogSpec ov;
ov.title = TR("change_pass_title"); ov.p_open = &show_change_passphrase_;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
if (BeginOverlayDialog(ov)) {
ImGui::Text("Current Passphrase:"); ImGui::TextUnformatted(TR("change_pass_current"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_), ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
ImGuiInputTextFlags_Password); ImGuiInputTextFlags_Password);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("New Passphrase:"); ImGui::TextUnformatted(TR("change_pass_new"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_), ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_),
ImGuiInputTextFlags_Password); ImGuiInputTextFlags_Password);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("Confirm New:"); ImGui::TextUnformatted(TR("change_pass_confirm"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_), ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_),
ImGuiInputTextFlags_Password); ImGuiInputTextFlags_Password);
@@ -1396,13 +1394,22 @@ void App::renderEncryptWalletDialog() {
strlen(change_new_pass_buf_) >= 8 && strlen(change_new_pass_buf_) >= 8 &&
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0; strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
ImGui::BeginDisabled(!valid || encrypt_in_progress_); ImGui::BeginDisabled(!valid || encrypt_in_progress_);
if (ui::material::TactileButton("Change Passphrase", ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(-1, 40))) {
changePassphrase(std::string(change_old_pass_buf_), changePassphrase(std::string(change_old_pass_buf_),
std::string(change_new_pass_buf_)); std::string(change_new_pass_buf_));
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
EndOverlayDialog(); EndOverlayDialog();
} }
// Wipe the passphrase buffers if the dialog was dismissed (X / Esc /
// outside-click) without submitting. The success path already zeroes
// them in changePassphrase(); the failure path keeps them for retry.
if (!show_change_passphrase_) {
memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_));
memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_));
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
}
} }
} }
@@ -1427,26 +1434,21 @@ void App::renderDecryptWalletDialog() {
bool canClose = wallet_security_workflow_.canClose(); bool canClose = wallet_security_workflow_.canClose();
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr; bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) { OverlayDialogSpec ov;
ov.title = TR("decrypt_title"); ov.p_open = pOpen;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 480.0f; ov.idSuffix = "decrypt";
if (BeginOverlayDialog(ov)) {
// ---- Phase 0: Passphrase entry ---- // ---- Phase 0: Passphrase entry ----
if (decryptState.phase == DecryptPhase::PassphraseEntry) { if (decryptState.phase == DecryptPhase::PassphraseEntry) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1)); DialogWarningHeader(TR("decrypt_warning"));
ImGui::TextWrapped(ICON_MD_WARNING
" This will remove encryption from your wallet. "
"Your private keys will be stored unprotected on disk.");
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped( ImGui::TextWrapped("%s", TR("decrypt_desc"));
"The wallet will be exported, the daemon restarted with a fresh "
"unencrypted wallet, and all keys re-imported. This may take "
"several minutes depending on wallet size.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("Current Passphrase:"); ImGui::TextUnformatted(TR("change_pass_current"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_, bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password | sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
@@ -1462,7 +1464,7 @@ void App::renderDecryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!valid || decryptState.inProgress); ImGui::BeginDisabled(!valid || decryptState.inProgress);
if (ui::material::TactileButton("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) { if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
std::string passphrase(decrypt_pass_buf_); std::string passphrase(decrypt_pass_buf_);
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_)); memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
wallet_security_workflow_.start(std::chrono::steady_clock::now()); wallet_security_workflow_.start(std::chrono::steady_clock::now());
@@ -1653,7 +1655,7 @@ void App::renderDecryptWalletDialog() {
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_)); memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
show_decrypt_dialog_ = false; show_decrypt_dialog_ = false;
} }
@@ -1662,11 +1664,11 @@ void App::renderDecryptWalletDialog() {
} else if (decryptState.phase == DecryptPhase::Working) { } else if (decryptState.phase == DecryptPhase::Working) {
// Step checklist // Step checklist
const char* stepLabels[] = { const char* stepLabels[] = {
"Unlocking wallet", TR("decrypt_step_unlock"),
"Exporting wallet keys", TR("decrypt_step_export"),
"Stopping daemon", TR("decrypt_step_stop"),
"Backing up encrypted wallet", TR("decrypt_step_backup"),
"Restarting daemon" TR("decrypt_step_restart")
}; };
const int numSteps = 5; const int numSteps = 5;
@@ -1743,10 +1745,9 @@ void App::renderDecryptWalletDialog() {
// Step-specific hints // Step-specific hints
if (decryptState.step == DecryptStep::RestartDaemon) { if (decryptState.step == DecryptStep::RestartDaemon) {
ImGui::TextWrapped("Waiting for the daemon to finish starting up..."); ImGui::TextWrapped("%s", TR("decrypt_wait_restart"));
} else { } else {
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, " ImGui::TextWrapped("%s", TR("decrypt_wait_general"));
"and re-importing. This may take several minutes.");
} }
// Total elapsed // Total elapsed
@@ -1763,15 +1764,13 @@ void App::renderDecryptWalletDialog() {
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE); ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont(); ImGui::PopFont();
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!"); ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "%s", TR("decrypt_success_title"));
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped( ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
"Your wallet is now unencrypted. A backup of the encrypted wallet "
"was saved as wallet.dat.encrypted.bak in your data directory.");
ImGui::Spacing(); ImGui::Spacing();
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
show_decrypt_dialog_ = false; show_decrypt_dialog_ = false;
} }
@@ -1781,23 +1780,31 @@ void App::renderDecryptWalletDialog() {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
ImGui::PopFont(); ImGui::PopFont();
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed"); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", TR("decrypt_error_title"));
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped("%s", decryptState.status.c_str()); ImGui::TextWrapped("%s", decryptState.status.c_str());
ImGui::Spacing(); ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ui::material::TactileButton("Try Again", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
wallet_security_workflow_.reset(); wallet_security_workflow_.reset();
} }
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
show_decrypt_dialog_ = false; show_decrypt_dialog_ = false;
} }
} }
EndOverlayDialog(); EndOverlayDialog();
} }
// Wipe the passphrase buffer if the dialog was dismissed (X / Esc / outside-
// click) without submitting. The submit and Cancel paths already memset it;
// this covers the BlurFloat dismiss paths. (canClose is false during Working,
// so an in-flight decrypt cannot be dismissed here.)
if (!show_decrypt_dialog_) {
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
}
} }
// =========================================================================== // ===========================================================================
@@ -1809,29 +1816,28 @@ void App::renderPinDialogs() {
// ---- Set PIN dialog ---- // ---- Set PIN dialog ----
if (show_pin_setup_) { if (show_pin_setup_) {
if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) { OverlayDialogSpec ov;
ov.title = TR("settings_set_pin"); ov.p_open = &show_pin_setup_;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 420.0f; ov.idSuffix = "pinsetup";
if (BeginOverlayDialog(ov)) {
ImGui::TextWrapped( ImGui::TextWrapped("%s", TR("pin_setup_desc"));
"Set a 4-8 digit PIN for quick wallet unlock. "
"Your wallet passphrase will be encrypted with this PIN "
"and stored locally.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("Wallet Passphrase:"); ImGui::TextUnformatted(TR("pin_wallet_passphrase"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_), ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_),
ImGuiInputTextFlags_Password); ImGuiInputTextFlags_Password);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("New PIN (4-8 digits):"); ImGui::TextUnformatted(TR("pin_new_label"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_), ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("Confirm PIN:"); ImGui::TextUnformatted(TR("wiz_pin_confirm"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_), ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
@@ -1848,7 +1854,7 @@ void App::renderPinDialogs() {
strcmp(pin_buf_, pin_confirm_buf_) == 0; strcmp(pin_buf_, pin_confirm_buf_) == 0;
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton("Set PIN", ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Verifying passphrase..."; pin_status_ = "Verifying passphrase...";
@@ -1904,30 +1910,39 @@ void App::renderPinDialogs() {
ImGui::EndDisabled(); ImGui::EndDisabled();
EndOverlayDialog(); EndOverlayDialog();
} }
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
// outside-click) without submitting. The submit path already memsets them.
if (!show_pin_setup_) {
memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_));
memset(pin_buf_, 0, sizeof(pin_buf_));
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
}
} }
// ---- Change PIN dialog ---- // ---- Change PIN dialog ----
if (show_pin_change_) { if (show_pin_change_) {
if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) { OverlayDialogSpec ov;
ov.title = TR("settings_change_pin"); ov.p_open = &show_pin_change_;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 420.0f; ov.idSuffix = "pinchange";
if (BeginOverlayDialog(ov)) {
ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN."); ImGui::TextWrapped("%s", TR("pin_change_desc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("Current PIN:"); ImGui::TextUnformatted(TR("pin_current_label"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_), ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("New PIN (4-8 digits):"); ImGui::TextUnformatted(TR("pin_new_label"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_), ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::Text("Confirm New PIN:"); ImGui::TextUnformatted(TR("pin_confirm_new_label"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_), ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
@@ -1944,7 +1959,7 @@ void App::renderPinDialogs() {
strcmp(pin_buf_, pin_confirm_buf_) == 0; strcmp(pin_buf_, pin_confirm_buf_) == 0;
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton("Change PIN", ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Changing PIN..."; pin_status_ = "Changing PIN...";
std::string oldPin(pin_old_buf_); std::string oldPin(pin_old_buf_);
@@ -1977,20 +1992,26 @@ void App::renderPinDialogs() {
ImGui::EndDisabled(); ImGui::EndDisabled();
EndOverlayDialog(); EndOverlayDialog();
} }
// Wipe the PIN buffers if the dialog was dismissed without submitting.
if (!show_pin_change_) {
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
memset(pin_buf_, 0, sizeof(pin_buf_));
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
}
} }
// ---- Remove PIN dialog ---- // ---- Remove PIN dialog ----
if (show_pin_remove_) { if (show_pin_remove_) {
if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) { OverlayDialogSpec ov;
ov.title = TR("settings_remove_pin"); ov.p_open = &show_pin_remove_;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 400.0f; ov.idSuffix = "pinremove";
if (BeginOverlayDialog(ov)) {
ImGui::TextWrapped( ImGui::TextWrapped("%s", TR("pin_remove_desc"));
"Enter your current PIN to confirm removal. "
"You will need to use your full passphrase to unlock.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("Current PIN:"); ImGui::TextUnformatted(TR("pin_current_label"));
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_), ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
@@ -2003,7 +2024,7 @@ void App::renderPinDialogs() {
ImGui::Spacing(); ImGui::Spacing();
bool valid = strlen(pin_old_buf_) >= 4; bool valid = strlen(pin_old_buf_) >= 4;
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton("Remove PIN", ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Verifying PIN..."; pin_status_ = "Verifying PIN...";
std::string oldPin(pin_old_buf_); std::string oldPin(pin_old_buf_);
@@ -2040,6 +2061,10 @@ void App::renderPinDialogs() {
ImGui::EndDisabled(); ImGui::EndDisabled();
EndOverlayDialog(); EndOverlayDialog();
} }
// Wipe the PIN buffer if the dialog was dismissed without submitting.
if (!show_pin_remove_) {
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
}
} }
} }

View File

@@ -19,6 +19,9 @@
#include "ui/sidebar.h" #include "ui/sidebar.h"
#include "ui/windows/send_tab.h" #include "ui/windows/send_tab.h"
#include "ui/windows/wallets_dialog.h" #include "ui/windows/wallets_dialog.h"
#include "ui/windows/export_transactions_dialog.h"
#include "ui/windows/export_all_keys_dialog.h"
#include "ui/windows/bootstrap_download_dialog.h"
#include "ui/windows/daemon_download_dialog.h" #include "ui/windows/daemon_download_dialog.h"
#include "ui/windows/xmrig_download_dialog.h" #include "ui/windows/xmrig_download_dialog.h"
#include "ui/pages/settings_page.h" #include "ui/pages/settings_page.h"
@@ -261,11 +264,67 @@ void App::buildSweepCatalog()
// Simple bool-flag modals. // Simple bool-flag modals.
add("modal-import-key", ui::NavPage::Overview, add("modal-import-key", ui::NavPage::Overview,
[](App& a) { a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; }); [](App& a) { a.import_view_mode_ = false; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
add("modal-import-viewkey", ui::NavPage::Overview,
[](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
add("modal-export-key", ui::NavPage::Overview, add("modal-export-key", ui::NavPage::Overview,
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; }); [](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
add("modal-export-transactions", ui::NavPage::Settings,
[](App&) { ui::ExportTransactionsDialog::show(); }, [](App&) { ui::ExportTransactionsDialog::hide(); });
add("modal-export-all-keys", ui::NavPage::Settings,
[](App&) { ui::ExportAllKeysDialog::show(); }, [](App&) { ui::ExportAllKeysDialog::hide(); });
add("modal-bootstrap", ui::NavPage::Settings,
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview, add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); }); [](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
// 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; },
[](App& a) {
a.show_encrypt_dialog_ = false; a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
a.encrypt_status_.clear();
memset(a.encrypt_pass_buf_, 0, sizeof(a.encrypt_pass_buf_));
memset(a.encrypt_confirm_buf_, 0, sizeof(a.encrypt_confirm_buf_));
});
add("modal-change-passphrase", ui::NavPage::Settings,
[](App& a) { a.show_change_passphrase_ = true; },
[](App& a) {
a.show_change_passphrase_ = false; a.encrypt_status_.clear();
memset(a.change_old_pass_buf_, 0, sizeof(a.change_old_pass_buf_));
memset(a.change_new_pass_buf_, 0, sizeof(a.change_new_pass_buf_));
memset(a.change_confirm_buf_, 0, sizeof(a.change_confirm_buf_));
});
// Remove-encryption dialog — the redesigned passphrase-entry phase (reset() keeps the
// workflow in PassphraseEntry; nothing fires the async unlock/export/restart pyramid).
add("modal-decrypt", ui::NavPage::Settings,
[](App& a) { a.wallet_security_workflow_.reset(); a.show_decrypt_dialog_ = true; },
[](App& a) {
a.show_decrypt_dialog_ = false; a.wallet_security_workflow_.reset();
memset(a.decrypt_pass_buf_, 0, sizeof(a.decrypt_pass_buf_));
});
// PIN setup / change / remove dialogs (never fire the async vault store/verify).
add("modal-pin-setup", ui::NavPage::Settings,
[](App& a) { a.show_pin_setup_ = true; },
[](App& a) {
a.show_pin_setup_ = false; a.pin_status_.clear();
memset(a.pin_passphrase_buf_, 0, sizeof(a.pin_passphrase_buf_));
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
});
add("modal-pin-change", ui::NavPage::Settings,
[](App& a) { a.show_pin_change_ = true; },
[](App& a) {
a.show_pin_change_ = false; a.pin_status_.clear();
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
});
add("modal-pin-remove", ui::NavPage::Settings,
[](App& a) { a.show_pin_remove_ = true; },
[](App& a) {
a.show_pin_remove_ = false; a.pin_status_.clear();
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
});
add("modal-about", ui::NavPage::Overview, add("modal-about", ui::NavPage::Overview,
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; }); [](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
add("modal-settings", ui::NavPage::Settings, add("modal-settings", ui::NavPage::Settings,

View File

@@ -175,6 +175,10 @@ bool Settings::load(const std::string& path)
loadScalar(j, "portfolio_style", portfolio_style_); loadScalar(j, "portfolio_style", portfolio_style_);
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0; if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
loadScalar(j, "scanline_enabled", scanline_enabled_); loadScalar(j, "scanline_enabled", scanline_enabled_);
loadScalar(j, "console_line_accents", console_line_accents_);
loadScalar(j, "console_text_color", console_text_color_);
loadScalar(j, "console_zoom", console_zoom_);
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) { if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
hidden_addresses_.clear(); hidden_addresses_.clear();
for (const auto& a : j["hidden_addresses"]) for (const auto& a : j["hidden_addresses"])
@@ -425,6 +429,9 @@ bool Settings::save(const std::string& path)
j["balance_layout"] = balance_layout_; // saved as string ID j["balance_layout"] = balance_layout_; // saved as string ID
j["portfolio_style"] = portfolio_style_; j["portfolio_style"] = portfolio_style_;
j["scanline_enabled"] = scanline_enabled_; j["scanline_enabled"] = scanline_enabled_;
j["console_line_accents"] = console_line_accents_;
j["console_text_color"] = console_text_color_;
j["console_zoom"] = console_zoom_;
j["hidden_addresses"] = json::array(); j["hidden_addresses"] = json::array();
for (const auto& addr : hidden_addresses_) for (const auto& addr : hidden_addresses_)
j["hidden_addresses"].push_back(addr); j["hidden_addresses"].push_back(addr);

View File

@@ -189,6 +189,15 @@ public:
bool getScanlineEnabled() const { return scanline_enabled_; } bool getScanlineEnabled() const { return scanline_enabled_; }
void setScanlineEnabled(bool v) { scanline_enabled_ = v; } void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
// Console output appearance: per-line left color accent bars, and per-channel text coloring.
// (Defaults match the ConsoleTab statics so an upgrade re-save doesn't flip visible behavior.)
bool getConsoleLineAccents() const { return console_line_accents_; }
void setConsoleLineAccents(bool v) { console_line_accents_ = v; }
bool getConsoleTextColor() const { return console_text_color_; }
void setConsoleTextColor(bool v) { console_text_color_ = v; }
float getConsoleZoom() const { return console_zoom_; }
void setConsoleZoom(float v) { console_zoom_ = v; }
// Hidden addresses (addresses hidden from the UI by the user) // Hidden addresses (addresses hidden from the UI by the user)
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; } const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
bool isAddressHidden(const std::string& addr) const { return hidden_addresses_.count(addr) > 0; } bool isAddressHidden(const std::string& addr) const { return hidden_addresses_.count(addr) > 0; }
@@ -482,6 +491,9 @@ private:
std::string balance_layout_ = "classic"; std::string balance_layout_ = "classic";
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero) int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
bool scanline_enabled_ = true; bool scanline_enabled_ = true;
bool console_line_accents_ = true; // left color accent bars in console output
bool console_text_color_ = true; // per-channel text coloring in console output
float console_zoom_ = 1.0f; // console output font zoom factor
std::set<std::string> hidden_addresses_; std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_; std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_; std::map<std::string, AddressMeta> address_meta_;

View File

@@ -101,10 +101,18 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
// (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.) // (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.)
if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded; if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded;
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded; if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded;
if (!key.empty() && key[0] == 's') return KeyKind::Shielded; // A "z"-prefixed key is a shielded viewing key (zxview…); "s"-prefixed is a legacy Sprout key.
if (!key.empty() && (key[0] == 's' || key[0] == 'z')) return KeyKind::Shielded;
return KeyKind::Transparent; return KeyKind::Transparent;
} }
bool WalletSecurityController::isViewingKey(const std::string& key)
{
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
return key.rfind("zxview", 0) == 0;
}
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key) bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
{ {
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
@@ -115,6 +123,11 @@ bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
return false; return false;
} }
bool WalletSecurityController::isRecognizedImportKey(const std::string& key)
{
return isRecognizedPrivateKey(key) || isViewingKey(key);
}
const char* WalletSecurityController::importSuccessMessage(KeyKind kind) const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
{ {
return kind == KeyKind::Shielded return kind == KeyKind::Shielded

View File

@@ -74,9 +74,13 @@ public:
std::size_t minLength = 4); std::size_t minLength = 4);
static KeyKind classifyAddress(const std::string& address); static KeyKind classifyAddress(const std::string& address);
static KeyKind classifyPrivateKey(const std::string& key); static KeyKind classifyPrivateKey(const std::string& key);
// True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only).
static bool isViewingKey(const std::string& key);
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key. // True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
// Single source of truth for the import dialog's indicator AND its submit guard.
static bool isRecognizedPrivateKey(const std::string& key); static bool isRecognizedPrivateKey(const std::string& key);
// As above, but also accepts a recognized shielded viewing key — the import dialog auto-detects
// both, so this is the single source of truth for its indicator AND its submit guard.
static bool isRecognizedImportKey(const std::string& key);
static const char* importSuccessMessage(KeyKind kind); static const char* importSuccessMessage(KeyKind kind);
static std::string decryptExportFileName(std::uint64_t timestampSeconds); static std::string decryptExportFileName(std::uint64_t timestampSeconds);
static void secureClear(std::string& value); static void secureClear(std::string& value);

View File

@@ -1666,11 +1666,21 @@ inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator
// TextColored(label). The label text is passed in (already translated). // TextColored(label). The label text is passed in (already translated).
inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4()) inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4())
{ {
ImGui::PushFont(Type().iconLarge()); ImFont* iconF = Type().iconLarge();
const float rowTop = ImGui::GetCursorPosY();
const float iconH = iconF->LegacySize; // already dp-scaled
const float textH = ImGui::GetFontSize();
ImGui::PushFont(iconF);
ImGui::TextColored(col, ICON_MD_WARNING); ImGui::TextColored(col, ICON_MD_WARNING);
ImGui::PopFont(); ImGui::PopFont();
ImGui::SameLine(); ImGui::SameLine();
// Vertically center the label with the taller warning glyph (stays within the icon's row height).
if (iconH > textH) ImGui::SetCursorPosY(rowTop + (iconH - textH) * 0.5f);
// Wrap so a long warning flows to a second line instead of clipping at the card edge; short
// warnings (which fit) are unaffected.
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(col, "%s", warningLabel); ImGui::TextColored(col, "%s", warningLabel);
ImGui::PopTextWrapPos();
} }
// 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` / // 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` /
@@ -1700,6 +1710,70 @@ inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
} }
} }
// Reference dialog footer: a centered primary + Close (or Cancel) TactileButton pair, fixed width,
// NO divider above (the reference dropped the separator). Sets outPrimary/outClose when the respective
// button is clicked — the caller runs the bodies (so the primary's action can be arbitrary). The
// primary is BeginDisabled'd when !primaryEnabled. btnW<=0 → 130·dp default.
inline void DialogActionFooter(const char* primaryLabel, bool primaryEnabled,
const char* closeLabel, bool& outPrimary, bool& outClose,
float btnW = 0.0f)
{
const float dp = Layout::dpiScale();
if (btnW <= 0.0f) btnW = 130.0f * dp;
const float total = btnW * 2.0f + ImGui::GetStyle().ItemSpacing.x;
const float off = (ImGui::GetContentRegionAvail().x - total) * 0.5f;
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
ImGui::BeginDisabled(!primaryEnabled);
if (TactileButton(primaryLabel, ImVec2(btnW, 0))) outPrimary = true;
ImGui::EndDisabled();
ImGui::SameLine();
if (TactileButton(closeLabel, ImVec2(btnW, 0))) outClose = true;
}
// RAII glass sub-section for grouping an optional/advanced block inside a dialog (the reference's
// scan-height panel). Auto-sizes to its content and paints a subtle glass panel behind it. Uses a
// LOCAL ImDrawListSplitter — safe with BeginCombo popups, unlike the shared ChannelsSplit that
// GlassCardScope uses. padX/padY are the interior insets (in logical px, dp-scaled here). Render the
// section body between construction and destruction; use interiorWidth() for full-width children.
struct GlassSectionScope {
ImDrawListSplitter splitter;
ImDrawList* dl;
ImVec2 cardMin;
float panelW, padX, padY;
GlassPanelSpec spec;
explicit GlassSectionScope(float padXLogical = 12.0f, float padYLogical = 10.0f)
{
const float dp = Layout::dpiScale();
padX = padXLogical * dp;
padY = padYLogical * dp;
spec.rounding = 8.0f * dp;
spec.fillAlpha = 20;
spec.borderAlpha = 30;
dl = ImGui::GetWindowDrawList();
panelW = ImGui::GetContentRegionAvail().x;
cardMin = ImGui::GetCursorScreenPos();
splitter.Split(dl, 2);
splitter.SetCurrentChannel(dl, 1); // content above the panel background
ImGui::Dummy(ImVec2(0, padY));
ImGui::Indent(padX);
}
~GlassSectionScope()
{
ImGui::Unindent(padX);
ImGui::Dummy(ImVec2(0, padY));
const ImVec2 cardMax(cardMin.x + panelW, ImGui::GetCursorScreenPos().y);
splitter.SetCurrentChannel(dl, 0);
DrawGlassPanel(dl, cardMin, cardMax, spec);
splitter.Merge(dl);
}
// Interior content width (panel width minus both side insets) for full-width children.
float interiorWidth() const { return panelW - 2.0f * padX; }
GlassSectionScope(const GlassSectionScope&) = delete;
GlassSectionScope& operator=(const GlassSectionScope&) = delete;
};
} // namespace material } // namespace material
} // namespace ui } // namespace ui
} // namespace dragonx } // namespace dragonx

View File

@@ -492,6 +492,31 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->save(); settings->save();
} }
// Console output color toggles (accent bars + per-channel text color), shown on their own row under
// the effects checkboxes. They mirror the console toolbar buttons and carry no GPU cost, so — unlike
// scanline/theme-effects — they stay enabled in low-spec: the caller has an open
// BeginDisabled(low_spec) around the effects row, so close it, draw these live-bound to the ConsoleTab
// statics, then reopen it. Persisted immediately (the startup restore in App loads them on launch).
static void renderConsoleColorToggles(App* app) {
ImGui::EndDisabled();
bool accents = ConsoleTab::s_line_accents_enabled;
if (ImGui::Checkbox(TrId("console_accents", "con_accents").c_str(), &accents)) {
ConsoleTab::s_line_accents_enabled = accents;
app->settings()->setConsoleLineAccents(accents);
app->settings()->save();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents"));
ImGui::SameLine(0, Layout::spacingLg());
bool textColor = ConsoleTab::s_line_text_color_enabled;
if (ImGui::Checkbox(TrId("console_text_colors", "con_textcol").c_str(), &textColor)) {
ConsoleTab::s_line_text_color_enabled = textColor;
app->settings()->setConsoleTextColor(textColor);
app->settings()->save();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color"));
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
}
// ============================================================================ // ============================================================================
// Settings Page Renderer // Settings Page Renderer
// ============================================================================ // ============================================================================
@@ -874,6 +899,9 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
// Console output color toggles (own row — no GPU cost, enabled even in low-spec).
renderConsoleColorToggles(app);
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above) // Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size; float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
float halfW = (contentW - Layout::spacingLg()) * 0.5f; float halfW = (contentW - Layout::spacingLg()) * 0.5f;
@@ -1126,6 +1154,9 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
// Console output color toggles (own row — no GPU cost, enabled even in low-spec).
renderConsoleColorToggles(app);
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2.0f); availWidth - pad * 2.0f);
ImGui::TextUnformatted(TR("acrylic")); ImGui::TextUnformatted(TR("acrylic"));
@@ -1318,12 +1349,14 @@ void RenderSettingsPage(App* app) {
float naturalW = 0; float naturalW = 0;
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX; naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float impViewW = ImGui::CalcTextSize(TR("settings_import_viewkey")).x + btnPadX;
naturalW += impViewW; // the extra "Import viewing key" button (rendered after Import key)
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f; float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f; float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
// Full-node-only "Seed phrase" + "Migrate to seed" buttons trail the Backup button. // Full-node-only "Seed phrase" + "Migrate to seed" buttons trail the Backup button.
float seedW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_backup_button")).x + btnPadX : 0.0f; float seedW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_backup_button")).x + btnPadX : 0.0f;
float migrateW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_migrate_button")).x + btnPadX : 0.0f; float migrateW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_migrate_button")).x + btnPadX : 0.0f;
float totalW = naturalW + sp * 5; float totalW = naturalW + sp * 6; // 6 base buttons (incl. Import viewing key)
if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + migrateW + sp * 4; if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + migrateW + sp * 4;
float scale = (totalW > contentW) ? contentW / totalW : 1.0f; float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
@@ -1333,6 +1366,11 @@ void RenderSettingsPage(App* app) {
if (TactileButton(r1[0], ImVec2(0, 0), btnFont)) if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
app->showImportKeyDialog(); app->showImportKeyDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]);
// Watch-only shielded viewing-key import (separate button — different key type + scan-height).
ImGui::SameLine(0, scaledSp);
if (TactileButton(TR("settings_import_viewkey"), ImVec2(0, 0), btnFont))
app->showImportViewingKeyDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey"));
ImGui::SameLine(0, scaledSp); ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[1], ImVec2(0, 0), btnFont)) if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
app->showExportKeyDialog(); app->showExportKeyDialog();

View File

@@ -680,19 +680,31 @@ void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::Separator(); ImGui::Separator();
if (ImGui::BeginMenu(TR("portfolio_add_to"))) { if (ImGui::BeginMenu(TR("portfolio_add_to"))) {
const auto& pf = app->settings()->getPortfolioEntries(); const auto& pf = app->settings()->getPortfolioEntries();
if (pf.empty()) { const std::string activeHash = app->activeWalletIdentityHash();
ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false); int shown = 0;
}
for (int pi = 0; pi < (int)pf.size(); pi++) { for (int pi = 0; pi < (int)pf.size(); pi++) {
// Only this wallet's groups (or legacy/unscoped), matching the Market summary —
// don't offer to add this address to another wallet's group.
if (!(pf[pi].scope.empty() || activeHash.empty() || pf[pi].scope == activeHash))
continue;
// PushID keeps the item ID unique even when two groups share a label (a
// label-derived MenuItem ID would collide); a blank label shows a placeholder
// instead of rendering as an invisible row.
ImGui::PushID(pi);
bool inSet = dragonx::data::PortfolioEntryContains(pf[pi].addresses, addr.address); bool inSet = dragonx::data::PortfolioEntryContains(pf[pi].addresses, addr.address);
if (ImGui::MenuItem(pf[pi].label.c_str(), nullptr, inSet)) { const char* lbl = pf[pi].label.empty() ? TR("portfolio_new_entry") : pf[pi].label.c_str();
if (ImGui::MenuItem(lbl, nullptr, inSet)) {
auto entries = pf; // copy, mutate, persist auto entries = pf; // copy, mutate, persist
if (inSet) dragonx::data::PortfolioEntryRemove(entries[pi].addresses, addr.address); if (inSet) dragonx::data::PortfolioEntryRemove(entries[pi].addresses, addr.address);
else dragonx::data::PortfolioEntryAdd(entries[pi].addresses, addr.address); else dragonx::data::PortfolioEntryAdd(entries[pi].addresses, addr.address);
app->settings()->setPortfolioEntries(entries); app->settings()->setPortfolioEntries(entries);
app->settings()->save(); app->settings()->save();
} }
ImGui::PopID();
shown++;
} }
if (shown == 0)
ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false);
ImGui::EndMenu(); ImGui::EndMenu();
} }
effects::ImGuiAcrylic::EndAcrylicPopup(); effects::ImGuiAcrylic::EndAcrylicPopup();

View File

@@ -47,6 +47,9 @@ public:
static bool isOpen() { return s_open; } static bool isOpen() { return s_open; }
// Close the dialog (used by the UI sweep teardown).
static void hide() { s_open = false; s_state = State::Confirm; }
static void render() { static void render() {
if (!s_app) return; if (!s_app) return;
if (!s_open) { if (!s_open) {
@@ -91,21 +94,8 @@ private:
ImGui::TextWrapped("%s", TR("bootstrap_desc")); ImGui::TextWrapped("%s", TR("bootstrap_desc"));
ImGui::Spacing(); ImGui::Spacing();
// Warning card // Care header (reference style: ⚠ + Warning color, wraps if long).
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.9f, 0.6f, 0.0f, 0.08f)); DialogWarningHeader(TR("bootstrap_warning"));
ImGui::BeginChild("##bsWarn", ImVec2(0, 0),
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
ImGuiWindowFlags_NoScrollbar);
{
ImFont* iconFont = Type().iconSmall();
ImGui::PushFont(iconFont);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Warning()), "%s", ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextWrapped("%s", TR("bootstrap_warning"));
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
@@ -124,9 +114,12 @@ private:
ImGui::Spacing(); ImGui::Spacing();
ImGui::Spacing(); ImGui::Spacing();
// Buttons: Download | Mirror | Cancel // Buttons: Download | Mirror | Cancel — centered as a group.
float btnW = 140.0f * dp; float btnW = 140.0f * dp;
float btnSm = 90.0f * dp; float btnSm = 90.0f * dp;
const float rowTotal = btnW * 2.0f + btnSm + ImGui::GetStyle().ItemSpacing.x * 2.0f;
const float rowOff = (ImGui::GetContentRegionAvail().x - rowTotal) * 0.5f;
if (rowOff > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rowOff);
if (TactileButton(TR("download"), ImVec2(btnW, 0))) { if (TactileButton(TR("download"), ImVec2(btnW, 0))) {
startDownload(""); startDownload("");

View File

@@ -14,106 +14,270 @@ constexpr int CountOf(const ConsoleCommandEntry (&)[N])
} }
const ConsoleCommandEntry kControlCommands[] = { const ConsoleCommandEntry kControlCommands[] = {
{"help", "List all commands, or get help for a specified command", "[\"command\"]"}, {"help", "List all commands, or get help for a specified command", "[\"command\"]",
{"getinfo", "Get general info about the node", ""}, "Lists every RPC command. Pass a command name to see the node's detailed help for just that one.",
{"stop", "Stop the daemon", ""}, "help \"getinfo\"", "commands list documentation what available"},
{"getinfo", "Get general info about the node", "",
"A quick snapshot of the node: version, block height, connection count, difficulty and wallet balance.",
"getinfo", "status version summary overview node health"},
{"stop", "Stop the daemon", "",
"Shuts the DragonX node down cleanly. The wallet stays disconnected until the node is started again.",
"stop", "quit shutdown exit close halt turn off", true},
}; };
const ConsoleCommandEntry kNetworkCommands[] = { const ConsoleCommandEntry kNetworkCommands[] = {
{"getnetworkinfo", "Return P2P network state info", ""}, {"getnetworkinfo", "Return P2P network state info", "",
{"getpeerinfo", "Get data about each connected peer", ""}, "The node's networking state: protocol version, which networks are active, listening addresses and relay fee.",
{"getconnectioncount", "Get number of peer connections", ""}, "getnetworkinfo", "network p2p connection status internet"},
{"getnettotals", "Get network traffic statistics", ""}, {"getpeerinfo", "Get data about each connected peer", "",
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\""}, "Detailed data for every peer you are connected to \xE2\x80\x94 address, version, ping time and bytes exchanged.",
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]"}, "getpeerinfo", "peers connections nodes who connected"},
{"listbanned", "List all banned IPs/subnets", ""}, {"getconnectioncount", "Get number of peer connections", "",
{"clearbanned", "Clear all banned IPs", ""}, "How many peers the node is currently connected to. Zero usually means the node is still starting or offline.",
{"ping", "Ping all peers to measure round-trip time", ""}, "getconnectioncount", "peers connections count how many"},
{"getnettotals", "Get network traffic statistics", "",
"Shows the total number of bytes your node has sent and received over the peer-to-peer network since it started. Useful for monitoring how much bandwidth the wallet is using.",
"getnettotals", "bandwidth usage data traffic upload download network stats"},
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\"",
"Manually connects your node to a specific peer by host or IP. Use 'add' to keep it in your list permanently, 'onetry' for a single connection attempt, or 'remove' to drop it. Handy when auto-discovery is not finding peers.",
"addnode \"node.dragonx.is\" \"add\"", "connect peer manual node ip host disconnect no peers not connecting"},
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]",
"Bans or unbans an IP or subnet from connecting. bantime is in seconds (0 = the default 24h); absolute treats it as a Unix timestamp.",
"setban \"192.168.0.6\" \"add\" 86400", "ban block ip peer firewall reject", true},
{"listbanned", "List all banned IPs/subnets", "",
"Lists the IP addresses and subnets your node has banned, usually for misbehaving. Check it to see who you have blocked or to confirm before unbanning a peer.",
"listbanned", "banned blocked ip ban list blacklist peers rejected"},
{"clearbanned", "Clear all banned IPs", "",
"Removes every entry from the ban list, letting all previously-banned peers connect again.",
"clearbanned", "unban reset bans allow clear", true},
{"ping", "Ping all peers to measure round-trip time", "",
"Queues a ping to every connected peer to measure round-trip latency. Results are not returned here but show up in getpeerinfo; use it to check how responsive your connections are.",
"ping", "latency ping peers round trip connection speed responsive test"},
}; };
const ConsoleCommandEntry kBlockchainCommands[] = { const ConsoleCommandEntry kBlockchainCommands[] = {
{"getblockchaininfo", "Get current blockchain state", ""}, {"getblockchaininfo", "Get current blockchain state", "",
{"getblockcount", "Get number of blocks in longest chain", ""}, "State of the chain: height, best block hash, difficulty, verification progress and how far the node has synced.",
{"getbestblockhash", "Get hash of the tip block", ""}, "getblockchaininfo", "chain sync height status blockchain progress"},
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]"}, {"getblockcount", "Get number of blocks in longest chain", "",
{"getblockhash", "Get block hash at a given height", "height"}, "The height of the longest chain \xE2\x80\x94 how many blocks the node has. Compare with the network to check sync.",
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"}, "getblockcount", "height blocks how many chain length synced"},
{"getdifficulty", "Get proof-of-work difficulty", ""}, {"getbestblockhash", "Get hash of the tip block", "",
{"getrawmempool", "Get all txids in mempool", "[verbose]"}, "Shows the ID (hash) of the newest block at the very top of the blockchain. Handy to confirm your node is synced to the current chain tip.",
{"getmempoolinfo", "Get mempool state info", ""}, "getbestblockhash", "latest block tip newest current top synced chain head"},
{"gettxout", "Get details about an unspent output", "\"txid\" n [includemempool]"}, {"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]",
{"coinsupply", "Get coin supply information", "[height]"}, "Returns a block by its hash or height. verbosity 0 = raw hex, 1 = decoded header + txids, 2 = full transactions.",
{"getchaintips", "Get all known chain tips", ""}, "getblock \"0000000000abc123\" 1", "block details transactions header"},
{"getchaintxstats", "Get chain transaction statistics", "[nblocks] [\"blockhash\"]"}, {"getblockhash", "Get block hash at a given height", "height",
{"verifychain", "Verify the blockchain database", "[checklevel] [numblocks]"}, "Looks up the unique ID (hash) of the block at a specific height (block number). Use it to find a block by its position, then feed the hash to other commands.",
{"kvsearch", "Search the blockchain key-value store", "\"key\""}, "getblockhash 1000000", "block by height number find block id lookup height hash"},
{"kvupdate", "Update a key-value pair on-chain", "\"key\" \"value\" days"}, {"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]",
"Returns the header (summary info) of a block, such as its time, difficulty, and links to neighboring blocks. Add true for readable fields instead of raw hex.",
"getblockheader \"0000000000abc123\" true", "block summary header time difficulty inspect block metadata"},
{"getdifficulty", "Get proof-of-work difficulty", "",
"Shows how hard it currently is to mine a new block. A higher number means more mining power is competing on the network.",
"getdifficulty", "mining difficulty how hard hashrate network pow proof of work"},
{"getrawmempool", "Get all txids in mempool", "[verbose]",
"Lists the transaction IDs waiting in the mempool (unconfirmed, not yet in a block). Pass true for extra details like fee and size per transaction.",
"getrawmempool true", "pending unconfirmed transactions waiting mempool queue not confirmed"},
{"getmempoolinfo", "Get mempool state info", "",
"Reports the overall state of the waiting-transaction pool: how many transactions are pending and how much memory they use.",
"getmempoolinfo", "mempool size pending count unconfirmed pool stats memory"},
{"gettxout", "Get details about an unspent output", "\"txid\" n [includemempool]",
"Checks a specific transaction output to see if it is still unspent and how many coins it holds. The last flag controls whether the mempool is also searched.",
"gettxout \"0000000000abc123\" 0 true", "unspent output utxo check coins available spendable balance"},
{"coinsupply", "Get coin supply information", "[height]",
"Reports the coin supply at a block height (transparent, shielded, and total), or at the current tip if no height is given. Useful for checking total supply.",
"coinsupply 1000000", "total supply circulating coins how many emission money supply shielded"},
{"getchaintips", "Get all known chain tips", "",
"Lists all block-chain branch tips the node knows about, including the main chain and any stale forks. Mostly useful for diagnosing chain forks or reorgs.",
"getchaintips", "forks branches chain tips reorg orphan stale diagnose"},
{"getchaintxstats", "Get chain transaction statistics", "[nblocks] [\"blockhash\"]",
"Gives statistics about transactions over a window of recent blocks, like total count and average transactions per second. Useful for gauging network activity.",
"getchaintxstats 2016", "transaction stats network activity tx rate throughput volume history"},
{"verifychain", "Verify the blockchain database", "[checklevel] [numblocks]",
"Runs an integrity check on your local blockchain database to confirm it is not corrupted. A higher checklevel and more blocks mean a deeper, slower check.",
"verifychain 3 288", "check database integrity verify blockchain corruption validate"},
{"kvsearch", "Search the blockchain key-value store", "\"key\"",
"Advanced/developer command. Looks up a value previously stored on-chain under a given key in DragonX's key-value store.",
"kvsearch \"mykey\"", "lookup key value store read on-chain data retrieve kv developer"},
{"kvupdate", "Update a key-value pair on-chain", "\"key\" \"value\" days",
"Advanced/developer command. Stores or updates a key-value entry on the blockchain for a number of days (costs a fee); an optional passphrase can protect the key. Rarely needed by wallet users.",
"kvupdate \"mykey\" \"myvalue\" 30", "write key value store on-chain data set update kv developer publish"},
}; };
const ConsoleCommandEntry kMiningCommands[] = { const ConsoleCommandEntry kMiningCommands[] = {
{"getmininginfo", "Get mining-related information", ""}, {"getmininginfo", "Get mining-related information", "",
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]"}, "Mining status: current difficulty, estimated network hash rate and whether this node is generating blocks.",
{"getgenerate", "Check if the node is mining", ""}, "getmininginfo", "mining hashrate difficulty generate"},
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"}, {"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]",
{"getblocksubsidy", "Get block reward at a given height", "[height]"}, "Turns built-in CPU mining on or off. Pass true or false, and optionally the number of threads (-1 = all cores).",
{"getblocktemplate", "Get block template for mining", "[\"jsonrequestobject\"]"}, "setgenerate true 4", "mine mining generate cpu start stop", true},
{"submitblock", "Submit a mined block to the network", "\"hexdata\""}, {"getgenerate", "Check if the node is mining", "",
"Tells you whether the node's built-in CPU miner is currently turned on and trying to mine blocks. Handy to confirm mining is running or stopped.",
"getgenerate", "mining on off status check am i mining is mining running generate"},
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]",
"Estimates the whole network's total mining power (solutions per second) over recent blocks. A higher number means more miners competing to find each block.",
"getnetworkhashps 120 -1", "network hashrate mining power total speed solutions per second competition"},
{"getblocksubsidy", "Get block reward at a given height", "[height]",
"Shows the block reward (newly minted DRGX) paid for mining the block at a given height. Useful for checking current or future rewards; omit the height to use the current tip.",
"getblocksubsidy 250000", "block reward mining payout coins subsidy how much earn per block"},
{"getblocktemplate", "Get block template for mining", "[\"jsonrequestobject\"]",
"Advanced/developer command: returns the raw data a mining program needs to assemble and work on a candidate block. Normal users mine via a pool or the built-in miner instead.",
"getblocktemplate {\"capabilities\":[\"coinbasetxn\",\"workid\"]}", "mining template block work developer pool solo advanced candidate"},
{"submitblock", "Submit a mined block to the network", "\"hexdata\"",
"Advanced/developer command: submits a fully mined block (as raw hex) to the network. Only used by external mining software that built the block itself.",
"submitblock \"0000000000abc123\"", "submit block mined broadcast solo mining developer advanced send block"},
}; };
const ConsoleCommandEntry kWalletCommands[] = { const ConsoleCommandEntry kWalletCommands[] = {
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]"}, {"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]",
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]"}, "Your confirmed transparent (t-address) balance in DRGX. Shielded funds are NOT included \xE2\x80\x94 use z_gettotalbalance for everything.",
{"z_getbalances", "Get all balances (transparent + shielded)", ""}, "getbalance", "balance money funds how much transparent"},
{"getnewaddress", "Generate a new transparent address", ""}, {"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]",
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]"}, "Your complete balance in DRGX \xE2\x80\x94 transparent plus shielded (private) funds together.",
{"listaddresses", "List all transparent addresses", ""}, "z_gettotalbalance", "balance total money funds shielded private how much"},
{"z_listaddresses", "List all z-addresses", ""}, {"z_getbalances", "Get all balances (transparent + shielded)", "",
{"sendtoaddress", "Send to a specific address", "\"address\" amount"}, "Breaks your balance down across each transparent and shielded address the wallet holds.",
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]"}, "z_getbalances", "balance breakdown addresses funds per address"},
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]"}, {"getnewaddress", "Generate a new transparent address", "",
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]"}, "Creates a fresh transparent (t-) address you can share to receive DRGX.",
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]"}, "getnewaddress", "receive new address deposit transparent create get paid"},
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]"}, {"z_getnewaddress", "Generate a new shielded address", "[\"type\"]",
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"}, "Creates a fresh shielded (z-) address for receiving DRGX privately.",
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"}, "z_getnewaddress", "receive new address private shielded create"},
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"}, {"listaddresses", "List all transparent addresses", "",
{"z_listoperationids", "List all async z operation IDs", ""}, "Lists the transparent (t-) addresses in your wallet.",
{"getwalletinfo", "Get wallet state info", ""}, "listaddresses", "addresses list my accounts transparent"},
{"backupwallet", "Back up wallet to a file", "\"destination\""}, {"z_listaddresses", "List all z-addresses", "",
{"dumpprivkey", "Dump private key for an address", "\"address\""}, "Lists the shielded (z-) addresses in your wallet.",
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]"}, "z_listaddresses", "addresses list my shielded private"},
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\""}, {"sendtoaddress", "Send to a specific address", "\"address\" amount",
{"importwallet", "Import wallet from a dump file", "\"filename\""}, "Sends DRGX to a transparent address. amount is in DRGX. Double-check the address \xE2\x80\x94 sent transactions cannot be reversed.",
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\""}, "sendtoaddress \"RyourRecipientAddr\" 1.5", "send pay transfer money spend", true},
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]"}, {"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]",
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\""}, "Sends DRGX from one address to one or more recipients, with shielded (private) support. Amounts are in DRGX.",
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]"}, "z_sendmany \"RfromAddr\" [{\"address\":\"zs1toAddr\",\"amount\":1.0}]", "send pay private shielded transfer money", true},
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\""}, {"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]",
{"signmessage", "Sign a message with an address key", "\"address\" \"message\""}, "Moves newly mined (coinbase) transparent funds into a private shielded z-address, since mined rewards must be shielded before they can be spent normally. Runs in the background and returns an operation id.",
{"settxfee", "Set the transaction fee per kB", "amount"}, "z_shieldcoinbase \"RyourMiningAddr\" \"zs1yourShieldedAddr\"", "shield mining rewards coinbase private hide mined funds move to shielded"},
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout"}, {"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]",
{"walletlock", "Lock the wallet", ""}, "Combines many small balances (from transparent and/or shielded addresses) into a single destination address in one transaction, to consolidate funds. Runs in the background and returns an operation id.",
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\""}, "z_mergetoaddress [\"RyourAddr\",\"zs1yourShieldedAddr\"] \"zs1destShieldedAddr\"", "merge combine consolidate funds sweep small balances into one address"},
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]",
"Your most recent wallet transactions, newest first \xE2\x80\x94 amounts, addresses and confirmations.",
"listtransactions", "transactions history recent payments received sent"},
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]",
"Lists your spendable transparent coins (unspent outputs), showing which addresses hold funds and how many confirmations each has. Useful to see exactly what's available to spend.",
"listunspent 1", "unspent coins utxo available balance spendable transparent outputs"},
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]",
"Lists your spendable shielded notes (private z-address funds), showing amounts and confirmations. The private-address counterpart to listunspent.",
"z_listunspent 1", "unspent shielded notes private balance available zaddr spendable notes"},
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]",
"Checks whether a background shielded operation (like sending or shielding from a z-address) is still running, has succeeded, or has failed. Use the operation id returned when you started it.",
"z_getoperationstatus [\"opid-00000000-abc1-2345-6789-000000000abc\"]", "check status pending operation progress is my send done async job"},
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]",
"Fetches the final result (transaction id or any error) of a finished background shielded operation and then removes it from memory. Use after the status shows it completed.",
"z_getoperationresult [\"opid-00000000-abc1-2345-6789-000000000abc\"]", "operation result txid outcome finished completed error async send"},
{"z_listoperationids", "List all async z operation IDs", "",
"Lists the ids of all your background shielded operations known to the node, so you can look up their status or results. Takes no arguments.",
"z_listoperationids", "list operations pending jobs opids background async queue"},
{"getwalletinfo", "Get wallet state info", "",
"Wallet summary: balances, transaction count, key-pool size and whether the wallet is encrypted and locked.",
"getwalletinfo", "wallet status info balance encrypted locked"},
{"backupwallet", "Back up wallet to a file", "\"destination\"",
"Saves a copy of your wallet.dat to the given path. Keep the backup somewhere safe and private.",
"backupwallet \"/home/you/drgx-backup.dat\"", "backup save wallet copy protect"},
{"dumpprivkey", "Dump private key for an address", "\"address\"",
"Reveals the private key for an address. Anyone with this key can spend from the address \xE2\x80\x94 never share it.",
"dumpprivkey \"RyourAddr\"", "private key export reveal secret spend", true},
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]",
"Imports a private key so the wallet can spend from its address, then rescans the chain for its history (can be slow).",
"importprivkey \"Uxxxxxxxxxxxx\"", "import private key restore recover add", true},
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\"",
"Writes a human-readable text file of your wallet's transparent private keys for backup. The file is saved into the daemon's configured -exportdir folder. Anyone with it can spend your funds, so store it securely.",
"dumpwallet \"wallet-backup\"", "backup export private keys save wallet dump keys to file", true},
{"importwallet", "Import wallet from a dump file", "\"filename\"",
"Loads keys from a dumpwallet text file back into your wallet and rescans the chain for their funds. Use to restore a backup or add keys from another wallet. Advanced/recovery use.",
"importwallet \"/home/you/wallet-backup.txt\"", "restore import keys recover load wallet backup from file", true},
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\"",
"Reveals the spending key for a shielded address. Anyone with it can spend your private funds \xE2\x80\x94 keep it secret.",
"z_exportkey \"zs1yourAddr\"", "private key export shielded secret spend", true},
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]",
"Adds a shielded (z-address) spending key from another wallet so you can see and spend its private funds here. By default it rescans the chain, which can take a while. Advanced/recovery use.",
"z_importkey \"secret-extended-key-main1yourZKeyHere\"", "import shielded key restore zaddr spending key recover private funds", true},
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\"",
"Exports a view-only key for a shielded address, letting someone see its incoming funds and balance without being able to spend. Safe to share for auditing/monitoring.",
"z_exportviewingkey \"zs1yourShieldedAddr\"", "export viewing key watch only audit read only shielded balance"},
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]",
"Imports a view-only key so your wallet can watch a shielded address's incoming funds without being able to spend them. By default it rescans the chain. For monitoring/auditing.",
"z_importviewingkey \"zviews1yourViewingKeyHere\"", "import viewing key watch only monitor shielded address audit read only", true},
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\"",
"Writes a text-file backup of all your keys including shielded (z-address) spending keys, unlike dumpwallet which is transparent-only. Saved into the daemon's -exportdir folder; anyone with it can spend your funds.",
"z_exportwallet \"full-wallet-backup\"", "backup export all keys shielded zkeys save wallet full to file", true},
{"signmessage", "Sign a message with an address key", "\"address\" \"message\"",
"Signs a message with the private key of one of your addresses, proving you control that address.",
"signmessage \"RyourAddr\" \"hello world\"", "sign message prove ownership"},
{"settxfee", "Set the transaction fee per kB", "amount",
"Sets the fee you pay per kilobyte on transparent transactions, in DRGX. A higher fee can speed confirmation; set 0 to use the default. Applies until you change it or restart.",
"settxfee 0.0001", "set fee transaction cost per kb miner fee change fee rate"},
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout",
"Unlocks an encrypted wallet for the given number of seconds so you can send funds. Your passphrase is typed in plain text \xE2\x80\x94 be sure no one is watching.",
"walletpassphrase \"your passphrase\" 60", "unlock passphrase password encrypted open", true},
{"walletlock", "Lock the wallet", "",
"Removes the encryption key from memory, locking the wallet so its keys can't spend until you unlock it again with your passphrase. Only works on an encrypted wallet.",
"walletlock", "lock wallet secure re-lock protect passphrase disable spending"},
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\"",
"Encrypts the wallet with a passphrase. The node shuts down afterward and you will need the passphrase to send. Encryption cannot be undone \xE2\x80\x94 keep the passphrase safe.",
"encryptwallet \"a strong passphrase\"", "encrypt password protect secure lock", true},
}; };
const ConsoleCommandEntry kRawTransactionCommands[] = { const ConsoleCommandEntry kRawTransactionCommands[] = {
{"createrawtransaction", "Create a raw transaction spending given inputs", "[{\"txid\":\"...\",\"vout\":n},...] {\"address\":amount,...}"}, {"createrawtransaction", "Create a raw transaction spending given inputs", "[{\"txid\":\"...\",\"vout\":n},...] {\"address\":amount,...}",
{"decoderawtransaction", "Decode raw transaction hex string", "\"hexstring\""}, "Advanced/developer command. Builds an unsigned raw transaction from transparent inputs (txid+vout) you pick and transparent outputs you specify; returns only hex that you must still sign and broadcast. Cannot spend to or from shielded (zs1) addresses. Most users should just use the Send tab.",
{"decodescript", "Decode a hex-encoded script", "\"hex\""}, "createrawtransaction [{\"txid\":\"0000000000abc123\",\"vout\":0}] {\"RyourAddr\":1.0}", "build unsigned transaction manually craft raw tx construct spend inputs outputs advanced"},
{"getrawtransaction", "Get raw transaction data by txid", "\"txid\" [verbose]"}, {"decoderawtransaction", "Decode raw transaction hex string", "\"hexstring\"",
{"sendrawtransaction", "Submit raw transaction to the network", "\"hexstring\" [allowhighfees]"}, "Developer tool. Takes a raw transaction hex string and shows its contents in readable JSON (inputs, outputs, amounts, addresses, scripts). Useful for inspecting a transaction before you sign or broadcast it.",
{"signrawtransaction", "Sign a raw transaction with private keys", "\"hexstring\""}, "decoderawtransaction \"0100000001abc123...\"", "inspect raw transaction hex read decode view contents parse tx examine"},
{"fundrawtransaction", "Add inputs to meet output value", "\"hexstring\""}, {"decodescript", "Decode a hex-encoded script", "\"hex\"",
"Developer tool. Turns a hex-encoded script into readable form and shows the address or spending condition it represents. Rarely needed unless you are debugging scripts, redeem scripts, or multisig setups.",
"decodescript \"76a914abc123...88ac\"", "decode script hex readable inspect multisig p2sh redeemscript debug advanced"},
{"getrawtransaction", "Get raw transaction data by txid", "\"txid\" [verbose]",
"Look up a transaction by its txid and return its raw hex, or a decoded readable view if you pass 1 for verbose. Only finds a tx that is in the mempool, has an unspent output, or when the node runs with -txindex.",
"getrawtransaction \"0000000000abc123\" 1", "lookup transaction by id fetch tx details verbose inspect view raw txindex"},
{"sendrawtransaction", "Submit raw transaction to the network", "\"hexstring\" [allowhighfees]",
"Advanced command. Broadcasts an already-signed raw transaction hex to the network so miners can include it in a block. This is the final step after building and signing a transaction by hand.",
"sendrawtransaction \"0100000001abc123...\"", "broadcast submit signed transaction publish push to network relay send hex"},
{"signrawtransaction", "Sign a raw transaction with private keys", "\"hexstring\"",
"Advanced command. Signs a raw transaction using the private keys in your wallet so it becomes valid to broadcast. Returns the signed hex plus a 'complete' flag telling you whether it is fully signed and ready to send.",
"signrawtransaction \"0100000001abc123...\"", "sign raw transaction authorize private key finalize validate ready to broadcast"},
{"fundrawtransaction", "Add inputs to meet output value", "\"hexstring\"",
"Advanced wallet command. Adds enough of your wallet's inputs (plus one change output) to a partial raw transaction so it covers its outputs and fee. The added inputs are unsigned, so you still need to sign and send it afterward.",
"fundrawtransaction \"0100000000010000000000...\"", "add inputs fund coins cover amount select utxos change fee complete transaction"},
}; };
const ConsoleCommandEntry kUtilityCommands[] = { const ConsoleCommandEntry kUtilityCommands[] = {
{"validateaddress", "Validate a transparent address", "\"address\""}, {"validateaddress", "Validate a transparent address", "\"address\"",
{"z_validateaddress", "Validate a z-address", "\"zaddr\""}, "Checks whether a transparent address is well-formed and whether it belongs to your wallet.",
{"estimatefee", "Estimate fee for a transaction", "nblocks"}, "validateaddress \"RsomeAddr\"", "validate check address valid mine"},
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""}, {"z_validateaddress", "Validate a z-address", "\"zaddr\"",
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"}, "Checks whether a shielded (zs1...) address is valid and reports its type (sapling) and, if your wallet is loaded, whether the address belongs to you. Handy for double-checking a private address before sending to it.",
{"invalidateblock", "Mark a block as invalid", "\"hash\""}, "z_validateaddress \"zs1someAddr\"", "check shielded address valid private zaddr is this address mine verify z-address sapling"},
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\""}, {"estimatefee", "Estimate fee for a transaction", "nblocks",
"Estimates the fee (per kilobyte) a transaction likely needs to start confirming within the given number of blocks. Returns -1 if the node hasn't seen enough activity to make a guess.",
"estimatefee 6", "estimate fee cost transaction how much confirmation speed fee per kb suggested network fee"},
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\"",
"Confirms that a signed message really came from the owner of a transparent (R...) address. You supply the address, its base64 signature, and the exact original message; it returns true or false.",
"verifymessage \"RyourAddr\" \"H1234base64signature==\" \"the exact signed message\"", "verify signed message check signature prove ownership authenticate confirm sender validate proof"},
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]",
"Advanced: builds a multi-signature address that needs several keys to approve spending (e.g. 2-of-3). Returns the address and a redeem script you must keep in order to spend from it.",
"createmultisig 2 [\"RfirstAddr\",\"RsecondAddr\",\"RthirdAddr\"]", "multisig shared wallet multiple signatures joint account m of n co-sign approval redeem script"},
{"invalidateblock", "Mark a block as invalid", "\"hash\"",
"Advanced/developer: forces your node to mark a specific block (and everything built on it) as invalid, rolling your local chain back to before it. Only for troubleshooting or testing; reversible with reconsiderblock.",
"invalidateblock \"0000000000abc123\"", "reject block roll back chain reorg force invalid remove block undo block troubleshoot fork", true},
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\"",
"Advanced/developer: clears the invalid mark on a block and its descendants so your node accepts them again and rejoins the normal chain. Used to reverse a previous invalidateblock.",
"reconsiderblock \"0000000000abc123\"", "undo invalidate accept block again re-enable block restore chain reconsider fix rollback fork", true},
}; };
} // namespace } // namespace

View File

@@ -7,8 +7,14 @@ namespace ui {
struct ConsoleCommandEntry { struct ConsoleCommandEntry {
const char* name; const char* name;
const char* desc; const char* desc; // terse one-line summary (always set)
const char* params; const char* params; // parameter template, e.g. "\"address\" amount [comment]"
// Optional novice-facing enrichment (C++17 aggregate defaults — entries that omit these keep
// the empty/false fallbacks, and the command explorer falls back to `desc`):
const char* details = ""; // longer plain-language explanation
const char* example = ""; // one concrete, runnable example line
const char* keywords = ""; // space-separated search synonyms ("balance money funds")
bool destructive = false; // consequential/sensitive -> safety badge + run confirmation
}; };
struct ConsoleCommandCategory { struct ConsoleCommandCategory {

View File

@@ -2,6 +2,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cmath>
#include <sstream> #include <sstream>
namespace dragonx { namespace dragonx {
@@ -116,9 +117,9 @@ std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::str
return lines; return lines;
} }
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command) std::vector<ConsoleArg> ParseConsoleCommandArgsTagged(const std::string& command)
{ {
std::vector<std::string> args; std::vector<ConsoleArg> args;
std::size_t index = 0; std::size_t index = 0;
while (index < command.size()) { while (index < command.size()) {
while (index < command.size() && (command[index] == ' ' || command[index] == '\t')) { while (index < command.size() && (command[index] == ' ' || command[index] == '\t')) {
@@ -126,11 +127,12 @@ std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
} }
if (index >= command.size()) break; if (index >= command.size()) break;
std::string token; ConsoleArg arg;
if (command[index] == '"' || command[index] == '\'') { if (command[index] == '"' || command[index] == '\'') {
arg.quoted = true;
char quote = command[index++]; char quote = command[index++];
while (index < command.size() && command[index] != quote) { while (index < command.size() && command[index] != quote) {
token += command[index++]; arg.text += command[index++];
} }
if (index < command.size()) ++index; if (index < command.size()) ++index;
} else if (command[index] == '[' || command[index] == '{') { } else if (command[index] == '[' || command[index] == '{') {
@@ -140,30 +142,69 @@ std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
while (index < command.size()) { while (index < command.size()) {
if (command[index] == open) ++depth; if (command[index] == open) ++depth;
else if (command[index] == close) --depth; else if (command[index] == close) --depth;
token += command[index++]; arg.text += command[index++];
if (depth == 0) break; if (depth == 0) break;
} }
} else { } else {
while (index < command.size() && command[index] != ' ' && command[index] != '\t') { while (index < command.size() && command[index] != ' ' && command[index] != '\t') {
token += command[index++]; arg.text += command[index++];
} }
} }
if (!token.empty()) args.push_back(token); // A quoted empty string ("") is a real argument; only skip genuinely-empty unquoted tokens.
if (!arg.text.empty() || arg.quoted) args.push_back(std::move(arg));
} }
return args; return args;
} }
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
{
std::vector<std::string> args;
for (auto& a : ParseConsoleCommandArgsTagged(command)) args.push_back(std::move(a.text));
return args;
}
namespace {
// Interpret a bare token as a JSON number only when the WHOLE token is a valid, finite number;
// otherwise return it as a string. Guards against std::stoll silently truncating "1e9"/"123abc"
// (it stops at the first non-digit) and std::stod yielding inf/subnormal for out-of-range input.
nlohmann::json ParseConsoleNumberOrString(const std::string& s)
{
if (!s.empty()) {
try {
std::size_t pos = 0;
long long ll = std::stoll(s, &pos);
if (pos == s.size()) return ll;
} catch (...) {}
try {
std::size_t pos = 0;
double d = std::stod(s, &pos);
if (pos == s.size() && std::isfinite(d)) return d;
} catch (...) {}
}
return s;
}
} // namespace
ConsoleRpcCall BuildConsoleRpcCall(const std::string& command) ConsoleRpcCall BuildConsoleRpcCall(const std::string& command)
{ {
auto args = ParseConsoleCommandArgs(command); auto args = ParseConsoleCommandArgsTagged(command);
ConsoleRpcCall call; ConsoleRpcCall call;
if (args.empty()) return call; if (args.empty()) return call;
call.valid = true; call.valid = true;
call.method = args.front(); call.method = args.front().text;
for (std::size_t argIndex = 1; argIndex < args.size(); ++argIndex) { for (std::size_t argIndex = 1; argIndex < args.size(); ++argIndex) {
const std::string& arg = args[argIndex]; const ConsoleArg& a = args[argIndex];
const std::string& arg = a.text;
// A quoted token is always a string — never coerced — so a genuinely-string argument that
// happens to look numeric (e.g. an all-digit label) is sent as a string, not a number.
if (a.quoted) {
call.params.push_back(arg);
continue;
}
if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) { if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) {
auto parsed = nlohmann::json::parse(arg, nullptr, false); auto parsed = nlohmann::json::parse(arg, nullptr, false);
if (!parsed.is_discarded()) { if (!parsed.is_discarded()) {
@@ -181,15 +222,7 @@ ConsoleRpcCall BuildConsoleRpcCall(const std::string& command)
} else if (arg == "false") { } else if (arg == "false") {
call.params.push_back(false); call.params.push_back(false);
} else { } else {
try { call.params.push_back(ParseConsoleNumberOrString(arg));
if (arg.find('.') != std::string::npos) {
call.params.push_back(std::stod(arg));
} else {
call.params.push_back(std::stoll(arg));
}
} catch (...) {
call.params.push_back(arg);
}
} }
} }

View File

@@ -46,6 +46,13 @@ std::string ConsoleHistoryEntry(const std::vector<std::string>& history,
ConsoleCompletionResult CompleteConsoleCommand(const std::string& input); ConsoleCompletionResult CompleteConsoleCommand(const std::string& input);
std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::string>& matches, std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::string>& matches,
std::size_t maxLineLength = 60); std::size_t maxLineLength = 60);
// A tokenized console argument. `quoted` is true when the token came from a "..."/'...' literal,
// so BuildConsoleRpcCall must send it verbatim as a JSON string (never coerce it to a number/bool).
struct ConsoleArg {
std::string text;
bool quoted = false;
};
std::vector<ConsoleArg> ParseConsoleCommandArgsTagged(const std::string& command);
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command); std::vector<std::string> ParseConsoleCommandArgs(const std::string& command);
ConsoleRpcCall BuildConsoleRpcCall(const std::string& command); ConsoleRpcCall BuildConsoleRpcCall(const std::string& command);
std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& result, std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& result,

View File

@@ -44,6 +44,8 @@ ImU32 ConsoleTab::COLOR_DAEMON = IM_COL32(160, 160, 160, 180);
ImU32 ConsoleTab::COLOR_INFO = IM_COL32(191, 209, 229, 255); ImU32 ConsoleTab::COLOR_INFO = IM_COL32(191, 209, 229, 255);
ImU32 ConsoleTab::COLOR_RPC = IM_COL32(120, 180, 255, 210); ImU32 ConsoleTab::COLOR_RPC = IM_COL32(120, 180, 255, 210);
bool ConsoleTab::s_scanline_enabled = true; bool ConsoleTab::s_scanline_enabled = true;
bool ConsoleTab::s_line_accents_enabled = true;
bool ConsoleTab::s_line_text_color_enabled = true;
float ConsoleTab::s_console_zoom = 1.0f; float ConsoleTab::s_console_zoom = 1.0f;
bool ConsoleTab::s_daemon_messages_enabled = true; bool ConsoleTab::s_daemon_messages_enabled = true;
bool ConsoleTab::s_errors_only_enabled = false; bool ConsoleTab::s_errors_only_enabled = false;
@@ -57,15 +59,16 @@ ConsoleTab* s_rpc_trace_console = nullptr;
std::string rpcTraceTimestamp() std::string rpcTraceTimestamp()
{ {
// Called on RPC worker threads. std::localtime shares a process-wide static tm, so a private
// mutex here can't stop another thread's localtime call from clobbering it between the call and
// the copy. Use the reentrant variant into a local tm instead (matches the codebase pattern).
std::time_t now = std::time(nullptr); std::time_t now = std::time(nullptr);
std::tm localTime{}; std::tm localTime{};
static std::mutex timeMutex; #ifdef _WIN32
{ localtime_s(&localTime, &now);
std::lock_guard<std::mutex> lock(timeMutex); #else
if (const std::tm* current = std::localtime(&now)) { localtime_r(&now, &localTime);
localTime = *current; #endif
}
}
char buffer[16]; char buffer[16];
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime); std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime);
@@ -188,6 +191,10 @@ void ConsoleTab::refreshColors()
ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const
{ {
using namespace material; using namespace material;
// Monochrome mode: collapse every channel (and JSON syntax role) to the neutral result-body
// color. COLOR_RESULT is theme-correct (contrast-floored on light terminals in refreshColors),
// so text stays readable. The left accent bars are gated separately and stay independent.
if (!s_line_text_color_enabled) return COLOR_RESULT;
switch (channel) { switch (channel) {
// COLOR_* channels are already palette-derived + contrast-floored in refreshColors(). The // COLOR_* channels are already palette-derived + contrast-floored in refreshColors(). The
// roles below are computed live from the theme palette, so floor them to a readable contrast on // roles below are computed live from the theme palette, so floor them to a readable contrast on
@@ -235,12 +242,11 @@ ConsoleTab::ConsoleTab()
s_rpc_trace_console = this; s_rpc_trace_console = this;
} }
rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) { rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
ConsoleTab* console = nullptr; // Dereference under the lock, not after releasing it: ~ConsoleTab clears
{ // s_rpc_trace_console under the same lock, so holding it here blocks destruction until the
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex); // call returns — closing the shutdown use-after-free window (this fires on RPC worker threads).
console = s_rpc_trace_console; std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
} if (s_rpc_trace_console) s_rpc_trace_console->addRpcTraceLine(source, method);
if (console) console->addRpcTraceLine(source, method);
}); });
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled); rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
@@ -278,6 +284,14 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
} }
} }
// Run a command the RPC-reference modal's "Insert & run" queued (the modal has no executor, so it
// defers to here). Done before drain so the echoed "> cmd" line surfaces this frame.
if (!pending_submit_.empty()) {
std::string cmd;
cmd.swap(pending_submit_);
submitConsoleCommand(exec, cmd);
}
// Pull passive log lines (daemon/xmrig output, or the lite diagnostics ring) and any // Pull passive log lines (daemon/xmrig output, or the lite diagnostics ring) and any
// completed command results from the backend executor. // completed command results from the backend executor.
exec.pollLogLines([this](const std::string& l, ConsoleChannel c) { addLine(l, c); }); exec.pollLogLines([this](const std::string& l, ConsoleChannel c) { addLine(l, c); });
@@ -292,6 +306,10 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
// model_) and the selection/scroll state are touched only on this (main) thread. // model_) and the selection/scroll state are touched only on this (main) thread.
drainModel(); drainModel();
// Compute the filtered visible-line set once per frame, BEFORE the toolbar — so its "<N> matches"
// label reflects the current frame (not last frame's stale count). renderOutput reuses the result.
computeVisibleLines(has_text_filter_, filter_lower_);
// Main console layout // Main console layout
ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false, ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
@@ -526,6 +544,7 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
// Commands reference button (full-node RPC reference only) // Commands reference button (full-node RPC reference only)
if (exec.hasRpcReference()) { if (exec.hasRpcReference()) {
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) { if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
command_search_[0] = '\0'; // fresh search each open (dismiss paths don't all reset it)
show_commands_popup_ = true; show_commands_popup_ = true;
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
@@ -544,6 +563,33 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
// Output filter input // Output filter input
drawFilterInput(); drawFilterInput();
// Color-accent toggle: hide/show the per-line left accent bars (appearance, grouped with zoom).
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
{
float btnSz = ImGui::GetFrameHeight();
const char* icon = s_line_accents_enabled ? ICON_MD_FORMAT_COLOR_FILL : ICON_MD_FORMAT_COLOR_RESET;
if (!s_line_accents_enabled)
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
if (TactileButton(icon, ImVec2(btnSz, btnSz), Type().iconMed()))
s_line_accents_enabled = !s_line_accents_enabled;
if (!s_line_accents_enabled) ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents"));
}
// Text-color toggle: colored per-channel text vs. monochrome (grouped with the accent toggle).
ImGui::SameLine();
{
float btnSz = ImGui::GetFrameHeight();
if (!s_line_text_color_enabled)
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
if (TactileButton(ICON_MD_FORMAT_COLOR_TEXT, ImVec2(btnSz, btnSz), Type().iconMed()))
s_line_text_color_enabled = !s_line_text_color_enabled;
if (!s_line_text_color_enabled) ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color"));
}
// Zoom +/- buttons (right side of toolbar) // Zoom +/- buttons (right side of toolbar)
ImGui::SameLine(); ImGui::SameLine();
ImGui::Spacing(); ImGui::Spacing();
@@ -724,11 +770,8 @@ void ConsoleTab::renderOutput()
output_scroll_y_ = ImGui::GetScrollY(); output_scroll_y_ = ImGui::GetScrollY();
scanline_rows_.clear(); scanline_rows_.clear();
// Build the filtered visible-line index list BEFORE mouse handling (screenToTextPos maps // visible_indices_ / has_text_filter_ / filter_lower_ were already computed once at the top of
// through visible_indices_). Also yields the text-filter state used for highlighting. // render() (before the toolbar). screenToTextPos maps through visible_indices_, so it's ready.
bool has_text_filter = false;
std::string filter_lower;
computeVisibleLines(has_text_filter, filter_lower);
// Calculate wrapped heights AND build sub-row segments for each visible line. Each // Calculate wrapped heights AND build sub-row segments for each visible line. Each
// segment records which bytes of the source text appear on that visual row, so // segment records which bytes of the source text appear on that visual row, so
@@ -748,12 +791,15 @@ void ConsoleTab::renderOutput()
win_min.y + ImGui::GetWindowHeight()); win_min.y + ImGui::GetWindowHeight());
bool mouse_in_output = (mouse_pos.x >= win_min.x && mouse_pos.x < win_max.x && bool mouse_in_output = (mouse_pos.x >= win_min.x && mouse_pos.x < win_max.x &&
mouse_pos.y >= win_min.y && mouse_pos.y < win_max.y && mouse_pos.y >= win_min.y && mouse_pos.y < win_max.y &&
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup)); !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup) &&
// The RPC reference is a BeginOverlayDialog (not an ImGui popup), so it
// isn't caught above; suppress the output's text cursor + selection under it.
!show_commands_popup_);
handleOutputInteraction(mouse_pos, mouse_in_output); handleOutputInteraction(mouse_pos, mouse_in_output);
// Draw the visible lines: accent bars, JSON indent guides, selection + filter highlight, // Draw the visible lines: accent bars, JSON indent guides, selection + filter highlight,
// scanline capture, and the text itself. // scanline capture, and the text itself.
drawVisibleLines(padX, line_height, has_text_filter, filter_lower); drawVisibleLines(padX, line_height, has_text_filter_, filter_lower_);
ImGui::Unindent(padX); ImGui::Unindent(padX);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -776,7 +822,7 @@ void ConsoleTab::renderOutput()
} }
// Filter indicator (text filter only — daemon toggle is already visible in toolbar) // Filter indicator (text filter only — daemon toggle is already visible in toolbar)
if (has_text_filter) { if (has_text_filter_) {
char filterBuf[128]; char filterBuf[128];
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"), snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
static_cast<int>(visible_indices_.size()), model_.size()); static_cast<int>(visible_indices_.size()), model_.size());
@@ -815,6 +861,10 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow
bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled || bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled ||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled || !outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
outputFilter.errorsOnly; outputFilter.errorsOnly;
// Folding is only honored in the unfiltered view; the filtered view is flat. Record it so the
// fold triangles aren't drawn/clickable while filtering (else clicks would silently mutate the
// collapsed flag with no visible effect and the glyph would disagree with the rendered block).
folding_active_ = !has_filter;
visible_indices_.clear(); visible_indices_.clear();
const int n = static_cast<int>(model_.size()); const int n = static_cast<int>(model_.size());
if (has_filter) { if (has_filter) {
@@ -850,8 +900,10 @@ void ConsoleTab::handleOutputInteraction(ImVec2 mousePos, bool mouseInOutput)
if (mouseInOutput) { if (mouseInOutput) {
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
} }
// Selection drag lifecycle (continues even if the mouse leaves the window). // Selection drag lifecycle (continues even if the mouse leaves the window). Ignore clicks in the
if (mouseInOutput && io.MouseClicked[0]) { // left gutter (< output_origin_.x) — that strip holds the accent bar + fold triangles, so a fold
// toggle there shouldn't begin (and thus clear) a text selection.
if (mouseInOutput && io.MouseClicked[0] && mousePos.x >= output_origin_.x) {
selection_.beginDrag(screenToTextPos(mousePos)); selection_.beginDrag(screenToTextPos(mousePos));
} }
if (selection_.dragging() && io.MouseDown[0]) { if (selection_.dragging() && io.MouseDown[0]) {
@@ -940,7 +992,8 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
// Left-edge channel accent bar, drawn in the padX margin so it never overlaps // Left-edge channel accent bar, drawn in the padX margin so it never overlaps
// the text or the selection highlight. Same color that tints the toolbar toggle. // the text or the selection highlight. Same color that tints the toolbar toggle.
if (line.channel != ConsoleChannel::None) { // Suppressed when the toolbar's color-accent toggle is off (cleaner monochrome gutter).
if (s_line_accents_enabled && line.channel != ConsoleChannel::None) {
ImU32 barCol = channelAccentColor(line.channel); ImU32 barCol = channelAccentColor(line.channel);
if (barCol != 0) { if (barCol != 0) {
float barW = 3.0f * Layout::hScale(); float barW = 3.0f * Layout::hScale();
@@ -961,16 +1014,19 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
for (size_t c = 2; c < leading; c += 2) { for (size_t c = 2; c < leading; c += 2) {
float gx = lineOrigin.x + static_cast<float>(c) * spaceW; float gx = lineOrigin.x + static_cast<float>(c) * spaceW;
dl->AddLine(ImVec2(gx, lineOrigin.y), ImVec2(gx, lineOrigin.y + totalH), dl->AddLine(ImVec2(gx, lineOrigin.y), ImVec2(gx, lineOrigin.y + totalH),
IM_COL32(255, 255, 255, 20), 1.0f); IM_COL32(255, 255, 255, 20), 1.0f * Layout::dpiScale());
} }
} }
} }
// JSON fold toggle — a small triangle in the left gutter for block-opener lines. // JSON fold toggle — a small triangle in the left gutter for block-opener lines.
// Openers carry no accent bar (result/JSON channels), so the gutter is free. // Openers carry no accent bar (result/JSON channels), so the gutter is free. Only shown in the
if (line.foldSpan > 0) { // unfiltered view, where folding actually applies (see folding_active_ in computeVisibleLines).
float sz = fontSize * 0.30f; if (folding_active_ && line.foldSpan > 0) {
float cx = output_origin_.x - padX + sz + 1.0f * Layout::hScale(); // Size from the (DPI/density-scaled) gutter width, not the zoomed font, and center it in
// the gutter band [origin-padX, origin) so the glyph and its clickable cell stay aligned.
float sz = std::min(fontSize * 0.30f, padX * 0.34f);
float cx = output_origin_.x - padX * 0.5f;
float cy = lineOrigin.y + lineHeight * 0.5f; float cy = lineOrigin.y + lineHeight * 0.5f;
ImU32 triCol = WithAlpha(OnSurfaceMedium(), 210); ImU32 triCol = WithAlpha(OnSurfaceMedium(), 210);
if (line.collapsed) { if (line.collapsed) {
@@ -1140,18 +1196,21 @@ void ConsoleTab::drawNewOutputIndicator()
// "New output" indicator when the user is scrolled up and new lines arrived. // "New output" indicator when the user is scrolled up and new lines arrived.
if (scroll_.autoScroll() || scroll_.newLines() <= 0) return; if (scroll_.autoScroll() || scroll_.newLines() <= 0) return;
float indicW = 140.0f; // The box geometry is hand-drawn absolute px, so scale it by dpiScale() (the font metrics below
float indicH = 24.0f; // are already scaled — left alone). Without this the pill renders native-size on a HiDPI display.
float dp = Layout::dpiScale();
float indicW = 140.0f * dp;
float indicH = 24.0f * dp;
ImDrawList* dlInd = ImGui::GetWindowDrawList(); ImDrawList* dlInd = ImGui::GetWindowDrawList();
ImVec2 wMin = ImGui::GetWindowPos(); ImVec2 wMin = ImGui::GetWindowPos();
ImVec2 wSize = ImGui::GetWindowSize(); ImVec2 wSize = ImGui::GetWindowSize();
float ix = wMin.x + (wSize.x - indicW) * 0.5f; float ix = wMin.x + (wSize.x - indicW) * 0.5f;
float iy = wMin.y + wSize.y - indicH - 8.0f; float iy = wMin.y + wSize.y - indicH - 8.0f * dp;
ImVec2 iMin(ix, iy); ImVec2 iMin(ix, iy);
ImVec2 iMax(ix + indicW, iy + indicH); ImVec2 iMax(ix + indicW, iy + indicH);
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f); dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f * dp);
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f); dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f * dp, 0, 1.0f * dp);
char buf[48]; char buf[48];
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines()); snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
@@ -1357,9 +1416,10 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
std::transform(first.begin(), first.end(), first.begin(), std::transform(first.begin(), first.end(), first.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); }); [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
} }
// 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the pending state. // 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the
static bool stopConfirmPending = false; // pending state. Member (not a function-local static) so clear() can also cancel it — otherwise a
if (first != "stop") stopConfirmPending = false; // toolbar/context-menu clear between the two 'stop's would leave a stale arm and skip the warning.
if (first != "stop") stop_confirm_pending_ = false;
auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); }; auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); };
if (first == "clear" || first == "cls") { if (first == "clear" || first == "cls") {
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history). // View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
@@ -1370,12 +1430,12 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
} else if (first == "quit" || first == "exit") { } else if (first == "quit" || first == "exit") {
addLine(TR("console_quit_note"), ConsoleChannel::Info); addLine(TR("console_quit_note"), ConsoleChannel::Info);
} else if (first == "stop") { } else if (first == "stop") {
if (!stopConfirmPending) { if (!stop_confirm_pending_) {
stopConfirmPending = true; stop_confirm_pending_ = true;
addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.", addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.",
ConsoleChannel::Warning); ConsoleChannel::Warning);
} else { } else {
stopConfirmPending = false; stop_confirm_pending_ = false;
if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error); if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error);
else exec.submit(cmd); else exec.submit(cmd);
} }
@@ -1390,192 +1450,473 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
namespace { namespace {
// True if any of a command's name/desc/params contains `filterLower` (already lowercased). // True if any of a command's name/desc/params contains `filterLower` (already lowercased).
// An empty filter matches everything. // An empty filter matches everything.
bool consoleCommandMatchesFilter(const char* name, const char* desc, const char* params, static bool lcContains(const char* s, const std::string& needleLower)
const std::string& filterLower)
{ {
if (filterLower.empty()) return true; std::string v(s);
auto contains = [&filterLower](const char* s) { std::transform(v.begin(), v.end(), v.begin(), ::tolower);
std::string v(s); return v.find(needleLower) != std::string::npos;
std::transform(v.begin(), v.end(), v.begin(), ::tolower); }
return v.find(filterLower) != std::string::npos; static bool lcStartsWith(const char* s, const std::string& needleLower)
}; {
return contains(name) || contains(desc) || contains(params); std::string v(s);
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
return v.rfind(needleLower, 0) == 0;
} }
// Draw a command's parameter string into the current table cell, dimming optional [params]. // Search relevance of a command for a lowercased query. -1 = no match; higher = better. Keywords
void drawConsoleCommandParams(const char* params) // let novices find a command by intent ("balance" -> getbalance) even without the exact name.
int consoleCommandRank(const ConsoleCommandEntry& cmd, const std::string& q)
{ {
using namespace material; if (q.empty()) return 0;
const char* p = params; if (lcStartsWith(cmd.name, q)) return 100;
bool first = true; if (lcContains(cmd.name, q)) return 60;
while (*p) { if (cmd.keywords[0] && lcContains(cmd.keywords, q)) return 40;
const char* bracketStart = strchr(p, '['); if (lcContains(cmd.desc, q)) return 25;
if (bracketStart) { if (cmd.details[0] && lcContains(cmd.details, q)) return 15;
// Draw the required part before the bracket. if (lcContains(cmd.params, q)) return 10;
if (bracketStart > p) { return -1;
if (!first) ImGui::SameLine(0, 0); }
std::string req(p, bracketStart);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", req.c_str()); // Split a parameter template into top-level tokens, respecting quote/bracket nesting so a space
first = false; // inside "..." or [{...}] doesn't split (e.g. `"address" [{"a":1}]` -> {`"address"`, `[{"a":1}]`}).
} std::vector<std::string> splitParamTemplate(const char* params)
const char* bracketEnd = strchr(bracketStart, ']'); {
if (bracketEnd) { std::vector<std::string> out;
if (!first) ImGui::SameLine(0, 0); std::string tok;
std::string opt(bracketStart, bracketEnd + 1); int depth = 0;
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", opt.c_str()); char q = 0;
first = false; for (const char* p = params; *p; ++p) {
p = bracketEnd + 1; char c = *p;
} else { if (q) { tok += c; if (c == q) q = 0; continue; }
if (!first) ImGui::SameLine(0, 0); if (c == '"' || c == '\'') { q = c; tok += c; continue; }
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", bracketStart); if (c == '[' || c == '{') { depth++; tok += c; continue; }
first = false; if (c == ']' || c == '}') { if (depth > 0) depth--; tok += c; continue; }
break; if (c == ' ' && depth == 0) { if (!tok.empty()) { out.push_back(tok); tok.clear(); } continue; }
} tok += c;
} else {
if (!first) ImGui::SameLine(0, 0);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", p);
first = false;
break;
}
} }
if (!tok.empty()) out.push_back(tok);
return out;
}
// A parsed parameter for the builder form. type in {string, number, json}; `raw` is the original
// template token (used as a placeholder when a required field is left empty).
struct ConsoleParamSpec {
std::string label;
std::string type;
bool optional = false;
std::string raw;
};
// Best-effort parse of a param template into fillable fields. The templates are human-readable, not
// a strict schema, so heuristics: [word] / ["word"] = an optional scalar; a [ / { with JSON content
// (a brace or comma) = a JSON field the user pastes; otherwise a required string/number.
std::vector<ConsoleParamSpec> parseParamSpecs(const char* params)
{
std::vector<ConsoleParamSpec> out;
for (const std::string& t : splitParamTemplate(params)) {
ConsoleParamSpec p;
p.raw = t;
std::string inner = t;
if (inner.size() >= 2 && inner.front() == '[' && inner.back() == ']') {
std::string body = inner.substr(1, inner.size() - 2);
bool looksJson = body.find('{') != std::string::npos ||
body.find(',') != std::string::npos ||
(!body.empty() && body.front() == '[');
if (!looksJson) { p.optional = true; inner = body; } // optional scalar wrapper
}
char c0 = inner.empty() ? 0 : inner.front();
if (c0 == '"' || c0 == '\'') {
p.type = "string";
if (inner.size() >= 2 && inner.back() == c0) inner = inner.substr(1, inner.size() - 2);
p.label = inner;
} else if (c0 == '{' || c0 == '[') {
p.type = "json";
p.label = "json";
} else {
p.type = "number";
p.label = inner;
}
out.push_back(p);
}
return out;
}
// Translate a command-category name from the static reference tables (English) for display. The
// per-command descriptions stay in English (technical RPC docs); only the 7 category labels are i18n'd.
const char* consoleCategoryLabel(const char* name)
{
if (!std::strcmp(name, "Control")) return TR("console_cat_control");
if (!std::strcmp(name, "Network")) return TR("console_cat_network");
if (!std::strcmp(name, "Blockchain")) return TR("console_cat_blockchain");
if (!std::strcmp(name, "Mining")) return TR("console_cat_mining");
if (!std::strcmp(name, "Wallet")) return TR("console_cat_wallet");
if (!std::strcmp(name, "Raw Transactions")) return TR("console_cat_raw_transactions");
if (!std::strcmp(name, "Utility")) return TR("console_cat_utility");
return name;
} }
} // namespace } // namespace
void ConsoleTab::insertCommandToInput(const ConsoleCommandEntry& cmd)
{
// Fill the console input with the command (+ its param template) and close the modal — the user
// reviews/edits it and presses Enter to run.
if (cmd.params[0] != '\0')
snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
else {
strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
input_buffer_[sizeof(input_buffer_) - 1] = '\0';
}
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel)
{
using namespace material;
float dp = Layout::dpiScale();
ImFont* mono = Type().mono();
// Heading: command name, then category + a safety badge for consequential commands.
ImGui::PushFont(Type().subtitle1());
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", cmd.name);
ImGui::PopFont();
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", catLabel);
if (cmd.destructive) {
ImGui::SameLine(0, Layout::spacingMd());
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
ImGui::PushFont(Type().iconSmall());
ImGui::TextColored(warn, ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(warn, "%s", TR("console_ref_destructive"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Plain-language explanation (falls back to the terse summary when not enriched).
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurface()), "%s",
cmd.details[0] ? cmd.details : cmd.desc);
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Parameters — an editable form that assembles the command. Reset the fields whenever the
// selected command changes so values don't carry across commands.
if (cmd_param_owner_ != &cmd) {
for (auto& b : cmd_param_bufs_) b[0] = '\0';
cmd_param_owner_ = &cmd;
}
std::vector<ConsoleParamSpec> specs = parseParamSpecs(cmd.params);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_parameters"));
if (specs.empty()) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", TR("console_ref_no_params"));
} else {
float labelW = 110.0f * dp;
ImGui::Indent(Layout::spacingSm());
for (size_t k = 0; k < specs.size() && k < 6; k++) {
const ConsoleParamSpec& s = specs[k];
ImGui::PushID((int)k);
ImGui::PushFont(mono);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(s.optional ? OnSurfaceMedium() : OnSurface()),
"%s", s.label.c_str());
ImGui::PopFont();
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(-1);
std::string hint = s.optional
? (s.type + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : s.type;
ImGui::InputTextWithHint("##pv", hint.c_str(), cmd_param_bufs_[k], sizeof(cmd_param_bufs_[k]));
ImGui::PopID();
}
ImGui::Unindent(Layout::spacingSm());
}
// Assemble the command from the filled fields. A required field left empty keeps its placeholder
// token (so Insert still shows what's needed) and marks the command incomplete (Insert & run
// disabled). String values are auto-quoted; trailing empty optionals are omitted.
auto trimStr = [](const std::string& s) -> std::string {
size_t a = s.find_first_not_of(" \t");
if (a == std::string::npos) return std::string();
return s.substr(a, s.find_last_not_of(" \t") - a + 1);
};
// Include fields up to the last one that is filled OR required; trailing empty optionals are
// dropped. An empty field that must still be included (a blank required field, or a gap before a
// later filled field — positional args can't skip a middle slot) keeps its placeholder and marks
// the command incomplete, so Insert & run stays disabled and no typed value is silently lost.
int lastNeeded = -1;
for (size_t k = 0; k < specs.size() && k < 6; k++)
if (!trimStr(cmd_param_bufs_[k]).empty() || !specs[k].optional) lastNeeded = (int)k;
bool complete = specs.size() <= 6;
std::string built = cmd.name;
for (int k = 0; k <= lastNeeded; k++) {
std::string val = trimStr(cmd_param_bufs_[k]);
if (val.empty()) {
built += " " + specs[k].raw;
complete = false;
} else {
if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
val.front() != '[' && val.front() != '{')
val = "\"" + val + "\"";
built += " " + val;
}
}
// Live "Builds" preview when the command takes parameters.
if (!specs.empty()) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_builds"));
ImGui::PushFont(mono);
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(FloorLight(IM_COL32(150, 200, 150, 255))), "%s", built.c_str());
ImGui::PopTextWrapPos();
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Example (curated commands only) — reference alongside the builder.
if (cmd.example[0]) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_example"));
ImGui::PushFont(mono);
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(FloorLight(IM_COL32(150, 200, 150, 255))), "%s", cmd.example);
ImGui::PopTextWrapPos();
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
}
// Actions (or the destructive run confirmation) — Insert/run use the assembled command.
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (run_confirm_cmd_ == &cmd) {
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
ImGui::PushFont(Type().iconSmall());
ImGui::TextColored(warn, ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(warn, TR("console_ref_run_confirm"), cmd.name);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float cbw = 120.0f * dp;
if (material::TactileButton(TR("console_ref_cancel"), ImVec2(cbw, 0))) run_confirm_cmd_ = nullptr;
ImGui::SameLine();
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
pending_submit_ = built;
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
} else {
if (material::TactileButton(TR("console_ref_insert"), ImVec2(200.0f * dp, 0))) {
snprintf(input_buffer_, sizeof(input_buffer_), "%s", built.c_str());
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
// Insert & run: enabled once every required field is filled (a template with unfilled
// placeholders would just error).
ImGui::SameLine();
ImGui::BeginDisabled(!complete);
if (material::TactileButton(TR("console_ref_insert_run"), ImVec2(140.0f * dp, 0))) {
if (cmd.destructive) {
run_confirm_cmd_ = &cmd;
} else {
pending_submit_ = built;
command_search_[0] = '\0';
show_commands_popup_ = false;
}
}
ImGui::EndDisabled();
}
}
void ConsoleTab::renderCommandsPopup() void ConsoleTab::renderCommandsPopup()
{ {
using namespace material; using namespace material;
float dp = Layout::dpiScale();
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
material::OverlayDialogSpec ov; material::OverlayDialogSpec ov;
ov.title = TR("console_rpc_reference"); ov.p_open = &show_commands_popup_; ov.title = TR("console_rpc_reference");
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading ov.p_open = &show_commands_popup_;
ov.cardWidth = popW; ov.cardBottomViewportRatio = 0.94f; // keep authored width ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
// FIXED height (fills most of the viewport) instead of auto-height: the command list's ov.cardWidth = 960.0f; // wide enough for two panes
// fill-height child reads GetContentRegionAvail() inside the dialog child, which is only stable ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.74f / dp; // fixed; both panes fill it
// when the child is fixed-height. Auto-height made it self-referential and it slid content ov.idSuffix = "cmdref";
// off-screen after a monitor move (same class of bug as the Wallets modal). if (!material::BeginOverlayDialog(ov)) return;
ov.cardHeight = ImGui::GetMainViewport()->Size.y * 0.84f / Layout::dpiScale();
if (!material::BeginOverlayDialog(ov)) { // Esc dismisses (a first Esc cancels a pending run-confirm). Not built into the overlay.
return; if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
if (run_confirm_cmd_) run_confirm_cmd_ = nullptr;
else show_commands_popup_ = false;
} }
// Search filter // Search box — auto-focus on open so the user can type immediately. Enter inserts the selected
static char cmdFilter[128] = {0}; // command's template; reset the param-builder fields on open.
if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); cmd_param_owner_ = nullptr; }
ImGui::SetNextItemWidth(-1); ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter)); bool searchEnter = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"),
command_search_, sizeof(command_search_),
ImGuiInputTextFlags_EnterReturnsTrue);
bool searchChanged = ImGui::IsItemEdited();
bool searchActive = ImGui::IsItemActive();
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
std::string q(command_search_);
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
const bool searching = !q.empty();
const auto& categories = consoleCommandCategories(); const auto& categories = consoleCommandCategories();
std::string filter(cmdFilter); // Flat display order of (cat,idx): ranked when searching, category order when browsing. Drives
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower); // keyboard nav + auto-selection; the browse view still renders grouped headers below.
std::vector<std::pair<int, int>> order;
ImGui::BeginChild("CmdListScroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - Layout::spacingXs()), if (searching) {
false); std::vector<std::pair<int, std::pair<int, int>>> scored; // (score, (cat,idx))
for (int c = 0; c < (int)categories.size(); c++)
ImGui::PushFont(Type().caption()); for (int i = 0; i < categories[c].count; i++) {
int s = consoleCommandRank(categories[c].commands[i], q);
float cmdMinWidth = schema::UI().drawElement("tabs.console", "cmd-min-width").sizeOr(500.0f); if (s >= 0) scored.push_back({s, {c, i}});
float popupInnerW = ImGui::GetContentRegionAvail().x;
bool showParams = popupInnerW >= cmdMinWidth;
int catIdx = 0;
for (const auto& cat : categories) {
// Count matching commands in this category
int matchCount = 0;
if (filter.empty()) {
matchCount = cat.count;
} else {
for (int i = 0; i < cat.count; i++) {
if (consoleCommandMatchesFilter(cat.commands[i].name, cat.commands[i].desc,
cat.commands[i].params, filter)) {
matchCount++;
}
} }
} std::stable_sort(scored.begin(), scored.end(),
if (matchCount == 0) { catIdx++; continue; } [](const auto& a, const auto& b) { return a.first > b.first; });
for (auto& s : scored) order.push_back(s.second);
// Default-open only the first category (Control); collapse the rest } else {
ImGuiTreeNodeFlags headerFlags = (catIdx == 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0; for (int c = 0; c < (int)categories.size(); c++)
// When filtering, open all matching categories for (int i = 0; i < categories[c].count; i++) order.push_back({c, i});
if (!filter.empty()) headerFlags = ImGuiTreeNodeFlags_DefaultOpen;
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
// Show match count badge when filtering
char headerLabel[128];
if (!filter.empty()) {
snprintf(headerLabel, sizeof(headerLabel), "%s (%d)", cat.name, matchCount);
} else {
snprintf(headerLabel, sizeof(headerLabel), "%s", cat.name);
}
bool open = ImGui::CollapsingHeader(headerLabel, headerFlags);
ImGui::PopStyleColor();
catIdx++;
if (open) {
float nameColW = schema::UI().drawElement("tabs.console", "cmd-name-col-width").size * Layout::hScale();
float paramsColW = schema::UI().drawElement("tabs.console", "cmd-params-col-width").size * Layout::hScale();
int numCols = showParams ? 3 : 2;
if (ImGui::BeginTable("##cmds", numCols, ImGuiTableFlags_None)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, nameColW);
if (showParams)
ImGui::TableSetupColumn("Parameters", ImGuiTableColumnFlags_WidthFixed, paramsColW);
ImGui::TableSetupColumn("Desc", ImGuiTableColumnFlags_WidthStretch);
for (int i = 0; i < cat.count; i++) {
const auto& cmd = cat.commands[i];
if (!consoleCommandMatchesFilter(cmd.name, cmd.desc, cmd.params, filter))
continue;
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(IM_COL32(100, 180, 255, 255)));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.4f, 0.6f, 0.3f));
char selId[128];
snprintf(selId, sizeof(selId), "%s##cmdRef", cmd.name);
if (ImGui::Selectable(selId, false)) {
if (cmd.params[0] != '\0') {
snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
} else {
strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
input_buffer_[sizeof(input_buffer_) - 1] = '\0';
}
ImGui::CloseCurrentPopup();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (cmd.params[0] != '\0')
material::Tooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
else
material::Tooltip(TR("console_click_insert"), cmd.name);
}
ImGui::PopStyleColor(3);
if (showParams) {
ImGui::TableNextColumn();
drawConsoleCommandParams(cmd.params);
}
ImGui::TableNextColumn();
ImGui::TextColored(
ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
"%s", cmd.desc);
}
ImGui::EndTable();
}
ImGui::Spacing();
}
} }
ImGui::PopFont(); // Keep a valid selection: reset to the top when the query changes or the current selection falls
// out of the visible set, so the detail pane is always populated.
int selPos = -1;
for (int k = 0; k < (int)order.size(); k++)
if (order[k].first == cmd_sel_cat_ && order[k].second == cmd_sel_idx_) { selPos = k; break; }
if (searchChanged || selPos < 0) {
if (!order.empty()) { cmd_sel_cat_ = order[0].first; cmd_sel_idx_ = order[0].second; selPos = 0; }
else { cmd_sel_cat_ = cmd_sel_idx_ = -1; }
}
// Keyboard nav from the SEARCH box only (so typing in a param field doesn't move the selection):
// Up/Down move, Enter inserts the selected command's template. Disabled while a run-confirm shows.
if (!run_confirm_cmd_ && searchActive && !order.empty() && selPos >= 0) {
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && selPos + 1 < (int)order.size()) selPos++;
else if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && selPos > 0) selPos--;
cmd_sel_cat_ = order[selPos].first;
cmd_sel_idx_ = order[selPos].second;
}
if (searchEnter && !run_confirm_cmd_ && cmd_sel_cat_ >= 0)
insertCommandToInput(categories[cmd_sel_cat_].commands[cmd_sel_idx_]);
// Two-pane body sized above the footer.
float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingXs();
float bodyH = std::max(120.0f, ImGui::GetContentRegionAvail().y - footerH);
float contentW = ImGui::GetContentRegionAvail().x;
float gap = Layout::spacingLg();
float masterW = std::min(std::max(contentW * 0.34f, 280.0f * dp), 380.0f * dp);
float detailW = contentW - masterW - gap;
ImFont* mono = Type().mono();
// The panes are theme-adaptive glass (light on light skins), so floor the link-blue to a readable
// contrast on light surfaces (FloorLight is a no-op on dark themes).
ImU32 nameCol = FloorLight(IM_COL32(100, 180, 255, 255));
float rowH = std::max(20.0f * dp, mono->LegacySize + 6.0f * dp);
// One master row: mono command name + a warning dot for consequential commands, with a soft
// rounded selection/hover fill (Material, not the default sharp Selectable highlight). Click
// selects (drives the detail pane); double-click inserts.
auto drawRow = [&](int c, int i) {
const ConsoleCommandEntry& cmd = categories[c].commands[i];
bool sel = (cmd_sel_cat_ == c && cmd_sel_idx_ == i);
ImGui::PushID(c * 1000 + i);
ImVec2 rmn = ImGui::GetCursorScreenPos();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); // we draw our own fill
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
bool clicked = ImGui::Selectable("##cmdrow", sel, ImGuiSelectableFlags_SpanAvailWidth,
ImVec2(0, rowH));
ImGui::PopStyleColor(3);
bool hov = ImGui::IsItemHovered();
if (clicked) { cmd_sel_cat_ = c; cmd_sel_idx_ = i; }
if (hov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) insertCommandToInput(cmd);
}
ImDrawList* dl = ImGui::GetWindowDrawList();
float rowW = ImGui::GetItemRectSize().x;
if (sel || hov)
dl->AddRectFilled(ImVec2(rmn.x, rmn.y + 1.0f * dp),
ImVec2(rmn.x + rowW, rmn.y + rowH - 1.0f * dp),
sel ? WithAlpha(Primary(), 52) : WithAlpha(OnSurface(), 16), 6.0f * dp);
float tx = rmn.x + Layout::spacingSm();
if (cmd.destructive) {
float r = 3.0f * dp;
dl->AddCircleFilled(ImVec2(tx + r, rmn.y + rowH * 0.5f), r, Warning());
tx += r * 2.0f + Layout::spacingXs();
}
dl->AddText(mono, mono->LegacySize, ImVec2(tx, rmn.y + (rowH - mono->LegacySize) * 0.5f),
sel ? OnSurface() : nameCol, cmd.name);
ImGui::PopID();
};
// Both panes sit on soft Material glass surfaces (no hard 1px child border) with inner padding.
GlassPanelSpec paneGlass;
paneGlass.rounding = 14.0f;
paneGlass.fillAlpha = 30;
paneGlass.borderAlpha = 30;
// MASTER pane.
{
ImVec2 mMin = ImGui::GetCursorScreenPos();
DrawGlassPanel(ImGui::GetWindowDrawList(), mMin, ImVec2(mMin.x + masterW, mMin.y + bodyH), paneGlass);
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingSm()));
ImGui::BeginChild("##cmdMaster", ImVec2(masterW, bodyH), ImGuiChildFlags_AlwaysUseWindowPadding);
{
if (order.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_ref_no_match"));
} else if (searching) {
for (auto& p : order) drawRow(p.first, p.second);
} else {
for (int c = 0; c < (int)categories.size(); c++) {
// Subtle accent-labelled section header (no heavy filled bar).
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
ImGui::ColorConvertU32ToFloat4(WithAlpha(OnSurface(), 14)));
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
ImGui::ColorConvertU32ToFloat4(WithAlpha(OnSurface(), 20)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
bool open = ImGui::CollapsingHeader(consoleCategoryLabel(categories[c].name),
ImGuiTreeNodeFlags_DefaultOpen);
ImGui::PopStyleColor(4);
if (open)
for (int i = 0; i < categories[c].count; i++) drawRow(c, i);
}
}
}
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::SameLine(0, gap);
// DETAIL pane.
{
ImVec2 dMin = ImGui::GetCursorScreenPos();
DrawGlassPanel(ImGui::GetWindowDrawList(), dMin, ImVec2(dMin.x + detailW, dMin.y + bodyH), paneGlass);
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingMd()));
ImGui::BeginChild("##cmdDetail", ImVec2(detailW, bodyH), ImGuiChildFlags_AlwaysUseWindowPadding);
if (cmd_sel_cat_ >= 0 && cmd_sel_cat_ < (int)categories.size() &&
cmd_sel_idx_ >= 0 && cmd_sel_idx_ < categories[cmd_sel_cat_].count) {
renderCommandDetail(categories[cmd_sel_cat_].commands[cmd_sel_idx_],
consoleCategoryLabel(categories[cmd_sel_cat_].name));
} else {
ImVec2 av = ImGui::GetContentRegionAvail();
ImGui::SetCursorPosY(av.y * 0.4f);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_ref_select_hint"));
}
ImGui::EndChild();
ImGui::PopStyleVar();
// Footer.
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Close button
if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) { if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) {
cmdFilter[0] = '\0'; command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false; show_commands_popup_ = false;
} }
@@ -1639,8 +1980,17 @@ void ConsoleTab::drainModel()
// with them so the highlight stays on the text the user selected. // with them so the highlight stays on the text the user selected.
selection_.shiftForEviction(static_cast<int>(dr.popped)); selection_.shiftForEviction(static_cast<int>(dr.popped));
// Track new output that arrived while the user is scrolled up (for the indicator). // Track new output that arrived while the user is scrolled up (for the "N new lines" indicator).
scroll_.onLinesAdded(static_cast<int>(dr.added)); // Count only the newly-added lines that pass the active filter, so the indicator isn't inflated
// by lines the current filter / errors-only view hides — the user wouldn't see those on jumping
// to the bottom. (The new lines are the last dr.added entries; eviction is from the front.)
ConsoleOutputFilter f{filter_text_, s_daemon_messages_enabled, s_errors_only_enabled,
s_rpc_trace_enabled, s_app_messages_enabled};
const int n = static_cast<int>(model_.size());
int visibleAdded = 0;
for (int i = std::max(0, n - static_cast<int>(dr.added)); i < n; i++)
if (consoleLinePassesFilter(model_[i].text, model_[i].channel, f)) ++visibleAdded;
scroll_.onLinesAdded(visibleAdded);
} }
void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method) void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)
@@ -1654,6 +2004,7 @@ void ConsoleTab::clear()
// View-only clear (main thread). The executor keeps its own log cursors, so new output // View-only clear (main thread). The executor keeps its own log cursors, so new output
// still appends. The "cleared" line is ingested and appears on the next frame's drain. // still appends. The "cleared" line is ingested and appears on the next frame's drain.
model_.clear(); model_.clear();
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
addLine(TR("console_cleared"), ConsoleChannel::Info); addLine(TR("console_cleared"), ConsoleChannel::Info);
} }

View File

@@ -25,6 +25,7 @@ namespace ui {
class ConsoleCommandExecutor; class ConsoleCommandExecutor;
struct ConsoleLogFilterCaps; struct ConsoleLogFilterCaps;
struct ConsoleCommandEntry;
/** /**
* @brief Console tab — a rich terminal shared by the full-node and lite variants. * @brief Console tab — a rich terminal shared by the full-node and lite variants.
@@ -69,6 +70,12 @@ public:
// Console output zoom factor (1.0 = default caption font size) // Console output zoom factor (1.0 = default caption font size)
static float s_console_zoom; static float s_console_zoom;
// Draw the per-line left color accent bars (channel-colored). Toggled from the toolbar.
static bool s_line_accents_enabled;
// Color output text per channel. When false the console is monochrome text. Toolbar-toggled.
static bool s_line_text_color_enabled;
// Show/hide daemon output messages // Show/hide daemon output messages
static bool s_daemon_messages_enabled; static bool s_daemon_messages_enabled;
@@ -109,6 +116,8 @@ private:
void addFormattedResult(const std::string& result, bool is_error); void addFormattedResult(const std::string& result, bool is_error);
void renderStatusHeader(ConsoleCommandExecutor& exec); void renderStatusHeader(ConsoleCommandExecutor& exec);
void renderCommandsPopup(); void renderCommandsPopup();
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel); // right pane
void insertCommandToInput(const ConsoleCommandEntry& cmd); // fill input + close the modal
// renderToolbar() draws the top bar; these are its sub-steps: // renderToolbar() draws the top bar; these are its sub-steps:
void renderToolbar(ConsoleCommandExecutor& exec); void renderToolbar(ConsoleCommandExecutor& exec);
@@ -144,6 +153,7 @@ private:
std::vector<std::string> command_history_; std::vector<std::string> command_history_;
int history_index_ = -1; int history_index_ = -1;
char input_buffer_[4096] = {0}; char input_buffer_[4096] = {0};
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor) // (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
// Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count). // Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count).
@@ -167,14 +177,24 @@ private:
mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar) mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar)
std::string context_token_; // hash/address under the cursor at right-click std::string context_token_; // hash/address under the cursor at right-click
mutable std::vector<int> visible_indices_; // Cached for selection mapping mutable std::vector<int> visible_indices_; // Cached for selection mapping
bool folding_active_ = true; // fold UI shown only in the unfiltered (foldable) view
bool has_text_filter_ = false; // computed once per frame (before the toolbar draws it)
std::string filter_lower_; // lowercased filter needle for match highlighting
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y), // Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and // recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and
// consumed by the renderer + hit-testing. // consumed by the renderer + hit-testing.
mutable ConsoleLayout layout_; mutable ConsoleLayout layout_;
// Commands popup // Commands popup (RPC command explorer)
bool show_commands_popup_ = false; bool show_commands_popup_ = false;
char command_search_[128] = {0}; // RPC-reference search filter (cleared when the modal opens)
int cmd_sel_cat_ = -1; // detail-pane selection: category index into consoleCommandCategories()
int cmd_sel_idx_ = -1; // detail-pane selection: command index within that category
std::string pending_submit_; // command to run next frame (deferred so the modal needs no executor)
const ConsoleCommandEntry* run_confirm_cmd_ = nullptr; // destructive "Insert & run" awaiting confirmation
char cmd_param_bufs_[6][256] = {{0}}; // parameter-builder input fields
const ConsoleCommandEntry* cmd_param_owner_ = nullptr; // command the param buffers currently hold
}; };
} // namespace ui } // namespace ui

View File

@@ -12,6 +12,7 @@
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
#include "../material/type.h" #include "../material/type.h"
#include "../widgets/copy_field.h"
#include "../theme.h" #include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h" #include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h" #include "imgui.h"
@@ -29,7 +30,8 @@ using json = nlohmann::json;
// Static state // Static state
static bool s_open = false; static bool s_open = false;
static bool s_exporting = false; static bool s_exporting = false;
static std::string s_status; static std::string s_status; // error / partial message (non-empty => red text)
static std::string s_saved_path; // full path on full success (=> green check + copy field)
static std::string s_exported_keys; static std::string s_exported_keys;
static int s_total_addresses = 0; static int s_total_addresses = 0;
static int s_exported_count = 0; static int s_exported_count = 0;
@@ -42,6 +44,7 @@ void ExportAllKeysDialog::show()
s_open = true; s_open = true;
s_exporting = false; s_exporting = false;
s_status.clear(); s_status.clear();
s_saved_path.clear();
s_exported_keys.clear(); s_exported_keys.clear();
s_total_addresses = 0; s_total_addresses = 0;
s_exported_count = 0; s_exported_count = 0;
@@ -60,73 +63,76 @@ bool ExportAllKeysDialog::isOpen()
return s_open; return s_open;
} }
void ExportAllKeysDialog::hide()
{
s_open = false;
s_status.clear();
s_saved_path.clear();
}
void ExportAllKeysDialog::render(App* app) void ExportAllKeysDialog::render(App* app)
{ {
if (!s_open) return; if (!s_open) return;
namespace m = material;
auto& S = schema::UI(); m::OverlayDialogSpec ov;
auto win = S.window("dialogs.export-all-keys"); ov.title = TR("export_keys_title");
auto exportBtn = S.button("dialogs.export-all-keys", "export-button"); ov.p_open = &s_open;
auto closeBtn = S.button("dialogs.export-all-keys", "close-button"); ov.style = m::OverlayStyle::BlurFloat;
ov.cardWidth = 600.0f;
material::OverlayDialogSpec ov; ov.idSuffix = "exportallkeys";
ov.title = TR("export_keys_title"); ov.p_open = &s_open; if (m::BeginOverlayDialog(ov)) {
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading m::DialogWarningHeader(TR("export_keys_danger"));
ov.cardWidth = win.width; ov.cardBottomViewportRatio = 0.94f; // keep authored width
if (material::BeginOverlayDialog(ov)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("%s", TR("export_keys_danger"));
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
if (s_exporting) { ImGui::BeginDisabled(s_exporting);
ImGui::BeginDisabled(); ImGui::TextUnformatted(TR("export_keys_options"));
}
// Options
ImGui::Text("%s", TR("export_keys_options"));
ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z); ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z);
ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t); ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t);
ImGui::Spacing(); ImGui::Spacing();
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
// Filename
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextDisabled("%s", TR("file_save_location")); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
ImGui::EndDisabled();
// Progress / result.
if (s_exporting) { if (s_exporting) {
ImGui::EndDisabled(); ImGui::Spacing();
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), TR("export_keys_progress"),
s_exported_count, s_total_addresses);
ImGui::SameLine(0, 0);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), "%s", m::LoadingDots());
} else if (!s_saved_path.empty()) {
ImGui::Spacing();
ImGui::PushFont(m::Type().iconSmall());
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_keys_success"));
widgets::AddressCopyField("##exportedkeyspath", s_saved_path);
} else if (!s_status.empty()) {
ImGui::Spacing();
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_status.c_str());
ImGui::PopTextWrapPos();
} }
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
// Export button bool doExport = false, doClose = false;
if (s_exporting) { m::DialogActionFooter(TR("export_keys_btn"), !s_exporting, TR("close"), doExport, doClose);
ImGui::BeginDisabled(); if (doClose) s_open = false;
} if (doExport) {
if (material::StyledButton(TR("export_keys_btn"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (!s_include_z && !s_include_t) { if (!s_include_z && !s_include_t) {
Notifications::instance().warning("Select at least one address type"); Notifications::instance().warning(TR("export_keys_select_type"));
} else if (!app->rpc() || !app->rpc()->isConnected()) { } else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon"); Notifications::instance().error(TR("export_keys_not_connected"));
} else { } else {
s_exporting = true; s_exporting = true;
s_exported_keys.clear(); s_exported_keys.clear();
s_exported_count = 0; s_exported_count = 0;
s_status = "Exporting keys..."; s_status.clear();
s_saved_path.clear();
const auto& state = app->getWalletState(); const auto& state = app->getWalletState();
@@ -137,7 +143,8 @@ void ExportAllKeysDialog::render(App* app)
if (s_total_addresses == 0) { if (s_total_addresses == 0) {
s_exporting = false; s_exporting = false;
s_status = "No addresses to export"; s_status = TR("export_keys_none_addrs");
m::EndOverlayDialog();
return; return;
} }
@@ -216,21 +223,21 @@ void ExportAllKeysDialog::render(App* app)
return [exported, total, filepath, writeOk]() { return [exported, total, filepath, writeOk]() {
s_exported_count = exported; s_exported_count = exported;
s_exporting = false; s_exporting = false;
char buf[320];
if (exported == 0) { if (exported == 0) {
s_status = "No keys exported (0 of " + std::to_string(total) + snprintf(buf, sizeof(buf), TR("export_keys_none_result"), total);
") — unlock the wallet (if encrypted) and try again."; s_status = buf;
Notifications::instance().error("No keys could be exported — is the wallet unlocked?"); Notifications::instance().error(TR("export_keys_none_toast"));
} else if (!writeOk) { } else if (!writeOk) {
s_status = "Failed to write file"; s_status = TR("export_keys_write_fail");
Notifications::instance().error("Failed to save key file"); Notifications::instance().error(TR("export_keys_write_fail"));
} else if (exported < total) { } else if (exported < total) {
s_status = "Exported " + std::to_string(exported) + " of " + snprintf(buf, sizeof(buf), TR("export_keys_partial"), exported, total);
std::to_string(total) + " keys to: " + filepath + s_status = buf;
" (INCOMPLETE — some addresses had no spending key or the wallet is locked)"; snprintf(buf, sizeof(buf), TR("export_keys_partial_toast"), exported, total);
Notifications::instance().warning("Partial export: " + std::to_string(exported) + Notifications::instance().warning(buf);
" of " + std::to_string(total) + " keys");
} else { } else {
s_status = "Exported to: " + filepath; s_saved_path = filepath;
Notifications::instance().success(TR("export_keys_success"), 5.0f); Notifications::instance().success(TR("export_keys_success"), 5.0f);
} }
}; };
@@ -239,23 +246,7 @@ void ExportAllKeysDialog::render(App* app)
} }
} }
if (s_exporting) { m::EndOverlayDialog();
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled(TR("export_keys_progress"), s_exported_count, s_total_addresses);
}
ImGui::SameLine();
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
material::EndOverlayDialog();
} }
} }

View File

@@ -27,6 +27,9 @@ public:
// Check if dialog is open // Check if dialog is open
static bool isOpen(); static bool isOpen();
// Close the dialog (used by the UI sweep teardown)
static void hide();
}; };
} // namespace ui } // namespace ui

View File

@@ -9,7 +9,10 @@
#include "../notifications.h" #include "../notifications.h"
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../widgets/copy_field.h"
#include "../theme.h" #include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h" #include "imgui.h"
#include <string> #include <string>
@@ -24,7 +27,8 @@ namespace ui {
// Static state // Static state
static bool s_open = false; static bool s_open = false;
static char s_filename[256] = ""; static char s_filename[256] = "";
static std::string s_status; static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
static std::string s_error; // error message on failure; empty otherwise
// Re-entrancy guard: true while a CSV write is in flight (disables the // Re-entrancy guard: true while a CSV write is in flight (disables the
// Export button so a second synchronous write can't be kicked off). // Export button so a second synchronous write can't be kicked off).
static bool s_exporting = false; static bool s_exporting = false;
@@ -52,7 +56,8 @@ static std::string escapeCSV(const std::string& field)
void ExportTransactionsDialog::show() void ExportTransactionsDialog::show()
{ {
s_open = true; s_open = true;
s_status.clear(); s_saved_path.clear();
s_error.clear();
// Generate default filename with timestamp // Generate default filename with timestamp
std::time_t now = std::time(nullptr); std::time_t now = std::time(nullptr);
@@ -66,104 +71,96 @@ bool ExportTransactionsDialog::isOpen()
return s_open; return s_open;
} }
void ExportTransactionsDialog::hide()
{
s_open = false;
s_saved_path.clear();
s_error.clear();
}
void ExportTransactionsDialog::render(App* app) void ExportTransactionsDialog::render(App* app)
{ {
if (!s_open) return; if (!s_open) return;
namespace m = material;
auto& S = schema::UI(); m::OverlayDialogSpec ov;
auto win = S.window("dialogs.export-transactions"); ov.title = TR("export_tx_title");
auto exportBtn = S.button("dialogs.export-transactions", "export-button"); ov.p_open = &s_open;
auto closeBtn = S.button("dialogs.export-transactions", "close-button"); ov.style = m::OverlayStyle::BlurFloat;
ov.cardWidth = 520.0f;
ov.idSuffix = "exporttx";
if (!m::BeginOverlayDialog(ov)) return;
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) { const auto& state = app->getWalletState();
const auto& state = app->getWalletState();
ImGui::Text(TR("export_tx_count"), state.transactions.size()); ImGui::Text(TR("export_tx_count"), state.transactions.size());
ImGui::Spacing();
// Filename + save-location hint.
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
// Result — success shows the saved path as a copy field; failure shows a red line.
if (!s_saved_path.empty()) {
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::PushFont(m::Type().iconSmall());
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_tx_success"));
widgets::AddressCopyField("##exportedpath", s_saved_path);
} else if (!s_error.empty()) {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(0.0f);
// Filename ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_error.c_str());
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename)); ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("file_save_location"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Export button (disabled while a write is already in flight)
ImGui::BeginDisabled(s_exporting);
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (state.transactions.empty()) {
Notifications::instance().warning(TR("export_tx_none"));
} else {
s_exporting = true;
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
std::ofstream file(filepath);
if (!file.is_open()) {
s_status = "Failed to create file";
Notifications::instance().error(TR("export_tx_file_fail"));
} else {
// Write CSV header
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
// Write transactions
for (const auto& tx : state.transactions) {
// Date
std::time_t t = static_cast<std::time_t>(tx.timestamp);
char datebuf[32];
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
file << datebuf << ",";
// Type
file << escapeCSV(tx.type) << ",";
// Amount
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << tx.amount;
file << amt.str() << ",";
// Address
file << escapeCSV(tx.address) << ",";
// TXID
file << escapeCSV(tx.txid) << ",";
// Confirmations
file << tx.confirmations << ",";
// Memo
file << escapeCSV(tx.memo) << "\n";
}
file.close();
s_status = "Exported " + std::to_string(state.transactions.size()) +
" transactions to: " + filepath;
Notifications::instance().success(TR("export_tx_success"), 5.0f);
}
s_exporting = false;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
material::EndOverlayDialog();
} }
ImGui::Spacing();
bool doExport = false, doClose = false;
m::DialogActionFooter(TR("export"), !s_exporting, TR("close"), doExport, doClose);
if (doClose) s_open = false;
if (doExport) {
s_saved_path.clear();
s_error.clear();
if (state.transactions.empty()) {
Notifications::instance().warning(TR("export_tx_none"));
} else {
s_exporting = true;
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
std::ofstream file(filepath);
if (!file.is_open()) {
s_error = TR("export_tx_file_fail");
Notifications::instance().error(TR("export_tx_file_fail"));
} else {
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
for (const auto& tx : state.transactions) {
std::time_t t = static_cast<std::time_t>(tx.timestamp);
char datebuf[32];
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
file << datebuf << ",";
file << escapeCSV(tx.type) << ",";
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << tx.amount;
file << amt.str() << ",";
file << escapeCSV(tx.address) << ",";
file << escapeCSV(tx.txid) << ",";
file << tx.confirmations << ",";
file << escapeCSV(tx.memo) << "\n";
}
file.close();
s_saved_path = filepath;
Notifications::instance().success(TR("export_tx_success"), 5.0f);
}
s_exporting = false;
}
}
m::EndOverlayDialog();
} }
} // namespace ui } // namespace ui

View File

@@ -25,6 +25,9 @@ public:
// Check if dialog is open // Check if dialog is open
static bool isOpen(); static bool isOpen();
// Close the dialog (used by the UI sweep teardown)
static void hide();
}; };
} // namespace ui } // namespace ui

View File

@@ -293,12 +293,29 @@ static void PortfolioBeginEdit(App* app, int index)
// ---- Portfolio-editor persistence helpers (operate on the s_pfEdit working state) ---- // ---- Portfolio-editor persistence helpers (operate on the s_pfEdit working state) ----
// Build a PortfolioEntry from the working set, preserving off-form fields (grid geometry) from base. // Build a PortfolioEntry from the working set, preserving off-form fields (grid geometry) from base.
// Normalized views of the two free-text buffers, applied identically by pfBuildWorking (what we store)
// and pfWorkingMatches (the dirty check) so the two can never disagree: a trailing space in the label
// or an emptied currency field must not leave the group perpetually "dirty" after a save.
static std::string pfEditLabel() // label with leading/trailing whitespace trimmed
{
std::string s(s_pfEdit.label);
size_t i = 0, j = s.size();
while (i < j && (unsigned char)s[i] <= ' ') i++;
while (j > i && (unsigned char)s[j - 1] <= ' ') j--;
return s.substr(i, j - i);
}
static std::string pfEditCurrency() // currency, defaulting an empty buffer to USD
{
return s_pfEdit.manualCcy[0] != '\0' ? std::string(s_pfEdit.manualCcy) : std::string("USD");
}
static config::Settings::PortfolioEntry pfBuildWorking(const config::Settings::PortfolioEntry& base) static config::Settings::PortfolioEntry pfBuildWorking(const config::Settings::PortfolioEntry& base)
{ {
config::Settings::PortfolioEntry e = base; config::Settings::PortfolioEntry e = base;
e.label = s_pfEdit.label; e.addresses = s_pfEdit.addrs; e.icon = s_pfEdit.icon; e.label = pfEditLabel(); e.addresses = s_pfEdit.addrs; e.icon = s_pfEdit.icon;
e.color = s_pfEdit.color; e.outlineOpacity = s_pfEdit.outlineOpacity; e.color = s_pfEdit.color; e.outlineOpacity = s_pfEdit.outlineOpacity;
e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice; e.manualCurrency = s_pfEdit.manualCcy; e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice;
e.manualCurrency = pfEditCurrency();
e.showDrgx = s_pfEdit.showDrgx; e.showValue = s_pfEdit.showValue; e.show24h = s_pfEdit.show24h; e.showDrgx = s_pfEdit.showDrgx; e.showValue = s_pfEdit.showValue; e.show24h = s_pfEdit.show24h;
e.showSparkline = s_pfEdit.showSparkline; e.sparklineInterval = s_pfEdit.sparkInterval; e.showSparkline = s_pfEdit.showSparkline; e.sparklineInterval = s_pfEdit.sparkInterval;
return e; return e;
@@ -307,14 +324,24 @@ static config::Settings::PortfolioEntry pfBuildWorking(const config::Settings::P
// True when the working set matches a stored entry (i.e. there are no uncommitted edits). // True when the working set matches a stored entry (i.e. there are no uncommitted edits).
static bool pfWorkingMatches(const config::Settings::PortfolioEntry& e) static bool pfWorkingMatches(const config::Settings::PortfolioEntry& e)
{ {
return e.label == std::string(s_pfEdit.label) && e.addresses == s_pfEdit.addrs && e.icon == s_pfEdit.icon return e.label == pfEditLabel() && e.addresses == s_pfEdit.addrs && e.icon == s_pfEdit.icon
&& e.color == s_pfEdit.color && e.outlineOpacity == s_pfEdit.outlineOpacity && e.color == s_pfEdit.color && e.outlineOpacity == s_pfEdit.outlineOpacity
&& e.priceBasis == s_pfEdit.priceBasis && e.manualPrice == s_pfEdit.manualPrice && e.priceBasis == s_pfEdit.priceBasis && e.manualPrice == s_pfEdit.manualPrice
&& e.manualCurrency == std::string(s_pfEdit.manualCcy) && e.manualCurrency == pfEditCurrency()
&& e.showDrgx == s_pfEdit.showDrgx && e.showValue == s_pfEdit.showValue && e.show24h == s_pfEdit.show24h && e.showDrgx == s_pfEdit.showDrgx && e.showValue == s_pfEdit.showValue && e.show24h == s_pfEdit.show24h
&& e.showSparkline == s_pfEdit.showSparkline && e.sparklineInterval == s_pfEdit.sparkInterval; && e.showSparkline == s_pfEdit.showSparkline && e.sparklineInterval == s_pfEdit.sparkInterval;
} }
// A group is only worth persisting when it has a name, at least one address (else it's always $0), and
// — on the Manual price basis — a positive price (0 would render as "unavailable"). Gates both the Save
// button and the commit path (Close/switch commit too, so validating only the button wouldn't be enough).
static bool pfWorkingValid()
{
return !pfEditLabel().empty() // a whitespace-only label is treated as empty (invisible group)
&& !s_pfEdit.addrs.empty()
&& !(s_pfEdit.priceBasis == 3 && s_pfEdit.manualPrice <= 0.0);
}
static void pfPersist(config::Settings* settings, const std::vector<config::Settings::PortfolioEntry>& entries) static void pfPersist(config::Settings* settings, const std::vector<config::Settings::PortfolioEntry>& entries)
{ {
settings->setPortfolioEntries(entries); settings->setPortfolioEntries(entries);
@@ -322,23 +349,48 @@ static void pfPersist(config::Settings* settings, const std::vector<config::Sett
} }
// Persist the working group when it is named and actually changed; unnamed drafts are dropped. // Persist the working group when it is named and actually changed; unnamed drafts are dropped.
// New groups default to the active wallet's scope (per-wallet visibility); existing groups keep // New groups adopt the active wallet's scope (per-wallet visibility). An existing group with no scope
// whatever scope they already had. // — a placeholder added before the wallet identity resolved, or a pre-scoping legacy group — is also
// claimed for the active wallet on edit, so it can't linger in the global (empty-scope) bucket and
// leak into every wallet's list. No-op when the identity is unavailable (stays unscoped until edited
// with a live identity).
static void pfCommitIfNeeded(App* app) static void pfCommitIfNeeded(App* app)
{ {
config::Settings* settings = app->settings(); config::Settings* settings = app->settings();
if (s_pfEdit.label[0] == '\0') return; if (!pfWorkingValid()) return; // drop invalid drafts (unnamed / no addresses / manual price <= 0)
auto entries = settings->getPortfolioEntries(); auto entries = settings->getPortfolioEntries();
bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size()); bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size());
config::Settings::PortfolioEntry base; config::Settings::PortfolioEntry base;
if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; } if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; }
auto e = pfBuildWorking(base); auto e = pfBuildWorking(base);
if (!existing) e.scope = app->activeWalletIdentityHash(); if (e.scope.empty()) {
const std::string h = app->activeWalletIdentityHash();
if (!h.empty()) e.scope = h;
}
if (existing) entries[s_pfEdit.sel] = e; if (existing) entries[s_pfEdit.sel] = e;
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; } else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
pfPersist(settings, entries); pfPersist(settings, entries);
} }
// A group is visible to the active wallet when it carries the wallet's scope, is unscoped (legacy —
// shown everywhere), or the wallet identity isn't resolved yet. The editor mirrors the Market summary's
// scope filter so it only lists/edits the current wallet's groups.
static bool pfIndexVisible(App* app, int i)
{
const auto& es = app->settings()->getPortfolioEntries();
if (i < 0 || i >= (int)es.size()) return false;
const std::string h = app->activeWalletIdentityHash();
return es[i].scope.empty() || h.empty() || es[i].scope == h;
}
static int pfFirstVisibleIndex(App* app)
{
const auto& es = app->settings()->getPortfolioEntries();
const std::string h = app->activeWalletIdentityHash();
for (int i = 0; i < (int)es.size(); i++)
if (es[i].scope.empty() || h.empty() || es[i].scope == h) return i;
return -1;
}
// ---- Detail-pane section renderers (drawn inside ##pfDetailBody; operate on s_pfEdit) ---- // ---- Detail-pane section renderers (drawn inside ##pfDetailBody; operate on s_pfEdit) ----
// Appearance: accent-color picker (+ custom), outline-opacity slider, icon grid. // Appearance: accent-color picker (+ custom), outline-opacity slider, icon grid.
@@ -513,7 +565,8 @@ static void pfDrawPriceSection()
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f; float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
ImGui::SetNextItemWidth(half); ImGui::SetNextItemWidth(half);
ImGui::InputDouble("##pfManPrice", &s_pfEdit.manualPrice, 0.0, 0.0, "%.6f"); ImGui::InputDouble("##pfManPrice", &s_pfEdit.manualPrice, 0.0, 0.0, "%.6f");
if (s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0; // Reject negatives and non-finite input (InputDouble parses "inf"/"nan"; neither is < 0).
if (!std::isfinite(s_pfEdit.manualPrice) || s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0;
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(-1); ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy)); ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy));
@@ -635,8 +688,8 @@ static void pfDrawAddressSection(App* app)
ImVec2 rmn = ImGui::GetCursorScreenPos(); ImVec2 rmn = ImGui::GetCursorScreenPos();
ImVec2 rmx(rmn.x + lw, rmn.y + rowH); ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx); bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
if (inSet) ldl->AddRectFilled(rmn, rmx, WithAlpha(Primary(), 22), 4.0f); if (inSet) ldl->AddRectFilled(rmn, rmx, WithAlpha(Primary(), 22), 4.0f * dp);
else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f); else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f * dp);
float rcy = rmn.y + rowH * 0.5f; float rcy = rmn.y + rowH * 0.5f;
float rx = rmn.x + Layout::spacingSm(); float rx = rmn.x + Layout::spacingSm();
float cb = 15.0f * dp; float cb = 15.0f * dp;
@@ -654,9 +707,9 @@ static void pfDrawAddressSection(App* app)
bool isZ = (a.type == "shielded"); bool isZ = (a.type == "shielded");
const char* chip = isZ ? "Z" : "T"; const char* chip = isZ ? "Z" : "T";
ImU32 chipCol = isZ ? Success() : Warning(); ImU32 chipCol = isZ ? Success() : Warning();
ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f); ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f * dp);
float chipW = material::DrawPill(ldl, chipPos, chip, capF, chipCol, float chipW = material::DrawPill(ldl, chipPos, chip, capF, chipCol,
WithAlpha(chipCol, 40), 0, ImVec2(4.0f * dp, 2.0f), 4.0f * dp).x; WithAlpha(chipCol, 40), 0, ImVec2(4.0f * dp, 2.0f * dp), 4.0f * dp).x;
rx += chipW + Layout::spacingSm(); rx += chipW + Layout::spacingSm();
std::string aicon = app->getAddressIcon(a.address); std::string aicon = app->getAddressIcon(a.address);
if (!aicon.empty()) { if (!aicon.empty()) {
@@ -706,9 +759,9 @@ static void RenderPortfolioEditor(App* app)
// ---------------- Full-window blur overlay (shared framework) ---------------- // ---------------- Full-window blur overlay (shared framework) ----------------
// Backdrop + capture-once, floating card, plain heading, outside-click dismiss and effect // Backdrop + capture-once, floating card, plain heading, outside-click dismiss and effect
// suppression are owned by material::BeginOverlayDialog (BlurFloat). Esc still closes here; // suppression are owned by material::BeginOverlayDialog (BlurFloat). Every dismiss gesture
// Esc/outside-click discard (only Close commits); LatchBlurOverlayActive (App::render) handles // (Close button, Esc, outside-click, switching/adding/deleting groups) auto-saves the current
// the acrylic re-capture on close. // group via pfCommitIfNeeded; LatchBlurOverlayActive (App::render) does the acrylic re-capture.
material::OverlayDialogSpec ov; material::OverlayDialogSpec ov;
ov.title = TR("portfolio_manage_title"); ov.title = TR("portfolio_manage_title");
ov.p_open = &s_pfEdit.open; ov.p_open = &s_pfEdit.open;
@@ -717,11 +770,28 @@ static void RenderPortfolioEditor(App* app)
ov.cardHeight = 760.0f; ov.cardHeight = 760.0f;
ov.idSuffix = "pf"; ov.idSuffix = "pf";
if (!material::BeginOverlayDialog(ov)) return; if (!material::BeginOverlayDialog(ov)) return;
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_pfEdit.open = false; // Outside-click dismiss is handled inside BeginOverlayDialog, which clears s_pfEdit.open mid-frame
// (the card still draws one final frame). Auto-save now, before that frame: the placeholder GC in
// mktDrawPortfolio already ran this frame while open was still true, so entry indices haven't
// shifted under us and pfCommitIfNeeded targets the right slot.
if (!s_pfEdit.open) pfCommitIfNeeded(app);
// A first Escape should dismiss only an open popup (e.g. the custom-color picker); don't also tear
// down the whole editor while a popup is capturing the key. Otherwise Escape auto-saves and closes.
if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) {
pfCommitIfNeeded(app);
s_pfEdit.open = false;
}
{ // clamp/validate the selection against the current entry list { // Clamp/validate the selection. The in-editor delete already reloads the working state, but if the
// list shrank or the selection landed on a group outside this wallet's scope via any other path,
// reset to the first visible group and reload so a later commit can't overlay stale fields.
const auto& entriesRO = settings->getPortfolioEntries(); const auto& entriesRO = settings->getPortfolioEntries();
if (s_pfEdit.sel >= (int)entriesRO.size()) s_pfEdit.sel = entriesRO.empty() ? -1 : 0; if (s_pfEdit.sel >= (int)entriesRO.size() ||
(s_pfEdit.sel >= 0 && !pfIndexVisible(app, s_pfEdit.sel))) {
s_pfEdit.sel = pfFirstVisibleIndex(app);
PortfolioBeginEdit(app, s_pfEdit.sel);
}
} }
float footerH = 56.0f * dp; // room for the 40px buttons + separator so they aren't clipped float footerH = 56.0f * dp; // room for the 40px buttons + separator so they aren't clipped
@@ -732,8 +802,8 @@ static void RenderPortfolioEditor(App* app)
float detailW = contentW - masterW - gap; float detailW = contentW - masterW - gap;
// Does the selected group have uncommitted edits? (Only the selected group can differ from the // Does the selected group have uncommitted edits? (Only the selected group can differ from the
// stored copy — others were committed on deselect.) Drives the master-list "unsaved" dot and the // stored copy — the working buffer only ever holds the selected group; switching reloads it and
// detail-pane Revert/Save enable state. // discards any uncommitted edits.) Drives the master-list "unsaved" dot and Revert/Save state.
bool selDirty = false; bool selDirty = false;
{ {
const auto& es = settings->getPortfolioEntries(); const auto& es = settings->getPortfolioEntries();
@@ -748,12 +818,20 @@ static void RenderPortfolioEditor(App* app)
ImGui::BeginChild("##pfMasterList", ImVec2(masterW, std::max(48.0f, bodyH - addH - Layout::spacingSm())), false); ImGui::BeginChild("##pfMasterList", ImVec2(masterW, std::max(48.0f, bodyH - addH - Layout::spacingSm())), false);
{ {
ImDrawList* mdl = ImGui::GetWindowDrawList(); ImDrawList* mdl = ImGui::GetWindowDrawList();
if (entries.empty()) // Only list groups scoped to the active wallet (or legacy/unscoped) — same filter the
// Market summary uses. `i` stays the storage index (selection/delete operate on storage).
const std::string activeHash = app->activeWalletIdentityHash();
std::vector<int> vis;
for (int i = 0; i < (int)entries.size(); i++)
if (entries[i].scope.empty() || activeHash.empty() || entries[i].scope == activeHash)
vis.push_back(i);
if (vis.empty())
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries")); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
float rowH = 46.0f * dp; float rowH = 46.0f * dp;
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
for (int i = 0; i < (int)entries.size(); i++) { for (int vi = 0; vi < (int)vis.size(); vi++) {
int i = vis[vi];
ImGui::PushID(i); ImGui::PushID(i);
const auto& en = entries[i]; const auto& en = entries[i];
bool selRow = (s_pfEdit.sel == i); bool selRow = (s_pfEdit.sel == i);
@@ -808,7 +886,9 @@ static void RenderPortfolioEditor(App* app)
ImGui::PopID(); ImGui::PopID();
} }
if (delRow >= 0) { if (delRow >= 0) {
// Operates on the stored entries; any uncommitted working edits are discarded. // Auto-save edits to a *different* selected group before mutating the list; only the
// deleted row's own working state is dropped. (Re-read entries after the commit.)
if (delRow != s_pfEdit.sel) pfCommitIfNeeded(app);
auto es = settings->getPortfolioEntries(); auto es = settings->getPortfolioEntries();
if (delRow < (int)es.size()) { if (delRow < (int)es.size()) {
es.erase(es.begin() + delRow); es.erase(es.begin() + delRow);
@@ -819,14 +899,16 @@ static void RenderPortfolioEditor(App* app)
PortfolioBeginEdit(app, s_pfEdit.sel); PortfolioBeginEdit(app, s_pfEdit.sel);
} }
} else if (clickedSel != -999) { } else if (clickedSel != -999) {
PortfolioBeginEdit(app, clickedSel); // switching groups discards uncommitted edits pfCommitIfNeeded(app); // auto-save the current group before switching
PortfolioBeginEdit(app, clickedSel);
} }
} }
ImGui::EndChild(); ImGui::EndChild();
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Add immediately creates a persisted "Untitled" group and selects it for editing. // Add immediately creates a persisted "Untitled" group and selects it for editing.
if (material::TactileButton(TR("portfolio_add_entry"), ImVec2(masterW, addH))) { if (material::TactileButton(TR("portfolio_add_entry"), ImVec2(masterW, addH))) {
// Adding switches to a new group; uncommitted edits to the current one are discarded. // Auto-save the current group, then start a fresh one selected for editing.
pfCommitIfNeeded(app);
auto es = settings->getPortfolioEntries(); auto es = settings->getPortfolioEntries();
config::Settings::PortfolioEntry ne; config::Settings::PortfolioEntry ne;
ne.label = TR("portfolio_untitled"); ne.label = TR("portfolio_untitled");
@@ -869,7 +951,7 @@ static void RenderPortfolioEditor(App* app)
DrawGlassPanel(pdl, pMin, pMax, g); DrawGlassPanel(pdl, pMin, pMax, g);
if (accent) { if (accent) {
int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f); int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f);
pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f, 0, 2.0f); pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f * dp, 0, 2.0f * dp);
} }
if (s_pfEdit.showSparkline && (s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)) { if (s_pfEdit.showSparkline && (s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)) {
std::vector<double> h = data::sparklineSeries(state.market, s_pfEdit.sparkInterval); std::vector<double> h = data::sparklineSeries(state.market, s_pfEdit.sparkInterval);
@@ -956,10 +1038,17 @@ static void RenderPortfolioEditor(App* app)
if (material::TactileButton(TR("portfolio_revert"), ImVec2(bw, addH))) if (material::TactileButton(TR("portfolio_revert"), ImVec2(bw, addH)))
PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry
ImGui::SameLine(0, sp); ImGui::SameLine(0, sp);
ImGui::BeginDisabled(s_pfEdit.label[0] == '\0'); ImGui::BeginDisabled(!pfWorkingValid());
if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app); if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app);
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::EndDisabled(); ImGui::EndDisabled();
// Explain why Save is disabled — but only for a validation gap, not the "nothing changed"
// (!selDirty) case, which is self-evident. Priority mirrors pfWorkingValid's checks.
if (!pfWorkingValid() && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (pfEditLabel().empty()) material::Tooltip("%s", TR("portfolio_save_need_name"));
else if (s_pfEdit.addrs.empty()) material::Tooltip("%s", TR("portfolio_save_need_address"));
else material::Tooltip("%s", TR("portfolio_save_need_price"));
}
} }
} }
ImGui::EndChild(); // ##pfDetail ImGui::EndChild(); // ##pfDetail
@@ -1406,7 +1495,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
bool bhov = material::IsRectHovered(bmn, bmx); bool bhov = material::IsRectHovered(bmn, bmx);
ImU32 bg = sel ? WithAlpha(Primary(), 200) ImU32 bg = sel ? WithAlpha(Primary(), 200)
: (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18)); : (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18));
dl->AddRectFilled(bmn, bmx, bg, 4.0f); dl->AddRectFilled(bmn, bmx, bg, 4.0f * mktDp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY), dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY),
sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl); sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl);
ImGui::SetCursorScreenPos(bmn); ImGui::SetCursorScreenPos(bmn);
@@ -1429,7 +1518,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART; const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART;
ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH); ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH);
bool thov = material::IsRectHovered(tmn, tmx); bool thov = material::IsRectHovered(tmn, tmx);
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f); if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon); ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon);
dl->AddText(icoF, icoF->LegacySize, dl->AddText(icoF, icoF->LegacySize,
@@ -1450,7 +1539,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
ImFont* iconSmall = material::Typography::instance().iconSmall(); ImFont* iconSmall = material::Typography::instance().iconSmall();
ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH); ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH);
bool refreshHov = material::IsRectHovered(rbMin, rbMax); bool refreshHov = material::IsRectHovered(rbMin, rbMax);
if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f); if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH); ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
dl->AddText(iconSmall, iconSmall->LegacySize, dl->AddText(iconSmall, iconSmall->LegacySize,
@@ -1754,6 +1843,20 @@ static void mktDrawPortfolio(const MktCtx& cx)
float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH; float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH;
char buf[128]; char buf[128];
// Garbage-collect abandoned placeholders. "Add entry" persists an empty group up front so it shows
// in the editor list; if the user closes the editor without ever adding an address, that phantom
// "$0 · 0" group would linger. A group with no addresses can't be Saved (the editor forbids it), so
// it is always junk — prune it, but only while the editor is closed so we never delete one the user
// is actively filling in.
if (!s_pfEdit.open) {
auto es = app->settings()->getPortfolioEntries();
size_t before = es.size();
es.erase(std::remove_if(es.begin(), es.end(),
[](const config::Settings::PortfolioEntry& e) { return e.addresses.empty(); }),
es.end());
if (es.size() != before) pfPersist(app->settings(), es);
}
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
// "Manage…" button, right-aligned on the header row — opens the portfolio editor. // "Manage…" button, right-aligned on the header row — opens the portfolio editor.
{ {
@@ -1762,8 +1865,9 @@ static void mktDrawPortfolio(const MktCtx& cx)
ImGui::SameLine(); ImGui::SameLine();
material::RightAlignX(mBtnW); material::RightAlignX(mBtnW);
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) { if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
// Open the combined editor selecting the first group (or the empty state if none). // Open the editor on the first group visible to this wallet (or the empty state if none)
PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0); // never a raw index 0 that might belong to a different wallet.
PortfolioBeginEdit(app, pfFirstVisibleIndex(app));
s_pfEdit.open = true; s_pfEdit.open = true;
} }
} }
@@ -1797,12 +1901,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
chgCol, chg); chgCol, chg);
} }
double portfolio_btc = total_balance * market.price_btc; // Only show the BTC equivalent when a BTC price is actually available — otherwise it would read a
snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ <n> BTC" // misleading "≈ 0.00000000 BTC" (price_usd can be present while price_btc is still 0).
float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; if (market.price_btc > 0) {
dl->AddText(capFont, capFont->LegacySize, double portfolio_btc = total_balance * market.price_btc;
ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)), snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ <n> BTC"
OnSurfaceMedium(), buf); float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)),
OnSurfaceMedium(), buf);
}
} else { } else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx0, cy), OnSurfaceDisabled(), TR("market_no_price")); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx0, cy), OnSurfaceDisabled(), TR("market_no_price"));
} }
@@ -1814,7 +1922,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance); snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance);
float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x;
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - brkW, cy + 2), OnSurfaceDisabled(), buf); ImVec2(rightEdge - brkW, cy + 2.0f * mktDp), OnSurfaceDisabled(), buf);
cy += body2->LegacySize + Layout::spacingSm(); cy += body2->LegacySize + Layout::spacingSm();
// Full-width shielded/transparent ratio bar + % label. // Full-width shielded/transparent ratio bar + % label.
@@ -1832,16 +1940,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
const bool barLight = IsLightTheme(); const bool barLight = IsLightTheme();
const int fillA = barLight ? 205 : 165; const int fillA = barLight ? 205 : 165;
dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH), dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH),
barLight ? IM_COL32(0, 0, 0, 20) : IM_COL32(255, 255, 255, 10), 3.0f); barLight ? IM_COL32(0, 0, 0, 20) : IM_COL32(255, 255, 255, 10), 3.0f * mktDp);
if (shieldedW > 0.5f) if (shieldedW > 0.5f)
dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH), dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH),
WithAlpha(Success(), fillA), WithAlpha(Success(), fillA),
transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f); transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f * mktDp);
if (transpW > 0.5f) if (transpW > 0.5f)
dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y), dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y),
ImVec2(barStart.x + barW, barStart.y + ratioBarH), ImVec2(barStart.x + barW, barStart.y + ratioBarH),
WithAlpha(Warning(), fillA), WithAlpha(Warning(), fillA),
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f); shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f * mktDp);
// market_pct_shielded is "%.0f%% Shielded" — pass a double (an int to %f is UB). // market_pct_shielded is "%.0f%% Shielded" — pass a double (an int to %f is UB).
snprintf(buf, sizeof(buf), TR("market_pct_shielded"), static_cast<double>(shieldedRatio) * 100.0); snprintf(buf, sizeof(buf), TR("market_pct_shielded"), static_cast<double>(shieldedRatio) * 100.0);

View File

@@ -2,21 +2,14 @@
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
// //
// Shared "Browse all releases" picker, used by both the miner updater (XmrigDownloadDialog) and the // Shared release-row model for the updater dialogs. Both the miner updater (XmrigDownloadDialog) and
// node updater (DaemonUpdateDialog). Renders a scrollable list of releases (tag, title, date, with // the node updater (DaemonUpdateDialog) map their updater's release list into ReleaseRow values to
// pre-release / installed badges) inside the current overlay dialog; the caller maps its updater's // drive their two-pane version picker (tag + title + date, with pre-release / installed badges). The
// release list into ReleaseRow values and acts on the clicked index. // two-pane list is the "browse all releases" UI; each dialog renders its own tactile Material cards.
#pragma once #pragma once
#include <string> #include <string>
#include <vector>
#include "../../util/i18n.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "imgui.h"
namespace dragonx { namespace dragonx {
namespace ui { namespace ui {
@@ -30,58 +23,5 @@ struct ReleaseRow {
bool installed = false; // matches the currently-installed version (Install -> Reinstall) bool installed = false; // matches the currently-installed version (Install -> Reinstall)
}; };
// Renders the picker. Returns the row index whose Install button was clicked this frame, or -1.
// Sets *back when the Back button is clicked. If `globalDisabledTooltip` is non-null, every Install
// button is disabled and shows that tooltip (e.g. "stop mining before updating the miner").
inline int RenderReleaseList(const std::vector<ReleaseRow>& rows, float dp, bool* back,
const char* globalDisabledTooltip = nullptr) {
using namespace material;
int clicked = -1;
Type().text(TypeStyle::Subtitle2, TR("upd_select_version"));
ImGui::Spacing();
const float listH = 300.0f * dp;
if (ImGui::BeginChild("##release_list", ImVec2(0, listH), true)) {
const ImVec4 dim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
const ImVec4 warn = ImVec4(1.0f, 0.78f, 0.25f, 1.0f);
const ImVec4 succ = ImGui::ColorConvertU32ToFloat4(Success());
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
const ReleaseRow& r = rows[i];
ImGui::PushID(i);
ImGui::TextUnformatted(r.tag.c_str());
if (r.prerelease) { ImGui::SameLine(); ImGui::TextColored(warn, "[%s]", TR("upd_prerelease")); }
if (r.installed) { ImGui::SameLine(); ImGui::TextColored(succ, "[%s]", TR("upd_installed_badge")); }
if (!r.title.empty() || !r.date.empty()) {
std::string meta = r.title;
if (!r.title.empty() && !r.date.empty()) meta += " · ";
meta += r.date;
ImGui::TextColored(dim, "%s", meta.c_str());
}
const char* lbl = r.installed ? TR("upd_reinstall") : TR("upd_install");
const bool disabled = !r.hasAsset || globalDisabledTooltip != nullptr;
ImGui::BeginDisabled(disabled);
if (TactileButton(lbl, ImVec2(140.0f * dp, 0))) clicked = i;
ImGui::EndDisabled();
if (disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
material::Tooltip("%s", globalDisabledTooltip ? globalDisabledTooltip
: TR("upd_no_build_platform"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::Spacing();
if (TactileButton(TR("upd_back"), ImVec2(ImGui::GetContentRegionAvail().x, 0))) *back = true;
return clicked;
}
} // namespace ui } // namespace ui
} // namespace dragonx } // namespace dragonx

View File

@@ -371,7 +371,7 @@ public:
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(cp.x + 8.0f * dp, cp.y + 3.0f * dp), Success(), al); dl->AddText(metaFont, metaFont->LegacySize, ImVec2(cp.x + 8.0f * dp, cp.y + 3.0f * dp), Success(), al);
} else { } else {
ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f)); ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f));
if (StyledButton(TR("wallets_open"), ImVec2(btnW, btnH))) { if (TactileButton(TR("wallets_open"), ImVec2(btnW, btnH))) {
if (r.inDatadir) { app->switchToWallet(r.fileName); s_open = false; } if (r.inDatadir) { app->switchToWallet(r.fileName); s_open = false; }
else { openInPlace(app, r); } else { openInPlace(app, r); }
} }
@@ -419,7 +419,7 @@ public:
ImGui::SetNextItemWidth(listW - createBtnW - style.ItemSpacing.x); ImGui::SetNextItemWidth(listW - createBtnW - style.ItemSpacing.x);
ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName)); ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName));
ImGui::SameLine(); ImGui::SameLine();
if (StyledButton(TR("wallets_create"), ImVec2(createBtnW, 0))) { if (TactileButton(TR("wallets_create"), ImVec2(createBtnW, 0))) {
std::string name = normalizeWalletName(s_newName); std::string name = normalizeWalletName(s_newName);
std::error_code ec; std::error_code ec;
if (name.empty() || isLinkName(name)) { // reserve the in-place-link prefix if (name.empty() || isLinkName(name)) { // reserve the in-place-link prefix
@@ -439,7 +439,7 @@ public:
// ---- Scan another folder — always visible, full-width, opens the in-app folder picker - // ---- Scan another folder — always visible, full-width, opens the in-app folder picker -
// Start in the DRAGONX data directory (where the datadir wallets live) so the user can // Start in the DRAGONX data directory (where the datadir wallets live) so the user can
// navigate up/out from a familiar anchor to find wallets in other folders. // navigate up/out from a familiar anchor to find wallets in other folders.
if (StyledButton(TR("wallets_scan_folder"), ImVec2(listW, 0))) { if (TactileButton(TR("wallets_scan_folder"), ImVec2(listW, 0))) {
FolderPicker::open(util::Platform::getDragonXDataDir(), [app](const std::string& dir) { FolderPicker::open(util::Platform::getDragonXDataDir(), [app](const std::string& dir) {
std::error_code ec; std::error_code ec;
if (!dir.empty() && std::filesystem::is_directory(dir, ec)) { if (!dir.empty() && std::filesystem::is_directory(dir, ec)) {
@@ -497,7 +497,6 @@ public:
} }
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();
// ---- Refresh: icon-only, de-emphasized next to the text actions ----------------------- // ---- Refresh: icon-only, de-emphasized next to the text actions -----------------------
{ {
float bh = ImGui::GetFrameHeight(); float bh = ImGui::GetFrameHeight();
@@ -510,10 +509,10 @@ public:
s_needScan = true; s_needScan = true;
} }
ImGui::SameLine(); ImGui::SameLine();
if (StyledButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0))) if (TactileButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0)))
util::Platform::openFolder(util::Platform::getDragonXDataDir()); util::Platform::openFolder(util::Platform::getDragonXDataDir());
ImGui::SameLine(); ImGui::SameLine();
if (StyledButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false; if (TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false;
EndOverlayDialog(); EndOverlayDialog();
} }

View File

@@ -428,7 +428,8 @@ void I18n::loadBuiltinEnglish()
strings_["settings_shield_mining"] = "Shield Mining..."; strings_["settings_shield_mining"] = "Shield Mining...";
strings_["settings_merge_to_address"] = "Merge to Address..."; strings_["settings_merge_to_address"] = "Merge to Address...";
strings_["settings_clear_ztx"] = "Clear Z-Tx History"; strings_["settings_clear_ztx"] = "Clear Z-Tx History";
strings_["settings_import_key"] = "Import Key..."; strings_["settings_import_key"] = "Import Private Key...";
strings_["settings_import_viewkey"] = "Import Viewing Key...";
strings_["settings_export_key"] = "Export Key..."; strings_["settings_export_key"] = "Export Key...";
strings_["settings_export_all"] = "Export All..."; strings_["settings_export_all"] = "Export All...";
strings_["settings_backup"] = "Backup..."; strings_["settings_backup"] = "Backup...";
@@ -507,6 +508,43 @@ void I18n::loadBuiltinEnglish()
strings_["wiz_encrypt_desc"] = "Encrypt your wallet to protect private keys with a passphrase."; strings_["wiz_encrypt_desc"] = "Encrypt your wallet to protect private keys with a passphrase.";
strings_["wiz_encrypt_warning"] = "If you lose your passphrase, you lose access to your funds."; strings_["wiz_encrypt_warning"] = "If you lose your passphrase, you lose access to your funds.";
strings_["wiz_passphrase"] = "Passphrase:"; strings_["wiz_passphrase"] = "Passphrase:";
// --- Encrypt / change-passphrase dialogs (settings) ---
strings_["enc_desc"] = "Encrypting your wallet protects your private keys with a passphrase. After encryption, the daemon will restart.";
strings_["enc_confirm"] = "Confirm:";
strings_["enc_encrypting"] = "Encrypting wallet...";
strings_["enc_wait"] = "Please wait, do not close the application.";
strings_["enc_success"] = "Wallet encrypted successfully!";
strings_["enc_pin_desc"] = "A 4-8 digit PIN lets you unlock your wallet without typing the full passphrase every time.";
strings_["enc_pin_set_ok"] = "PIN set successfully";
strings_["enc_pin_vault_fail"] = "Failed to create PIN vault";
strings_["enc_pin_skipped"] = "PIN skipped. You can set one later in Settings.";
strings_["change_pass_title"] = "Change Passphrase";
strings_["change_pass_current"] = "Current Passphrase:";
strings_["change_pass_new"] = "New Passphrase:";
strings_["change_pass_confirm"] = "Confirm New:";
// --- Remove-encryption (decrypt) dialog (settings) ---
strings_["decrypt_title"] = "Remove Wallet Encryption";
strings_["decrypt_warning"] = "This will remove encryption from your wallet. Your private keys will be stored unprotected on disk.";
strings_["decrypt_desc"] = "The wallet will be exported, the daemon restarted with a fresh unencrypted wallet, and all keys re-imported. This may take several minutes depending on wallet size.";
strings_["decrypt_step_unlock"] = "Unlocking wallet";
strings_["decrypt_step_export"] = "Exporting wallet keys";
strings_["decrypt_step_stop"] = "Stopping daemon";
strings_["decrypt_step_backup"] = "Backing up encrypted wallet";
strings_["decrypt_step_restart"] = "Restarting daemon";
strings_["decrypt_wait_restart"] = "Waiting for the daemon to finish starting up...";
strings_["decrypt_wait_general"] = "Please wait. The daemon is exporting keys, restarting, and re-importing. This may take several minutes.";
strings_["decrypt_success_title"] = "Wallet decrypted successfully!";
strings_["decrypt_success_desc"] = "Your wallet is now unencrypted. A backup of the encrypted wallet was saved as wallet.dat.encrypted.bak in your data directory.";
strings_["decrypt_error_title"] = "Decryption failed";
strings_["try_again"] = "Try Again";
// --- PIN setup / change / remove dialogs (settings) ---
strings_["pin_setup_desc"] = "Set a 4-8 digit PIN for quick wallet unlock. Your wallet passphrase will be encrypted with this PIN and stored locally.";
strings_["pin_wallet_passphrase"] = "Wallet Passphrase:";
strings_["pin_new_label"] = "New PIN (4-8 digits):";
strings_["pin_change_desc"] = "Change your unlock PIN. You need your current PIN and a new PIN.";
strings_["pin_current_label"] = "Current PIN:";
strings_["pin_confirm_new_label"] = "Confirm New PIN:";
strings_["pin_remove_desc"] = "Enter your current PIN to confirm removal. You will need to use your full passphrase to unlock.";
strings_["wiz_confirm"] = "Confirm:"; strings_["wiz_confirm"] = "Confirm:";
strings_["wiz_strength_weak"] = "Weak"; strings_["wiz_strength_weak"] = "Weak";
strings_["wiz_strength_fair"] = "Fair"; strings_["wiz_strength_fair"] = "Fair";
@@ -731,6 +769,7 @@ void I18n::loadBuiltinEnglish()
// Settings: additional tooltips (keys/data row) // Settings: additional tooltips (keys/data row)
strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet"; strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet";
strings_["tt_import_viewkey"] = "Import a shielded viewing key to watch an address (read-only)";
strings_["tt_export_key"] = "Export the private key for the selected address"; strings_["tt_export_key"] = "Export the private key for the selected address";
strings_["tt_export_all"] = "Export all private keys to a file"; strings_["tt_export_all"] = "Export all private keys to a file";
strings_["tt_backup"] = "Create a backup of your wallet.dat file"; strings_["tt_backup"] = "Create a backup of your wallet.dat file";
@@ -1200,6 +1239,8 @@ void I18n::loadBuiltinEnglish()
strings_["backup_created"] = "Wallet backup created"; strings_["backup_created"] = "Wallet backup created";
strings_["backup_description"] = "Create a backup of your wallet.dat file. This file contains all your private keys and transaction history. Store the backup in a secure location."; strings_["backup_description"] = "Create a backup of your wallet.dat file. This file contains all your private keys and transaction history. Store the backup in a secure location.";
strings_["backup_destination"] = "Backup destination:"; strings_["backup_destination"] = "Backup destination:";
strings_["backup_warn"] = "This file holds all your private keys \xE2\x80\x94 keep it somewhere safe.";
strings_["backup_overwrite_confirm"] = "A file already exists there \xE2\x80\x94 Save again to overwrite it.";
strings_["backup_tip_external"] = "Store backups on external drives or cloud storage"; strings_["backup_tip_external"] = "Store backups on external drives or cloud storage";
strings_["backup_tip_multiple"] = "Create multiple backups in different locations"; strings_["backup_tip_multiple"] = "Create multiple backups in different locations";
strings_["backup_tip_test"] = "Test restoring from backup periodically"; strings_["backup_tip_test"] = "Test restoring from backup periodically";
@@ -1290,6 +1331,31 @@ void I18n::loadBuiltinEnglish()
strings_["console_welcome"] = "Welcome to ObsidianDragon Console"; strings_["console_welcome"] = "Welcome to ObsidianDragon Console";
strings_["console_zoom_in"] = "Zoom in"; strings_["console_zoom_in"] = "Zoom in";
strings_["console_zoom_out"] = "Zoom out"; strings_["console_zoom_out"] = "Zoom out";
strings_["console_toggle_accents"] = "Toggle line color accents";
strings_["console_toggle_text_color"] = "Toggle line text colors";
strings_["console_accents"] = "Color accents";
strings_["console_text_colors"] = "Text colors";
strings_["console_cat_control"] = "Control";
strings_["console_cat_network"] = "Network";
strings_["console_cat_blockchain"] = "Blockchain";
strings_["console_cat_mining"] = "Mining";
strings_["console_cat_wallet"] = "Wallet";
strings_["console_cat_raw_transactions"] = "Raw Transactions";
strings_["console_cat_utility"] = "Utility";
strings_["console_ref_search_hint"] = "Search by name or task\xE2\x80\xA6";
strings_["console_ref_parameters"] = "Parameters";
strings_["console_ref_no_params"] = "Takes no parameters.";
strings_["console_ref_optional"] = "optional";
strings_["console_ref_example"] = "Example";
strings_["console_ref_builds"] = "Builds";
strings_["console_ref_destructive"] = "Consequential";
strings_["console_ref_run_confirm"] = "Run %s now? This is a consequential command.";
strings_["console_ref_cancel"] = "Cancel";
strings_["console_ref_run"] = "Run";
strings_["console_ref_insert"] = "Insert into console";
strings_["console_ref_insert_run"] = "Insert & run";
strings_["console_ref_select_hint"] = "Select a command to see what it does.";
strings_["console_ref_no_match"] = "No commands match.";
// --- Export All Keys Dialog --- // --- Export All Keys Dialog ---
strings_["export_keys_btn"] = "Export Keys"; strings_["export_keys_btn"] = "Export Keys";
@@ -1298,6 +1364,14 @@ void I18n::loadBuiltinEnglish()
strings_["export_keys_include_z"] = "Include Z-addresses (shielded)"; strings_["export_keys_include_z"] = "Include Z-addresses (shielded)";
strings_["export_keys_options"] = "Export options:"; strings_["export_keys_options"] = "Export options:";
strings_["export_keys_success"] = "Keys exported successfully"; strings_["export_keys_success"] = "Keys exported successfully";
strings_["export_keys_select_type"] = "Select at least one address type";
strings_["export_keys_not_connected"] = "Not connected to the daemon";
strings_["export_keys_none_addrs"] = "No addresses to export";
strings_["export_keys_none_result"] = "No keys exported (0 of %d) \xE2\x80\x94 unlock the wallet and try again.";
strings_["export_keys_none_toast"] = "No keys could be exported \xE2\x80\x94 is the wallet unlocked?";
strings_["export_keys_write_fail"] = "Failed to write the key file.";
strings_["export_keys_partial"] = "Exported %d of %d keys \xE2\x80\x94 incomplete (some had no spending key, or the wallet is locked).";
strings_["export_keys_partial_toast"] = "Partial export: %d of %d keys";
strings_["export_keys_title"] = "Export All Private Keys"; strings_["export_keys_title"] = "Export All Private Keys";
// --- Export Transactions Dialog --- // --- Export Transactions Dialog ---
@@ -1321,9 +1395,42 @@ void I18n::loadBuiltinEnglish()
strings_["import_key_tooltip"] = "Enter one or more private keys, one per line.\nSupports both z-address and t-address keys.\nLines starting with # are treated as comments."; strings_["import_key_tooltip"] = "Enter one or more private keys, one per line.\nSupports both z-address and t-address keys.\nLines starting with # are treated as comments.";
strings_["import_key_warning"] = "Warning: Never share your private keys! Importing keys from untrusted sources can compromise your wallet."; strings_["import_key_warning"] = "Warning: Never share your private keys! Importing keys from untrusted sources can compromise your wallet.";
strings_["import_key_z_format"] = "Z-address spending keys (secret-extended-key-...)"; strings_["import_key_z_format"] = "Z-address spending keys (secret-extended-key-...)";
strings_["import_key_warn"] = "Only import a key you own \xE2\x80\x94 it grants access to its funds.";
strings_["import_key_need_node"] = "Connect a running node to import a key.";
strings_["import_key_field"] = "Key";
strings_["import_key_reveal_tip"] = "Show/hide the key";
strings_["import_key_type_tkey"] = "Transparent private key";
strings_["import_key_type_zspend"] = "Shielded spending key";
strings_["import_key_type_zview"] = "Shielded viewing key (watch-only)";
strings_["import_key_type_unknown"] = "Unrecognized key format";
strings_["import_key_rescanning"] = "Importing & rescanning \xE2\x80\x94 this can take several minutes";
strings_["import_key_done"] = "Imported. Wallet is rescanning.";
strings_["import_key_address"] = "Address:";
strings_["import_key_import"] = "Import";
// Viewing-key (watch-only) import — separate button + dialog mode.
strings_["import_viewkey_title"] = "Import Viewing Key";
strings_["import_viewkey_note"] = "Watch-only: a viewing key reveals an address's balance and transactions but cannot spend its funds.";
strings_["import_viewkey_field"] = "Viewing key";
strings_["import_key_wrong_type"] = "This looks like a viewing key. Use \"Import Viewing Key\" instead.";
strings_["import_viewkey_wrong_type"] = "This looks like a spending key. Use \"Import Private Key\" instead.";
strings_["import_scan_label"] = "Scan from block height (optional)";
strings_["import_scan_hint"] = "0 = rescan from the start";
strings_["import_scan_transparent"] = "Transparent keys always rescan fully";
strings_["import_scan_tip"] = "current height";
strings_["paste_clip_empty"] = "Clipboard is empty";
// Sweep (import a spending key, then move all its funds to your own address).
strings_["sweep_toggle"] = "Sweep to my wallet (don't keep the key)";
strings_["sweep_caveat"] = "Imports the key to sign one transaction moving all its funds to your address. The key stays in your wallet with an empty balance.";
strings_["sweep_dest_label"] = "Send swept funds to";
strings_["sweep_dest_new"] = "New shielded address (recommended)";
strings_["sweep_button"] = "Sweep";
strings_["sweep_done"] = "Done \xE2\x80\x94 funds swept to your address.";
strings_["sweep_to"] = "Swept to:";
strings_["sweep_tx"] = "Transaction:";
// --- Key Export Dialog --- // --- Key Export Dialog ---
strings_["key_export_fetching"] = "Fetching key from wallet..."; strings_["key_export_fetching"] = "Fetching key from wallet...";
strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again.";
strings_["key_export_private_key"] = "Private Key:"; strings_["key_export_private_key"] = "Private Key:";
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties."; strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
strings_["key_export_reveal"] = "Reveal Key"; strings_["key_export_reveal"] = "Reveal Key";
@@ -1405,6 +1512,9 @@ void I18n::loadBuiltinEnglish()
strings_["portfolio_cancel"] = "Cancel"; strings_["portfolio_cancel"] = "Cancel";
strings_["portfolio_revert"] = "Revert"; strings_["portfolio_revert"] = "Revert";
strings_["portfolio_close"] = "Close"; strings_["portfolio_close"] = "Close";
strings_["portfolio_save_need_name"] = "Enter a name to save this group.";
strings_["portfolio_save_need_address"] = "Add at least one address to save.";
strings_["portfolio_save_need_price"] = "Enter a manual price above 0 to save.";
strings_["portfolio_untitled"] = "Untitled"; strings_["portfolio_untitled"] = "Untitled";
strings_["portfolio_detail_empty"] = "Select a group on the left, or add one, to edit it."; strings_["portfolio_detail_empty"] = "Select a group on the left, or add one, to edit it.";
strings_["portfolio_add_to"] = "Add to portfolio"; strings_["portfolio_add_to"] = "Add to portfolio";

View File

@@ -3200,6 +3200,23 @@ void testConsoleInputModel()
EXPECT_TRUE(call.params[0].get<bool>()); EXPECT_TRUE(call.params[0].get<bool>());
EXPECT_EQ(call.params[1].get<long long>(), 4LL); EXPECT_EQ(call.params[1].get<long long>(), 4LL);
// Quoted numeric-looking args must stay strings (not coerced to numbers).
auto qcall = dragonx::ui::BuildConsoleRpcCall("setaccount \"123\" \"456.7\"");
EXPECT_TRUE(qcall.valid);
EXPECT_EQ(qcall.params.size(), static_cast<size_t>(2));
EXPECT_TRUE(qcall.params[0].is_string());
EXPECT_EQ(qcall.params[0].get<std::string>(), std::string("123"));
EXPECT_TRUE(qcall.params[1].is_string());
// Bare finite numbers become numbers; non-finite / non-whole-token tokens fall back to strings
// (no std::stoll truncation of "1e999" to 1, no inf).
auto ncall = dragonx::ui::BuildConsoleRpcCall("foo 42 3.5 1e999 123abc");
EXPECT_TRUE(ncall.params[0].is_number_integer());
EXPECT_EQ(ncall.params[0].get<long long>(), 42LL);
EXPECT_TRUE(ncall.params[1].is_number_float());
EXPECT_TRUE(ncall.params[2].is_string());
EXPECT_EQ(ncall.params[2].get<std::string>(), std::string("1e999"));
EXPECT_TRUE(ncall.params[3].is_string());
auto resultLines = dragonx::ui::FormatConsoleRpcResultLines("{\n \"balance\": 12,\n \"ok\": true\n}", false); auto resultLines = dragonx::ui::FormatConsoleRpcResultLines("{\n \"balance\": 12,\n \"ok\": true\n}", false);
EXPECT_EQ(resultLines.size(), static_cast<size_t>(4)); EXPECT_EQ(resultLines.size(), static_cast<size_t>(4));
EXPECT_EQ(resultLines[0].role, dragonx::ui::ConsoleResultLineRole::JsonBrace); EXPECT_EQ(resultLines[0].role, dragonx::ui::ConsoleResultLineRole::JsonBrace);