383 Commits

Author SHA1 Message Date
061db16004 i18n: route explorer and shield status strings through TR()
Centralise the remaining hardcoded English literals in two otherwise
fully-translated files (explorer_tab: 40 TR calls, shield_dialog: 23) so
they join the i18n system with an English source key + fallback:

- explorer_tab: the three search-error messages (invalid response, hash
  not found, not-connected) now use explorer_* keys. Also guards a hash
  lookup behind an active daemon connection and disables the block-detail
  prev/next nav while a fetch is in flight (both were the reason these
  error paths could fire).
- shield_dialog: the operation status/error strings (submitting, submitted,
  failed, status, error-checking-status, shield/merge failed) now use
  shield_* keys.
- i18n: add the new explorer_* and shield_* English source keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:09:18 -05:00
ec48538881 refactor(mining): surface inconclusive benchmark; dedup idle combo
- mining_benchmark: when a thread benchmark finishes with no nonzero
  hashrate sample (e.g. the pool never reported a rate), don't finish
  silently — set a new `inconclusive` flag on ThreadBenchmarkUpdate and
  let mining_tab warn. Keeps the state-machine core pure/no-I/O (it is
  unit-tested), rather than calling the Notifications singleton from it.
- mining_controls: fold the two byte-identical idle-delay combo blocks
  (non-scaling + thread-scaling branches) into one lambda parameterised
  by combo id; removes ~30 lines of duplication.
- request_payment: drop the dead s_selected_addr_idx state and write the
  address-selected comparisons as std::string == char* consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:09:08 -05:00
658c0f355b fix(ui): validate and clamp dialog input
Tighten input handling across several dialogs so bad/edge input can't
produce a doomed request or a confusing display:

- app_security: passphrase strength meter now factors character-class
  diversity — an all-one-class string downgrades one tier so "aaaaaaaa"
  no longer scores as high as a mixed one.
- block_info: clamp the height field to the chain tip and hide/deny
  "Next" at the tip (was only yielding a raw RPC error).
- address_label: trim surrounding whitespace before saving; a
  whitespace-only label clears it (mirrors clearing the icon).
- send_tab: re-clamp the amount when a Max send has its fee bumped in the
  confirm popup (kept the total within budget) and NUL-terminate the
  strncpy'd address/memo when a payment URI overwrites the form.
- settings_page: reject an empty/whitespace-only lite wallet path before
  dispatching an unusable open/restore request.
- peers_tab: guard ExtractIP against an empty address.
- transaction_details: disable "View in explorer" when the configured
  explorer URL is empty/whitespace so we never open a garbage link.
- app: seed-backup "Skip" now requires a second, deliberate click with a
  fund-loss warning before it dismisses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:08:59 -05:00
41f5548593 fix(ui): prevent stuck spinners and double-submits in dialogs
Address a cluster of low-severity robustness nits where an async
callback could be skipped (leaving a spinner spinning forever) or a
synchronous action could be re-fired mid-flight:

- app_network: add catch(...) fallbacks around the importPrivateKey and
  submitZSendMany worker lambdas so a non-std throw can't escape the
  worker and skip the main-thread callback (stuck "Importing…"/"Sending…").
- export_transactions: re-entrancy guard disabling Export while a write
  is in flight.
- bootstrap_download: disable Cancel once clicked so the request is
  visibly acknowledged and can't be re-fired before the worker stops.
- network_tab: give clear "already in list" feedback on a duplicate
  server (keep the inputs, clear only the stale invalid-URL error) and
  disable/relabel Refresh while a probe is in flight.
- key_export: offer Retry from the error branch (falls back to Reveal)
  and guard against an empty/malformed address before dispatching.
- receive_tab: only enter the "generating…" state when a dispatch is
  actually possible (guards a stuck spinner when disconnected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:08:43 -05:00
1dbbd44759 fix: Tier-3 feature gaps — transfer result screen + accurate RPC display
- Address-transfer dialog closed itself on submit (s_open=false the same frame),
  so its in-dialog result screen was dead code. Keep the dialog open on submit:
  s_sending drives the button to a disabled "Sending…" state, and the async
  callback's result (success txid / error) now shows in the result screen with
  its own Close button.
- Settings RPC connection editor was inert AND showed compile-time defaults.
  Populate Host/Port/Username/Password from the auto-detected daemon config
  (rpc::Connection::autoDetectConfig) and make the fields read-only — the RPC
  credentials come from the daemon's DRAGONX.conf, so these now accurately
  DISPLAY the live connection instead of pretending to be an editor (which did
  nothing). The existing "auto-detected" note now matches the behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:27:59 -05:00
b0cc6bcef4 fix: Tier-2 remaining mediums — maintenance/mining feedback + force-quit hang detection
The last four (more involved) robustness items from the audit:

- deleteBlockchainData: post the deleted-item count to the main thread (via an
  atomic, since Notifications isn't thread-safe) and show a completion toast
  ("Blockchain data deleted (N items). Daemon restarting…") — previously the
  result was only logged.
- Pool mining: pool start has a connect delay with no feedback; announce
  "Starting pool miner — connecting…" on a successful start and "Pool miner
  connected and hashing." once the poll confirms it (contained pool_starting_
  flag; the shared mining-toggle state machine is untouched).
- Force Quit (shutdown screen): gate it on the status text having STALLED (a
  genuine hang) rather than a bare 10s timer — with a 20s hard-ceiling backstop —
  and show a state-aware caution naming the stuck step (force-quitting mid daemon
  flush risks the chainstate).
- Benchmark: require a confirming second click that first builds candidates to
  estimate the duration ("Benchmark takes ~Ns and interrupts mining"), instead
  of interrupting mining immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:14:56 -05:00
dd9bc0bd2b fix: Tier-2 involved mediums — send confirm re-validation + console JSON errors
- Send: re-validate against LIVE state at the "Confirm & Send" click (connected,
  not syncing, amount > 0, total <= available). The confirm dialog persists
  across frames, so a balance drop / sync restart / fee bump after Review could
  otherwise broadcast a now-invalid transaction; it now shows a clear message
  instead of sending.
- Console: a malformed JSON argument ({...}/[...] that fails to parse) was
  silently sent to the daemon as a plain string. Now the command fails with a
  clear "Argument N is not valid JSON" line instead of being silently mangled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:00:16 -05:00
b2a8c4487e fix: Tier-2 quick mediums — view-only guard, memo counter, updater/close guards
- Send: block Review from a view-only source (no spending key) with a tooltip,
  instead of failing at submit; label the memo counter as bytes and warn in
  colour once the field hits its cap (Sapling memos are byte-limited).
- Shield: show "No shielded (z) address yet — create one on the Receive tab
  first" when the destination combo is empty, instead of an empty dropdown.
- Updater dialogs (xmrig/daemon): suppress the window X during an active
  download/verify/extract so closing can't orphan/block on the worker — the
  in-dialog Cancel is the way to abort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:54:55 -05:00
65bb98cd09 fix: Tier-2 mediums — input trims, confirmations, amount normalization
More robustness fixes from the audit (the import-key pattern, lower severity):

- Trim pasted input before use/validation: network add-server URL/label,
  lite-wallet key import (+ block empty), validate-address, address-book entry.
- Confirmations for destructive/irreversible actions:
  - Address-book Delete now needs a confirming second click (no undo).
  - Console `stop` needs a confirming second `stop` (it shuts down the node).
  - Wizard "Skip" encryption needs a confirming second click (keys stored
    unencrypted).
- Send amount: normalize to 8dp (satoshi precision) on both the DRGX and USD
  inputs so digits past 8dp aren't silently dropped between the preview/review
  and what's actually sent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:18:57 -05:00
129a8e6449 fix: Tier-2 UX robustness — action guards, in-progress guards, input trimming
The app-wide "import-key pattern" fixes (missing guards / untrimmed input / no
re-entrancy protection), from the robustness audit:

- Import-key dialog: unify the type indicator and the RPC dispatch on one
  classifier (classifyPrivateKey is now prefix-aware — an "SK..." shielded key no
  longer misroutes to the transparent RPC), trim manual input (not just Paste),
  and gate both the indicator and the Import button on a shared
  isRecognizedPrivateKey() so unrecognized/empty input can't be submitted.
- Shield: guard the submit button on connected + !syncing (like Send), with a
  disabled tooltip, instead of firing into a raw daemon error.
- Mining: disable the pool Mine button when the payout address is empty
  ("enter a payout address first"), and trim the pool URL/worker on persist.
- Console: track in-flight RPC commands (atomic counter + busy() override) so the
  input is disabled while a command runs instead of piling up on the worker.
- Maintenance (rescan/repair/delete-chain/reinstall-daemon): re-entrancy guard —
  bail with "already in progress" instead of launching a duplicate destructive op.
- Bootstrap dialog: re-attach to an in-flight download on reopen instead of
  resetting state and orphaning the running worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:09:23 -05:00
df14533ad3 fix: Tier-1 UX robustness — backup/export integrity, verified installs, input validation
From the app-wide robustness audit (calibrated to the import-key dialog). These
are the safety-critical fixes; several could cost users funds:

- Backup/export false success (FUND LOSS): exportAllKeys pre-seeded a header, so
  a keyless result (the usual case when the wallet is encrypted+locked) still
  looked non-empty and backupWallet wrote a private-key-less file and reported
  "Backup saved". Now exportAllKeys returns an exported count; backupWallet and
  the export-all dialog refuse to write / report success on 0 keys, disclose the
  count, flag partial results as INCOMPLETE, and the backup dialog confirms
  before overwriting an existing file (+ trims the path).
- Bootstrap: fail CLOSED — refuse to install an unverified multi-GB archive when
  no checksum is published (was `return true`), mirroring the xmrig/daemon updaters.
- Restore-from-seed: require a valid BIP39 word count (12/15/18/21/24) with a live
  "should be 24 words — you have N" hint instead of accepting any non-empty text.
- Encryption passphrase: detect leading/trailing whitespace and BLOCK with a
  warning (not a silent trim, which would change the passphrase and lock the user
  out).
- Receive payment QR: emit the canonical `drgx:` scheme with a URL-encoded memo
  via a new shared util::buildPaymentUri (the request-payment dialog now routes
  through it too, so they can't diverge); the old "dragonx:" + raw memo was
  unparseable by the wallet's own scanner.

Also clamps the receive "Recent Received" list to the 4 most recent (companion to
the recent-lists commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:54:05 -05:00
24bb32eb61 feat(mining): label the pool fee inline + widen the auto-balance card
Each pool row showed "<hashrate>  N%" with nothing saying the % is the pool fee.
Make it "<hashrate>  N% fee" and widen the left auto-balance card (25%/180dp ->
30%/210dp min) so the added text fits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:53:46 -05:00
d3dcb5a015 feat(console): zebra contrast, edge outline, blinking caret, external-daemon status
- Lower the zebra-stripe contrast (scanline-line-alpha 4 -> 2; the derived dark
  band softens with it) so the alternating rows are subtler.
- Give the input's terminal bar a 1px outline along its own edges (theme-aware),
  matching the output panel's glass rim, and a near-white surface on light skins.
- Add a blinking terminal caret at the end of the input when it isn't focused.
- Report a daemon started before the wallet ("no wallet-managed EmbeddedDaemon"
  but RPC connected) as "Running" instead of the misleading "no daemon".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:53:46 -05:00
7249d11899 feat(ui): clamp recent-activity lists to the 4 most recent
The overview "Recent Transactions" and the send tab "Recent Sends" lists showed
every matching transaction in a scroll region. Cap them at the 4 most recent
(state.transactions is sorted newest-first, so a simple head-limit). The receive
"Recent Received" list gets the same clamp in the Tier-1 robustness commit
(same file as the QR fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:53:46 -05:00
ee6903ae56 fix(theme): mop-up per-skin nits — ratio bar + light-skin muted text
Lower-severity polish from the per-skin review:
- Market portfolio ratio bar: was full-saturation Success/Warning at a208 over a
  white a10 track — the green read as "neon" on dark skins and the track was
  invisible on light skins. Make it theme-aware: muted fills (a165) on dark, a
  dark track on light so the bar is defined on white/pastel surfaces.
- Light-skin muted text: bump --on-surface-disabled 0.52 -> 0.62 on light/
  color-pop-light/dune/marble/iridescent so secondary text (addresses, category
  labels) clears the pastel/gradient backgrounds (analog of the dark-skin bump).

Remaining review nits are the "white-overlay-assumes-dark" track/hover pattern
(a broader separate sweep) and per-skin aesthetic trade-offs; deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:46:53 -05:00
1c5bc0d4d6 fix(theme): per-skin legibility polish from the sweep review
Address the high-severity, systemic legibility issues found in the per-skin
screenshot review (verified across all 9 skins):

- Status pills (history): "Confirmed" text was 55%-alpha (dim); make it full
  opacity, and give status/shield pills a visible tinted fill (a48) + 1px border
  (a90) so they stop vanishing on dark-red / near-white / gradient skins.
- Console light theming: light skins now use a near-white terminal surface, and
  the log text/JsonBrace colors switch to the dark defaults in light themes (the
  schema colors are dark-terminal-authored — honor them only in dark themes) so
  text isn't pale-on-white.
- Dark-skin muted text: bump --on-surface-medium 0.85 / --on-surface-disabled
  0.58 on dragonx/dark/color-pop-dark/obsidian (obsidian addresses were ~1.1:1).
- Disabled Send button: the disabled fill was hardcoded white (invisible on
  light skins) — use a dark overlay in light themes.
- Light-skin inputs: stronger frame fill + border color and a 1px frame border
  for light themes so fields (and buttons) have defined boundaries on white/
  pastel surfaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:33:48 -05:00
dfca9bbd92 refactor(ui): add LabeledInputRow + LabeledInputMultiline; complete inputs layer
Extend the inputs layer with the two remaining form-field idioms, pixel-identical:
- material::LabeledInputRow(label, labelPos, ...) — horizontal label-beside-input
  (Text + SameLine + SetNextItemWidth + InputText). Adopt at the settings RPC
  host/port/user/pass rows.
- material::LabeledInputMultiline(label, id, buf, size, flags) — label above a
  sized multiline input. Adopt at the address-book notes and request-payment
  memo (drops the redundant SetNextItemWidth that InputTextMultiline's explicit
  size already governs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 21:41:42 -05:00
840ead3645 refactor(ui): add material::LabeledInput helper + adopt at 8 form fields
UI-standardization audit: the labeled single-line form field (a plain
ImGui::Text label above a fixed-width InputText/InputTextWithHint) was
open-coded across dialogs. Add material::LabeledInput(label, id, buf, size,
width=-1, hint=nullptr, flags=0) mirroring that idiom exactly — pixel-identical,
but a single chokepoint so input styling (label typography, frame, spacing) can
later evolve in one place. Returns the input's edited bool.

Adopt at the clean vertical (label-above) single-line sites: address-book
add/edit (label + address), shield from-address, export-all-keys + export-
transactions filename, validate-address input, request-payment label + URI.
Left as-is: horizontal label-beside-input rows (settings RPC host/port/user/
pass use Text + SameLine), multiline (InputTextMultiline), numeric (InputDouble),
combos, and hint-only inputs — none match the vertical single-line idiom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 21:11:02 -05:00
1c8e97c0a4 refactor(ui): adopt material::SegmentedControl for the receive All/Z/T filter
UI-standardization audit: the receive address-type filter (All/Z/T) was three
hand-rolled gapped ImGui::Button pills with per-segment PushStyleColor. Replace
with the shared material::SegmentedControl (rounded track + Primary inset-pill
on the active cell + centered caption labels), the same control the market
portfolio editor uses. Keeps the same right-aligned footprint (toggleTotalW)
and drives s_addr_type_filter from the returned clicked index. Verified across
all 9 skins via the screenshot sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:58:58 -05:00
f1e66c08d1 refactor(ui): adopt material::IconButton for the idle/scale/gpu mining toggles
Follow-up to c83348a: convert the three mining solo-mode header toggles
(idle-mining, thread-scaling, gpu-aware) from the hand-rolled "draw active
pill + glyph, then hit-test" idiom to material::IconButton using the restBg
(active-pill) path. Each keeps its exact behavior pixel-for-pixel: a circular
Primary-tinted pill when active (alpha 60 idle / 40 scale+gpu), a state-colored
glyph (Primary when on, muted when off), hand cursor + tooltip on hover, and
the idle button's cursor save/restore (savedCur, restored at block end) is
preserved. Completes the clean icon-button adoption; the raw-rect IsRectHovered
sites remain intentionally separate (different hit mechanism).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:12:15 -05:00
c83348ab63 refactor(ui): add material::IconButton helper + adopt at 8 frameless icon buttons
UI-standardization audit (icon-button discovery pass): the app had ~22 clean
hand-rolled icon buttons repeating the same "InvisibleButton hit-target +
centered glyph + hover feedback + tooltip" boilerplate. Add a shared
material::IconButton(id, glyph, font, size, IconButtonStyle) — centered glyph,
optional resting + hover backgrounds (pill/rounded-rect), hover recolor, hand
cursor + tooltip; caller resolves state-dependent glyph/colors and returns
clicked. Adopt at the homogeneous, self-contained sites, pixel-faithful:
- mining_mode_toggle: pool/worker dropdown + bookmark + reset (×5)
- network: server hide/unhide eye (×1)
- mining_controls: pool benchmark start (×1)
- mining_stats: pool-balance refresh (×1)

Deferred (own follow-up + sweep): the idle/scale/gpu active-pill toggles use a
"draw pill+glyph earlier, hit-test later" idiom needing restructuring; the
raw-rect (IsRectHovered, no InvisibleButton) sites in market/mining_stats/
balance are a different hit-test mechanism left intentionally. Framed icon
pagers already ride TactileButton after the prior commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:25:20 -05:00
da56bb73a0 refactor(ui): consolidate raw ImGui buttons onto material::TactileButton
UI-standardization audit: ~66 call sites still used raw ImGui::Button /
ImGui::SmallButton instead of the app's canonical TactileButton, so those
buttons missed the standard glass fill + rim + tactile overlay (and the
light-theme flat treatment). Convert 59 plain action-button sites across the
wizard, security dialogs, settings, market/console/explorer tabs, and the
address dialogs to material::TactileButton / TactileSmallButton (drop-in:
same signature/return, args preserved verbatim). Danger buttons keep their
PushStyleColor wrappers — Tactile renders through them then overlays.

Deliberately left raw (not plain buttons): InvisibleButton hit-targets, the
frameless transparent-bg collapsible-header toggles (RPC/Debug), the icon-only
pagination chevrons, and the receive All/Z/T segmented filter — a glass rim
would clash with those intentionally borderless/segmented looks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:00:14 -05:00
ad59d62b82 refactor(ui): extract shared ProgressBar / CopyableTextOverlay / DrawPill helpers
UI-standardization audit: three draw patterns were copy-pasted across tabs and
dialogs. Promote each to a material:: helper and adopt at every verbatim site.
Output is pixel-identical (same colors/geometry threaded through as params).

- material::DrawProgressBar / ProgressBar — replaces 3 byte-identical rounded
  fill bars in the daemon/xmrig/bootstrap download dialogs.
- material::CopyableTextOverlay — the click-to-copy affordance (hit button +
  hand cursor + tooltip + hover underline + clipboard) shared by the peers
  best-block hash and the 3 mining stats (difficulty/block/address). Returns
  "copied this frame" so callers keep their own toast (no ui-layer dependency).
- material::DrawPill / PillSize — rounded bg + optional border + inset text
  badge, shared by the explorer status pill, the market Z/T chip, and the
  network official/custom tag.

Divergent one-offs left in place (transactions status pills use schema rounding
around pre-positioned multi-line text; the mining filter-pill is a button bg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:48:08 -05:00
07f6d2efc9 refactor(ui): delete dead button code (buttons.h + ApplyTactile)
UI-standardization audit found two dead, contradictory "how to draw a button"
surfaces with zero call sites:
- src/ui/material/components/buttons.h (353 lines: TextButton/Outlined/Contained/
  Button(spec)/IconButton/FAB) — included by 3 mining files but never used.
- ApplyTactile (draw_helpers.h) — a retrofit-tactile-onto-plain-button wrapper,
  never called.
Remove the file, the 3 dead includes, and ApplyTactile. TactileButton/StyledButton
remain the canonical button helpers. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:34:12 -05:00
e84cea27d9 feat(theme): overarching light-theme contrast (buttons, dividers, accent bars)
From the theme sweep review, three overarching light-theme fixes:
- Buttons: bump ImGuiCol_Button light overlays 13/20/28% -> 16/23/30% so the
  small/secondary buttons read as clearly as the main ones.
- Dividers + outlines: raise --divider (0.12-0.14 -> 0.20) and --outline
  (-> 0.24) across all light skins so section/list dividers are visible on white.
- Ratio/balance bars: deepen the muted --success/--warning greens/ambers in the
  light skins that were pale (light, marble, dune) so the shielded/transparent
  bars + success text are vivid (color-pop-light + iridescent were already vivid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:19:31 -05:00
a457f0262e fix(theme): define light-theme input fields (darker FrameBg)
From the theme screenshot sweep: input fields, dropdowns and amount boxes were
nearly invisible on light themes (Send form especially) — ImGuiCol_FrameBg was
black@4% vs the dark theme's white@7%, so boxes blended into the white surface.
Bump the light-theme FrameBg / hovered / active to 9/14/18% so inputs read as
defined boxes. Affects all light skins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:13:51 -05:00
4c466d78d1 feat(debug): per-tab screenshot subfolders + overwrite + Open location button
- Organize sweep output into per-tab subfolders: <config>/screenshots/<tab>/<skin>.png
  (was one timestamped folder with idx_skin_tab.png names).
- Fixed folder (no timestamp): subsequent sweeps overwrite the existing PNGs in
  place instead of creating a new directory each run.
- Add an "Open location" TactileButton next to "Run screenshot sweep" that opens
  the screenshots folder via Platform::openFolder(app->screenshotDir()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:58:47 -05:00
0d4eaa02e7 fix(debug): put screenshot sweep in the renamed DEBUG OPTIONS section
Correct the placement: instead of a new "Debug options" button + dialog (and
moving the Verbose logging checkbox), rename the existing collapsible "DEBUG
LOGGING" section to "DEBUG OPTIONS" and put the "Run screenshot sweep" button at
the top of it, above the dragonxd daemon debug= categories. Restore the Verbose
logging checkbox to the wallet row (unchanged). Drop the now-unused
debug_options / debug_options_title i18n keys + the debug_options_open flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:50:58 -05:00
dddf1b6ec7 feat(debug): theme x tab screenshot sweep + Debug options dialog
Add a debug tool that cycles every skin across every build-enabled tab and saves a
PNG of each to a timestamped folder under the config dir (screenshots/sweep_<ts>/)
— for gathering visual context on theme-specific UI work.

- App state machine (app_network.cpp): startScreenshotSweep() builds the
  skin+enabled-page lists, saves the current skin/page, and steps through them;
  updateScreenshotSweep() (top of App::render) pins the page + settles ~4 frames
  after each skin/page change (skin switches reload TOML + reset the acrylic
  capture, so they need to settle); wantsScreenshotThisFrame()/screenshotSweepPath()
  /onScreenshotCaptured() coordinate with main.cpp. Restores the original skin/page
  when done; nothing persisted.
- Framebuffer capture (main.cpp): read the finished frame and encode PNG via the
  bundled miniz (tdefl_write_image_to_png_file_in_memory_ex). GL reads FBO 0 (RGBA,
  bottom-up -> flip); DX11 copies the backbuffer to a staging texture + maps it
  (BGRA, top-down -> channel-swap). Alpha forced opaque.
- Settings UI: move the Verbose logging checkbox off the wallet row into a new
  "Debug options" button that opens a dialog housing the verbose toggle + a "Run
  screenshot sweep" button. New i18n keys.

Both platforms build; no-crash smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:41:49 -05:00
a523611779 feat(theme): flat light-theme buttons (match the portfolio modal)
Per the request for a flatter look like the Manage-portfolio modal's plain
buttons, render TactileButton FLAT on light themes: DrawButtonGlassOverlay now
early-returns in light themes with just a subtle defining edge (--rim-light),
skipping the white glass sheen (--glass-fill) and the tactile 3D depth (bright top
edge + the hardcoded bottom shadow that couldn't be removed via TOML).
ImGui::Button still draws the fill/hover/active (the darkened ImGuiCol_Button
overlays), so the button reads as a flat, defined element. Dark themes keep the
raised glass/tactile look. Re-adds the IsLightTheme() luminance helper (early in
the header so the overlay can use it). Applies to all TactileButtons app-wide in
light themes, not just Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:16:22 -05:00
7c90d72ded feat(theme): define light-theme buttons (TactileButton glass)
The settings-page buttons (TactileButton = ImGui::Button + a glass overlay) looked
faint in light themes: --glass-fill was a 55-62% WHITE wash that paled the button
body, and --rim-light was a 6-10% (sometimes light-colored) edge that was invisible
on light surfaces, so buttons had no defined border. Across all light skins:
- --glass-fill white wash 0.55-0.62 -> 0.20 (button body no longer washed out;
  panels use the acrylic blur on capable systems, so only low-spec panel fills are
  affected).
- --rim-light -> each skin's on-surface tint @ 0.22 (a defined dark button edge
  instead of an invisible/light rim).
- --hover-overlay alpha -> 0.10 (visible hover).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:07:09 -05:00
bb2337b552 feat(theme): darker text + buttons in light themes (revert modal backing)
Replace the light-theme modal backing (19e3138, now reverted) with a root-cause
fix: darken the light themes' UI elements for contrast.
- Buttons: the light-theme ImGuiCol_Button overlays were barely-there grey
  (5/8/12% black); bump to 13/20/28% so buttons read as buttons on light
  surfaces (one edit in ApplyColorThemeToImGui covers all light skins).
- Text: raise --on-surface-medium (60-72% -> 86%) and --on-surface-disabled
  (38-40% -> 52%) across the light skins (light, color-pop-light, dune, marble,
  iridescent) — the dominant secondary/label text was too faint. Base
  --on-surface was already near-black, left as-is.
The active theme is TOML-skin-driven, so the code GetMaterialLightTheme() is not
the live source; edits are in the skin TOMLs + the ImGui-style mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:57:38 -05:00
19e31388d9 feat(ui): light-theme contrast for floating modals
Floating (BlurFloat) modals draw no card, so in a light theme the light content
sits on a light frosted backdrop with almost no separation (washed out — the
Manage-portfolio modal being the clearest case). Add a faint elevated sheet
(translucent Surface) + a defining edge (OnSurface) behind floating content in
LIGHT themes only, gated on a new IsLightTheme() (background luminance) helper.
Dark themes are unchanged (they already read fine). Kept translucent to preserve
the airy floating feel; respects "no dark backdrop in light theme".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:44:17 -05:00
30139a5698 feat(security): focus ring on the lock-screen input
The PIN/passphrase input uses default ImGui framing, which is subtle on the glass
card — when focused there was no clear indication the field is active. Draw an
accent (Primary) ring around the input while it's focused/being edited so it's
obvious the field is ready for typing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:32:28 -05:00
94b57e91e3 fix(security): restore lock-screen interactivity (AllowOverlap on input blocker)
Regression from the full-window lock overlay: the full-window ##LockInputBlocker
InvisibleButton (drawn first) shadowed the lock card's widgets (PIN/passphrase
input, Unlock button, mode toggle) that overlap it, so clicks never reached them
(dead) and the app's global "hand cursor on clickable hover" fired for the whole
screen. Mark the blocker SetNextItemAllowOverlap() so the later widgets take
hit-test priority — the exact use case in ImGui's docs (invisible button covering
an area where subsequent items are added). The portfolio avoids this by putting
its content in a child window; the lock screen draws direct, so it needs the flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:12:01 -05:00
10289b33b5 fix(ui): theme-aware modal backdrop + full-window lock overlay
Address two GUI-review points:
1. Light themes no longer get a dark modal backdrop: DrawFullWindowBlurBackdrop
   now tints toward the theme app background (WithAlpha(Background(), ...)) for
   both the opaque base and the frost, so a light theme gets a light frosted
   backdrop and a dark theme a dark one. Fixes all blur overlays, not just the
   lock screen.
2. The lock screen now blurs the WHOLE window (sidebar included), not just the
   content area: renderLockScreen opens a full-viewport borderless overlay window
   (##LockOverlay, same pattern as the modal overlays / portfolio, opened from
   within ##ContentArea) with an input blocker, instead of drawing in the content
   child. Removed the theme-independent dark scrim (contradicted point 1).

Auth logic untouched. Needs GUI re-verify of all auth paths + that the whole
screen (incl. sidebar) obscures on lock and unlock returns cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:51:57 -05:00
42df658275 fix(security): dim the lock-screen backdrop so the card pops in all themes
GUI review (light theme) showed the blurred backdrop is light there, washing out
the (now glass) card. Add a theme-independent dark scrim over the blur so the lock
screen reads as a clearly-dimmed "locked" state and the card stands out in both
light and dark themes — standard lock-screen dimming. Alpha tunable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:36:21 -05:00
9d2c4a8ad3 feat(security): match the lock-screen card to the dialog cards
GUI review showed the lock card was a flat light SurfaceVariant panel that stood
out against the dark glass cards of the other modals. Draw it with the same
GlassPanelSpec (rounding 16, fill 35, border 50) that BeginOverlayDialog uses, so
it renders as the same opaque-dark glass card on the blur backdrop. Visual only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:49:42 -05:00
65c06d0e6b chore(theme): drop orphaned lock-screen backdrop-alpha key
The lock screen no longer reads screens.lock-screen.backdrop-alpha (replaced by
the live-blur backdrop in c7f89e5). Remove the now-dead schema key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:20:01 -05:00
c7f89e5cd3 feat(security): live-blur backdrop for the lock screen (Phase 4)
The security DIALOGS (encrypt/decrypt/change-passphrase/remove-encryption, PIN
set/change/remove) already adopted the blur backdrop via the Phase 2 flip (they
use BeginOverlayDialog). This finishes Phase 4 by giving the bespoke lock screen
the same live-blur backdrop over the wallet content — restyle only, auth logic
(PIN/passphrase input, attempt counting, lockout timer) untouched.

The lock screen previously defaulted to backdrop-alpha=0 (wallet fully visible
behind the card); the blur now obscures it — a privacy improvement as well as a
consistency one. Capture-once on lock-open + resize (frame-gap detected via the
shared BlurCaptureStateFor), MarkBlurOverlayDrawn drives the theme-effect
suppression + acrylic re-capture on unlock. The opaque base in
DrawFullWindowBlurBackdrop guarantees the wallet is obscured even if blur is
unavailable (low-spec / acrylic-off / capture failure) — no content leak. UV
mapping is screen-space, so the content-area sub-rect blurs the correct region.
Needs GUI verification of every auth path (PIN/passphrase unlock, lockout).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:14:27 -05:00
34ded6b441 feat(ui): shutdown-screen consistency polish (Phase 3)
The two Phase 3 "bespoke overlays" aren't content modals: the send-error is an
in-tab glass banner (already consistent — left as-is), and the shutdown screen is
a teardown screen where a blur backdrop would be semantically wrong (should read
as "closing") + risky on the teardown path. Per that finding, apply minor
consistency polish to the shutdown screen only:
- Share the overlay-scrim tone via a new material::OverlayScrimColor(opacity),
  used by both the dialog scrim and the shutdown scrim (was a slightly different
  hardcoded 0.06/0.06/0.08 → now the 0.04/0.04/0.06 dialog base).
- "Shutting Down" title → theme Warning() amber (was hardcoded gold).
- Force-Quit confirm destructive button → theme Error() (alpha-varied) instead of
  hardcoded reds.
No behavior change; the shutdown scrim stays opaque (no blur).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:07:36 -05:00
0ea423dcea feat(ui): float the remaining large dialogs (Phase 2 float rollout)
Per the user's selection, convert the large multi-section dialogs to the BlurFloat
style (floating content on the blur, plain heading, no glass card), each KEEPING
its authored width so width-coupled content is untouched:
- Export all keys, Console RPC reference, Explorer block detail
- Updater dialogs: daemon update, xmrig update, bootstrap download (multi-state)

Settings and Address book intentionally kept as cards (the modal Settings *window*
is dead code — show_settings_ is never set; the live Settings surface is the nav
page, not a modal; Address book has a nested edit that reads better contained).
All other confirm/input dialogs stay card-on-blur ("card for small"). Updater
dialogs are multi-state — GUI-check each state (confirm/progress/done/failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:20:13 -05:00
61738ef24a feat(ui): float Transaction Details + Block Info dialogs (Phase 2 large-dialog example)
Convert two clean detail-view dialogs to the BlurFloat style (floating content on
the blur, plain h6 heading, no glass card) — the "float for large" half of the
plan, applied conservatively: keep each dialog's authored card width so its
width-coupled content (absolute SameLine positions, GetWindowWidth math) is
unchanged, and rely on the footer Close + outside-click for dismissal (the plain
heading drops the title-bar close X, matching the portfolio modal). Auto-height
preserved. A representative pair to GUI-review before rolling the treatment out to
the other large dialogs (or reverting if the card-on-blur reads better).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:34:13 -05:00
ccbb0783b5 chore(ui): remove dead dialog duplicates (Phase 2 cleanup)
Grep-verified dead, zero live callers (the live paths all go through
BeginOverlayDialog and already got the blur backdrop):
- App::renderAboutDialog (app.cpp/app.h) — dispatch uses ui::RenderAboutDialog.
- src/ui/windows/import_key_dialog.{h,cpp} (ImportKeyDialog class) — live path is
  App::renderImportKeyDialog.
- src/ui/windows/backup_wallet_dialog.{h,cpp} (BackupWalletDialog class) — live
  path is App::renderBackupDialog.
Drop the two dead files from CMakeLists. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:29:38 -05:00
c6b32e86c0 feat(ui): all overlay dialogs adopt the blur backdrop (Phase 2 core)
Flip the positional BeginOverlayDialog overload's default to blurBackdrop=true, so
every app dialog (~43 call sites) now renders its card on the live-blur backdrop
instead of the opaque scrim — matching the Manage-Portfolio look. Cards stay
auto-height (small dialogs stay small = "card for small") and render opaque via
the existing sole-consumer fallback (a translucent glass card would thrash the
single-slot blur cache against the radius-64 backdrop). Nested dialogs (address
book edit) already pass a distinct idSuffix, so their blur windows/capture keys
don't collide. A dialog that wants the old scrim can pass a spec with
blurBackdrop=false. Big visual change — needs a GUI sweep; trivially reverted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:55:40 -05:00
5caffc84b0 fix(material): make blur-overlay effect guard truly close-equivalent
The adversarial review found the ModalRenderGuard lagged the old
PortfolioEditorActive() signal by one extra frame on CLOSE (a possible 1-frame
"opaque panels" flash). Latch the overlay's open flag (already available via
spec.p_open) — "drew this frame AND still open after Esc/Close/outside handling" —
instead of just "drew last frame", so the guard drops and the acrylic re-capture
fire on the same frame the overlay closes, matching the old inline behavior on
both edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:46:11 -05:00
ebf01bd858 feat(market): migrate portfolio modal onto the shared blur-overlay framework (Phase 1)
Dogfood the Phase 0 framework: RenderPortfolioEditor now opens via
material::BeginOverlayDialog(BlurFloat) + EndOverlayDialog instead of its own
inline overlay scaffold, and uses the promoted material:: SegmentedControl /
RightAlignX / BeginFadeScrollChild helpers (overlay_scroll.h). Delete the
portfolio's inline backdrop/capture-once state machine + its PfEditState capture
statics + the four local helper copies. App::render's ModalRenderGuard is now
driven by the modal-agnostic AnyBlurOverlayActiveLastFrame() (timing-equivalent),
and LatchBlurOverlayActive() at frame end handles the acrylic re-capture on close.

Framework tweak: floating cards pop the card WindowPadding after BeginChild (so
nested children don't inherit it) and center button labels, keeping the net
style-var count at 2 so EndOverlayDialog is unchanged and GlassCard stays
byte-identical. RenderPortfolioEditor: 357 -> 282 lines. Needs GUI verification
that the portfolio modal is visually/behaviourally unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:33:15 -05:00
d91be3b34e feat(material): spec-driven BeginOverlayDialog (Phase 0b)
Refactor BeginOverlayDialog to run through an OverlayDialogSpec + OverlayStyle
(GlassCard vs BlurFloat) with orthogonal flags (blurBackdrop / floatingContent /
plainHeading, fixed vs auto card height). The classic look is now GlassCard with
all flags off; the existing positional overload forwards to it → all ~43 current
dialogs are byte-identical (verified line-by-line). The BlurFloat path — live-blur
backdrop with per-overlay capture-once (frame-gap arming), floating content, plain
h6 heading — is implemented but unused until Phase 1 dogfoods it on the portfolio.
Also replace the unusable open-refcount with a per-frame drawn flag + last-frame
latch that invalidates the acrylic capture on the overlay's close transition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:13:47 -05:00
4b46d4a20c feat(material): promote blur-overlay modal primitives (Phase 0a)
First step toward unifying all app modals on the Manage-Portfolio visual style.
Add, as shared material:: helpers (additive — no caller yet, no visible change):
- blur-overlay open refcount + 1-frame latch (Push/Pop/AnyBlurOverlayActive[LastFrame],
  LatchBlurOverlayActive) so App::render's effect guard can be modal-agnostic;
- capture-frame query (IsCapturingBlurBackdrop) for scroll-fade suppression;
- RightAlignX and SegmentedControl (promoted verbatim from market_tab);
- BeginFadeScrollChild/EndFadeScrollChild in a dedicated overlay_scroll.h (kept
  out of the widely-included draw_helpers.h because effects::ScrollFadeShader's GL
  headers clash with SDL's in some TUs).
Also fix IsCurrentWindowOverlayDialog to prefix-match "##OverlayScrim" so suffixed
overlay windows are recognized by the modal-aware hover check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:05:03 -05:00
69cd8ba048 i18n(market): route chart interval labels + "Updated" through TR()
Add market_iv_1h/1d/1w/1m and market_updated ("· Updated %s") keys and use them
in the chart interval strip (previously hardcoded "1H"/"1D"/"1W"/"1M") and the
pair-selector attribution line (previously a literal "· Updated %s"). Closes the
audit's i18n gaps in the market tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:19:30 -05:00
4543cdc495 refactor(market): extract portfolio section into mktDrawPortfolio
Move the balance summary (fiat/DRGX/BTC + shielded ratio bar) and the
draggable/resizable custom-group card grid (drag/resize gestures, dot grid,
click-to-edit) out of RenderMarketTab into mktDrawPortfolio(MktCtx); the local
x-coord `cx` renamed `cx0` (ctx param is `cx`). Drop the now-unused
gap/dl/glassSpec/buf locals. RenderMarketTab is now ~150 lines (from 962) — a
thin orchestrator: precompute -> build MktCtx -> hero/chart/pair/portfolio.
Verbatim bodies. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:02:11 -05:00
71fb81670f refactor(market): extract price chart into mktDrawPriceChart
Move the ~250-line chart block (interval strip + 24h stats + refresh, plotted
curve with grid/area-fill/hi-lo+time labels/hover crosshair+tooltip, empty
state) out of RenderMarketTab into mktDrawPriceChart(MktCtx), recomputing cheap
locals and reading the precomputed series via the ctx. Drop the now-unused
hs/pad locals. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:57:59 -05:00
a092097141 refactor(market): extract MktCtx + price hero into mktDrawPriceHero
Introduce a MktCtx struct holding RenderMarketTab's precomputed geometry/series,
built once after the precompute, and extract the combined hero card (glass panel
spanning header + chart, price/ticker/period-badge/trade button) into
mktDrawPriceHero(MktCtx). Cheap locals (dl/fonts/dp/market) recomputed in the
helper; the hero's local x-coord `cx` renamed `cx0` (the ctx param is `cx`).
Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:53:27 -05:00
50873d1112 refactor(market): extract pair selector into mktDrawPairSelector
Move the exchange/pair chip grid (wrapping flat chips + click-to-switch +
attribution line) out of RenderMarketTab into mktDrawPairSelector(app, registry,
availWidth), recomputing its own schema/fonts/market. Drop the now-unused ovFont
local. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:43:43 -05:00
7e8628f94c refactor(market): extract drawCard lambda to mktDrawCard function
Lift the ~90-line [&]-capturing drawCard closure out of RenderMarketTab's grid
code into a file-static mktDrawCard(MktCardCtx, min, max, entry, hov). The
captured locals (dl/state/market/mktDp/pfInset/pfCardPad/sub1/capFont) are
bundled into a MktCardCtx bound once per frame. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:40:56 -05:00
0ae52ee095 refactor(market): extract pure portfolio grid-layout into pfComputeGridLayout
Pull the ~30-line row-major cell placement (honor stored placement else
auto-place) out of RenderMarketTab into a pure, deterministic helper taking
(entries, cols, minW, minH). Also bump the grid resize-readout buffer 16->32 to
silence a worst-case -Wformat-truncation the extraction surfaced. No behavior
change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:38:08 -05:00
529a74f6a7 refactor(market): extract portfolio detail sections into helpers
Pull the three detail-pane section blocks out of RenderPortfolioEditor into
pfDrawAppearanceSection / pfDrawPriceSection / pfDrawAddressSection (each
recomputes its own dp/fonts/state; the section dispatch is a 3-line switch).
Verbatim move — same widgets, IDs and drawing. RenderPortfolioEditor drops from
~760 to ~357 lines. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:20:25 -05:00
88f1415bcc refactor(market): lift portfolio-editor lambdas to file-static functions
Convert the four [&]-capturing lambdas (persist / buildWorking / workingMatches /
commitIfNeeded) into file-static pfPersist / pfBuildWorking / pfWorkingMatches /
pfCommitIfNeeded that operate on the s_pfEdit struct + take Settings*. Removes
the per-frame closure setup and lets the upcoming section helpers reuse them.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:13:42 -05:00
b8cfc1c79c refactor(market): group ~35 file-scope statics into three structs
Collapse the loose market_tab.cpp statics into MarketViewState (s_mkt),
PfEditState (s_pfEdit) and PfGridDrag (s_grid). Pure mechanical rename
(compile-verified, no behavior change) that shrinks the global surface and gives
the upcoming function decompositions a single state handle per concern instead
of ~35 free names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:10:59 -05:00
a0fd9aff82 refactor(market): extract pure price-series math to data/market_series.h + tests
Move the no-I/O, no-ImGui series helpers (resampleHistory, bucketBySeconds,
sparklineSeries, chartSeries) out of market_tab.cpp into an inline header under
data/ (mirroring data/portfolio.h) and cover them with a testMarketSeries()
group in test_phase4.cpp (resample block averaging + partial blocks, window
bucketing + guards, sparkline fallback, chart time-window filtering + live
timestamp synthesis). market_tab now calls data::sparklineSeries/chartSeries.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:03:19 -05:00
14b08d1358 refactor(market): extract pfSegmentedControl helper
Collapse the two identical equal-width segmented controls (the detail section
switcher Appearance/Price/Addresses and the address type filter
All/Shielded/Transparent) into one pfSegmentedControl(dl, origin, w, h, labels,
count, selected, font, idBase, dp) that returns the clicked index. Same track /
active-pill / centered-label drawing and per-cell hit-test as before; the
variable-width chart-interval strip is left as-is. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:56:51 -05:00
d689b02de9 refactor(market): extract pfProjectSeries + pfRightAlignX helpers
Dedup the identical min/max→rect projection shared by pfDrawSparkline and
pfDrawSparklineFilled into pfProjectSeries, and the repeated right-align cursor
math (footer Close, detail Revert/Save, summary Manage button) into
pfRightAlignX. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:54:10 -05:00
84498b1312 refactor(market): drop dead statics, fix BTC preview/card precision mismatch
Audit cleanup of market_tab.cpp:
- Remove the write-only statics s_history_initialized and s_last_refresh_time
  (assigned in several places, never read) and their now-dead assignments.
- Align the portfolio grid card's BTC value to %.8f to match the editor preview
  (pfBuildDisplay) and the summary rows; it was the lone %.6f, so the same
  balance rendered with different precision in the preview vs the card.
- Fix the stale s_pf_sel comment (no "-2 = unsaved draft" state exists).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:49:09 -05:00
8c2b0521a6 feat(market): combine portfolio address filter + selection onto one row
Merge the two address-section rows into one: the All/Shielded/Transparent
segmented type filter sits on the left and Select all / Clear / Funded are
right-aligned on the same line, both vertically centered. Select-all is deferred
to a flag applied after the filtered set is built, so it still targets the
current filter (and stays idempotent via PortfolioEntryAdd's dedup).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:37:10 -05:00
4411cb45e2 feat(market): Esc / outside-click discard portfolio edits; only Close saves
Complete the explicit-save model: leaving the Manage-portfolio modal via Esc or
an outside-click now discards the current group's uncommitted edits (matching
the discard-on-switch behavior), while the Close button still commits. Only the
Save and Close buttons persist; every other exit path is a cancel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:27:16 -05:00
7db6564a0e feat(market): discard uncommitted group edits when switching away
Switching to another group in the Manage-portfolio list (or adding/deleting a
group) now discards the current group's uncommitted working edits instead of
auto-saving them — only the Save button (and closing the modal) persists. This
matches the explicit-save model signalled by the Revert/Save buttons and the
unsaved-changes dot: navigating between drafts is ephemeral until saved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:24:52 -05:00
faf8e4e430 feat(market): rework portfolio-modal actions, addresses row, price & unsaved state
- Move the group-scoped Revert/Save buttons into the detail container (sized to
  the Add-entry height) so they read as lower-hierarchy than the modal-level
  Close, which now stands alone in the footer. The detail body is wrapped in a
  child that reserves the bottom row.
- Add an amber "unsaved changes" dot on the selected group row while its working
  copy differs from the stored entry; also gate the detail Revert/Save on that
  dirty state.
- Addresses: narrow the All/Shielded/Transparent segmented control (360px) and
  right-align Select all / Clear / Funded on one line; give the Select all /
  Clear pills more text padding.
- Appearance: shrink the icon search field and right-align it on the heading
  line, with a "Search icons…" hint.
- Price: move the sparkline toggle onto the 24h line; the interval radios stay
  below, enabled only when the sparkline is shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:14:40 -05:00
1affea0273 feat(market): icon-picker search + Settings-style scroll for portfolio modal
Rework the Manage-portfolio detail pane's Appearance and Addresses sections:

- Icon picker: search field inline with the "Icon" heading (filters by name,
  hides the "None" slot while searching); grid now always fits 12 icons per
  row, scaling cell size to width and centering the block.
- Both the icon grid and the address list scroll via a shared helper
  (pfBeginScrollChild/pfEndScrollChild): smooth scrolling + the Settings-tab
  edge-fade shader + a thicker, rounded scrollbar inset from the container edge
  (bordered outer frame wrapping an inner scrolling child). Smaller container
  radius. Fade gates off in low-spec and while the acrylic backdrop is being
  captured — latched per-frame so the last capture frame no longer clashes with
  the render-state reset.
- Addresses: All/Shielded/Transparent segmented control moved to its own row,
  capped at 580px; Select all / Clear pills moved left of the Funded checkbox
  on one left-aligned row (Funded vertically centered against the pills).
- Custom colour "+" swatch inset a few px so its selection outline no longer
  clips at the modal's right edge.
- Remove the divider above the footer Revert/Save/Close buttons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:50:26 -05:00
32d8d5663a feat(market): portfolio detail polish — bigger icons, inline filter+funded, pill buttons, single-row colors
- Appearance: icon-picker cells doubled (56px, iconXL glyphs); color swatches back to a
  single row that hides overflow but always keeps the custom "+" cell visible at the right.
- Price: basis radios are now horizontal, matching the sparkline-interval radios.
- Addresses: All/Shielded/Transparent segmented control shrinks to sit on one line with
  the Funded toggle to its right; Select all / Clear are rounded (pill) buttons with
  padding, right-aligned on their own row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:15:50 -05:00
3568b12dbf feat(market): portfolio detail — radio price options, segmented address filter, +17 colors
- Price: the basis and sparkline-interval dropdowns become radio groups (all options
  visible), now that the segmented layout gives the section full height.
- Addresses: the All/Shielded/Transparent chips become a rounded segmented control
  matching the top switcher. "Select shown" renamed to "Select all"; Select all + Clear
  moved onto the Funded row, right-aligned. Address list gets inner WindowPadding.
- Appearance: 17 more preset colors (25 total), and the swatches now wrap into a grid
  (with the custom-color cell) instead of a single overflowing row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:42:08 -05:00
e74989ec2c feat(market): replace portfolio detail accordion with a segmented control
Swap the collapsible Appearance/Price/Addresses dropdown sections for a single
segmented pill control (one section shown at a time) — no dropdown headers. The
active section now owns the full detail height, so the icon grid and address list
fill the available space instead of fixed heights. State: s_pf_section (0/1/2)
replaces the three accordion open-flags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:24:17 -05:00
fe18628216 feat(market): bump the portfolio backdrop blur radius (64) via sole-consumer isolation
Raising the backdrop radius alone would thrash the shared radius-keyed blur cache (the
glass panels blur at ~30). Instead, while a full-window blur overlay is active, glass
panels use their opaque fallback (IsFullWindowBlurOverlayActive) — they're covered by the
backdrop anyway — so the backdrop is the SOLE applyBlur caller and can use a strong custom
radius (64) with the cache staying coherent and frozen. The flag is toggled alongside the
theme-effect suppression by a RAII guard in App::render().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:15:12 -05:00
4dc6b2ef8f perf(market): capture the portfolio backdrop once on open (+resize), not every frame
Switch the live-blur behind the modal from a per-frame capture (which flickered from
per-frame re-blur non-determinism) to capture-once-on-open, re-captured only on resize.

- Insert the acrylic live-capture callback into the overlay draw list ONLY for a few
  frames on open and whenever the viewport size changes; other frames reuse the frozen
  blurred snapshot (blurCacheValid_), so there's no per-frame capture/re-blur cost. State:
  s_pf_was_open (reset on close), s_pf_capture_frames, s_pf_capture_w/h.

Two fixes from an adversarial review of the timing/pipeline interaction:
- Do NOT override the backdrop blurRadius to 40. applyBlur caches one blurred buffer
  keyed on radius, shared with every glass panel (market cards + the modal preview card,
  radius ~30). A different radius thrashed that cache every frame and made the backdrop
  sample the wrong blur, defeating the frozen-blur guarantee. Using the theme card's
  radius keeps one blur serving all surfaces, frozen.
- Skip the blur on the overlay's first frame (allowBlur=false) — the live content isn't
  captured until that frame's render, so blurring then would show stale/background pixels
  for one frame; draw the plain opaque scrim instead.

Compiles on OpenGL (Linux) + DX11 (Windows). Needs on-device visual verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:05:16 -05:00
5afb339ba7 fix(market): portfolio modal — hover assert, cut-off footer, plain heading, FX bleed, bigger delete icon
- Fix the ImGui error on hovering a group row: the per-row delete was an
  overlapping InvisibleButton + SetCursorScreenPos over the row Selectable. Replace
  it with a manual IsMouseHoveringRect / IsMouseClicked hit-test (no overlapping
  ImGui item, no cursor manipulation).
- Delete icon is larger (iconMed) and inset from the row edge (spacingMd).
- Footer no longer clipped: reserve 56px for the 40px buttons + separator.
- Manage-portfolio heading is now plain text (h6) with no title-bar background or
  divider — content floats on the backdrop.
- Sidebar theme-effect borders no longer bleed through the overlay: suppress panel
  theme-effects frame-wide (RAII guard in App::render via ui::PortfolioEditorActive)
  while the modal is open, instead of only during the market render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:47:33 -05:00
0e8816e220 feat(effects): overlay-time live-framebuffer blur behind the portfolio modal
The acrylic system only blurred a frozen snapshot of the static app background. Add
an overlay-time re-capture so the Manage-portfolio modal blurs the LIVE tab content
behind it.

- AcrylicMaterial::captureLiveFramebuffer() (GL blit of FB 0 / DX11 CopyResource of the
  backbuffer + no-backend stub) mirrors captureBackgroundDirect but bypasses the
  dirtyFrames_ gate so it refreshes every frame the overlay is open, forcing a re-blur.
- ImGuiAcrylic::GetLiveCaptureCallback() returns a draw callback for it.
- market_tab: the modal inserts that callback (+ ImDrawCallback_ResetRenderState) at the
  START of its overlay draw list, before the backdrop, so the capture holds the app UI
  drawn below and the backdrop blurs it. On close, InvalidateCapture() re-captures the
  background so other glass panels don't keep blurring the stale live capture.
- Panel theme-effects are suppressed during the market render while the modal is open
  (their foreground-draw-list borders draw above all windows and would otherwise bleed
  over the overlay). Market tab body is rendered again (no longer skipped).
- Backdrop draws the acrylic at full strength (fallbackColor.w=1) over an opaque base so
  the blurred content reads clearly; lighter dim.

Design mapped via an Explore investigation of the capture/blur pipeline; compiles on
both OpenGL (Linux) and DX11 (Windows cross-build). Needs on-device visual verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:26:11 -05:00
2035f71c65 fix(market): stop portfolio group rows overlapping; center modal button text
- The per-row delete icon's SetCursorScreenPos left the ImGui cursor at the icon,
  so the next group row started too high and rows overlapped. Capture the row's
  end cursor after the Selectable and restore it after drawing the delete icon.
- Force centered button-label alignment for the modal (ButtonTextAlign 0.5,0.5)
  so the footer/Add buttons read consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:12:24 -05:00
d007f3de39 feat(market): portfolio modal — per-row delete, footer Close, instant Add, no bleed-through
- Delete moved from the footer to a per-row trash icon on each group (shown on
  hover/selected), with deferred deletion + selection fix-up.
- Removed the title-bar X; the footer is now right-aligned Revert / Save / Close
  (larger buttons), Close commits + dismisses. Esc and outside-click still close.
- Add entry now immediately creates a persisted "Untitled" group and selects it
  (no more invisible draft state); the -2 draft path is gone.
- Backdrop bleed-through fixed: when the modal is open the Market tab body is not
  rendered, so its content and foreground theme-effect borders no longer show over
  the overlay — the modal floats on the app's blurred/opaque backdrop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:02:25 -05:00
1fdb6656aa feat(market): fix portfolio Add button clipping; add accordion summaries
- Add button can no longer be cut off: it's now a normal widget below the group
  list (in a group with the scroll child) instead of nested inside the list child.
- Stop the modal's 28x20 WindowPadding from cascading into the nested master/detail/
  list children (pop it right after the card child begins) — that double-padding
  was squeezing the master column and the Add button.
- Right pane navigability: collapsed accordion sections now show a compact summary
  on the right (Appearance -> icon, Price -> basis, Addresses -> N selected), so a
  group's state is readable without expanding. Section titles are consistent
  title-case ("Appearance"/"Price"/"Addresses").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:13:35 -05:00
3305be6318 feat(market): drop the portfolio modal card panel — float content on the backdrop
Remove the centered glass card behind the Manage-portfolio content so it floats
directly on the blurred/opaque full-window backdrop. The little live-preview card
keeps its panel (it previews the actual group card).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:57:28 -05:00
19482214f1 fix(market): draw the portfolio overlay above the tab content
Drop ImGuiWindowFlags_NoBringToFrontOnFocus on the overlay window — it stopped
the per-frame SetNextWindowFocus() from raising the modal, so it rendered behind
the market chart/cards. Now matches the proven BeginOverlayDialog behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:52:06 -05:00
4aed72c73b feat(market): rework Manage-Portfolio as a full-window master-detail modal
Combine the old list/edit modes into one master-detail view rendered as a
full-window overlay instead of a floating dialog.

- Backdrop: new material::DrawFullWindowBlurBackdrop draws an opaque base first
  (so the sheet is opaque regardless of the UI-opacity slider — the acrylic path
  multiplies its alpha by GetUIOpacity), then a strong full-window blur of the
  backdrop, gated exactly like DrawGlassPanel so low-performance mode skips the
  blur entirely (just the opaque scrim).
- Frame: bespoke full-window overlay (not BeginOverlayDialog) that mirrors the
  scrim plumbing and preserves both overlay popup gotchas verbatim (no focus-steal
  and no outside-click dismiss while a combo / color picker is open). Title-bar X,
  Esc, and outside-click all dismiss.
- Left (master): selectable group list with accent bar + icon + name + value line,
  and a pinned "Add group" button.
- Right (detail): live preview + label header, then a single-open accordion
  (Appearance = color + outline opacity + icon; Price; Addresses) so only one
  control group shows at a time — far less cluttered than the old two-column form.
- Footer: Delete / Revert / Save. Editing auto-commits named groups on navigation
  and on close (preserving grid geometry); unnamed drafts are dropped.
- s_pf_editing tri-state replaced by s_pf_sel (+ accordion open-state); the Manage
  button and grid-card click open with a valid selection.

Design + adversarial review done via multi-agent workflows (understand/design, then
a 3-lens ImGui-lifecycle / state-safety / backdrop review; zero confirmed defects).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:46:46 -05:00
3e6987d72f fix(market): remove the chart divider between interval buttons and the plot
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:01:44 -05:00
42c511b13a fix(market): tighten blank space above interval buttons; pad below the divider
- Drop the extra gap the hero header reserved below the price row, so the interval
  buttons sit closer to the top of the chart area.
- Place the divider just below the buttons with a comfortable gap (spacingMd)
  between it and the plot, instead of centering it in the strip gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:59:16 -05:00
ef9141c248 feat(market): default chart to 1M, divider below interval buttons, drop chart dots
- Default the price chart to the 1M range on open (was 1D).
- Move the hero divider from below the price row to below the interval buttons
  (centered in the gap between the buttons and the plot).
- Remove the dot markers on the chart: no per-point dots, no current-price dot,
  and no high/low dot anchors — just the clean line + area fill (the high/low
  price labels and the hover crosshair remain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:25:26 -05:00
8c88afe16e feat(market): card/chart refinements — 8-cell min width, larger labels, flat fill, range-matched change
Portfolio group cards:
- Always show the secondary DRGX amount, including on 2-cell cards.
- Larger group icon + label (subtitle1); value stays subtitle1 for consistency.
- Minimum group width is now 8 cells (default width 8).

Price chart:
- Area fill is now a flat translucent fill matching the group-card sparklines
  (dropped the vertical gradient + its primitive-API helper).
- Interval buttons sit near the top of the chart area (removed the empty gap
  above them) with a comfortable gap below before the plot.
- The change badge's suffix now matches the SELECTED range (1H->1h, 1D->24h,
  1W->7d, 1M->30d) instead of the raw data span (which read 23h / 6d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:09:40 -05:00
d48c6db21c feat(market): range-based chart intervals; redesign group cards (value top-right + body sparkline)
- Chart interval buttons are now time RANGES, not bucket sizes: 1H = last hour,
  1D = last 24h (5-min intraday), 1W = last 7 days, 1M = last 30 days (daily).
  pfChartSeries filters the timestamped series to the window; X-axis "ago" labels
  and the period-change span now derive from the real timestamps.
- Portfolio group cards redesigned: name (+ secondary DRGX) on the left; the value
  with its % change stacked in the top-right corner; the sparkline fills the card
  body. The sparkline now renders on 2-cell cards too (down to ~8px of body), with
  tighter padding on short cards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:52:09 -05:00
ced1773e70 feat(market): chart gradient fill, period change %, timestamped hover, high/low markers
Builds on the timestamped historical series (pfChartSeries returns [time,price]):

- Gradient area fill: replace the flat translucent fill with a true vertical
  gradient (solid at the line, fading to transparent) via the draw-list primitive
  API (pfFillGradientArea).
- Period-aware change: the price badge now reports the change over the DISPLAYED
  chart period with a span label derived from real timestamps (e.g. "+42% 1.0y"),
  falling back to 24h for Live. Line/fill/marker colors follow that direction.
- Timestamped hover: the crosshair tooltip shows the real date/time at the hovered
  point (time for Live/1H, date for day+ intervals) alongside the price.
- High/low markers: hollow dot + price label at the range's extremes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:53:41 -05:00
d76c109063 feat(market): 2-cell min group height; declutter + polish the price chart
- Portfolio group minimum height is now 2 cells (was 3). Added a 2-cell card tier
  that also drops the sparkline (not just the footer) so a ~58px card keeps the
  header + hero value legible.
- Price chart: stop drawing a dot on every point — for a dense historical series
  (>40 points) just the clean line + fill, and always mark the latest point
  (current price) with a dot + ring.
- Y-axis labels use the shared adaptive price formatter (e.g. $0.0143) instead of
  a fixed 6 decimals, so they're cleaner and narrower.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:45:10 -05:00
5aa3cc4d51 feat(market): historical chart intervals, stats overlay, configurable outline, easier resize
Chart:
- The price chart now plots the historical CoinGecko series with a Live/1H/1D/1W/1M
  interval selector (top-left of the chart). Live = in-session buffer; 1H uses the
  intraday 5-min series, 1D/1W/1M the daily series (via pfSparklineSeries). X-axis
  labels are interval-aware ("~5h"/"~3d"/"~2w"/"~4mo"). Market-tab open kicks the
  self-throttled market_chart fetch so history populates promptly.
- 24H volume + market cap moved from their own row onto the chart's top strip
  (right side, next to the refresh button), reclaiming the empty band above the
  chart; the hero header now holds just the price row + separator.
- More bottom padding under the plot so the time labels and the DRGX/USDT pair
  buttons aren't crowded against the card edges.

Portfolio groups:
- Accent-outline opacity is now configurable per group (0-100%, default 25%) via a
  slider in the editor's Appearance column; applied to the card + preview outline.
- Resize grip enlarged (14->20px) and a live "W x H" size readout is drawn while
  resizing, so shrinking a group to the 4x3 minimum is reliable and visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:37:31 -05:00
4e15a2b1ba feat(market): 3-cell min groups, full accent outline, tighter chart, no scrollbar
- Portfolio groups now default to the 3-cell minimum height (was 4). The card
  design adapts: below ~112px it drops the redundant DRGX footer and uses the
  compact hero font so a 3-cell card stays legible.
- Group accent color is now a colored outline around the whole card (and the
  editor's live preview) instead of a stripe on the left edge; the text no longer
  reserves the old left-stripe offset.
- Price chart: trim the empty band between the 24H VOLUME stats and the chart
  (smaller hero-header bottom pad), and keep the Y-axis price labels inside the
  card on narrow windows (clamp to the card's left edge + bump min y-axis padding
  40->54) so they no longer spill onto the tab background.
- Market tab no longer draws a scrollbar (NoScrollbar); the portfolio grid still
  scrolls via the mouse wheel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:15:17 -05:00
021ba88522 fix(market): extend portfolio drag dot-grid to the bottom of the viewport
While rearranging, the dot grid only reached two rows below the lowest card, so
the empty space beneath the cards showed no dots even though a card can be
dropped there. Fill the grid down to the bottom of the visible scroll viewport
(dl clip rect), keeping the two-row buffer as a minimum. The drop-target row is
already unclamped, so dropping into that lower area places the card and grows the
scrollable content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:00:07 -05:00
c1d5f79502 feat(market): real historical sparklines via CoinGecko market_chart
Portfolio-group sparklines previously resampled the ~24-minute in-session price
buffer, so the hour/day/week/month intervals could never fill. Fetch real
historical USD series from CoinGecko /market_chart and back each interval with
appropriately-grained data.

- MarketInfo gains two timestamped series: price_chart_intraday (days=1, 5-min)
  and price_chart_daily (days=365, daily), plus a fetch timestamp.
- App::refreshMarketChart() fetches both on the RPC worker via the TLS-verifying
  util::httpGetString helper, self-throttled to ~30 min (historical data moves
  slowly). Gated by getFetchPrices(); triggered on connect and each price tick.
- Pure NetworkRefreshService::parseCoinGeckoMarketChart() parses {"prices":
  [[ms,price],...]} into (unix-seconds, price); malformed rows skipped. Unit-tested.
- market_tab pfSparklineSeries() maps interval -> series+bucket: minute = live
  buffer; hour = intraday bucketed to 1h; day/week/month = daily bucketed to
  1d/7d/30d. Falls back to the in-session buffer until the fetch populates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:50:17 -05:00
db212a81c6 feat(market): fine 32px square portfolio grid that scales to width and scrolls
Replace the fill-to-height portfolio dashboard with a fine gapless grid of
~32px square cells whose column count scales to fill the tab width. The grid
extends past the window and the Market tab scrolls when cards are placed lower.

- Cells are a fixed square edge (>= ~32px), gapless; cards inset by 3dp so
  neighbours don't touch. Column count = availWidth / 32dp (min pfMinW cols).
- Card minimum span is pfMinW x pfMinH (~128x96); layout/resize clamps to it.
  New groups default to 6x4 cells.
- Content height reserves two extra rows below the lowest card so a card can be
  dragged past the current bottom; the scroll child grows to match.
- Dot grid (shown while rearranging) softened and extended into the buffer rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:30:04 -05:00
67f6156030 feat(market): stat-tile group cards + grid fills available width and height
Recommendation #1 + fill-to-fit:
- Group cards are now stat tiles instead of top-heavy rows: a header (icon +
  name, with a 24h badge right), the value as a large hero centered in the
  middle band, a footer DRGX amount, and a dedicated sparkline strip along the
  bottom (soft fill under the line). Uses the taller square cells properly.
- The grid now fills the available height too: cell height is computed at render
  time so the rows reach the bottom of the tab (no dead space), and the dot grid
  extends to the bottom edge while rearranging. Cell height = availableHeight /
  rows, so adding a row shrinks cells to keep filling. The hero font drops from
  h4 to subtitle1 on short cells so it always fits.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Interactive/visual —
needs a screenshot + drag/resize check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:13:16 -05:00
76ed09fad0 feat(market): square grid cells (2x1 min), preserve grid on edit, sparkline interval
- Grid cells are now square (cellH = cellW); column count is responsive with a
  2-column minimum, and a group's minimum size is 2x1 cells. New groups default
  to 2x1; drag/resize/auto-place all enforce the 2-wide minimum.
- Fix: editing a group no longer resets its grid placement/size. The editor's
  Save now starts from the existing entry and only overwrites the form fields,
  so grid_col/row/w/h (and anything else off-form) are preserved.
- Add a per-group sparkline interval (Minute/Hour/Day/Week/Month) next to the
  Sparkline toggle. The price history (~1 sample/min) is resampled by averaging
  each interval's worth of minute-samples into one point.

PortfolioEntry gains sparklineInterval and defaults gridW=2. Persisted in
settings.json. Full-node + Lite build clean; ctest 1/1; hygiene clean.

Note: the app only accumulates in-session minute samples, so Hour/Day/Month
show points only once enough data has been collected; true long-range history
(instant day/month charts) would need a CoinGecko market_chart fetch, which can
be added separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 04:15:38 -05:00
77219c5ce7 feat(market): portfolio groups as a draggable/resizable dashboard grid
Turn the portfolio group area into a dashboard grid:
- Responsive column count (1-4 by width); each 1x1 cell is one card.
- Drag a card to move it to another cell; drag its bottom-right corner to
  resize (span multiple cells). Both snap to the grid.
- A subtle dot grid appears while rearranging (dragging or resizing), with a
  highlighted drop-target / resize footprint.
- Click (no drag past a 4px threshold) still opens the group editor.
- Grid placement (col/row) and span (w/h) persist per group in settings.json;
  positions are resolved each frame — stored placement when it fits, else
  auto-placed row-major into free cells. Dropping onto an occupied cell swaps.

PortfolioEntry gains grid_col/grid_row/grid_w/grid_h. The card render was
refactored into a drawCard() lambda reused for the in-grid, dragged, and
resizing states.

Full-node + Lite build clean; ctest 1/1; hygiene clean. This is an interactive
feature I can't verify without the GUI — needs drag/resize testing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:54:20 -05:00
b26aff392b feat(market): optional price-trend sparkline per portfolio group
Add a per-group "Sparkline" toggle (live-market bases only, like 24h). When on,
the group card draws a faint price-trend line in its lower band — the group's
value tracks the DRGX price, so it plots the market price_history (normalized),
colored green/red by overall direction. Behind the text so amounts stay
readable. The editor's live preview reflects it too.

Persisted as show_sparkline in settings.json; wired through PortfolioEntry +
the editor working state + the pfDrawSparkline() helper shared by card+preview.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change —
needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:15:23 -05:00
b0ded528b1 fix(dialogs): let Combo dropdowns / popups work inside overlay dialogs
BeginOverlayDialog called ImGui::SetNextWindowFocus() every frame to keep the
dialog on top. That forced the scrim back to the front each frame and closed
any popup opened from inside the dialog — so the portfolio editor's price-basis
Combo and the custom color-picker popup flashed open then vanished, and clicking
a dropdown item that overhung the card could trip the outside-click-dismiss and
close the whole dialog.

Make the overlay popup-aware: compute overlayPopupOpen (IsPopupOpen with
AnyPopupId|AnyPopupLevel) and (1) skip SetNextWindowFocus while a popup is
showing so it stays focused/on-top, and (2) skip the outside-click-dismiss while
a popup is open so clicks inside it don't close the dialog. No change when no
popup is open, so existing dialogs are unaffected.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:02:18 -05:00
301b3df77e feat(market): per-group configurable price data
Each portfolio group can now configure how it is valued and which price
fields its card shows:

- Price basis: Market · USD, Market · BTC, DRGX only, or Manual (a custom
  price-per-DRGX + currency label). Manual mode values the group independently
  of the live DRGX market price.
- Field toggles: show DRGX amount / show value / show 24h change (24h enabled
  only for the live-market bases; value disabled for DRGX-only).
- The editor's left "Appearance" column gains a Price section (basis combo,
  manual price + currency inputs, field checkboxes); the live preview and the
  group cards both render through one pfBuildDisplay() helper so they stay in
  sync. Defaults preserve the prior behavior (DRGX + USD).

Persisted in settings.json (price_basis / manual_price / manual_currency /
show_drgx / show_value / show_24h).

Note: "price source per exchange/pair" is limited to USD/BTC/Manual — the app
only fetches the aggregate DRGX price (USD + BTC); ExchangePair carries no
price, so valuing against an arbitrary pair's live price would need a new
market fetch (can be added separately).

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change —
needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:10:42 -05:00
ae4e6486c6 feat(market): portfolio editor — taller icon grid + custom color picker
- Reorder the Appearance column so Color sits above the icon grid, and let the
  icon grid fill the remaining height of the left column (via
  GetContentRegionAvail().y). Fixes the large empty space that sat below the
  color row and gives the icon picker much more room.
- Add a custom-color circle after the preset swatches: it shows the current
  color when it isn't a preset (else a "+"), and clicking it opens a
  ColorPicker3 popup. The picked RGB packs straight into the group's color.

New i18n key portfolio_custom_color. Full-node + Lite build clean; ctest 1/1;
hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:53:18 -05:00
d838243caa feat(market): redesign the Manage-portfolio editor as a two-column layout
Full redesign of the group edit view (list mode unchanged):

- Two columns in a wider (780px) dialog:
  * LEFT "Appearance": a live preview of the group card (icon + color + name +
    summed DRGX/USD), then Label, the icon picker, and the color picker.
  * RIGHT "Addresses": a search box, Shielded/Transparent/All type filters, a
    "Funded" toggle, Select-shown / Clear, and the address list.
- Reworked address rows: a custom checkbox, a Z/T type chip, the address's own
  label + icon (from the address book), a middle-truncated address, and the
  balance right-aligned. Selected rows sort to the top (then by balance); the
  whole row toggles selection. Replaces the old wall of raw address strings.
- Search filters by address or label; empty-state hint when nothing matches.

New i18n keys (portfolio_appearance/addresses_hdr/search/funded/select_all/
select_shown/group_name/no_addr_match). Full-node + Lite build clean; ctest 1/1;
hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:44:16 -05:00
75f33e41eb feat(market): portfolio groups — PORTFOLIO heading, clickable cards, icon + color
- Rename the portfolio heading "MY DRGX" -> "PORTFOLIO".
- Group cards are now clickable: clicking one opens the editor directly on that
  group (edit mode), with a hover highlight + hand cursor.
- PortfolioEntry gains an `icon` (project_icons wallet-icon name) and a `color`
  (packed IM_COL32 accent); both are persisted in settings.json.
- Editor edit-mode gains an icon picker (the same wallet icon set as the overview
  tab's address-label dialog, via material::project_icons) with a "None" option,
  and a color picker (preset accent palette + a default/no-color swatch).
- Group cards render the chosen icon to the left of the name and, when a color
  is set, a left accent bar + the icon tinted in that color.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change — needs
a screenshot check of the Market portfolio + the editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:20:21 -05:00
1263058ba7 fix(market): remove empty band between the stats row and the chart
The hero-card header height was derived from an inflated schema value
(hero-card-height * vs), reserving far more vertical space than the price row +
separator + stats row actually use. Since the chart is drawn at the bottom of
that header, it started well below the "24H VOLUME"/"Market Cap" row, leaving an
empty band. Size heroHeaderH to the actual content so the chart begins right
after the stats.

Full-node build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:49:09 -05:00
9bcdf51895 feat(market): compact chart w/ time axis; floating portfolio + redesigned group cards
Follow-up to the Market tab rework, per feedback:

- Chart is more compact: a modest responsive height (~22% of the tab, floored
  at 110px) instead of stretching to fill.
- Chart now has a time axis. The price series is one sample per ~60s market
  refresh (RefreshScheduler::kPrice), so the x-axis is labelled in approximate
  minutes-ago ("~45m … ~15m … now") instead of only "Now" — you can now read
  the interval/span of each plot point.
- Portfolio: removed the top-level card background — the summary (fiat hero +
  24h change + BTC, DRGX balance + Z/T, ratio bar) now floats directly under the
  MY DRGX header, and the "GROUPS" heading is gone.
- Group cards redesigned: a max width (280px, shrinks on narrow tabs) so they
  float rather than stretch; the group name is larger (subtitle font) top-left;
  the DRGX + fiat amounts moved to the card's top-right corner; and more inner
  padding around the content.

Removed the now-unused portfolio_groups i18n key. Full-node + Lite build clean;
ctest 1/1; hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:32:19 -05:00
67feaea767 feat(market): rework tab layout — responsive chart + refined portfolio + group cards
The Market tab is non-scrolling, but an oversized fixed-height chart ate ~65%
of the height and squeezed the portfolio into a cramped strip, with custom
groups rendered as tiny afterthought rows at the very bottom.

- Precompute section geometry (hero header + portfolio card height) up front and
  let the price chart ABSORB the remaining vertical space, capped at its schema
  height and floored at 130px*vs so it stays useful. The chart now shrinks to
  make room instead of dominating. Replaces the barely-used SectionBudget.
- Portfolio summary refined: fiat value stays the hero with the 24h change beside
  it and the BTC value right-aligned; DRGX balance + Z/T breakdown on the next
  row; a full-width shielded/transparent ratio bar + % label.
- Custom groups now render as a wrapping grid of mini glass cards (label + DRGX +
  fiat each), sized to the card width, instead of a cramped one-line list. A
  "GROUPS" subheader separates them from the all-funds summary. The card grows to
  fit the grid.

New i18n key portfolio_groups ("GROUPS"). Full-node + Lite build clean; ctest
1/1; hygiene clean. Rendering change — needs a screenshot check of the Market tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:12:27 -05:00
3e1cdd15f3 fix(market): portfolio editor address checklist was collapsing to zero height
The "Manage portfolio" editor renders its address checklist inside the
overlay dialog's content child, which is ImGuiChildFlags_AutoResizeY (the
dialog sizes to its content). The checklist used a negative "fill remaining
space" height (-GetFrameHeightWithSpacing()*1.5f), which is meaningless in an
auto-resizing parent — there is no fixed height to subtract from — so ImGui
collapsed the child to ~0 and no addresses were visible, even with
state.addresses fully populated.

Size the list to its rows instead, capped at 40% of the viewport height so a
large wallet scrolls rather than running off-screen. Pre-existing bug from
when the editor was added; distinct from the earlier state.addresses
population fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:53:08 -05:00
af2db4366c refactor(audit): de-duplicate the main.cpp backdrop draw + DPI transition
Two render-loop de-duplications, zero behavior change (verified byte-equivalent
on both the GL and DX11 backends via the mingw Windows cross-build):

- drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active, lowSpec): the ~80-line
  gradient/texture/acrylic-capture/WindowBg-alpha block was copy-pasted in the
  main loop (with low-spec guards) and the resize-watcher lambda (without them).
  Extracted once; the main loop calls it with lowSpec=isLowSpecMode() and the
  resize watcher with lowSpec=false — each reproducing its CURRENT behavior
  exactly (pure dedup, no change). The intentional differences around it
  (resize's immediate present(0) vs the main loop's vsync, the main loop's
  try/catch, the BeginFrame low-spec guard) are left untouched.

- handleDisplayScaleChange(window, newScale, savedSizeForScale, lastKnownW/H,
  dpiTargetW/H, dpiResizeRetries, dpiResizePending): the SDL display-scale-change
  handler was duplicated ~verbatim in the idle-wait and poll event paths
  (differing only in debug-log text). Moved the body into one helper called from
  both; the #ifdef DRAGONX_USE_DX11 g_borderlessDpi and #ifdef __APPLE__ scale
  normalization are preserved.

Deliberately did NOT unify the full frame/present sequence — the resize path's
immediate present and lack of try/catch are intentional.

Full-node (GL) + Lite build clean; Windows/DX11 cross-build compiles main.cpp
clean; ctest 1/1; hygiene clean. NEEDS RUNTIME VERIFICATION: a live window
drag-resize (backdrop draws, no flicker) and a monitor DPI-scale change (window
resizes + fonts reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:00:40 -05:00
cb74fbbff0 refactor(audit): unify config-dir resolution through Platform::getConfigDir
Settings::getDefaultPath and AddressBook::getDefaultPath each hand-rolled the
same per-platform #ifdef (Win SHGetFolderPath / macOS Application Support /
XDG ~/.config) with the DRAGONX_APP_NAME variant suffix. Route both through
util::Platform::getConfigDir() instead — one place owns the split — and add
the missing __APPLE__ branch to getConfigDir so it too returns the
macOS-native ~/Library/Application Support/<AppName>.

This also fixes a latent macOS inconsistency: transaction_history_cache
already resolves via getConfigDir(), which lacked the __APPLE__ branch, so on
macOS the tx cache landed in ~/.config while settings/addressbook went to
~/Library/Application Support. Now all three agree. No migration needed:
Linux/Windows paths are unchanged, and macOS packaging is not yet shipped.
Removes ~50 lines of duplicated #ifdef.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:49:24 -05:00
121a79e768 refactor(audit): batch 9 — de-duplicate balance/peers/transactions UI cards
Three UI de-duplications (rendering changes; the test suite skips the GUI, so
these need a manual screenshot check of the Balance/Peers/Transactions tabs).

1. Classic balance layout — the DEFAULT layout — hand-rolled its own ~550-line
   address-list + recent-tx renderers that had DIVERGED from the shared
   RenderSharedAddressList/RenderSharedRecentTx used by the other 9 layouts.
   Deleted the inline copies and called the shared renderers (as the minimal
   layout already does), so the default layout now gains set-label +
   add-to-portfolio context items, custom address icons, drag-to-reorder,
   keyboard nav, copy-flash, and fully TR()'d strings. Hero row and Classic
   sizing (tabs.balance.classic address-table-height=340) preserved; addrH is
   computed before the call exactly as before. This is a deliberate
   feature-parity behavior change for the default layout.

2. peers_tab info cards — extracted drawStatCell() (label + value/em-dash with
   the shared offset math) and one drawCardDivider() replacing two duplicate
   divider lambdas; plain cells drive off a per-card loop. Bespoke cells
   (Blocks "(X left)", copy-on-click Best Block, TLS check-icon) kept inline.

3. transactions_tab — one drawSummaryCard() replaces the three near-identical
   Received/Sent/Mined blocks (same glass panel, icon, hover outline,
   click-to-filter). Byte-equivalent.

Full-node + Lite build clean (net -651 lines, no new warnings); ctest 1/1;
hygiene clean. NEEDS SCREENSHOT VERIFICATION.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:18:02 -05:00
262891229a refactor(audit): batch 8 — share the post-encrypt daemon-restart block
Extract App::restartDaemonAfterEncryption(taskName, announceRestartStatus)
from the two verbatim copies in encryptWalletWithPassphrase and
processDeferredEncryption. The two differed only in the async task name and
whether they set connection_status_ = "restarting_after_encryption" first
(the immediate encrypt path does; the deferred path does not) — both
parameterized. The 20x100ms settle loop, cancel/shutdown guards, and
stop/startEmbeddedDaemon sequence are preserved exactly.

Deliberately NOT done: the audit's "decrypt 5-deep callback pyramid" finding.
On inspection the nesting is load-bearing, not gratuitous — WalletSecurityWorkflow
is unsynchronized main-thread-only state, and each step's worker->MainCb bounce
is what keeps its mutations on the main thread (the UI reads snapshot()/importActive()
every frame); collapsing the pipeline onto one worker/async task would race the
UI, and the final restart+import stage requires an AsyncTaskManager::Token that
worker_->post can't supply. Left byte-for-byte intact.

Full-node + Lite build clean; ctest 1/1 (incl. WalletSecurity controller tests);
hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:03:40 -05:00
dcd09dc542 refactor(audit): batch 7 — RPC/settings/lite/effects mechanical dedups
Six behavior-preserving consolidations from the audit:

- RPCClient::call() overloads share a private performCall() (payload dump +
  curl_easy_perform + http code) and a static parseRpcResult() (error->RpcError
  extraction). The timeout overload restores the prior timeout in both the
  success and catch paths, exactly as before.
- The six UnifiedCallback methods delegate to one splitUnified() that builds
  the (Callback, ErrorCallback) pair once (null-check preserved).
- One liteTrimCopy() in lite_connection_service replaces 5 file-local
  trim-copy helpers across the lite slice.
- lite_wallet_controller's inline height JSON parse now routes through the
  tested parseLiteHeightResponse().
- theme_effects.cpp: unpackRGB()/scaledAlpha() replace 14 RGB-unpack + 5
  alpha-scale copy-paste blocks.
- settings.cpp load() uses loadScalar()/loadClamped() helpers for ~44 scalar
  fields. Besides removing boilerplate this HARDENS loading: ~44 fields that
  previously only checked contains() now also verify the JSON type, so a
  malformed value in settings.json falls back to the default instead of
  throwing/misreading. Custom cases (enum parses, legacy migrations, arrays)
  stay inline; save() is unchanged.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:56:54 -05:00
4635a56e89 refactor(audit): batch 6 — de-duplicate the app_network send/balance paths
Three behavior-preserving consolidations in src/app_network.cpp:

1. Fold the three near-identical pending-send balance-delta blocks
   (upsertPendingSendTransaction debit, removePendingSendTransactions
   restore, applyPendingSendBalanceDeltas debit) into one
   App::applyPendingSendDelta(from, signedAmount, includeAggregates). The
   restore path's unclamped `+amt` is provably identical to the clamped
   signed form (balance/amount are >=0 invariants), so a single clamped
   helper reproduces all three exactly.

2. Collapse createNewZAddress/createNewTAddress into one
   App::createNewAddress(bool shielded, cb) with thin public forwarders,
   preserving the lite early-return, the dual push into the type list AND
   the combined state_.addresses view, and the dirty-flag/refresh bookkeeping.

3. Extract App::submitZSendMany() shared by sendTransaction and
   resendWithFeeGapWorkaround — the only differences (the TraceScope label
   and the retry-only send_feegap_retried_opids_.insert) become parameters.
   The in-flight counter, opid tracking, upsertPendingSendTransaction, and
   pending_send_callbacks_ bookkeeping are byte-equivalent.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:36:27 -05:00
9c533fa485 refactor(audit): batch 5 — share the updater HTTP/download layer
XmrigUpdater and DaemonUpdater carried byte-identical libcurl code —
httpGet, downloadToFile, and the write/progress callbacks — including the
security-critical TLS-verify (SSL_VERIFYPEER=1 / SSL_VERIFYHOST=2) and
max-body-size options, duplicated across two signature-verified
download-and-execute files that had to be hand-kept in sync.

Hoist that machinery into src/util/http_download.{h,cpp}:
- util::httpGetString(url, logTag)
- util::httpDownloadToFile(url, dest, logTag, maxBytes, onProgress)
threading progress + cancellation through a std::function (returning false
aborts the transfer) instead of the class `this` pointer. Each updater's
httpGet/downloadToFile become one-line forwarders that bind their own log
tag and archive-size cap (64 MiB miner / 256 MiB node, both preserved).

Every curl option is preserved byte-identically (verified by diffing the
setopt lines against the pre-change code — the only deltas are the
now-parameterized MAXFILESIZE cap and the XFERINFODATA payload). No behavior
change; the TLS/size-guard options now live in exactly one place.

Deliberately scoped DOWN per the audit's own guidance: the divergent
per-updater logic (release DTOs/parsers, checksum-table parsing, version
compare, and installResolved's per-member-SHA vs transitive-trust verify,
install strategy, and chmod scope) stays in each class — NOT merged behind a
base-class/virtual-hook layer, which would raise review cost on
signature-verified execute-code and risk a subtle security regression. The
shared-DTO/parser consolidation (~77 test refs of churn on stable code) is
left as a separate, lower-value follow-up.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:17:44 -05:00
de11850c74 refactor(audit): batch 4 — UI design-system helpers + i18n
Mechanical, behavior-preserving consolidations from the audit. Rendering
extractions were kept byte-equivalent; sites that couldn't be made identical
were left as-is (noted below).

Shared helpers:
- util::truncateMiddle(s,maxLen) / (s,front,back) in text_format.h — replaces
  6 file-local middle-ellipsis truncators + several inline substr sites
  (send/receive/transactions/balance_recent_tx/explorer + app.cpp + 3 dialogs).
  Carries the maxLen<=3 guard, fixing the latent unsigned-underflow copies.
- material::LoadingDots() in draw_helpers.h — one animated-ellipsis source for
  8 copy-pasted spinner sites (identical GetTime()*3 phase preserved).
- Reuse the existing FormatHashrate() in explorer_tab + peers_tab (dropped two
  inline hashrate ladders).
- material::DrawButtonGlassOverlay() — the glass-fill/rim/tactile overlay block
  shared by TactileButton / TactileSmallButton / schema TactileButton.
- material::CollapsibleHeader() — the invisible-button + label + chevron idiom
  (3 of 5 settings_page sites; RPC/Debug headers skipped — non-identical).
- material::GlassCardScope (RAII) — the ChannelsSplit/Indent/DrawGlassPanel/
  ChannelsMerge card scaffold (5 settings_page cards; About/send/receive skipped
  — different padding / logo interleaving).
- material::DialogWarningHeader()/DialogConfirmFooter() — warning header +
  50/50 Cancel/danger footer across the confirm dialogs; button height moved
  from a hardcoded 40px into ui.toml (components.overlay-dialog.confirm-btn-height).
- File-local enterLowSpec()/exitLowSpec() collapse the 3 low-spec snapshot copies.

i18n: wrapped hardcoded English in the shared balance render paths and the
whole Lite lifecycle/security section in TR(), with English defaults added to
loadBuiltinEnglish() (res/lang/*.json left for the translation tooling — TR
falls back to English, so output is unchanged).

Full-node + Lite build clean; ctest 1/1; hygiene clean. These are rendering
changes — the Settings page (cards/headers/confirm dialogs), all tactile
buttons, and the Lite settings section warrant a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:29:12 -05:00
280a71e973 refactor(audit): batch 3 — decompose App::update() + collapse acrylic fork
Three P1 structural refactors from the audit; no behavior change.

1. Extract the ~180-line daemon-stdout rescan/witness parser out of the
   ~790-line App::update() into a pure, static, unit-testable
   NetworkRefreshService::parseDaemonRescanOutput() returning a
   DaemonRescanScan result struct (sits next to the existing static parse*
   siblings). The scanning half moved verbatim; App::update() keeps the
   state-application half, now reading scan.* fields. Adds
   testParseDaemonRescanOutput() covering all six daemon signals + edge
   cases against fixture log snippets — previously untestable without a
   live daemon.

2. Extract the ~95-line global keyboard-shortcut block (Ctrl+, / theme
   cycle / F5 / low-spec / effects / gradient / wizard) out of
   App::update() into App::handleGlobalShortcuts().

3. Collapse the ImGuiAcrylic frontend fork: the GLAD and DX11 namespaces
   were ~375 lines of semantically-identical code (the frontend makes zero
   direct GL/DX calls — all backend work delegates to AcrylicMaterial, which
   has a backend per API). Extended the single frontend's guard to
   #if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11) and deleted the
   DX11 duplicate, leaving the no-backend stub. Kills the Windows/DX11 drift
   hazard (acrylic frontend changes now made once).

Verified: full-node + Lite build clean; ctest 1/1 (incl. the new parser
tests); source-hygiene clean; and the collapsed acrylic path was
compile-verified under DX11 via the mingw-w64 Windows cross-build
(ObsidianDragon.exe links).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:40:40 -05:00
7891c689fb refactor(audit): batch 2 — remove dead scaffolding from the shipping binary
Two dead-code removals surfaced by the audit; no runtime behavior change.

1. Delete the lite-lifecycle "readiness" scaffold (~1,586 lines). The pure
   dry-run evaluateLiteWalletServerLifecycleReadiness (whose own status text
   says "wallet lifecycle execution is still disabled in this scaffold") had
   zero callers; executeLiteWalletServerSelectionUi had zero callers; and
   executeLiteWalletLifecycleUiRequest's only caller was a fallback in
   settings_page reached only when app->liteWallet() is null. Deleted
   lite_wallet_server_lifecycle_readiness.{h,cpp} and the readiness /
   UI-execution machinery + enums + translation tables from both adapters,
   keeping the genuinely-live helpers (liteConnectionSettingsFromAppSettings,
   applyLiteConnectionSettingsToAppSettings, redactLiteServerSelectionValue,
   and the LiteWalletLifecycleUiExecutionInput DTO the live create/open/
   restore path still uses). The null-backend fallback now sets a plain
   "Lite wallet backend unavailable" status. This is exactly the
   readiness/scaffold pattern CLAUDE.md forbids regrowing.

2. Split the HushChat fixture/capture-manifest/seed-projection tooling
   (~2,050 lines, incl. the libsodium seed-projection) out of
   chat_protocol.cpp (1854 -> 284) and chat_protocol.h (586 -> 105) into a
   new chat_fixture_tooling.{h,cpp} compiled ONLY into the HushChatFixtureCheck
   dev tool — never into the app, lite, or test binaries (verified via nm).
   The app keeps only the runtime slice (memo parsing + tx metadata
   extraction) that network_refresh_service actually calls. Also moved the
   decrypt-preflight/hex helpers, which likewise had no runtime caller.

Note: the HushChat split is on gated-off experimental code and should be
coordinated with the pending dormant-HushChat-content handling (commit
af06b8b) before the lite PR — done here as a pure mechanical split, no redesign.

Full-node + Lite + HushChatFixtureCheck build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:20:53 -05:00
f8150434d4 fix(audit): batch 1 — latent unlock/path/rescan correctness bugs
Four correctness fixes surfaced by the codebase audit:

1. Lite address-book path bug: AddressBook::getDefaultPath() hardcoded
   "ObsidianDragon" while Settings::getDefaultPath() uses DRAGONX_APP_NAME,
   so the Lite build wrote addressbook.json into the full-node app's config
   dir instead of ObsidianDragonLite/. Now uses DRAGONX_APP_NAME on all
   platforms (no macOS path change — the getConfigDir consolidation, which
   would move the macOS dir, is deferred).

2. PIN-unlock lockout bypass: the PIN path duplicated unlockWallet's
   success/lockout logic and its RPC-error branch bumped the attempt counter
   but skipped the escalating-lockout math entirely — a PIN user hitting an
   RPC error escaped the lockout curve. Extracted App::applyUnlockSuccess()
   and App::applyUnlockFailure() and routed all unlock paths (passphrase,
   PIN vault-fail, PIN RPC-fail) through them, so the lockout curve now
   applies uniformly. (noVault stays a mode-switch, not a failed attempt.)

3. Witness/rescan reset drift: the ~11-line rescan+witness completion reset
   was copy-pasted at four sites (app.cpp x2, app_network.cpp x2); adding a
   witness field and missing a copy would leave stale progress. Folded into
   App::resetWitnessRescanProgress(). Left the distinct phase-transition
   reset in App::update() untouched (it sets witness_phase, not 0).

4. Truncation underflow: send_tab/receive_tab's size_t TruncateAddress
   copies computed (maxLen - 3) without guarding maxLen <= 3, which wraps
   and throws std::out_of_range on short inputs. Added the guard.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:54:36 -05:00
530bf24abf fix(market): populate combined address list so portfolio sees addresses
The Market-tab portfolio (editor checklist + SumPortfolioBalance) reads the
combined WalletState::addresses view, but the full-node refresh path only ever
updated the authoritative z_addresses/t_addresses lists — rebuildAddressList()
was never called, so `addresses` stayed empty. Result: the manage-portfolio
modal showed no addresses to pick, and addresses added via the overview
right-click menu contributed no value to their entry (SumPortfolioBalance
found nothing in the empty combined list).

Rebuild the combined view in NetworkRefreshService::applyAddressRefreshResult
(the sole bulk updater of the address lists), and push newly created addresses
into the combined view immediately in the full-node create paths for
zero-latency parity with the overview (matching the lite branch). This also
repairs two other silently-broken full-node consumers of state.addresses: the
auto-shield target-address finder and the pool-mining transparent fallback.

Add a regression test (testWalletStateAddressListRebuild) covering the
empty-before / union-after rebuild and pending-send delta reflection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 03:27:44 -05:00
e3b53258b7 fix(settings): swap Install bundled/Test connection; unique daemon Refresh ID
Swap the "Install bundled" and "Test connection" daemon-binary buttons:
Install bundled now sits in the left common row (Check for updates | Refresh
| Install bundled); Test connection leads the right-aligned maintenance
group. Each button keeps its own disabled-predicates, action, and tooltip.

Give the daemon-binary Refresh button a unique ImGui ID
("Refresh##daemonRefresh") so it no longer collides with the theme-section
Refresh buttons, which triggered Dear ImGui's "2 visible items with
conflicting ID!" error. The label still displays "Refresh".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 03:09:43 -05:00
f9e4db4abf feat(settings): merge BACKUP & DATA into the WALLET card
Move the Backup & Data button row (Import/Export key, Export all, Backup, Export
CSV, and the full-node Download Bootstrap / Setup Wizard) into the WALLET card as
a "Backup & Data" sub-section under the privacy toggles + Tools & Actions, and
delete the standalone BACKUP & DATA card (its own glass panel + header). One
fewer top-level card; all actions preserved.

Full-node build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 02:59:31 -05:00
bdccddfeee feat(settings): rename RPC toggle to "RPC Connection..." (Tools & Actions style)
Rename the RPC collapsible header from "RPC CONNECTION" to "RPC Connection..." and
render it in the body2 font so it matches the "Tools & Actions..." toggle exactly.

Full-node build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 02:53:08 -05:00
d68bc27aac feat(settings): make RPC CONNECTION a collapsible toggle (like Tools & Actions)
Replace the static "RPC CONNECTION" overline header with a full-width clickable
toggle (label + expand/collapse arrow, transparent button with hover) that
reveals the Host/Port/Username/Password fields + auto-detected caption only when
expanded — same idiom as the "Tools & Actions…" section. Collapsed by default
(rpc_expanded). Field rendering/bindings unchanged.

Full-node build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 02:49:19 -05:00
51667b5a27 feat(settings): add "Open app folder" button next to "Open data folder"
Add a button to the left of "Open data folder" that opens the ObsidianDragon
config folder (util::Platform::getObsidianDragonDir() — ~/.config/ObsidianDragon
etc.: settings, themes, logs), distinct from the daemon data/blockchain folder.
The two buttons render as a right-aligned group on the data-dir row, each an
explicit button-font width so the group's right edge lands exactly on the card
edge (no overflow). New i18n: settings_open_app_dir / tt_open_app_dir.

Full-node build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 02:44:43 -05:00
96e05b4ba6 feat(settings): daemon maintenance actions right-aligned on Test row; fix Open-folder overflow
- Remove the "Advanced" toggle. The four maintenance actions (Install bundled /
  Rescan / Delete blockchain / Repair wallet) now render right-aligned on the same
  row as Check-for-updates / Refresh / Test connection (wrapping to a right-aligned
  next row only when the window is too narrow). Predicates preserved.
- Fix the Open-data-folder button overflowing the card's right edge (worse at high
  display scaling): its width was estimated with the CURRENT font, not the button
  font, so the estimate was wrong. Measure with the button font and give the button
  an explicit width so its right edge lands exactly on the card edge regardless of
  DPI/font-scale timing.
- Drop the now-unused node_advanced_expanded flag.

Full-node build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 02:26:58 -05:00
36386b2b84 fix(settings): keep "Open data folder" on the data-dir row
The right-aligned Open-data-folder button captured its Y from
GetCursorScreenPos() after the wallet-size text, by which point ImGui had already
advanced to the next line — so the button dropped below the data-dir row. Capture
the row's top Y before rendering the row and pin the button to it, so Data Dir /
Wallet Size / Open data folder sit on one line.

Full-node build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 01:56:29 -05:00
2fc577fb18 feat(settings): SECURITY on one fill-width row + Advanced daemon actions shown by default
- SECURITY: the auto-lock + PIN controls now continue on the same row as the
  encrypt/change/lock/remove controls (full-node), so the whole security band
  fills the card width instead of leaving the right half blank. Lite (no encrypt
  row) still starts auto-lock/PIN on a fresh row at the left edge.
- Daemon Binary "Advanced" (Install bundled / Rescan / Delete blockchain / Repair
  wallet) now defaults to expanded (node_advanced_expanded = true) so the actions
  are visible without a click; the toggle can still collapse them.

Full-node + lite build clean; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 01:43:46 -05:00
5180f74a68 feat(settings): rebuild Node & Security as one fill-width card
Replace the column/grid/masonry machinery (which kept leaving empty gaps and
buried the daemon Advanced actions) with a single full-width card whose controls
are arranged horizontally to use the width:
- NODE: Data Dir + Wallet Size + Open-folder on one row.
- RPC: Host / Port / Username / Password four-across when wide (>=700), wrapping
  to 2x2 when narrow; auto-detected + plaintext warning kept.
- SECURITY: encryption buttons + Auto-lock combo + PIN controls on horizontal
  rows (factored into renderSecuritySection so lite gets auto-lock+PIN and
  full-node adds the RPC-backed encrypt/lock/remove block — same gating as before).
- DAEMON BINARY: inline Installed/Bundled/status; Check/Refresh/Test row; and the
  four maintenance actions (Install bundled / Rescan / Delete blockchain / Repair)
  behind a now-prominent, clearly-clickable "Advanced ▾" toggle instead of the
  easy-to-miss faint overline — fixing the "daemon options missing" report.

Lite keeps its existing wallet-lifecycle/backup/security block, full width.
Removed useGrid/stacked/column clips/dividers. Every control + state branch +
confirm flag + tooltip preserved (verified). Full-node + lite build clean; ctest
green; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 01:28:14 -05:00
fc5ce019e2 feat(settings): 2x2 grid for Node & Security (fills the card width)
Single-column left every row hugging the left with a blank right half (and a
split RPC row); two-column variants left a bottom-corner gap. Rework the card as
a 2x2 grid on wide full-node windows so both halves carry real content:
  TOP-LEFT NODE/Data | TOP-RIGHT RPC
  BOTTOM-LEFT SECURITY | BOTTOM-RIGHT DAEMON BINARY
with the two top cells sharing a row bottom, a thin vertical + horizontal
divider, and the Daemon "Advanced" destructive buttons dropping to a full-width
4-button strip below the grid when expanded.

Gated behind one `useGrid` flag (supportsFullNodeLifecycleActions() &&
availWidth >= node-grid-breakpoint, default 900). When false — narrow windows and
lite — it falls through to the existing single full-width column, unchanged
(one-flag-revertible). Wallet Size moved to its own row (removes a mid-row gap in
both layouts). RPC / Security / Daemon rebased to per-cell X/width via aliases;
Advanced buttons factored into one lambda so the disabled predicates live in a
single place. Design produced + adversarially critiqued via a multi-agent pass.

New: components.settings-page.node-grid-breakpoint (ui.toml), "rpc_connection"
i18n string. Full-node + lite build clean; ctest green; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:55:52 -05:00
7826be4b13 feat(settings): single full-width Node & Security column (no empty gap)
Two-column layouts left an unbalanced empty gap for this content (Node/RPC vs
Security+Daemon can't be evened into two columns), so drop the split entirely:
Node -> RPC -> Security -> Daemon Binary now stack vertically at full card width.
This guarantees no horizontal gap and gives the RPC grid and the Daemon Binary
button rows the full width they want.

Implemented by forcing the existing vertical-flow path (`stacked`) always on, so
the section bodies are unchanged; removes the now-unused column-split math and the
node-sec-stack-width schema key.

Full-node + lite build clean; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:20:30 -05:00
5d6e78c719 feat(settings): masonry Node & Security — fill the empty bottom-right
The 60/40 split left the right (Security) column short while Node/RPC on the left
was tall, and Daemon Binary sat full-width below — leaving a large empty gap in
the card's bottom-right.

Rework it as a greedy two-column masonry: even 50/50 columns, and the Daemon
Binary section is placed into whichever column is currently shorter (clipped to
that column's width). In practice Node/RPC fills the left and Security + Daemon
Binary stack on the right, so the gap is filled and the two sides balance in
height. When the window is narrow the columns stack and Daemon Binary spans full
width below, as before. Daemon Binary now sizes to its column (dbColW) instead of
the full card width.

Full-node + lite build clean; source hygiene clean. Visual QA needed (column
balance + the daemon button rows in the narrower column).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:08:32 -05:00
a1e4acfe86 feat(settings): fix Node & Security empty space — 70/30 split + Advanced in right column
The fixed 60/40 split left a large empty gap in the bottom-right: the SECURITY
column is short while NODE (data dir + RPC grid) is tall, and the DAEMON BINARY
row spans full width below both. Two changes:

- Widen the side-by-side split to 70/30 so NODE (the content-heavy column, esp.
  the RPC grid) gets the room and the right-side gap shrinks.
- Move the "Advanced" destructive-actions expander out of the full-width DAEMON
  BINARY row and into the right column below Security, filling the gap. Its four
  actions now stack vertically to fit the narrow column.

Full-node + lite build clean; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:48:45 -05:00
419f8bb9cb fix(console): fill scanline banding into the empty space below the last row
The zebra banding is emitted per text row, so it stopped at the last line and
left the bottom fade-padding (up to 18% of the panel) unbanded — very visible
when scrolled/pinned to the bottom. After the text-row bands, continue the
alternating pattern (same parity, lineHeight pitch) down to the panel bottom so
the zebra fills the whole output area. Also covers the case where the content is
shorter than the panel.

Full-node + lite build clean; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:35:01 -05:00
c5ef9e42ed fix(console): correct CRT scanline alternation (was skipping rows)
The per-line scanline shade alternated on rowIndex = floor((cumulativeY +
yOffset) / lineHeight + 0.5), but cumulativeY includes the 2px inter-line gap
while the divisor is lineHeight alone. That drifts ~0.14 row per line and, with
the rounding, jumps the index by 2 every ~7 rows — so two adjacent rows get the
same shade and the alternation visibly breaks in patches.

Replace it with a true per-visual-row counter (one increment per drawn sub-row),
seeded with the first on-screen line's sub-row ordinal so the shading stays
stable while scrolling. Parity now alternates exactly every row.

Full-node + lite build clean; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:25:09 -05:00
f0243307f1 feat(portfolio): Overview right-click to add/remove an address to a portfolio
Add a "Portfolio" submenu to the Overview address-list context menu: it lists
each Market-tab portfolio entry with a checkmark showing whether this address is
in it, and clicking toggles membership (add/remove), persisting immediately. If
no entries exist yet, a disabled hint points to creating one.

Full-node + lite build clean; ctest green; source hygiene clean. Completes the
Market configurable-portfolio item (last of the todo.md batch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:44:04 -05:00
95ddfa05ee feat(portfolio): Market card entries + "Manage" editor dialog
Build the configurable-portfolio UI on the persisted model:
- The portfolio card keeps the "all funds" summary and now renders each custom
  entry below it as a compact row (label · summed DRGX · USD), live-computed via
  SumPortfolioBalance over the wallet's per-address balances; the card grows to
  fit the entries.
- A right-aligned "Manage…" button on the card header opens a modal editor
  (material overlay dialog): list entries with Edit/Delete, "Add entry", and an
  edit form = label field + quick "All shielded / All transparent / Clear"
  selectors + a checklist of wallet addresses (with balances). Persists to
  Settings on each mutation.
- i18n English defaults for the new portfolio_* strings.
- Test namespace fix (AddressInfo is dragonx::, not dragonx::data::).

Full-node + lite build clean; ctest green; source hygiene clean. Remaining:
the Overview-tab right-click "add/remove address to a label" integration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:41:20 -05:00
ecc0043559 feat(portfolio): persisted portfolio entries + pure sum helpers
Foundation for the Market tab's configurable portfolio (custom labels tied to
groups of addresses):

- Settings gains a PortfolioEntry {label, addresses[]} list, persisted in
  settings.json as a JSON array-of-objects (same mechanism as lite_servers_),
  with get/setPortfolioEntries().
- data/portfolio.h (header-only, ImGui-free): SumPortfolioBalance() sums a
  group's DRGX balance against the live per-address list (unknown addresses
  contribute 0), plus PortfolioEntryContains/Add/Remove group helpers.
- Unit-tested (testPortfolioHelpers): empty/known/unknown-address sums, and the
  add/remove/contains membership semantics.

UI (editor dialog, Market card rework, Overview right-click) comes next.
Full-node + lite build clean; ctest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:32:53 -05:00
3a9ff5f021 feat(settings): make Node & Security columns responsive (stack when narrow)
The NODE + SECURITY split was a fixed 60/40 two-column layout regardless of
width, which cramps the RPC grid and wastes space on narrow windows. Make it
responsive: below a configurable content width (node-sec-stack-width, default
600) the two columns stack into one full-width column — SECURITY flows below
NODE — so each section gets the full width; side-by-side 60/40 is kept when
there's room. Column-geometry-only change (leftX/rightX/widths + the right
column's top Y); the content blocks are untouched. The rejoin already used
max(leftBottom, rightBottom), correct in both modes.

Full-node + lite build clean; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:46:11 -05:00
1faaa54a07 feat(market): merge price + chart into one card; simplify stats
Combine the top two Market cards: the price-summary hero and the chart now share
a single glass panel (one panel spans the price/stats header + the chart drawn
below it; the chart no longer draws its own panel or an inter-card gap).

Simplify the stat strip from three columns to two — drop the niche BTC-price
column, keeping 24h Volume + Market Cap — and remove the "updated Ns ago"
staleness line (data freshness is still shown by the attribution "Updated" line
under the pair buttons).

Full-node + lite build clean; ctest green; source hygiene clean. Completes the
Market "round buttons + combine/simplify top cards" item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:31:29 -05:00
61f8c54742 feat(market): flatten pair selection into round buttons (no exchange dropdown)
Replace the exchange combo dropdown + the horizontally-scrolling pair chip bar
with a single flat selector: every trading pair across all exchanges is shown as
a round button (pair name + dimmed exchange sublabel) that wraps to multiple
rows. Clicking one selects that exchange+pair, persists it, and refreshes market
data. The CoinGecko attribution + last-updated line moves under the buttons.

Removes the now-unused pair-scroll state (scroll/drag/arrow machinery) and the
pair-bar-height budget input.

Full-node + lite build clean; source hygiene clean. (Second half of the Market
"round buttons + combine cards" item — the hero/chart card merge is next.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:20:12 -05:00
48fc9ba8bb fix(settings): clamp About-logo height so it doesn't overlap the buttons
The About card draws its logo deferred in the left column, scaled to the card
height (cardMax.y - bottomPad). Because the buttons row is full-width at the
bottom of the card, the tall logo extended down over it. Clamp the logo to end
just above the buttons row (captured Y minus a small gap) instead of the card
bottom — aspect + reserved-width caps are unchanged.

Full-node + lite build clean; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:06:39 -05:00
2552234610 feat(mining): fetch pool hashrates + miner version at startup
Warm the Mining tab's data during App::init() (both off the UI thread) instead of
only when the user opens the tab or clicks a mine / auto-balance button:
- kick the idempotent one-shot `xmrig --version` detection, and
- kick a background pool-stats refresh over the known pools.

Gated on supportsPoolMining(). The pool-stats snapshot lands on a later frame and
the auto-balance driver's self-throttling still applies, so this is just an
earlier first fetch — hashrates + version are visible as soon as the tab opens.

Full-node + lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:01:35 -05:00
ff20a8a44e feat(console): terminal polish — lite/color-coded toggles, dark bg, monospace font
Work through the console items in todo.md:

- Lite filter toggles (#5): replace the single hasLogFilters() bool with a
  ConsoleLogFilterCaps struct so each backend advertises which toggles apply.
  Full node = daemon/errors/rpc-trace/app; lite = errors-only + app (its
  diagnostics ring maps to the App/Error channels; the text filter is always
  shown). Lite previously showed no toggles at all.
- Color-coded toggles (#6): each filter checkbox is tinted with its channel's
  accent color (daemon=blue, errors=red, rpc=secondary, app=teal) via a new
  channelAccentColor() that also drives the output's left accent bar — one source
  of truth for channel color.
- Darker terminal look (#7): drop the light glass rectangle on the input; both
  output and input now get a terminal-dark overlay (tabs.console.bg-darken-alpha,
  default 110) and the input field blends into it (transparent frame bg).
- Monospace font (#8): bundle Ubuntu Mono (res/fonts/UbuntuMono-R.ttf, Ubuntu
  Font License — same family as the existing Ubuntu fonts) via INCBIN, load it at
  caption size as Type().mono(), and render console output + input in it so
  pretty-printed JSON columns and terminal text align. Falls back to the
  proportional caption font if unavailable.

Full-node + lite build clean; ctest green; source hygiene clean; sandboxed
startup smoke confirms the mono font loads + atlas builds without crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:53:08 -05:00
245d9bf976 feat(console): collapsible JSON in command output
Add fold/unfold of JSON objects and arrays in console command results, built on
the refactored channel + model foundation.

- ComputeConsoleFoldSpans() (console_input_model): pure brace/bracket matcher
  over a block of pretty-printed JSON lines; each opener line gets the offset to
  its matching closer at the same indentation (empty/one-line blocks are not
  foldable). Unit tested (nested objects, arrays, empty blocks, non-JSON, and a
  string value that merely contains a brace).
- ConsoleModelLine gains foldSpan (relative, so it survives front-eviction by the
  line cap) + a collapsed flag; ingest() carries the span and toggleCollapsed()
  flips openers (main thread). addFormattedResult computes the block's spans and
  ingests them atomically.
- computeVisibleLines() skips a collapsed block's interior (opener stays, its
  foldSpan lines through the closer are hidden); folding is bypassed while a
  filter is active so every match stays reachable.
- drawVisibleLines() draws a fold triangle in the free left gutter of opener
  lines (result/JSON channels carry no accent bar there), toggled by a click in
  the gutter cell, and appends a dim " ... }" / " ... ]" summary on collapsed
  openers.

Full-node + lite build clean; ctest green (incl. fold-span + model-fold tests);
source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:11:44 -05:00
1c248d727a refactor(console): phase 5b — decompose toolbar/input/commands-popup methods
Finish the thin-view pass on the remaining large render methods (pure code
motion, no behavior change):

- renderToolbar (225 -> 99 lines): extracted drawToolbarStatus(), the
  drawLogFilterToggles() checkbox group, drawFilterInput(), and drawZoomControls().
- renderInput: factored the command echo + built-in interception + submit into
  submitConsoleCommand(exec, cmd) -> bool, leaving renderInput to own just the
  glass panel + InputText + completion/history callback.
- renderCommandsPopup (194 -> 135 lines): removed the duplicated lowercase-and-
  find filter logic (match-count loop and per-row filter now share
  consoleCommandMatchesFilter()) and extracted the [optional]-param dimming loop
  into drawConsoleCommandParams().

Full-node + lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:25:12 -05:00
57291ef28c refactor(console): phase 5 — decompose renderOutput into focused sub-steps
renderOutput() was a ~410-line method doing filter-building, mouse/keyboard
interaction, the per-line draw loop, the new-output pill, and the right-click
context menu all inline. Split it into a thin orchestrator that calls five
single-purpose private methods (pure code motion, no behavior change):

  - computeVisibleLines()      -> the filter -> visible_indices_ + match count
  - handleOutputInteraction()  -> wheel-up, selection drag lifecycle, Ctrl+C/A
  - drawVisibleLines()         -> accent bars, JSON indent guides, selection +
                                  filter highlight, scanline capture, text
  - drawOutputContextMenu()    -> the right-click menu
  - drawNewOutputIndicator()   -> the jump-to-bottom pill

Ordering and every operation are preserved exactly (interaction still runs
after layout build and before the draw; selection bounds are recomputed post-
interaction; scanline_rows_ cleared before the draw pushes to it). Full-node +
lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:16:36 -05:00
18c1de87db refactor(console): phase 4b — extract ConsoleScrollController
Pull the auto-scroll concern out of ConsoleTab into a focused, ImGui-free
ConsoleScrollController (console_scroll_controller.{h,cpp}). It owns the three
coupled fields that were loose on the god-class — auto_scroll_, the wheel-up
cooldown, and the new-lines-while-scrolled-up backlog count — and the state
machine tying them together: wheel-up detaches + starts a cooldown, the cooldown
gates the at-bottom re-enable check, backlog counting happens only while
detached, and re-pinning (checkbox / jump pill / reaching the bottom) clears it.

The view keeps only what needs ImGui: it measures scroll position, hands the
controller a plain `atBottom` bool + frame delta, and issues the actual
SetScrollHereY. ConsoleTab loses three more members and all the scattered
auto-scroll bookkeeping collapses to scroll_.* calls.

Adds testConsoleScrollController: pinned start ignores backlog, wheel-up detach +
cooldown gating, re-enable only when at bottom, checkbox toggle semantics, and
the jump-to-bottom pill. Full-node + lite build clean; ctest green; source
hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:52:15 -05:00
0cdbbd024e refactor(console): phase 4 — extract ConsoleSelectionController from the god-class
Pull the text-selection concern out of ConsoleTab into a focused, ImGui-free
ConsoleSelectionController (console_selection_controller.{h,cpp}). It owns the
anchor/caret state and all the fiddly logic around it: the mouse-drag lifecycle
(beginDrag/updateDrag/endDrag), select-all, range ordering, text extraction over
the visible model, and the index shift applied when the buffer cap evicts lines
from the top.

ConsoleTab now delegates: the four scattered selection fields (is_selecting_,
has_selection_, sel_anchor_, sel_end_) and five helper methods (selectionStart/
End, isPosBeforeOrEqual, getSelectedText, clearSelection) collapse to a single
selection_ member plus a thin selectedText() wrapper. screenToTextPos (still
view-side — it needs the frame's layout) now returns the shared ConsoleTextPos,
so ConsoleTab::TextPos is retired.

The byte-offset / eviction math was previously only reachable through the live
ImGui renderer; it now has direct unit coverage (testConsoleSelectionController:
ordering, drag lifecycle, upward drag, select-all, partial/full/no-op eviction
shift, and filtered extraction). Full-node + lite build clean; ctest green;
source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:46:06 -05:00
779c9682d4 refactor(console): phase 3 — thread-safe ConsoleModel ingest queue; fix data race
Introduce ConsoleModel (console_model.{h,cpp}): an ImGui-free, thread-safe line
store. Producers on any thread call ingest(), which only briefly locks a small
pending queue; the main thread calls drain() once per frame to move pending
lines into the visible deque and enforce the line cap, reporting how many were
added and evicted from the front.

This fixes the real data race. Previously a single lines_mutex_ guarded both the
line container AND the UI-only interaction state (selection, scroll, auto-scroll
counters), and it was held across the whole render pass — yet render() touched
auto_scroll_ / new_lines_since_scroll_ outside that lock, racing the background
RPC-trace and app-logger producers that call addLine() on worker threads.

Now:
- addLine() only ingests (lock-free w.r.t. the model); safe from any thread.
- render() drains once per frame, then does the cap/selection/scroll bookkeeping
  on the main thread (new drainModel()).
- The visible deque + all selection/scroll fields are main-thread-only, so every
  lines_mutex_ lock is gone — including the one wrapping the entire renderOutput.

Adds testConsoleModel() covering ingest/drain ordering, the line-cap eviction
counts, view-only clear (queued lines survive), and an 8-thread concurrent
ingest/drain stress with no lost lines. Full-node + lite build clean; ctest
green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:35:25 -05:00
33735b1d52 refactor(console): phase 2 — channel-as-semantic key; decouple executor from UI colors
Make ConsoleChannel the canonical semantic classification of every console
line. Producers (command executor, app log forwarders, result formatter) tag
each line with a channel; the UI derives both the text color and the left
accent-bar color from it at draw time, and the output filter keys off it.

This replaces the prior scheme of storing an ImU32 color per line and then
reverse-engineering the source from color-equality plus a "[daemon] "/"[xmrig] "
/"[app] "/"[rpc] " text prefix. Consequences:

- ConsoleLine now stores {text, ConsoleChannel} — no per-line color.
- addLine()/addRpcTraceLine() take a channel; the redundant text prefixes are
  gone (the accent bar carries the origin).
- The executor's ConsoleAddLineFn hands a channel, not an ImU32, so the backend
  no longer depends on ConsoleTab::COLOR_* — FullNode/Lite executors emit clean
  text + channel.
- The theme-remap loop that rewrote stored colors on light/dark flip is deleted;
  colors are resolved per-channel each frame (refreshColors on flip only).
- ConsoleOutputFilter drops its color fields; consoleLinePassesFilter() keys off
  the channel. Test updated to the channel-based API.
- Added a Warning channel (amber severity peer of Error/Success) so the
  notification + logger warning forwarders keep their color.

Full-node + lite build clean; ctest green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:22:35 -05:00
19e83eb12a refactor(console): phase 1 — extract pure ConsoleTextLayout (wrap/hit-test/selection)
Lift the console's word-wrap layout, screen->text hit-testing, and selection extraction
out of ConsoleTab into a pure, ImGui-free module (console_text_layout.{h,cpp}). Text
measurement is injected via a ConsoleTextMeasure interface — production uses an
ImFont-backed impl (identical CalcWordWrapPositionA/CalcTextSizeA calls as before), while
tests drive a fixed-width stub.

- BuildConsoleLayout: per-line wrap segments + heights + cumulative Y (was inline in
  renderOutput).
- HitTestConsoleLayout: point -> (visibleRow, byte col) (was screenToTextPos).
- ExtractConsoleSelection: selected text across visible lines (was getSelectedText).
- ConsoleTab now holds a single `ConsoleLayout layout_` (replacing 4 mutable members;
  drops the dead cached_wrap_width_) and calls the pure functions.
- New unit test testConsoleTextLayout covers wrapping, multi-row hit-testing, bottom
  clamp, and filtered/unfiltered selection — the biggest previously-untested surface.

No behavior change (algorithm preserved verbatim). Full-node + lite build clean, ctest
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:58:29 -05:00
f12153cdd0 refactor(console): phase 0 — remove dead code + fix channel/arg bugs
Zero-behavior-change cleanup ahead of the console refactor:
- Delete dead API/members: addCommandResult (unused), handleSelection (declared, no
  def), isAutoScrollEnabled (no callers), and scroll_to_bottom_ (written 8× but never
  read) with all its writes.
- Drop the unused screenToTextPos `line_height` parameter (+ 4 call sites) and the unused
  `segEnd` local — clears the two -Wunused warnings.
- Remove the toolbar filter checkboxes' dead `static bool s_prev_*` change-detectors,
  whose only effect was setting the dead scroll_to_bottom_.
- Fix the console_new_lines format/arg mismatch (format takes one %d; the call passed a
  spurious plural-suffix arg).
- Fix the channel/color incoherence: a "[daemon] error:" line kept red text but got a
  blue daemon bar — the prefix no longer downgrades an error channel.
- Delete 11 orphaned lite_console_* i18n keys left over from the console merge (keep
  lite_console_help_passthrough).

Full-node + lite build clean, ctest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:40:38 -05:00
8abc0ec113 feat(console): JSON indent guides on result lines
Draw faint vertical guides per 2-space nesting level on result (non-channel) lines, so
nested RPC JSON is easier to read. Draw-only in the per-line loop (like the channel bar),
so it doesn't touch the text layout or selection.

Note: collapsible JSON nodes were intentionally deferred — folding entangles with the
wrap cache / filter / selection line indices and warrants a runtime-verified pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:43:34 -05:00
5a677f6202 feat(console): right-click "Copy value" for hashes/addresses under the cursor
Right-clicking the output now offers "Copy <value>" when a long alphanumeric token
(txid/blockhash/address, >=16 chars) is under the cursor — resolved via screenToTextPos.
Uses right-click so it never conflicts with the left-drag text selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:40:38 -05:00
efec3a50a1 revert(console): drop the redundant jump-to-bottom pill
renderOutput already draws a clickable "N new lines" jump-to-bottom indicator
(console_tab.cpp ~996), so the pill added earlier duplicated it — and reused the
"console_new_lines" format string incorrectly. Remove the pill and the stray i18n entry;
the existing indicator stands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:38:05 -05:00
4ed69d4e9f feat(console): filter match highlight + match count
- Highlight the matched substring (case-insensitive) in every visible line with a
  translucent yellow behind the text, drawn per wrap-segment like the selection highlight.
- Show a live match count next to the filter input ("N matches", red when zero).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:34:18 -05:00
e0d6d6bc4d feat(console): left-edge channel accent bar (replaces inline log prefixes)
Each line gets a `channel` inferred in addLine() from its color and any
"[daemon]"/"[xmrig]"/"[app]"/"[rpc]" prefix, which is then stripped. renderOutput draws
a thin colored bar in the existing left margin (padX) per line — node log (blue), mining
(amber), app (teal), rpc (secondary), command (accent), error (red). Drawn in the margin
so it never touches the text layout or selection; filtering is unaffected (it keys off
color, not the prefix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:30:43 -05:00
e9121fe49e feat(console): jump-to-bottom "N new" pill when scrolled up
Surface new_lines_since_scroll_ (already tracked) as a floating pill at the output
panel's bottom-right whenever the user is scrolled up with unseen output; clicking it
re-enables auto-scroll and snaps to the bottom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:25:34 -05:00
b5001ad4db refactor(console): unify lite + full-node console behind a pluggable executor
The two consoles were separate implementations (the rich full-node ConsoleTab and a
minimal RenderLiteConsoleTab). Introduce ConsoleCommandExecutor to abstract the only
real differences — command execution + log source — so both variants share the one rich
terminal (the lite console now gains selection, zoom, copy, and JSON-colored output).

- console_command_executor.{h,cpp}: interface + FullNodeConsoleExecutor (dragonxd log
  ingestion + RPC command execution, result queue) and LiteConsoleExecutor (lite
  diagnostics ring + backend runConsoleCommand/takeConsoleResult, connection/sync status).
- ConsoleTab::render() now takes a ConsoleCommandExecutor&; log ingestion, command
  submission, and command results flow through it. The RPC command-reference popup and
  the daemon/errors/rpc-trace/app filter toggles are gated on the executor's capabilities;
  the toolbar status dot + a status header come from the executor.
- Safety preserved: `clear`/`cls` is intercepted as a view-only clear in the shared UI so
  it is NEVER forwarded (the lite backend's `clear` wipes tx history); command output is
  not persisted to diagnostics.
- App owns the executor (created per variant) and routes both NavPages to console_tab_;
  lite_console_tab.{h,cpp} deleted.

Needs runtime verification of BOTH consoles (full-node RPC + lite: clear, seed/export
secrecy, sync status, command results).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:17 -05:00
1266b6e68e feat(market): auto-populate exchange pairs from CoinGecko (adds OurBit); DRGX dashboard
- Fetch /coins/dragonx-2/tickers once per session (App::refreshExchanges, reusing the
  price-fetch worker path) and build the exchange registry at runtime into
  state.market.exchanges; parseCoinGeckoTickers groups tickers by venue. The Market tab
  sources from this live list and falls back to the compiled-in registry (now including
  OurBit + Nonkyc.io) when offline. Fixes the missing OurBit pair and auto-tracks future
  listings.
- Fix stale selection defaults (TradeOgre / DRGX-BTC -> Nonkyc.io / DRGX/USDT) that never
  matched the registry, and make the attribution generic ("Price data from CoinGecko").
- Reframe the single-asset portfolio card as a DRGX holdings dashboard: relabel to
  "MY DRGX" and show the 24h change on the holdings value (colored by direction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:25:22 -05:00
57f52b7a6c feat(ui): semibold window title + "Lite" suffix on the lite build
- Draw the brand word "ObsidianDragon" in Ubuntu Medium (the closest weight the
  project ships — no true 600 face) as a stand-in for semibold; append "Lite" in the
  Regular font under DRAGONX_LITE_BUILD so the lite window reads "ObsidianDragonLite".
- Fix a misleading comment: pool mining (and thus auto-balance) is available in both
  builds; only solo mining is full-node-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:13:40 -05:00
a6964621a8 feat(settings): About logo scales to card height; Node&Security Advanced expander; lite gating
- About: draw the logo deferred, scaled to the card's full height (aspect-preserved,
  capped to the reserved width) so there's no empty space below it.
- Node & Security: split the daemon toolbar — Refresh + Test connection stay visible;
  the rare/destructive actions (Install bundled, Rescan, Delete blockchain, Repair
  wallet) move behind a collapsible "Advanced" header (reusing the DEBUG LOGGING idiom).
- Lite gating: hide the DEBUG LOGGING card (dragonxd debug= categories, no daemon in
  lite) and the RPC-backed encrypt/change/lock/remove block (lite has its own encryption
  in the NODE column) behind full-node guards; label the lite left column "WALLET" not
  "NODE". (Auto-lock + PIN remain in both.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:13:40 -05:00
14f5fdc559 fix(overview): mining addresses visible at 0 balance; drag-reorder always applies
- Address list: a mining-flagged address stays visible even at 0 balance when
  "hide zero balances" is on (payout addresses shouldn't vanish).
- Drag-reorder: persist dense sort orders (0..N-1) for the whole visible list on
  drop via App::reorderAddresses, instead of a pairwise swap that no-ops when both
  rows are still at the default un-ordered state. First drag now always takes
  effect, and explicit order keeps overriding the starred/type/balance sort.
- Tests: 0-balance mining row survives hide-zero; ordered non-favorite outranks a
  favorite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:02:15 -05:00
1a00ec3359 feat(mining): left pool card (Manual/Auto, metrics, scrollable pool list)
Split the pool-mode hashrate card into a 25%-width left card + 75% chart so the
Manual/Auto toggle and pool metrics no longer reflow the tab:
- left card: Manual|Auto toggle (with tooltips), a Local/Pool/Shares/Uptime
  metric stack, and a fixed-height scrolling pool list — rows click-to-select in
  Manual (reusing the existing save+restart path) and show a "mining here" dot in
  Auto. Fixed height keeps the card constant as pools are added.
- mode-toggle row: keep the suggested-pools dropdown + auto-mode URL disable;
  the toggle and pool list moved into the card.
- mining_controls: show the installed/running miner version ("Current: …")
  before and during mining instead of "none".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:35:33 -05:00
5c490c8d53 feat(mining): auto-balance driver, per-pool algo, live miner version
- app: periodic weighted-random auto-balance of the pool while pool-mining
  (~30-min cadence; restarts the miner only when the pick actually changes),
  plus snapshot + refresh accessors for the UI.
- app_network: resolve the xmrig algo per pool at start (pool.dragonx.cc =
  rx/dragonx, pool.dragonx.is = rx/hush; custom hosts keep the setting).
- xmrig_manager: schema-aware pool-side hashrate readout (fixes a silent 0 for
  Miningcore pools); expose the running miner's version from its HTTP API and a
  cached `xmrig --version` detection so the UI can show it before mining.
- wallet_state: carry the running miner's version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:35:22 -05:00
d0586e5e75 feat(mining): pool registry, schema-aware stats fetcher, pool-select setting
Data + service layer for supporting multiple mining pools:
- pool_registry: KnownPool table (pool.dragonx.is + pool.dragonx.cc) carrying
  each pool's stratum host, xmrig algo, and stats-API schema; pure helpers
  findKnownPoolByUrl / resolvePoolAlgo / parsePoolHashrate / chooseWeightedPool
  (weighted-random by hashrate with incumbent stickiness).
- pool_stats_service: background, schema-aware hashrate fetcher (libcurl, TLS,
  browser UA for Cloudflare) modelled on XmrigUpdater.
- settings: pool_select_mode (manual | auto_balance), string-serialized.
- i18n strings + unit tests (both stats schemas, algo resolution, weighted pick).

Note: pool.dragonx.cc's stratum host is us.dragonx.cc:3333 — the .cc domain is
only the Cloudflare-proxied web/API host and does not accept stratum on :3333.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:35:13 -05:00
24e8fb4942 chore: bump full-node to 2.0.0; stop tracking the generated manifest
Set project(ObsidianDragon VERSION 2.0.0) (was 1.3.0); the lite variant keeps
its own independent DRAGONX_LITE_VERSION (1.0.0). res/ObsidianDragon.manifest is
generated per-variant by configure_file from res/ObsidianDragon.manifest.in, so
it was wrongly tracked (it kept showing dirty, stamped with whichever variant
built last) - untrack it and gitignore it; the .in template remains the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:20:56 -05:00
1393d9ae1a feat(lite): vendor SDXL backend source; source deps from git.dragonx.is
Vendor the two local Rust crates that build the lite backend artifact into
third_party/silentdragonxlite/ (the qtlib C-ABI wrapper + the silentdragonxlitelib
core, with proto/res and all the lite-send fixes), and point
build-lite-backend-artifact.sh's default --backend-dir there, so the lite wallet
builds without the upstream SilentDragonXLite repo.

External build inputs are now only the Rust toolchain + git.dragonx.is + crates.io:
- the 6 librustzcash git deps point at the git.dragonx.is/DragonX/librustzcash
  mirror (pinned rev acff1444), not git.hush.is;
- the Sapling params are gitignored (not committed, no Git LFS) - the build fetches
  them from the git.dragonx.is/DragonX/zcash-params 'sapling-v1' release and verifies
  their SHA-256 before rust-embed bakes them in (ensure_sapling_params).

For fully offline builds, cargo vendor into lib/vendor/ and add a vendored-sources
redirect (vendor/ is gitignored; the script symlinks it into the prepared dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:06:42 -05:00
3ce62326f9 chore(lite): send-smoke harness + checkpoint generator
tools/lite_send_smoke drives the real SDXL backend's exact GUI send path
(newaddr/status/list/tree/send/rescan) for diagnosing shielded sends.
scripts/gen-lite-checkpoints.sh generates verified mainnet checkpoints from a
synced dragonxd (getblockhash + getblockmerkletree), self-checking against a
known checkpoint before emitting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:35 -05:00
b20e7efb16 feat(lite): network-tab polish, reworked key-export/QR dialogs
Network tab: glow only the active node, drop the left accent bar. Key-export
dialog: fix the lite-wallet "Not connected" failure by exporting the key
locally via the SDXL backend when there's no daemon; rework the layout to
wrapping click-to-copy fields with a side QR (empty placeholder when hidden),
85% modal width, HRP-preserving key chunking, and a centered, emphasized
warning. QR popup matched to the same sizing and click-to-copy address. Shared
field rendering extracted to widgets/copy_field.h so both dialogs stay in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:25 -05:00
4473e7e00a feat(updater): in-app dragonxd updater + browse-all-releases
Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog)
reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 +
enforced ed25519 signature) and atomically installs the latest dragonxd from
the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all
releases..." picker (release_list_view) to both the miner and daemon updaters
so users can pin older/pre-release builds. Pure no-I/O cores
(daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh
signs release archives offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:13 -05:00
2e8e214689 refactor(ui): app-wide compact, translucent tooltips
Add material::Tooltip / BeginTooltip / EndTooltip wrappers (tooltip_style.h)
that scope a small window padding (8x4, dpi-scaled) and ~85% opacity to
tooltips only, then route the tooltip call sites through them. Menus and combo
dropdowns are untouched (they keep the global opaque PopupBg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:26:52 -05:00
69a6fb3e64 fix(fullnode): smooth witness-cache progress from the new daemon's done/total
Use the new daemon's "Reading blocks for witness rebuild: <done> / <total>" as an
exact fraction: the reported total is the denominator directly, so the bar sweeps
0..1 smoothly instead of being held near the top by the peak-anchored remaining
heuristic (kept only as a fallback for older daemons that log bare "<n> remaining").
Also snap to 100% on the parallel rebuild's completion line ("rebuilt <n> note
witness cache(s) … using <t> thread(s)"), which otherwise logs no progress, so the
bar visibly finishes before the rescan-complete signal clears it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:48:33 -05:00
ed6120eef9 feat(fullnode): parse the new multi-threaded daemon's witness-rebuild progress
The updated dragonxd (parallel witness rebuild) replaced the per-block
"Building Witnesses for block <h> <frac> complete, <n> remaining" log line with a
clean serial read counter "Reading blocks for witness rebuild: <done> / <total>".
Parse the new line and map it onto the existing phase-2 path (remaining = total -
done), so the witness progress bar shows done/total against this daemon. The old
"Building Witnesses" matcher is kept for backward compatibility with older daemons;
"Setting Initial Sapling Witness for tx … i of N" (phase 1) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:41:36 -05:00
a5bd2dadd7 fix(fullnode): make witness sub-phase upgrade-only to stop progress thrash
The initial-witness pass ("Setting Initial Sapling Witness") and the cache walk
("Building Witnesses for block … remaining") interleave during a rescan — the daemon
does both per block. The phase selector picked phase 1 whenever a batch had only
initial-pass lines, so once the cache walk started an interleaved initial line would
flip the phase back to 1 and reset the bar to 0 every batch. Make the phase
upgrade-only (once the cache walk is seen it never drops back), so the reset happens
at most twice (→1, →2) and the cache-walk percentage advances monotonically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:09:29 -05:00
25ee1496b4 fix(fullnode): make witness/rescan progress work on the real daemon
Verified by running the app against a live node and watching a real rescan. Three
issues that only surfaced at runtime:

- Wrong RPC name: this daemon (hush/komodo) exposes the runtime rescan as
  "rescan <height>", not bitcoin's "rescanblockchain". runtimeRescan() and
  RPCClient::rescanBlockchain() used the bitcoin name and failed with "Method not
  found" on every node. Corrected to "rescan".

- Witness/rescan progress never surfaced during a rescan: the daemon-output parser
  that drives it was gated behind rpcConnected, but a heavy rescan holds cs_main so
  getinfo times out and the RPC reads disconnected — silencing the parser exactly
  when it's needed. The parser reads the daemon's stdout pipe (no RPC), so it now
  runs whenever the daemon process is alive. It also now parses INLINE on the main
  thread instead of via fast_worker_, so it can't be starved when the worker is
  blocked on a getrescaninfo call (which waits on cs_main during a witness rebuild).

- Witness rebuild has TWO sub-phases with different scales — the initial-witness
  pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") and the cache
  walk ("Building Witnesses for block <h> <frac> complete, <n> remaining"). Tracking
  them with one monotonic value pinned the bar at the initial pass's ~100% through
  the whole cache walk. They're now tracked as distinct phases (witness_phase) with
  their own monotonic progress and labels ("Setting witnesses" vs "Rebuilding
  witnesses"), so neither resets/bounces and the long phase shows real movement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:00:36 -05:00
e2bc3623b6 fix(fullnode): stable overall progress for Sapling witness rebuild
The witness-rebuild bar reset repeatedly because the daemon's "Building Witnesses
for block <h> <frac> complete" line reports per-call progress: BuildWitnessCache is
re-invoked for each connected block and each call walks from its own start height to
the tip, so the fraction restarts every time. The earlier "Setting Initial Sapling
Witness for tx <i> of <m>" counter resets per call too, so neither is a usable
overall metric.

Derive a stable, monotonic percentage from the "<n> remaining" count instead: track
the largest "remaining" seen during the phase as the full span and show how far
remaining has fallen below it. The longest pass defines 0→100%; the short per-block
follow-up passes only nudge the bar near the end rather than resetting it. The
"Setting Initial" line now only marks the phase active. Per-phase tracking resets at
phase start and every rescan-completion site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:44:01 -05:00
b32fe07cb1 feat(fullnode): show Sapling note witness-rebuild progress
The daemon's post-rescan witness rebuild ("Building Witnesses for block ...")
is a distinct, often-long phase that previously showed only as an indeterminate
"Rescanning..." with no progress. Parse the daemon's witness-build log lines and
surface a dedicated progress indicator.

- Parse "Building Witnesses for block <h> <frac> complete, <n> remaining" (and the
  earlier "Setting Initial Sapling Witness for tx ..., <i> of <m>") from daemon
  output, extracting a 0..1 fraction and remaining-block count.
- New SyncInfo fields building_witnesses / witness_progress / witness_remaining,
  cleared at every rescan-completion site (warmup-end, getrescaninfo poll, runtime
  rescan callback, daemon-log "finished").
- Status bar shows "Rebuilding witnesses NN%" (priority over the generic rescan
  text); the loading overlay (shown during -rescan warmup) gets a labelled witness
  progress bar with the remaining-block count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:23:52 -05:00
a0532275dd feat(fullnode): auto-reconcile wallet after bootstrap; runtime rescan for pruned nodes
A wallet bootstrapped from a snapshot keeps its wallet.dat but never rescans, so
its spent-state is stale and the first send tries to spend already-spent notes and
is rejected. The startup -rescan flag can't fix it either: the snapshot lacks the
pre-snapshot block history -rescan needs, so it errors. The working fix is a runtime
rescanblockchain RPC from a height the snapshot actually has.

- Add App::runtimeRescan(startHeight): runs rescanblockchain via the worker, drives
  the rescanning UI state, and owns completion via the RPC callback (getrescaninfo
  is unavailable on this daemon). Suppresses the per-second mining/rescan pollers
  and the Core/balance/tx refreshes while the daemon holds cs_main for the scan.
- Add App::detectLowestAvailableBlockHeight(): async binary search via getblock for
  the lowest height whose block data is on disk → the snapshot base, and whether the
  node still has full history.
- Auto-reconcile after bootstrap: both completion sites (wizard + Settings download
  dialog) mark a pending rescan; once the daemon is back up and the tip is known,
  detect the base and runtimeRescan() from it (or -rescan restart on a full node).
- Settings "Rescan Blockchain" now probes first: full-history nodes get the existing
  -rescan restart; bootstrapped/pruned nodes get a prompt pre-filled with the
  detected base height that runs the runtime rescan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:48:30 -05:00
7df00b0909 fix(ui): move Daemon binary into its own full-width row below Node & Security
The Daemon binary panel (info + the all-actions toolbar) lived inside the 60%-wide
NODE column, so the six-button toolbar clipped. Pull it out into a dedicated full-width
row rendered after the NODE + SECURITY columns reconcile, so it spans the whole card:
Installed | Bundled info side by side, status line, and the Install bundled | Refresh |
Test connection | Rescan | Delete blockchain | Repair wallet toolbar now have the full
container width and no longer clip. The NODE column keeps only the node/RPC info.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:41:33 -05:00
090e288c44 fix(ui): put all node action buttons on one toolbar row under Daemon binary
Move Test Connection / Rescan Blockchain / Delete Blockchain / Repair Wallet onto the
same line as and right after the Daemon binary buttons (Install bundled | Refresh), so
all node actions form a single toolbar row beneath the Daemon binary panel. Buttons are
auto-sized to pack onto one line; each disabled-state group (connection vs embedded
daemon vs bundle present) keeps its own guard. Removes the former two stacked button rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:18:57 -05:00
6544c10ac1 fix(ui): tidy Node maintenance layout + widen the Daemon binary panel
- Delete Blockchain and Repair Wallet now sit on one line, paired with the same
  uniform button width as Test Connection / Rescan Blockchain (a clean 2-column grid;
  all maintenance buttons share one rowBtnW sized across the four labels).
- The Daemon binary panel lays Installed and Bundled side by side across the node
  column (version + size·date | version + size) instead of stacked narrow lines, with
  the status line spanning underneath and Install bundled / Refresh paired below.
- Shorten the install button label to "Install bundled" so it fits the shared width;
  the tooltip still explains the full action. Date shown as YYYY-MM-DD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:21:06 -05:00
b2e104358d feat(fullnode): manage the daemon binary in Settings; stop auto-overwriting it
Previously the wallet re-extracted the bundled dragonxd on startup whenever the
installed binary's size differed from the bundle ("stale" overwrite), which could
replace a node a user had deliberately placed in dragonx/.

Now dragonx binaries (dragonxd/cli/tx) are auto-placed ONLY when missing — never
auto-overwritten on a size mismatch (needsParamsExtraction + extractEmbeddedResources).
Params/asmap keep their size-based refresh; a daemon dropped next to the wallet exe
still takes priority and is never touched.

Replacing the daemon is now an explicit action: Settings → "Daemon binary" reports the
installed binary's version (scanned from the file), size and modified date, compares it
to the version bundled in this build, and offers an "Install bundled daemon" button.
That stops the node, overwrites dragonxd/cli/tx with the bundled copies (waiting for the
process to release the file lock), and restarts — wallet/keys/chain data untouched.

Adds resources::{getInstalledDaemonInfo,getBundledDaemonInfo,reextractBundledDaemon}
(+ a version-string scanner) and App::reinstallBundledDaemon().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:06:59 -05:00
de70e68472 fix(fullnode): work around daemon note-selection fee-gap on shielded sends
dragonxd's z_sendmany picks notes to cover the recipient total (nTotalOut) but not
the miner fee, then rejects the build unless the selected notes cover amount+fee
(rpcwallet.cpp:5312 vs asyncrpcoperation_sendmany.cpp:278). So a shielded send whose
largest notes sum exactly to the amount fails with "Insufficient shielded funds,
have H, need H+fee" despite ample balance — e.g. sending exactly 2.0 from an address
whose biggest note is 2.0.

Since the failure is async (reported via the opid poll), detect it there: when a
shielded send fails with that message and the selected total H >= the requested
amount (selection covered the amount but stopped one note short of the fee — vs a
genuine shortfall where H < amount), re-issue the send once with a tiny self-output
(= fee) back to the from-address. That lifts the daemon's selection target past the
boundary so it grabs another note and can cover the fee; the recipient still receives
the exact amount. Retries are tracked so a second failure surfaces normally (no loop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:27:52 -05:00
0fe12d65df fix(ui): drop duplicate daemon log on the startup overlay
renderLoadingOverlay() rendered the daemon's recent output twice during startup:
a bare 4-line centered tail (section 2c, init/warmup only) and the styled
terminal-style box (section 4, always shown when the embedded daemon exists).
The bare tail was a strict subset of the box, so the same dragonxd output showed
stacked twice. Remove the redundant bare tail; keep the terminal box (which also
matches the shutdown screen's panel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:48:53 -05:00
37c8287a12 feat(fullnode): add "Repair Wallet" (-zapwallettxes=2) to Settings
When a note's stored record is corrupt or its tx isn't in the canonical chain,
z_sendmany fails to build a valid sapling spend proof even after a full -rescan,
because a plain rescan replays witnesses but keeps the existing tx/note records.
The zcashd repair for this is -zapwallettxes=2, which deletes all wallet tx/note
records and rebuilds them from the chain (keys/addresses preserved).

Adds a RepairWallet lifecycle operation that mirrors the existing -rescan plumbing
(one-shot zapOnNextStart flag on the embedded daemon; -zapwallettxes=2 implies and
supersedes -rescan), an App::repairWallet() that reuses the rescan status UI (so the
status bar + warmup-end completion detection apply), and a confirmed "Repair Wallet"
button + dialog in Settings → node maintenance (embedded daemon only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:42:10 -05:00
6ff80354df fix(fullnode): reliable rescan completion + self-explaining shielded send errors
Two related fixes for the post-bootstrap "send fails / rescan stuck at 99%" trap:

1) Rescan completion now keys off warmup-end. A -rescan runs entirely inside daemon
   warmup (every RPC returns -28 until it finishes), so warmup completing IS the rescan
   completing. The old detectors relied on getrescaninfo (which some daemons answer with
   "Method not found") or a "Done rescanning"/bench log line the daemon may never print,
   leaving the status bar stuck at 99% — so users killed the rescan before it finished.
   When warmup ends and a rescan was confirmed active, clear the rescan state, flip to
   100%, refresh history/balance, and toast completion.

2) z_sendmany failures that mean stale shielded note data (shielded-requirements-not-met,
   missing sapling anchor, invalid sapling spend proof, bad-txns-sapling-*) now append a
   plain-language hint telling the user to run a full rescan, instead of surfacing only the
   raw daemon string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 22:43:24 -05:00
f58d009703 fix(lite): show real block height when synced (was 0)
The lite status bar showed "blocks: 0" once fully synced. The backend's
`syncstatus` only includes synced_blocks/total_blocks WHILE actively scanning;
at rest it returns just {"syncing":"false"}, so the parsed syncedBlocks was 0
and became state.sync.blocks. On the synced refresh path, additionally query the
backend `height` command (wallet last-scanned height, a fast local read) and use
it as the synced height/tip, so the block count is correct at rest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:36:58 -05:00
0e2c786ebf fix(lite): welcome "Restore from seed" now prompts for the seed inline
On first run the lite welcome screen's "Restore from seed" button only showed a
hint toast and bounced the user to Settings, dismissing the welcome with no
wallet open — it never prompted for a seed. Add a real restore step to the
welcome wizard: a seed-phrase field + optional birthday height, which calls
beginRestoreWalletAsync() (same server failover as create/open), shows
"Restoring…" progress, then completes (wallet syncs) or surfaces the error to
retry. The seed buffer is wiped on success/Back and in finish().

(The Settings -> Lite -> Restore path already prompted for a seed; this fixes
the first-run welcome path.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 02:11:19 -05:00
d54c7f9e11 fix(lite): "Open data folder" points to the actual lite wallet dir
The lite SilentDragonXLite backend stores its wallet in its own directory
(dirs::data_dir()/silentdragonxlite — %APPDATA%\silentdragonxlite on Windows,
~/.silentdragonxlite on Linux, ~/Library/Application Support/silentdragonxlite on
macOS), NOT the full-node getDragonXDataDir() (…/Hush/DRAGONX). The newly added
lite "Open data folder" button opened the wrong (full-node) directory.

Add Platform::getLiteWalletDataDir() mirroring the backend's get_zcash_data_path
for the "main" chain, and point the lite button at it. The full-node button is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 02:01:40 -05:00
b3251e9244 feat(lite): mirror errors into the lite Console (copyable), not just toasts
Lite send/shield, unlock, and key-import failures were shown only as transient
toasts — impossible to copy. Route them through liteLog() so they also appear in
the lite Console (which has a Copy button), alongside the lifecycle/open/sync
errors the controller already logs:
- send/shield broadcast failures (App broadcast-result delivery)
- wallet unlock failure
- key import failure (controller; logs the error text only, never the key)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:59:34 -05:00
c40f4d5815 feat(settings): add an "Open data folder" button (wallet + block data)
Add an explicit button in Settings that opens the wallet/blockchain data
directory (getDragonXDataDir()) in the OS file manager via the existing
Platform::openFolder(). Placed in the full-node connection section (next to the
data-dir path, which was only a subtle clickable link) and in the lite section
(always available). i18n strings added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:14:52 -05:00
5547ab1cac feat(lite): add "Redownload blocks" (rescan from lite server) to Settings
Add a maintenance option for the lite wallet to re-download and re-scan every
block from the lite server — useful when balances or history look wrong.

- LiteWalletController::startRescan() runs the backend `rescan` command (which
  clears the wallet's synced block cache and re-syncs from its birthday) on a
  detached thread, reusing the existing sync progress/refresh machinery: it
  resets syncDone_ so refreshModel() shows progress again and refreshes data on
  completion. No-op if no wallet is open or a scan is already running.
- scanInProgress() exposes the initial-sync-or-rescan state.
- Settings (lite, open wallet) gains a "Redownload blocks" button behind a
  confirmation modal, disabled while a scan is running. i18n strings added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:05:27 -05:00
f0867084f3 fix(ui): stop spurious "failed to read" logo errors in the portable build
The header and coin logos load disk-first (for dev builds / theme drop-ins) and
fall back to the copies embedded in the exe. The portable single-file build has
no res/img/ folder beside it, so the disk read always failed and logged
"LoadTextureFromFile: failed to read ..." before the (successful) embedded
fallback. Guard each disk load with std::filesystem::exists() so the missing
file is skipped silently and we go straight to the embedded logo — no error
line, logos unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:58:19 -05:00
5167b52cbd feat(console): add an "App" toggle to show/hide [app] log lines
The console mixed RPC traces, daemon output, and the wallet's own "[app] ..."
log lines with no way to hide the latter. Add an "App" checkbox alongside the
existing Daemon/Errors/RPC toggles. Since [app] lines share COLOR_INFO with
other info text, the filter matches them by their "[app] " prefix rather than by
color. Default on; unit test + i18n added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:52:42 -05:00
e7d11f620a fix(network): populate the peer count on connect, not just on the Peers tab
On a fresh open the status-bar peer count stayed 0 until the Network tab was
opened. refreshData() — the one-shot refresh run on connect / warmup-complete /
unlock — only refreshed peers when the active tab was Peers, so on any other tab
nothing populated the count until a tab visit forced it. Refresh peers
unconditionally there so the count appears right after connecting; the periodic
20s Peers timer (all tabs) keeps it current after that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:45:50 -05:00
168cae9306 feat(explorer): fuzzy search — filter the block list by partial hash/height
Add a fuzzy mode to the explorer search: a non-numeric, non-full-hash query now
filters the list to cached blocks whose hash (or height text) contains the query
substring, live as you type. Backed by a new ExplorerBlockCache::searchBlocks()
(SQLite LIKE with escaped wildcards), memoized per query so it doesn't hit the DB
every frame. Exact queries still navigate precisely: a block height re-anchors
the list, and a full 64-char hash is resolved via RPC. Row clicks still open the
detail modal. Empty results show "No matching cached blocks".

Note: fuzzy matching covers cached (browsed/prefetched) blocks only — the daemon
has no partial-hash index — while exact height/hash lookups reach any block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:38:24 -05:00
317d9028a3 fix(explorer): search re-anchors the block list live instead of opening a modal
Typing in the explorer search ran an exact lookup that popped the block-detail
modal. It now updates the recent-blocks LIST as you type: a block height
re-anchors the list to that height (offline-friendly, no RPC), and a complete
64-char hash is resolved to its height and the list jumps there (a txid still
shows the inline tx view) — all without a modal. Clearing the box returns to the
recent (tip) blocks. Row clicks still open the detail modal for an explicit
full view. Removed the now-unused fetchBlockDetailByHash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:19:47 -05:00
09ab8d52c5 feat(explorer): live (debounced) search as the user types
The explorer search only ran on Enter or the Search button. Now it also fires
automatically ~350ms after the user stops typing, once the query is resolvable
— a block height (all digits) or a complete 64-char hash/txid. Partial hex is
ignored so it won't flash "invalid query" mid-type, per-keystroke RPC spam is
avoided via the debounce, and the same query isn't re-run. Enter/button still
work for an immediate search.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:07:01 -05:00
101c835c46 feat(settings): confirmation modals for rescan and restart-daemon
The Rescan blockchain and Restart daemon buttons fired immediately on click —
both are disruptive (long offline rescan / connection drop) and easy to hit by
accident. Route them through confirmation modals, matching the existing
delete-blockchain / clear-ztx confirmations: the button now sets a confirm flag
and an overlay dialog performs the action only on explicit confirm. New i18n
strings added with English defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:52:44 -05:00
c71c3c3378 fix(network): keep the status-bar peer count current on every tab
The peer count in the status bar is state_.peers.size(), refreshed only by
getpeerinfo — and the peers refresh interval was 0 (disabled) on every tab
except Peers. So the count never changed until you opened the Peers/Network
tab. Give peers a slow 20s cadence on all tabs (30s on Console); the Peers tab
keeps its fast 5s for the live list. During sync this is still overridden by
kSyncProfile (peers 0) so it can't contend with block download. Test updated to
the new intervals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:12:13 -05:00
2e115aef39 fix(daemon): replace the embedded daemon even when the old binary is locked
When a newer wallet build embeds a newer daemon, extractEmbeddedResources()
detects the size change and tries to overwrite dragonxd.exe in the daemon dir —
but the write is a plain truncating ofstream, which fails silently if the file
is locked. A running (or just-killed, handle-not-yet-released) daemon locks the
.exe on Windows (and Linux returns ETXTBSY), so the stale binary was kept and
the wallet kept launching the old daemon version.

If the direct write fails, move the stale binary aside to "<name>.old" (renaming
a running/locked executable is permitted on both Windows and Linux — the running
process keeps the moved copy) and write the fresh one at the original path. A
best-effort pass removes leftover .old files once the old process has exited.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:51:10 -05:00
574307f6ac build(setup): warn when a prebuilt daemon is older than the dragonx source
build.sh bundles whatever daemon binary already sits in prebuilt-binaries/, and
setup.sh only rebuilds a platform's daemon when its flag (--win/--mac) is passed
— so a daemon left over from an older source revision silently shipped in the
wallet (the Network tab showed dragonxd v1.0.1 while the source was v1.0.2).

Add a stale-daemon guard: compare the vX.Y.Z baked into each prebuilt daemon
against CLIENT_VERSION_* in the checked-out dragonx source. On the present/skip
and --check paths it now prints either "matches dragonx source" or a STALE
warning naming both versions and the rebuild command, plus a summary reminder at
the end of the daemon section. Version is read with grep -a (no binutils/strings
dependency); no-ops cleanly when the source or a binary is absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:14:11 -05:00
88851f5eea fix(history): unstick the unconfirmed-tx badge on confirmed shields
The History badge counts transactions with confirmations==0, iterating the raw
transaction list. Autoshield transactions have two legs sharing one txid, and
the send leg parsed from z_viewtransaction carries confirmations=0 even when the
transaction is long confirmed (the receive leg holds the real count). So the
badge counted those stale legs and stuck at a non-zero number (e.g. 7) with no
pending transactions.

Treat a txid with ANY confirmed leg as confirmed, and count UNIQUE unconfirmed
txids rather than legs — so confirmed multi-leg transactions don't inflate the
badge and genuinely pending ones still count once each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:06:46 -05:00
5796664b51 feat(history): add date and amount sorting to the History tab
Add a sort selector next to the type filter with four modes: Newest first
(default), Oldest first, Largest amount, Smallest amount. The mode folds into
the merged-list memoization cache key (so the list re-sorts only when the mode
changes) and the comparator branches on it, keeping txid as a deterministic
tiebreak. Changing the sort resets to page 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:55:24 -05:00
b71f8ae0a8 perf(history): toggle mining address without a full chain re-scan
Marking/unmarking a mining address triggered a long history reload: it called
invalidateShieldedHistoryScanProgress() + forced a transaction refresh, which
re-scans every z-address over many RPC cycles. But "mined" vs "receive" is a
pure function of the LOCAL mining-address set — the daemon knows nothing about
it — so a chain re-scan is pointless.

Relabel the affected rows in the in-memory history directly and persist just
those to the encrypted SQLite history cache. The History tab updates instantly
(its display cache rebuilds on the type change), with no daemon round-trip and
no reload. Only re-save when something actually changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:37:56 -05:00
555f541c84 fix(history): update tx labels immediately when a mining address is toggled
Unmarking (or marking) a mining address didn't change the history. The refresh
re-scanned the affected transaction as "receive", but appendMissingPreviousTransactions
carries over not-yet-rescanned prior transactions and dedupes by txid+TYPE — so
the stale "mined" copy was carried over right alongside the fresh "receive", and
the change never appeared.

Re-label state_.transactions in setMiningAddress() the moment the flag changes
(mined vs receive is just whether the receiving address is mining-flagged). The
History tab updates instantly, and the next refresh's carry-over now matches the
fresh scan instead of duplicating the old label. The reclassified list is also
persisted via the existing cache save.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:13:36 -05:00
2b70ee5cd8 feat(history): show "Loading older history (N%)" during the initial bulk load
History streams in over many refresh cycles (the incremental shielded scan
walks every z-address), so the first batch appears long before the list is
complete — with no indication more is still coming. The existing loading banner
deliberately goes quiet once any rows are on screen.

Track whether the first full shielded scan has finished
(initial_history_scan_complete_) and, until it has, surface a progress percentage
(fraction of z-addresses scanned) in transactionRefreshProgressText() — which the
History tab already renders as its pulsing loading indicator. Goes quiet once the
first scan completes; routine per-block re-scans don't re-trigger it. Reset on a
full history invalidation (rescan / session reset) so it shows again on reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:05:37 -05:00
d8c055c125 fix(history): count shielding txs in the Sent card to match the Sent filter
The Sent summary card showed 0 while selecting the Sent filter listed
transactions. The card counted only plain "send" rows and deliberately excluded
both legs of an autoshield pair as an "internal move", but the list shows the
merged "shield" row under the Sent filter. With only shielding transactions and
no plain sends, the card read 0 against a non-empty Sent list.

Count each shield pair toward the Sent card (with the shielded receive-leg
amount, which is what the merged row displays), so the card and the filter agree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:56:51 -05:00
2470675746 fix(history): keep shielded txs in date order (they were stuck at the top)
History looked unsorted because every merged "shield" row carried
confirmations=0, and the list sorts 0-conf (pending) transactions to the very
top. So long-confirmed shielding transactions floated above newer ones — and
when the type filter was switched off "All" they vanished (shield rows only
match the "Sent" filter), which read as "transactions disappear when sorting".

Root cause: the autoshield merge set the row's confirmations to
min(send, recv). Both legs are the SAME transaction (one real confirmation
count), but the send leg (parsed from z_viewtransaction) routinely arrives with
confirmations=0, so min() picked 0. Use max() to take the populated value.

Also give the sort a txid tiebreak so same-block transactions keep a stable
order instead of reshuffling every time a new block bumps confirmations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:52:44 -05:00
3a597482da fix(rescan): detect this daemon's completion ("rescan <N>ms"), unstick 99%
A rescan ran to completion but the status bar stayed at "Rescanning 99%"
forever. The daemon-output parser only treated "Done rescanning"/"Rescan
complete" as finished, but this daemon prints neither — it logs the rescan
benchmark timing line exactly when the scan ends:

    2026-... rescan             16760577ms

then resumes normal block processing. So the parser saw the last
"Still rescanning ... Progress=0.99" and never the finish, leaving it stuck.

- Recognise the " rescan <N>ms" bench line as completion (it ends in "ms",
  which the "Still rescanning"/"Rescanning..." progress lines never do).
- When the parser reads "Still rescanning" straight from the daemon log, mark
  rescan_confirmed_active_ — hard proof the scan is running that doesn't depend
  on catching a getrescaninfo warmup error, so the RPC completion path can also
  fire after the daemon leaves warmup. Clear it on finish.

The parser reads the daemon's debug.log via the controller (not RPC), so this
completes the rescan UI even if the RPC connection hasn't re-established yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:16:25 -05:00
b9b2d469d4 fix(rescan): stop the per-second getmininginfo error flood during rescan
While the daemon processes -rescan it sits in RPC warmup and rejects every call
with -28 ("Rescanning..."). The balance/tx/address refreshes already skip warmup
(state_.warming_up), but the 1-second mining poll didn't — so getmininginfo fired
the whole rescan and flooded the log with "getMiningInfo error: Rescanning..."
(~680 entries in one capture).

Gate refreshMiningInfo() on !state_.warming_up like the other refreshes. The
getrescaninfo progress poll still runs (it's how the warmup/rescan is tracked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:47:01 -05:00
25fef8ff4d fix(rescan): stop the instant false "rescan complete"; show live status
Clicking Settings → Rescan restarted the daemon with -rescan correctly, but the
progress poll fired "Blockchain rescan complete" the instant it was clicked,
then showed nothing for the entire (multi-hour) rescan — so it looked broken.

Cause: the very first getrescaninfo poll runs before the daemon has restarted
and hits the still-running pre-restart daemon, which answers rescanning=false.
The completion branch took that as "done", cleared the rescanning flag, and the
real rescan then ran invisibly. (Confirmed from a Windows debug-log capture: an
instant OK{"rescanning":false}, then ~6400 warmup errors over ~5h, all swallowed.)

Fixes:
- Gate completion on a new rescan_confirmed_active_ flag that's only set once we
  actually observe the rescan running, so a pre-restart rescanning=false can't be
  misread as completion.
- While the daemon is in -rescan RPC warmup it rejects every call with the live
  phase as the message ("Loading block index..." -> "Rescanning..."). Treat that
  as proof-of-progress: surface it as rescan_status and mark confirmed-active,
  instead of silently swallowing it. The status bar keeps its animated
  "Rescanning..." for the whole run, then reports complete when warmup ends.
- Read rescan_progress whether the daemon returns it as a string or a number
  (the get<std::string>() would have thrown on a numeric field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:34:44 -05:00
bc788d008e fix(send): poll z_getoperationstatus without the per-opid filter
The opid poll called z_getoperationstatus(["opid"]) to check a specific
operation, but this daemon rejects the filtered form with "JSON value is not a
number/array as expected" (a UniValue error returned as an RPC error). The
poll's catch swallowed it, so every completed send stayed stuck on "Waiting
for operation" forever — confirmed via a Windows debug-log capture showing the
throw on every 2s cycle. The no-arg form works (verified in the console).

Call z_getoperationstatus with no arguments (returns ALL operations) and filter
to the opids we're tracking in parseOperationStatusPoll(). The parser now skips
any operation whose id isn't in the requested set, so unrelated/old operations
can't fire a spurious error toast or pollute send state. The stale-opid logic
is unchanged (the no-arg form still reports in-progress ops, so a genuinely
pending opid is never misread as stale).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:31:50 -05:00
9ee8f9a43b fix(send): restart the fast-lane worker on reconnect so the opid poll runs
A completed send could spin forever on "Waiting for operation (N)". Root
cause: onDisconnected() stopped fast_worker_ but kept the unique_ptr, so
onConnected()'s `if (!fast_worker_)` guard never restarted it — after the
first reconnect (daemon warmup, restart, any RPC blip) the fast lane stayed
dead for the whole session.

The opid poll was the only fast_worker_ user that posted to it directly with
no fallback, so it alone broke: its post() landed on a stopped thread, the
result MainCb never ran, opid_poll_in_progress_ stuck true, and the poll never
fired again — leaving the operation (already "success" on the daemon, with a
txid) untracked.

Two fixes:
- onDisconnected() now reset()s fast_worker_ after stop(), so onConnected
  recreates and starts a fresh one (restores the fast lane for all its users,
  not just the poll).
- the opid poll now falls back to worker_ when the fast lane isn't running,
  matching every other fast_worker_ call site — defense in depth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:53:30 -05:00
bf91c4eb6c fix(send): pass the z_sendmany fee as a number, not a string
A prior change passed the user-selected fee to z_sendmany as a fixed-decimal
string (mirroring the recipient amount). But the daemon reads the fee param
with UniValue::get_real(), which rejects a string with "JSON value is not a
number as expected" — breaking every z_sendmany send (surfaced via the
address-to-address transfer feature).

Pass the raw double instead. get_real() parses it directly and accepts any
number notation (including the "5e-05" form of a small fee), so this is
correct for all fee values. The recipient "amount" stays a fixed-decimal
string on purpose — that field is parsed with ParseFixedPoint, which a
scientific-notation double would break.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:36:37 -05:00
4d78ca0d7d fix(keys): auto-width action buttons + content-fit key/address fields
Three layout fixes in the export-key modal, all symptoms of widths/heights
authored as raw pixels while text scales with the user's font setting:
- "Copy to Clipboard" no longer clips — the Show/Hide · Copy · QR buttons are
  auto-width (size 0) so they always fit their label;
- those buttons now share one font, so Show/Hide matches Copy (was a smaller
  toggle-button font);
- the read-only address and key fields are sized to the wrapped text instead
  of a fixed 60/80px, removing the empty space below their value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:38:20 -05:00
00ee61fe64 fix(dialogs): size the overlay glass card to its content
The overlay dialog's content child is AutoResizeY, but the glass card behind
it was drawn to a fixed viewport ratio — leaving a tall band of empty glass
below short dialogs (e.g. the key-export modal had a gap under its Close
button). Measure the rendered card height each frame and reuse it next frame
to draw the glass to the content; fall back to (and stay capped at) the ratio
so tall dialogs are unchanged and can't run off-screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:38:19 -05:00
3a4998f57c fix(keys): stop the key-export warning text clipping; scale field heights with font
From a screenshot at a non-default font scale: the red WARNING box clipped its text
("...balance, but" cut off) because it used a fixed 80px child height while the text inside
scales with the font. Make the warning box auto-size to its content (ImGuiChildFlags_AutoResizeY)
so it never clips at any scale, and scale the address / key read-only field heights by
Layout::dpiScale() for the same reason. Complements the card-width scaling fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:09:45 -05:00
d27017daeb fix(ui): scale overlay-dialog card width with the font/DPI setting (fixes modal overflow)
Every BeginOverlayDialog is passed a raw pixel card width (550, 620, …), but the fonts and
spacing inside scale with Layout::dpiScale() — which includes the user's font-size setting. At
any non-default scale the content outgrew the fixed card, so text overflowed the card edge and
elements misaligned. Scale the card width by dpiScale() (no-op at the default 1.0 scale) and clamp
it to the viewport so a large scale can't push it off-screen. Fixes all overlay dialogs at once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:47:53 -05:00
2182c060e6 feat(keys): improve the key-export modal — auto-clear copy, inline QR, cleaner actions
- Auto-clear: the Copy button now routes through App::copySecretToClipboard, so a copied
  private/viewing key is wiped from the clipboard after ~45s (same protection as the seed) with
  a "auto-clears" notice — instead of the raw SetClipboardText that left it indefinitely.
- QR: once the key is revealed, a Show/Hide QR toggle renders the key's QR inline (via the same
  GenerateQRTexture/RenderQRCode widget the Receive tab uses) for scanning into another wallet.
  The QR texture is cached, regenerated on key change, and freed on hide/close/dismiss; hiding the
  key also hides its QR.
- Actions row tightened to Show/Hide · Copy · QR, and the key + QR texture are now cleared on any
  dismissal (Close button, scrim click, Esc), not just the Close button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:59:06 -05:00
4ee830c5dd fix(balance): disambiguate address drag — edge to reorder, centre to transfer
The address list supported two drag gestures that collided: dragging a row onto another
transferred funds, dragging into a gap reordered. Since rows are contiguous, a reorder-drag was
almost always over another row, so it triggered a fund transfer instead of reordering.

Disambiguate by WHERE on the target row the drag is released (user's suggestion): the top/bottom
~30% edge bands = reorder (an insertion line is shown), the centre = transfer (the row highlights).
A zero-balance row or an off-row drop always reorders. Tooltip and i18n hint updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:53:54 -05:00
b6567ee196 refactor(balance): extract shared rendering components into balance_components.{h,cpp} (audit #10)
First slice of decomposing balance_tab.cpp (3449 lines). The five rendering helpers used by every
balance layout — UpdateBalanceLerp, RenderCompactHero, RenderSharedAddressList (599 lines, the
drag-reorderable address list), RenderSharedRecentTx, RenderSyncBar — are moved verbatim into
balance_components.cpp. balance_tab.cpp is now 2680 lines.

Clean extraction: the helpers' interactive statics (drag/copy/hide/show) are function-local and
move WITH them; the only file-scope state they share is the balance-lerp animation values
(s_dispTotal/Shielded/Transparent/Unconfirmed) and s_generating_z_address, now non-static and
declared `extern` in balance_components.h (defined once in balance_tab.cpp, so both TUs share the
same objects). RenderCompactHero's default arg moved to the header declaration. The layouts (still
in balance_tab.cpp) call the helpers via the new header.

Verified: full-node + Windows + lite build (links cleanly -> extern state resolves), tests,
hygiene. This touches every layout's address list / recent-tx / hero / sync bar, so needs a
hands-on pass across the balance layouts before the next slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:50:44 -05:00
1a8d6fd30f refactor(mining): extract the Mode toggle into mining_mode_toggle.{h,cpp} (audit #10, slice 4)
Final slice of decomposing mining_tab.cpp. The ~529-line "Mode toggle" section (SOLO | POOL
segmented control + pool URL/worker inputs) is moved verbatim into RenderMiningModeToggle().
mining_tab.cpp is now 311 lines (was 2628) — just the tab dispatch, thread-sync glue, benchmark
advance, section-budget setup, and four card calls.

State the toggle mutates is passed BY REFERENCE so behaviour is identical: the pool-mode flag,
the settings-dirty flag, and the pool URL / worker char[256] buffers (the text inputs write into
them) — passed as char(&)[256] references and named with their original identifiers so the body
stays byte-identical.

Verified: full-node + Windows + lite build, tests, hygiene. Audit #10 complete: the 2628-line
monolith is now five focused files (earnings, stats, controls, mode-toggle + the 311-line shell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:42:16 -05:00
9389859ee9 fix(ui): stop overlay dialogs flashing open-then-closed (BeginOverlayDialog)
BeginOverlayDialog dismisses on a click outside the card via IsMouseClicked (mouse-down). When
the dialog is opened by a button that fires on the same frame (e.g. the mining tab's
"Update miner…" button), that opening click is still registered as an outside-click, so the
dialog opens and instantly closes — it just "flashes". Skip the outside-click dismissal on the
frame the scrim window first appears (ImGui::IsWindowAppearing()); normal outside-click closing
is unaffected on every subsequent frame. Fixes all overlay dialogs, not just the xmrig updater.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:27:18 -05:00
e63aba6959 fix(build): embed xmrig in the Windows exe (extract it from the published zip)
The wallet is meant to ship xmrig embedded (HAS_EMBEDDED_XMRIG -> getXmrigPath() extracts it
at runtime), but build.sh only embedded a flat prebuilt-binaries/xmrig-hac/xmrig.exe — while
the published DRG-XMRig archive ships the binary inside a versioned subdir
(drg-xmrig-6.25.3-win-x64/xmrig.exe). So xmrig.exe was never present, HAS_EMBEDDED_XMRIG stayed
undefined, and the Windows wallet ran with no miner: "xmrig binary not found", pool mining and
the thread benchmark both fail.

build.sh now extracts xmrig.exe (flattened) from the matching win-x64 zip when a raw binary
isn't already staged, so the existing embed step fires. (Checks the extracted file rather than
unzip's exit code, which is non-zero when a glob matches nothing.) Verified: --win-release now
logs "Extracted xmrig.exe", stages it (6.7M), and defines HAS_EMBEDDED_XMRIG=1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:23:53 -05:00
fa240e7b99 refactor(mining): extract the Controls/CPU-grid card into mining_controls.{h,cpp} (audit #10, slice 3)
Third and largest slice of decomposing mining_tab.cpp. The ~843-line "Controls" card (CPU-core
grid + drag-to-select, mining start/stop button, benchmark + miner-update controls) is moved
verbatim into RenderMiningControls(). mining_tab.cpp is now 839 lines (was 2628 originally).

The most coupled section, so mutated state is passed BY REFERENCE — the benchmark
(ThreadBenchmark&), selected thread count (int&), and drag state (bool&/int&) — with local
reference aliases so the body stays byte-identical and interactions (drag, benchmark, start/stop)
behave exactly as before. Read-only context is passed by value/const; the compiler verified
const-correctness. Local statics inside the block moved with it.

Verified: full-node + Windows + lite build, tests, hygiene, no startup crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:57:09 -05:00
e21f7bf8aa refactor(mining): extract the Hashrate+Stats card into mining_stats.{h,cpp} (audit #10, slice 2)
Second slice of decomposing mining_tab.cpp. The ~313-line "Hashrate + Stats" card (stat values +
hashrate chart / live-log view) is moved verbatim into RenderMiningStats(); mining_tab.cpp is now
1680 lines (was 1992 after slice 1, 2628 originally). Body byte-identical apart from a s_pool_mode
alias; the chart/log toggle statics (s_show_pool_log/s_show_solo_log) moved with the card, and the
log buffer was already a function-local static. No App dependency in this section.

Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on
visual check before the next slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:03:51 -05:00
47f228fa47 refactor(mining): extract the Earnings card into mining_earnings.{h,cpp} (audit #10, slice 1)
First incremental slice of decomposing the 2628-line mining_tab.cpp monolith (one giant
RenderMiningTabContent function). The ~636-line "Earnings" card section is moved verbatim into
RenderMiningEarnings(); mining_tab.cpp is now 1992 lines and calls it with the immediate-mode
layout context as parameters (draw list, fonts, scale/spacing, glass spec, pool-mode flag).

Behavior-preserving by construction: the body is byte-identical (the only additions are a
`const bool s_pool_mode = poolMode` alias and a local scratch `buf` so the moved code keeps its
original identifiers). The earnings-filter static moved with the card it belongs to. The
compiler surfaced every enclosing dependency, which became explicit parameters.

Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on
visual check of the Earnings card before extracting the next section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:47:34 -05:00
9f82bba260 perf(node): skip the mining-info poll during sync too
The sync throttle (kSyncProfile) covers the core/transactions/addresses/peers timers, but
getmininginfo runs off the separate 1s Fast timer and so still polled ~every 5s during
sync — another cs_main contender slowing block connection. Skip it while syncing unless the
user is on the Mining tab or actively mining (where live stats are wanted). Completes the
"no RPC contention during sync beyond the 10s progress poll" goal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:21:44 -05:00
323cb341f1 perf(node): throttle RPC polling during sync so block download isn't slowed
The full-node wallet polled the daemon at the per-tab cadence regardless of sync state.
On the Peers/Network tab that meant getpeerinfo every 5s + core every 5s + a full
transaction scan on every new block — and blocks arrive fast during sync. Each of those
calls takes the daemon's cs_main lock, the same lock block connection needs, so the node
synced noticeably slower than on the lightweight Console tab (core 10s, no peer polling).

Make the refresh cadence sync-aware:
- RefreshScheduler::kSyncProfile {core 10s, transactions/addresses/peers disabled} is applied
  to ALL tabs while state_.sync.syncing, and reverts to the per-tab profile when sync ends.
  applyRefreshPolicy() picks the profile; update() re-applies it on the syncing<->synced
  transition. This suppresses getpeerinfo and the per-block tx scan during sync (that data is
  incomplete mid-sync anyway) — every tab now syncs as fast as Console.
- collectCoreRefreshResult(rpc, includeBalance): skip z_gettotalbalance (wallet lock + cs_main)
  while syncing; only getblockchaininfo runs, which is also what drives sync-progress detection.
  applyCoreRefreshResult already leaves the balance untouched when balanceOk is false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:06:05 -05:00
ee6cac41c4 fix(robustness): guard malformed RPC error JSON + send single-flight (audit #7-8)
- rpc_client::callRaw: a daemon error object is no longer assumed to carry a string
  "message" — a malformed error now yields a clean "RPC error: <dump>" instead of throwing
  a json type-exception from .get<std::string>().
- sendTransaction (full-node): add a single-flight guard so a rapid double-click can't issue
  two z_sendmany before the first returns its opid. The lite path already guarded this; the
  send form guards it in the UI, but the controller entry point now does too.

(#9 from the audit was mostly false positives on verification — all popen sites already
null-check and the xmrig download FILE* path has no throwing calls. The payment-URI
checksum idea was dropped: the send flow already checksum-validates the recipient before
broadcasting, and tightening the parser would reject the placeholder addresses the existing
test relies on; added a comment noting this is format-only by design.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:05:43 -05:00
094771af81 security: wipe RPC creds, lock down generated conf, auto-clear secret clipboard (audit #4-6)
- rpc_client: wipe the plaintext "user:password" temporary with sodium_memzero after
  base64-encoding it into the auth header (std::string doesn't zero its buffer on
  destruction).
- connection: the auto-generated DRAGONX.conf holds rpcuser/rpcpassword in plaintext but
  was written with the default umask (often world-readable 0644). Restrict it to owner
  read/write after creation so another local user can't read the credentials.
- app: copying a seed phrase / private key to the clipboard now arms an auto-clear —
  App::copySecretToClipboard() copies the secret and, after 45s, wipes the clipboard IF it
  still holds that secret (compared via a stored hash, never the plaintext). Wired into the
  lite first-run wizard's seed Copy and the Settings export-secret Copy, with a
  "clipboard auto-clears in 45s" notice. pumpSecretClipboardClear() runs each frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:00:45 -05:00
e40962cdf2 perf(ui): dedupe time-ago + allocation-free case-insensitive filter (audit #1-3)
Per-frame hot paths in the immediate-mode UI were allocating needlessly:

- Address filtering in the balance tab rebuilt a std::string filter per address AND
  containsIgnoreCase() lower-cased two fresh copies per call — ~6×N allocations/frame
  on large wallets. New util::containsIgnoreCase(string_view, string_view) is
  allocation-free, and the filter is now built once outside the loop.
- Four duplicated "time ago" implementations (balance_tab_helpers, balance_recent_tx,
  send_tab, transactions_tab) are consolidated into util::formatTimeAgo (localized long
  form) + util::formatTimeAgoShort (compact "5s ago"), preserving each call site's exact
  display style. Both use snprintf, no per-row string concatenation.
- The send-tab address-suggestion scan (a walk over the whole tx list) is memoized on the
  typed text + tx count, so it no longer recomputes every frame while the user pauses.

New src/util/text_format.{h,cpp}; the two existing containsIgnoreCase/timeAgo definitions
now delegate to it. Added to both the app and test targets (test target also gains i18n.cpp,
which text_format's localized path needs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:54:27 -05:00
63b3a04716 fix(history): let the shielded scan complete + unstick send-progress on many-z-addr wallets
Two issues shared one root cause: the shielded-receive scan marked each z-address "scanned
at the EXACT current tip," but a new block (~36s on DRGX) advances the tip and invalidates
every prior per-address scan. A wallet with more z-addresses than one refresh cycle can
scan therefore never reached "all scanned at tip" — so shieldedScanComplete stayed false
and transactions_dirty_ stayed true forever, which (a) kept the history-refresh banner lit
and the full rescan churning every cycle, and (b) blocked maybeFinishTransactionSendProgress
(it waited on transactions_dirty_), leaving the send-progress indicator stuck on.

Fix 1 — completion tolerance. Add TransactionRefreshSnapshot::shieldedScanTipTolerance: an
address counts as fresh if its last scan is within N blocks of the tip (0 = old strict
behavior, so existing tests are unchanged). The app scales N with the z-address count
(2 + count/96, capped at 50), so a multi-block pass can COMPLETE before its earliest scan
goes stale. This also throttles full rescans to ~N blocks instead of every block —
transactions_dirty_ clears, the banner stops, and CPU/RPC churn drops. Already-fresh
addresses are skipped, so the per-block cost falls back to just the (cheap) transparent
listtransactions.

Fix 2 — send-progress gate. maybeFinishTransactionSendProgress() no longer waits on the
transaction history scan (transactions_dirty_ / Transactions job): the sent tx is already
shown via the optimistic pending insert, and the spend is reflected once the balance
refresh lands, so it now finishes on the address/balance signal alone.

Test: a tolerant snapshot skips recently-scanned addresses (shieldedAddressesScanned == 0,
shieldedScanComplete) while a strict one re-scans them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:55:30 -05:00
cf77c6cbe0 fix(history): stop the "refreshing wallet history" banner from never clearing
The banner was driven by transactions_dirty_, which refreshTransactionData() sets to
!shieldedScanComplete. The shielded-receive scan marks each z-address "scanned at tip,"
but every new block (~36s on DRGX) advances the tip and invalidates all prior per-address
scans, so for a wallet with more z-addresses than the per-cycle budget (8 on History) the
scan can never catch the tip — shieldedScanComplete stays false, transactions_dirty_ stays
true, and the banner stayed lit indefinitely.

Decouple the user-facing banner from that perpetual background scan:
- A just-sent transaction being enriched still surfaces (the user is waiting on it).
- Once any history is displayed, stay quiet for routine background refreshes — new receives
  still appear live as they're scanned.
- The loading banner now only shows during the genuine INITIAL load (nothing displayed yet)
  and send enrichment.

This is a UI-visibility fix; the underlying per-block full shielded rescan (and the related
send-progress flag that maybeFinishTransactionSendProgress gates on transactions_dirty_) are
separate follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:41:03 -05:00
560f2bcf91 perf(history): memoize the transaction display list instead of rebuilding it every frame
The History tab rebuilt its entire display list on every render frame: indexing all
transactions by txid, merging autoshield send+receive pairs into "shield" rows, and
std::sort-ing the result — O(N log N) plus several heap allocations at ~60fps, only to
show one 50-row page. The data is already sorted newest-first by the refresh service,
so the per-frame sort was redundant on top.

Memoize the merged+sorted list, rebuilding only when the underlying transactions
actually change. The cache key is a cheap, allocation-free FNV-1a fingerprint over the
display-relevant fields (count, last update time, and each tx's confirmations /
timestamp / type+address first char) — a new block bumps every confirmation so the key
changes and we rebuild; otherwise (the common read/scroll case) the cache is reused.
Filtering, search, and pagination still run per-frame over the cached list (cheap linear
scans that depend on interactive state).

Also document that App::shouldRefreshTransactions() is block-height/dirty driven (not
interval-gated) — the Transactions timer only paces the check; the recent-poll handles
between-block mempool/unconfirmed deltas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:24:58 -05:00
255d9399fa fix(build): stop disabling the embedded daemon on full-node builds (1.3.0 regression)
The 1.3.0 lite-capability work gated isUsingEmbeddedDaemon() on the compile flag
DRAGONX_ENABLE_EMBEDDED_DAEMON (in 1.2.0 it was hardcoded true, so the daemon
always launched). The lite branch in CMakeLists set that flag OFF with
`CACHE BOOL ... FORCE`, which POISONS the build dir's cache: a later full-node
reconfigure of the same dir keeps the forced-OFF value (the full-node branch
never re-asserts it), so embeddedDaemonAvailable=false and the wallet extracts
dragonxd but never starts it — exactly the reported "unpacks dragonxd.exe but
does not start the daemon, manual start works."

Note the two gates are independent: the binary is EMBEDDED/extracted via build.sh
(HAS_EMBEDDED_DAEMON), while LAUNCHING is gated by DRAGONX_ENABLE_EMBEDDED_DAEMON
— so they diverged (extract yes, launch no).

The forced cache write was also pointless: makeWalletCapabilities() already
forces the embedded-daemon capability off for any lite build via
`fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
regardless of the flag.

Fix:
- CMakeLists: remove the FORCE cache poisoning (the root cause).
- build.sh: set DRAGONX_ENABLE_EMBEDDED_DAEMON explicitly per variant (ON for
  full-node, OFF for lite), mirroring the existing DRAGONX_BUILD_LITE handling,
  so an already-poisoned build dir is HEALED on the next build rather than
  silently keeping the stale OFF.

Verified: a poisoned Windows cache (=0) flips to =1 on reconfigure; full-node
builds define =1, lite =0; tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:50:34 -05:00
1fb6dc44d9 fix(node): fail the localhost connect probe fast (8s, not 30s)
The connection probe (getinfo) used a 30s request timeout, so when something on
the local RPC port accepts TCP but never answers — a daemon still loading the
block index, or a wedged/foreign occupant — every attempt blocked the full 30s
before the wallet could retry or update its status. That is the "stuck, timing
out every 30s" behaviour users hit.

A healthy local daemon answers getinfo in milliseconds, and a warming one
returns -28 just as fast, so a long hang on localhost only ever means trouble.
Probe localhost with an 8s timeout (remote/TLS keeps the 30s budget). The
per-call override restores the persistent 30s afterwards, so normal RPC calls
that legitimately take longer are unaffected — only the probe fails faster, so
the wallet retries promptly and reflects "initializing" / recovery within
seconds of the daemon becoming ready.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:25:22 -05:00
2ba8a799ff fix(node): surface why an embedded daemon dies right after spawning
The daemon can spawn successfully (CreateProcess OK) and then exit immediately —
a missing runtime DLL, wrong architecture, corrupt binary, datadir lock, etc.
EmbeddedDaemon's crash monitor already builds a detailed reason for this
(translated Windows exit code, e.g. "STATUS_DLL_NOT_FOUND — required DLL not
found", plus the launch command and a debug.log tail) and stores it in
lastError(), but it runs on a background thread and was never shown. The result
was the exact symptom users reported: the wallet unpacks dragonxd.exe, looks
"stuck connecting", and the node silently dies-and-respawns until it gives up —
with no visible reason (manually starting dragonxd works, so the wallet then
connects to it).

tryConnect now watches the daemon's crash count (on the main thread, where it
already logs daemon state) and surfaces each NEW crash's lastError() once, as a
sticky error notification, with a concise "Couldn't start dragonxd" status. The
counter resets on a successful connect (alongside the daemon's own crash-count
reset), so a later crash re-notifies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:14:25 -05:00
41b380449e fix(node): don't get stranded when the daemon can't start on startup
Two failure modes left the wallet stuck on a silent "connecting / Starting
dragonxd…" spinner with no path forward:

1. Stale external-daemon latch. EmbeddedDaemon::start() sets
   external_daemon_detected_ whenever the RPC port was busy at a prior attempt
   and never re-checks it, so tryConnect's no-config branch trusted that latch
   and waited forever for a config the phantom would never write — even after a
   stale/half-dead process freed the port. Now the port is re-evaluated LIVE
   (EmbeddedDaemon::isRpcPortInUse()) each attempt: if it's genuinely busy we
   keep waiting (and, after a bounded ~20s with no config, warn that whatever
   owns the port isn't a usable DragonX node and how to fix it); if it's free we
   fall through and start our own daemon.

2. Silent start failure. When startEmbeddedDaemon() failed (binary not found,
   Sapling params missing, spawn failure) the status stayed on "Starting
   dragonxd…" with the real reason only in a VERBOSE log. Now the reason
   (daemon_controller_->lastError()) is surfaced once as a sticky error
   notification, with a short "Couldn't start dragonxd" status.

Both counters reset on a successful connect so the messages re-arm for the next
disconnect. Lite is unaffected (tryConnect returns early for lite builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:55:41 -05:00
4a65dce947 feat(lite): make the Console tab interactive (run backend commands)
The lite backend's litelib_execute() is the same command interface as
silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync,
syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real
interactive console — like the full-node RPC console — instead of a read-only
diagnostics log.

Controller: add an async arbitrary-command runner mirroring the broadcast
pattern — runConsoleCommand() splits "<command> [args]" (the first token is the
command, the remainder is passed as the single arg string litelib_execute
expects, since it does NOT whitespace-split), runs the bridge call on a detached
thread that captures the shared bridge (never `this`), and delivers the result
to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed
through LiteDiagnostics (seed/export can return secrets).

Console tab: a command input (Enter to run, Up/Down history via the shared
console_input_model helpers) over a unified scroll buffer that interleaves the
automatic diagnostics events with user command I/O, colour-coded, with the live
status header preserved. The input is disabled while a command runs.

Two backend footguns are intercepted at the UI layer before forwarding:
`clear` (the backend command WIPES wallet tx history — re-bound to clearing the
view, what the user expects) and `quit`/`exit` (would only save; the embedded
backend must stay running with the app).

Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs"
-> exercises the command/arg split; blank line rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:32:03 -05:00
c8183241c3 feat(node): show a live daemon console tail on the initializing overlay
The full-node Console tab already streams the daemon's output, but during
startup the user is held on the loading overlay (wallet-data tabs are blocked),
so they can't watch progress without navigating away. Surface the last few
console lines the node printed (UpdateTip height=…, "Verifying blocks…", etc.)
directly under the status/description on the overlay while initializing or
warming up, so progress is visible where the user is already looking.

Full-node only (guarded on daemon_controller_); each line is trimmed and
ellipsis-truncated to one row. Reuses DaemonController::recentLines().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:37:48 -05:00
0bf80d2757 feat(node): show "node initializing" feedback when the daemon isn't answering yet
When the full-node connect probe (getinfo) times out, the daemon is reachable
at the TCP level but busy initializing (loading the block index, verifying,
activating best chain, …) and won't answer RPC. The wallet only recognized the
JSON-RPC -28 warmup reply, so a raw socket timeout fell through to a bare,
alarming "Connection failed" retry with no indication of what the user was
waiting on.

Add a daemon-initializing UI state that drives the existing loading overlay:

  - WalletState::daemon_initializing — daemon up/launching but not serving yet
    (distinct from warming_up, which needs a -28 reply).
  - App::applyDaemonInitStatus() infers the current phase from the daemon's own
    console output (scanning recent lines for Loading/Verifying/Activating/
    Rescanning/Rewinding/Pruning) and the latest block height, producing a
    friendly title + description, e.g. "Processing blocks… (Block 123456)".
  - The connect loop calls it from the daemon-starting and external-detected
    branches: a timeout -> "reachable but initializing", a connect refusal ->
    "launching, waiting to come online". Cleared on a real connect.
  - The loading overlay now shows the description for daemon_initializing too,
    and the status-bar amber indicator covers it (so Peers/Console tabs without
    the overlay still explain the wait).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:32:35 -05:00
6f9123f651 feat(lite): async + failover for Settings-page create/open/restore
The Settings page drove the controller's synchronous createWallet/openWallet/
restoreWallet, which blocks the UI thread on the (often flaky) lightwalletd and
gives up after the first server. Add a generic async lifecycle path that mirrors
the async-open failover but carries the full request (passphrase, restore seed/
birthday/account/overwrite):

  - beginCreateWalletAsync / beginOpenWalletAsync / beginRestoreWalletAsync run
    on a detached thread that builds its OWN local LiteWalletLifecycleService
    from captured value copies + the shared bridge (never `this`, so it can
    safely outlive the controller). Each request type's serverUrl override field
    feeds the failover: try the preferred server, then every other usable
    default; stop on the first ready wallet or a structural block; keep the
    preferred server's error on total failure. The request's secrets are wiped
    once the attempt finishes.
  - pumpLifecycleResult() finalizes on the main thread (flip walletOpen, persist,
    start sync) and caches the result for the UI; wired into App::update next to
    pumpAsyncOpen(). beginAsyncLifecycle() now also yields to an in-flight
    lifecycle request so the auto-open loop can't race it on the same bridge.
  - settings_page kicks off the async op, disables the button while in flight,
    and polls the cached result each frame for the status/summary.

Tests: testLiteWalletControllerAsyncLifecycleFailover covers async create (with
passphrase) and restore failing over preferred->fallback, plus all-servers-down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:42:47 -05:00
320c659689 feat(lite): async wallet creation with server failover
Mirror the async-open path for wallet creation. beginOpenExisting() and
beginCreateWallet() now both delegate to beginAsyncLifecycle(bool create),
which runs the backend init on a detached thread and walks the failover
server list (preferred server first, then all usable defaults), reporting
the preferred server's error on total failure. The first-run wizard's
Create button drives this through a non-blocking "creating" poll state so
the UI no longer freezes while the backend contacts a (possibly flaky)
lightwalletd. The created seed response is securely wiped immediately and
read back via exportSeed for the reveal/verify steps.

Safe because litelib_initialize_new contacts the server before writing any
wallet file and LightClient::new errors if a wallet already exists, so a
failed candidate leaves no partial state.

Tests: fake backend's initialize_new now honors the dead/warmup server
substrings; testLiteWalletControllerOpenFailover gains a create-failover
case (preferred dead, fallback good -> walletOpen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:29:59 -05:00
6ff1fda870 feat(lite): harden seed restore + backup UX in Settings
- Restore: live "N / 24 words" count, a one-line birthday explanation, and a guard
  that rejects a restore unless all 24 words are entered (the secret scrubber still
  wipes the input on the early return).
- Backup: "Show seed" now also shows the birthday (needed to restore quickly) with a
  "back this up too" note, a stronger "only way to restore" warning, and a "Save to
  file" button that writes the seed + birthday to an owner-only (0600) file in the
  config dir via the atomic-write helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:12:56 -05:00
8c51b092f8 feat(lite): guided seed backup on wallet creation
Creating a wallet was one-click and silent — it never showed the seed, relying on
the user to later find Settings -> Show seed, which is an easy-to-miss fund-loss
risk. Replace the first-run prompt with a 3-step guided flow mirroring the upstream
SilentDragonXLite wizard:

  1. Welcome (Create / Restore / Later) — unchanged entry.
  2. Reveal: after createWallet, read the seed back via exportSeed and show all 24
     words (numbered grid) + the birthday, with a strong "only way to restore"
     warning, plus Copy. ("Skip" leaves the wallet created, seed backable later.)
  3. Verify: tap the words in order (shuffled chips) to confirm the backup before
     finishing; out-of-order taps are rejected with a hint.

The seed is held only for the wizard and securely wiped (sodium_memzero) on finish.
Builds clean for full-node, lite, and Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:12:56 -05:00
788d71a549 fix(lite): report the preferred server's error on a failed open
The failover overwrote outcome.error on every attempt, so a total open failure
reported whichever (often broken) fallback was tried last — e.g. lite5's
"CertNotValidForName" — instead of what the user's preferred server actually did.
Keep the first (preferred) server's error as the summary so "Open failed: …" names
the actionable reason; the per-server attempts are still in the Console log, and
the warmup flag is still set if any server was warming up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:26:06 -05:00
3d4b013b0c fix(lite): fast retry when a server is only warming up (-28)
When the preferred lightwalletd server is reachable but warming up (JSON-RPC -28
/ "Activating best chain"), the failover treated it like a dead server and fell
through to the others, so the wallet didn't open until the next 20s retry — even
though the healthy server was ready within seconds.

Detect the warmup error during failover, flag it on the open outcome
(lastOpenWasWarmup()), and have the App retry on a short ~4s interval in that case
instead of 20s, so the wallet opens promptly once warmup clears. A unit test
covers a warming-preferred + dead-fallback open setting the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:26:14 -05:00
dc07491abb fix(lite): name the cause when the backend isn't linked
A lite build compiled without the SDXL backend (DRAGONX_ENABLE_LITE_BACKEND off,
i.e. built with --lite instead of --lite-backend) leaves the controller null, so
the wallet never opens and the UI shows a silent "disconnected" state. The Console
status now states the cause and the fix directly ("Lite backend not linked in this
build (rebuild with --lite-backend)") instead of a vague "unavailable".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 20:11:16 -05:00
7e568e4bf1 fix(lite): always-populated Console (live status) + single-instance log
The Console could look empty if the wallet produced few events. Make it useful
in every state and remove a cross-platform footgun:

- Add a live status header read straight from the controller (connected /
  connecting / disconnected, sync %, and the last open error) — independent of the
  diagnostics event log, so the Console always shows the current connection +
  wallet-open state even when the log is sparse.
- Move LiteDiagnostics::instance() into a single .cpp so there is exactly one
  instance across the binary, rather than relying on the linker folding an
  inline-function static across translation units (a known fragility, especially
  on mingw/Windows — the most likely cause of a stuck-empty event log there).

Verified the writer and reader share one instance on Linux; builds clean for
full-node, lite, and Windows cross-compile; tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:28:28 -05:00
85a1080b52 feat(lite): Console tab with connection + open/create diagnostics
The lite variant had no visibility into why a wallet failed to open — just a
"disconnected" spinner. Add a lite-only Console tab (full-node keeps its RPC
console) that shows a live diagnostic log.

- LiteDiagnostics: a small thread-safe, bounded ring buffer (header-only). The
  controller writes to it from its background threads: each failover server
  attempt and result, wallet open/create/restore outcomes, sync start, and
  blocked-open reasons. The App logs controller (re)builds with the preferred
  server.
- lite_console_tab: a terminal-styled, read-only view of the log (newest at the
  bottom, error/success lines coloured) with Clear / Copy / Auto-scroll. Reachable
  even when the wallet is locked (it's diagnostics, no secrets). Registered as
  NavPage::LiteConsole, gated lite-only via WalletUiSurface::LiteConsole.

A unit test drives an open-with-failover and asserts the log records the
connection attempt and the successful open. Built clean for full-node, lite, and
Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:46:25 -05:00
dbeae3ac98 feat(lite): async wallet open with server failover
Opening an existing lite wallet ran synchronously on the UI thread and used a
single server, so a dead/unreachable lightwalletd server froze startup for the
connect timeout and then stranded the wallet ("disconnected" spinner) — and the
DragonX lite servers are flaky (often several down at once).

Add LiteWalletController::beginOpenExisting() / pumpAsyncOpen(): the open runs on
a background thread (mirroring the sync/broadcast shared-lifetime pattern — it
captures only shared_ptrs + value copies, never `this`), trying the preferred
server first and then every other usable default until one succeeds. The main
thread finalizes the result (flips walletOpen, starts sync) or records the reason.
The rollout gate is still checked up-front on the main thread.

App: auto-open now calls beginOpenExisting() and pumps it each tick, retrying on
a 20s interval so a transient outage self-heals once a server returns; a failed
open surfaces its reason (notification + Network tab) instead of a silent spinner.

Tested: a fake bridge that fails specific servers exercises both
preferred-dead -> fallback-opens and all-dead -> fails-with-reason. Built clean
for full-node, lite, and Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:53:24 -05:00
9ff5508989 fix(lite): re-open the wallet after a controller rebuild (server-switch recovery)
The wallet auto-open is a one-shot (lite_autoopen_done_), but rebuildLiteWallet()
creates a fresh, closed controller — so switching the lite server from the Network
tab (rebuildLiteWallet force=true), or any later rebuild, left the wallet
permanently closed ("disconnected" spinner) because auto-open never fired again.

Re-arm the one-shot (and clear the surfaced open-error) in rebuildLiteWallet so
the next update() tick reopens the existing wallet against the new server. This is
the recovery path when the configured lightwalletd server is unreachable: the
Network tab surfaces the failure reason, the user picks a reachable server, and
the wallet reopens. Also makes the Network tab's apply-immediately server switch
actually take effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:34:37 -05:00
79e5adcbd3 fix(lite): give the lite variant its own config folder (ObsidianDragonLite)
Both variants hardcoded "ObsidianDragon" as the per-user config folder
(settings.json, themes, the lite_rollout cache), so the lite app and the
full-node app shared one settings.json. That cross-variant pollution can leave
the lite server selection in a bad state — and since openWallet() contacts the
selected lightwalletd server, a wrong/empty server URL there makes an existing
wallet fail to open (a silent "disconnected" spinner).

Use DRAGONX_APP_NAME (already "ObsidianDragon" / "ObsidianDragonLite" per variant)
for the config-dir name in Settings::getDefaultPath, Platform::getConfigDir and
getObsidianDragonDir (and the theme-setup exe-name probe). Full-node is unchanged;
lite now reads/writes %APPDATA%\ObsidianDragonLite (and ~/.config/ObsidianDragonLite),
so it starts from a clean, isolated config and uses default servers.

Note: the lite wallet file itself lives in the litelib backend's own data dir
(unaffected); this isolates the GUI config only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:06:12 -05:00
6531d0c4d2 fix(lite): surface auto-open failures instead of a silent disconnected spinner
The startup auto-open of an existing lite wallet discarded openWallet()'s result,
so when initialize_existing failed (e.g. the lightwalletd server is unreachable)
the UI just showed a "disconnected" spinner with no reason — and DEBUG_LOGF is
compiled out of release builds, so there was no way to see why. Capture the
failure: store the reason, show it in the Network tab status line (in place of
"no wallet open"), and raise a notification. Cleared once a wallet opens.

This doesn't change open behaviour — it makes a stuck open diagnosable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:39:22 -05:00
142a6826af fix(rpc): abort in-flight curl on disconnect/shutdown to avoid UI freezes
stop()-ing a worker that is mid curl_easy_perform joined on the UI thread, so a
slow/hung transfer froze the UI until the request timeout. Add RPCClient::
requestAbort() (a thread-safe atomic read by a curl progress callback that aborts
the transfer), and call it before stopping the workers on disconnect
(onDisconnected) and shutdown (beginShutdown + the synchronous fallback). The
flag is cleared on each connect() so a fresh connection never starts aborted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:43:34 -05:00
070a516f4e fix(send): validate recipient address checksums (Base58Check + Bech32)
The send screen labelled any prefix+length match as a "Valid" address, so a
mistyped address that still matched the pattern passed the gate. Add pure,
offline checksum validation — Base58Check (transparent R-addresses) and Bech32
(Sapling zs-addresses) — and require it in the validity check. Both verifiers are
version-byte/HRP agnostic (the HRP is taken from the string, the Base58 checksum
is chain-independent), so a correct implementation never rejects a genuine
address while catching transcription errors. Works for both build variants
(no daemon round-trip), unit-tested against standard BIP173 / Base58Check vectors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:43:34 -05:00
3cec333d84 fix(storage): fsync the vault secure-delete overwrite
removeVault() overwrote vault.dat with zeros then unlinked it, but never flushed
to stable storage, so the zeros could stay in the OS cache and never reach disk.
flush + fsync before unlink on POSIX (still best-effort on CoW/SSD, but now does
what it claims).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:44 -05:00
fc438ab962 fix(rpc): invalidate stale in-flight refreshes on reset/reconnect
resetJobs() cleared the in-progress flags but left generations_ untouched, so a
refresh WorkFn still executing on the worker when a disconnect cleared state_
could pass completeDispatch's generation check and apply last-connection data
onto the new session. Bump every job's generation in resetJobs() so any
pre-reset ticket is treated as stale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:44 -05:00
8c2c1c2aaf fix(lite): import-key fallback on mis-routed key + clamp shield fee
- importKey routed transparent vs. shielded purely by the first character, which
  can mis-route (e.g. testnet/regtest WIFs). On failure, try the other import
  command before reporting an error (each validates the encoding, so a wrong
  command rejects rather than mis-imports). The key copy is wiped after both tries.
- Clamp the shield dialog's fee input to [0, 1] DRGX, mirroring the UTXO-limit
  clamp, so a negative or fat-fingered huge fee can't be submitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:44 -05:00
a605e35409 fix(ui): consistent hashrate units, full-address tooltips, drop dead vars
- Balance card hashrate now uses the shared FormatHashrate() (TH/GH/MH/KH/H)
  instead of a bespoke two-tier KH/s formatter.
- Recent-tx rows show the full untruncated address on hover — two z-addresses can
  truncate to the same first/last window — and the truncate helpers guard maxLen<=3.
- Remove the unused viewTop/viewBot "viewport culling" locals in the tx list
  (pagination already bounds per-frame work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:43 -05:00
6ed80d2d79 fix(send): result-driven status styling + full-precision USD preview
The transaction-status overlay decided error vs. success styling by searching the
status string for "Error"/"Failed" — so under a non-English locale a failed send
rendered as a green success. Drive it from the existing s_status_success flag
instead. Also show the USD-mode DRGX preview at 8 dp so it matches the confirm
panel and the amount actually sent (was 4 dp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:43 -05:00
e978db85ca test: cover audit fixes (atomic writes, opid routing, sqlite GC, lite tx)
- testAtomicFileWrite: Platform::writeFileAtomically creates dirs, overwrites,
  leaves no .tmp, and honors owner-only perms.
- failureByOpid assertion in the operation-status poll parser test.
- testTransactionHistoryCachePrunesOldWallets: a save under a new identity prunes
  the prior identity's snapshot.
- testLiteSendShowsRecipientFromOutgoing / testLitePartialRefreshKeepsPriorAddressBalances.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:18:34 -05:00
e00772db6e fix(i18n): reject format-incompatible translations
Many strings are used directly as printf/ImGui format strings, and translations
are loaded from user/installer-modifiable JSON with no validation. A translated
value that drops or changes a conversion specifier would be passed to printf with
mismatched varargs (undefined behavior) on a wallet screen.

overlayTranslations() now compares each translated value's argument signature
against the English source and keeps English on mismatch. Also adds the
send_status_unconfirmed string used by the deferred-send-result path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:18:25 -05:00
8f22db5eea fix(send): resolve source balance by address, not list index
GetAvailableBalance() read state.addresses[s_selected_from_idx], but the index
desyncs from s_from_address (the value actually debited) after an address-list
refresh, and is left at -1 when the source is chosen from another tab's "Send
from this address" — which made the sufficiency check see a 0 balance and wrongly
block a valid send. Look the balance up by matching the source address string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:18:16 -05:00
5c883d4b91 fix(lite): faithful tx list, balances, and persistence on partial results
- A Send record carries its recipient in outgoing_metadata, not the top-level
  address/memo, so sent txs showed a blank destination + memo. Surface the first
  recipient (single-recipient case) into the transaction list.
- A tolerated partial refresh where the notes/utxo command failed (addresses
  present, spendable outputs absent) zeroed every per-address balance, which looks
  like fund loss. Preserve the last-known per-address balances in that case.
- Retry the post-send/shield save once on transient failure instead of ignoring
  the result (the backend does not auto-save after send/shield).
- An unparseable broadcast response now uses cautious wording ("status could not
  be confirmed — check Transactions before retrying") rather than implying a hard
  failure, avoiding a blind double-spend retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:18:05 -05:00
274f7ea1af fix(storage): owner-only secret files + bound SQLite cache growth
- Write vault.dat atomically and 0600 (it holds the PIN-encrypted passphrase, so
  a world-readable copy enables an offline brute-force of the short PIN), and
  chmod the tx-history SQLite + its WAL/SHM sidecars to 0600 on open.
- The tx-history snapshot and key-salt rows are keyed on a hash of the full
  address set, which changes whenever a new address is generated — orphaning the
  prior hash's full-history blob and salt forever. pruneOtherWallets() now drops
  rows for every non-live wallet hash on each save, bounding the database.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:17:54 -05:00
3799330bb0 fix(ui): show real data and consistent values across tabs
- Market chart now plots the real accumulated price_history instead of a
  rand()-generated curve; the hover tooltip no longer claims a specific "Xh ago"
  price and the x-axis only labels the truthful "Now" point. Falls back to the
  existing empty state until there are >=2 real samples.
- Transactions summary cards exclude autoshield legs (same txid send + receive-to-z)
  so a shield isn't double-counted into both Sent and Received, matching the list.
- Send/Receive sync banners use verification_progress like every other surface,
  instead of the blocks/headers ratio that over-reports during early sync.
- Fix printf format/type mismatches: %.0f<-int (market % shielded), %d<-size_t
  (peer counts), %ld<-int64_t (peer byte counters, wrong on Windows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:17:42 -05:00
53a10e149d fix(rpc): detect mid-session disconnects and stop blocking the UI thread
The connection state machine never tore down on a lost connection: refresh-loop
RPC errors were swallowed, rpc_->isConnected() stayed true after a daemon
crash/restart/socket drop, and the UI showed stale balances with no reconnect.
Several operations also ran synchronous curl straight from ImGui handlers.

- Add handleLostConnection(): after N consecutive cycles where BOTH core RPCs
  fail (warmup excluded, so no reconnect loop), disconnect so update()'s
  reconnect branch re-enters tryConnect().
- Move banPeer/unbanPeer/clearBans and key export/import onto the worker thread
  (import requests a rescan that could freeze the UI for the curl timeout).
- Run the block-info dialog's two chained RPCs on the worker thread (+ guard the
  getblockhash result type).
- Detect daemon warmup via the JSON-RPC -28 code (new RpcError carrying the code;
  message text preserved so 401/warmup string-matching is unaffected), and widen
  CONNECTTIMEOUT to 10s for remote/TLS hosts (2s localhost).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:17:17 -05:00
1bc7f5c8cd fix(tx): track async operations to completion (send/shield/auto-shield)
z_sendmany returns an opid immediately; the tx is built/signed/broadcast
asynchronously afterward. The send path showed "Transaction sent successfully!"
and cleared the form on opid receipt, so a later async failure contradicted it.
Shield/merge stored the opid only in a dialog-local static (never polled), and
auto-shield ran a blocking z_shieldcoinbase on the UI thread and discarded its
opid — async failures of all three were silently lost.

- Add App::trackOperation(opid) so shield/merge/auto-shield register with the
  shared opid poller (failures surface, balances refresh on completion).
- Defer the full-node send's success/failure to the poller via per-opid callbacks
  (parseOperationStatusPoll now exposes failureByOpid); the "Sending..." spinner
  covers the finalizing window, and the form is kept until terminal status.
- Dispatch auto-shield through the worker thread and use the configured fee.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:16:48 -05:00
20b22410e9 fix(persistence): atomic + owner-only settings/address-book writes
settings.json and addressbook.json were written in place with a bare ofstream —
a crash or power loss mid-write truncated the file, and on the next launch the
parse failure silently reset every preference (hidden/favorite addresses, labels,
pool workers, language, theme, lite-server list) because the next save overwrote
the corrupt file with defaults.

Add Platform::writeFileAtomically() (temp file -> fsync -> atomic rename; dir
fsync on POSIX, MoveFileEx on Windows; optional owner-only 0600) and route both
saves through it. On a parse failure, quarantine the unreadable settings file to
settings.json.corrupt-<ts> instead of clobbering it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:16:10 -05:00
7195c25376 fix(send): pass the user-selected fee to z_sendmany
The full-node send built the recipients array and called z_sendmany with only
(fromaddress, amounts) — dropping the minconf and fee positional args. The whole
fee-tier UI (Low/Normal/High, send-max math, the confirmation fee) was collected
and shown but never sent, so the daemon silently applied its own default fee and
the Low/High tiers were cosmetic.

Pass {from, recipients, 1, fee}, with the fee formatted fixed-decimal so the
daemon's ParseFixedPoint accepts it (a small double like 0.00005 would otherwise
serialize to "5e-05" and be rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:15:58 -05:00
a6921bca60 feat(lite): show connection + sync status in the Network tab
Add a status panel at the top of the Network tab driven by the live WalletState:
- Connection: a colored dot + Connected / Syncing / Not connected, with the in-use server host
  (or "Random server") and its latency on the right.
- Sync: "<pct>%  ·  <walletHeight> / <chainHeight>" while syncing (with a thin progress bar),
  "Synced · block N" when complete, or "No wallet open" when disconnected.

Reads app->state().sync (populated by the lite refresh: progress / wallet+chain height / complete)
and state().connected (= walletOpen). Advances with a Dummy so the bounds grow correctly.

Both variants build; suite passes; hygiene clean; lite GUI smoke OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:41:55 -05:00
8ba4233b9b fix(lite): grow the Network tab scroll region with a Dummy (ImGui layout)
Each server card advanced to the next via a bare SetCursorScreenPos, which ImGui won't use to
extend the scroll region's content height ("Code uses SetCursorPos() to extend window boundaries
... submit an item e.g. Dummy() afterwards"). Beyond the warning, this meant cards past the fold
wouldn't scroll. Advance with an ImGui::Dummy(cardW, gap) below each card so the content height
grows correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:34:26 -05:00
732d892d4d feat(lite): ObsidianDragonLite Network tab — server browser
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant)
to manage lightwalletd servers, replacing the basic selector that was in Settings.

- Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an
  Official/Custom pill. Official DragonX servers get a glowing outline.
- Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode);
  selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the
  controller against the new server and resyncs — its dtor detaches the uninterruptible sync
  thread, so this doesn't block).
- Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle).
- Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does
  the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording
  APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button.

Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in
the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added
to lite_connection_service. The Settings page lite-server selector + its plumbing are removed
(single source of truth = the tab).

Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI
primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested
(liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is:
online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI
smoke-launched without crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:09:27 -05:00
afd612be7e fix(build): don't clobber the other variant's release artifacts
The linux/windows release packaging did `rm -rf "$out"` on the whole output dir, so building
ObsidianDragonLite into release/<os>/ wiped the ObsidianDragon artifacts already there (both
variants share release/linux and release/windows). Remove only the CURRENT variant's prior
artifacts (by APP_BASENAME, which can't cross-match — "ObsidianDragon-*" excludes
"ObsidianDragonLite-*"), so full-node and lite releases coexist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:17:37 -05:00
4d769c8719 feat(mining): move "Update miner" into the benchmark row, showing latest + current version
Relocate the miner-update control from a standalone full-width button into the mining-control
header row, immediately left of the benchmark button:
- The button now shows the latest available version ("Update <tag>"), with the current installed
  version as text to its left ("Current: <tag>" / "none").
- A one-shot background version check (util::XmrigUpdater::startCheck) runs the first time the pool
  section is shown, so the latest tag can be displayed; until it arrives the button reads
  "Update miner…". Clicking opens the existing dialog; disabled (greyed, with tooltip) while the
  miner is running.
- New i18n keys: xmrig_update_short, xmrig_current, xmrig_none.

Both variants build; suite passes; GUI smoke-launched without crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:13:18 -05:00
0e1b19d0f2 build: bump full-node to 1.3.0 + give ObsidianDragonLite an independent version (1.0.0)
The full-node app and ObsidianDragonLite are now versioned separately:
- project() VERSION -> 1.3.0 (suffix cleared); DRAGONX_LITE_VERSION -> 1.0.0.
- A DRAGONX_APP_VERSION* set (resolved per variant in the lite/full block) feeds the generated
  header (version.h.in), the Windows VERSIONINFO/.rc + manifest, and the build summary — so each
  variant reports its own version. The .rc/manifest name fields also follow DRAGONX_APP_NAME so a
  lite .exe's properties read "ObsidianDragonLite".
- build.sh resolves the release-filename version per variant by parsing CMakeLists (single source
  of truth) instead of a hardcoded string.

Also fixes a latent variant-bleed: build.sh now passes DRAGONX_BUILD_LITE and
DRAGONX_ENABLE_LITE_BACKEND explicitly (ON *and* OFF), so switching variants in a shared build dir
can't reuse a stale cached value (a prior --lite build was making a subsequent full-node build
produce the lite name/version).

Both variants build + report the right version (full 1.3.0, lite 1.0.0); suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 09:51:28 -05:00
b24212fb8f docs: document the xmrig miner updater + release-signing requirement
Add a "Miner updater (xmrig)" section to CLAUDE.md: the update flow + verification
(TLS + archive SHA-256 + enforced ed25519 signature against a pinned key), and the
release-process consequence — every drg-xmrig release must be signed
(scripts/sign-xmrig-release.sh) with the .sig uploaded per archive, or the in-app
updater refuses it; the signing secret key stays offline (gitignored), only the base64
public key is pinned in source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 09:35:39 -05:00
64fe8fc6c9 i18n(mining): route xmrig updater strings through TR()
Replace the English string literals in the miner-update dialog + the "Update miner…" mining-tab
button/tooltip with TR() keys, and register their English text in i18n.cpp's loadBuiltinEnglish()
(the in-code English fallback that non-English locales overlay). Reuses the existing cancel/close/
retry keys. Labeled values use a "%s %s" literal format with a TR'd label (no -Wformat-security
risk). Non-English locales fall back to English for the new xmrig_* keys until translations are
added to res/lang/*.json.

Both variants build; suite passes; hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 09:34:14 -05:00
b9881278af feat(mining): enforce xmrig signatures + fix multi-platform checksum/asset bugs
Now that the release publishes a valid .sig per archive (verified against the pinned key for
linux/win/macOS), enable enforcement and fix two bugs that the newer multi-platform release
(v6.25.3, which added a macOS build) exposed:

- kXmrigRequireSignature = true: refuse any install whose release doesn't publish a valid
  ed25519 signature over the archive. Verified live end-to-end against the signed v6.25.3
  (archive SHA-256 + signature -> install).
- Drop the redundant inner-binary SHA-256 check. It keyed on the inner filename, but both the
  linux and macOS archives contain a binary literally named "xmrig", so the two "xmrig (…)"
  checksum lines collided in the map and the linux install compared against the macOS hash ->
  spurious "could not verify" failure. The whole archive is already verified (SHA-256 +
  signature), so every extracted member is authentic by transitivity — the per-member check
  added nothing but ambiguity.
- Fix the macOS platform token: the asset is named "...-macos-x86_64.zip", not "...-macos-x64",
  so selectXmrigAsset never matched it. currentXmrigPlatformToken() now returns "macos-x86_64"
  on Intel macs (arm64 has no build -> Unavailable). Added a matcher test for the macOS naming.

Both variants build; suite stable (0 failures / multiple runs); live require-mode install verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 09:29:37 -05:00
85b53baeaf feat(mining): pin xmrig release-signing key + fix raw-signature parsing bug
- Pin the ed25519 public key in xmrig_updater.h, activating signature verification in soft mode
  (kXmrigRequireSignature=false): a release's ".sig" asset is verified when present, but an
  unsigned release still installs on TLS + SHA-256. Verified live against the current release
  (v6.25.2, which ships no .sig yet) — still installs.
- gitignore *.ed25519.key / *.ed25519.pub.b64 so a signing secret key can never be committed.
- Add a unit test that the pinned key decodes to a valid 32-byte ed25519 key (a malformed paste
  fails the build, not silently disabling verification).

Bug fix (found via a flaky test): verifyXmrigSignature trimmed trailing whitespace BEFORE the
raw-64-byte check, so a raw signature whose last byte equals '\n'/'\r'/space/tab (~1.6% of
signatures) was corrupted and rejected. Now base64 is tried first (safe to trim) and the raw
path uses the exact untrimmed bytes. Added a deterministic regression test that forces a
whitespace-terminated raw signature. Suite is stable (0 failures in 10 runs; was ~3/8).

Also de-brittled the live integration test: it no longer pins a release-specific binary hash
(reaching Done already means the worker verified the binary against the release's own checksum).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:44:53 -05:00
eece57c025 chore(mining): make xmrig release-signing script OpenSSL-based (no PyNaCl)
Rewrite scripts/sign-xmrig-release.sh to use OpenSSL (>= 1.1.1) instead of PyNaCl, so signing
needs no Python deps. OpenSSL's ed25519 is PureEdDSA (RFC 8032) — interop-verified against the
wallet's libsodium crypto_sign_verify_detached (script-produced .sig -> VERIFY-OK; tamper ->
VERIFY-FAIL). keygen/pubkey/sign subcommands; emits base64 raw-64-byte signatures as <file>.sig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:31:25 -05:00
8765fdf362 feat(mining): opt-in ed25519 signature verification for the xmrig updater (#1)
Closes the supply-chain gap the review flagged: today the archive and its SHA-256 share one
trust root (the release body), so a compromised/edited release can ship an arbitrary binary
that still "verifies". This adds authenticity via a detached ed25519 signature checked against
a public key PINNED IN THE BINARY (not fetched), using libsodium's crypto_sign_verify_detached.

Opt-in / soft rollout:
- kXmrigSignaturePublicKeyBase64 in xmrig_updater.h is EMPTY by default -> signatures are not
  checked and behavior is unchanged (TLS + SHA-256 only). Paste the base64 public key to enable.
- Once a key is pinned, an install verifies a "<archive>.sig" asset (base64/raw 64-byte ed25519
  signature over the archive bytes) when present; kXmrigRequireSignature=true additionally
  refuses installs that publish no signature.
- The check runs after the SHA-256 check, over the same already-read archive bytes; refuses on
  a missing key-but-required, unreachable .sig, or invalid signature.

- verifyXmrigSignature + selectXmrigSignatureAsset are pure (libsodium only) and unit-tested:
  valid base64 + raw-64-byte signatures verify; tampered data, wrong key, and malformed/empty
  inputs all fail closed. Cross-tool interop verified (Python stdlib base64 == sodium base64).
- scripts/sign-xmrig-release.sh: keygen / sign / pubkey helper (PyNaCl = same libsodium ed25519)
  to produce the .sig assets and the public key to pin.

No behavior change until a key is pinned. Both variants build; suite passes; live worker
re-verified (signatures off by default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:29:49 -05:00
98e0cce8ec fix(mining): harden xmrig updater per adversarial review
Addresses confirmed findings from the multi-lens review of the updater:

- Cancelable + live progress (was: download uncancelable, progress stuck at 0%, closing
  the dialog mid-download blocked the UI thread on the worker join). Wire a libcurl
  CURLOPT_XFERINFOFUNCTION that publishes byte counts and returns abort when cancel() is
  requested; add a Cancel button. The dialog's destructor now aborts the transfer promptly,
  so closing mid-download no longer freezes the UI.
- Graceful "unavailable" instead of a red error on platforms with no published build
  (macOS / ARM): new terminal State::Unavailable rendered neutrally, not as a failure.
- Install-time running guard (TOCTOU): App::isPoolMinerRunning() re-checked in the dialog
  before each install, so a dialog opened before mining started can't replace a live binary.
- Size caps: CURLOPT_MAXFILESIZE on the download and a per-archive-member ceiling before
  decomphressing into memory, to bound an attacker-controlled archive.
- Distinguish a local read failure of the downloaded archive from a checksum mismatch
  (was reported misleadingly as "possible tampering").
- Reword the dialog's verification note to "checked against the release's published SHA-256
  checksum" (integrity, not authenticity — see the signing note below).

Not fixed here (needs your input): WinRing0x64.sys has no per-file hash published, but it is
covered by the verified archive checksum (it is inside the verified zip); and the release is
not cryptographically signed — checksums and binary share one trust root. Adding a pinned-key
ed25519/minisign signature is the real supply-chain hardening and needs an offline signing key
+ a release-process change.

Both variants build; suite passes; live worker re-verified end-to-end on linux-x64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:35:17 -05:00
5c87bc6e87 feat(mining): "Update miner" button + dialog wiring the xmrig updater
Wires util::XmrigUpdater into the GUI:

- ui/windows/xmrig_download_dialog.h: a modal (mirrors BootstrapDownloadDialog) that drives
  the updater — Checking -> Up-to-date/Update-available -> Downloading/Verifying/Extracting ->
  Done/Failed, with a progress bar and a "verified against its published checksum" note. On
  success it persists the installed release tag to settings. Rendered each frame from App::render.
- mining_tab: an "Update miner…" button in the pool section, disabled (with a tooltip) while
  xmrig is running so a live binary is never replaced.
- settings: persist the installed DRG-XMRig tag (xmrig_version) for update detection.

Both variants build; suite passes; GUI smoke-launched without crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:16:28 -05:00
946958b591 feat(mining): xmrig updater service — fetch/verify/install the latest miner from Gitea
Adds util/XmrigUpdater: a background-thread service (mirrors util/Bootstrap) that pulls
the latest DRG-XMRig release from the project's Gitea, verifies it, and installs the miner
binary into the daemon directory. Service layer only; the mining-tab UI hook comes next.

Flow: GET /api/v1/repos/DragonX/drg-xmrig/releases/latest -> pick the asset matching this
platform (…-linux-x64.zip / …-win-x64.zip; no macOS build -> graceful "unavailable") ->
download (libcurl, TLS verified) -> verify the archive SHA-256 -> extract with miniz,
flattening the versioned subdir the archive nests the binary in -> verify the extracted
binary's SHA-256 in memory before writing it -> atomic install (+chmod +x on POSIX). On
Windows also extracts WinRing0x64.sys; config.json/README.md are skipped.

Security (download-and-execute): TLS is verified, and BOTH the archive and the inner binary
are checked against the SHA-256 checksums published in the release body (parsed as
"<hex>  <name>" lines) — install is refused on a missing or mismatched checksum.

Split into a pure core (xmrig_updater_core.cpp: release parse, asset/platform match, checksum
parse, SHA-256) and the curl/miniz worker (xmrig_updater.cpp). The core is unit-tested against
a real captured release fixture (tests/fixtures/xmrig/release_latest.json); an env-gated
(DRAGONX_TEST_NETWORK=1) integration test exercises the worker live and was verified end-to-end
on linux-x64 (inner binary SHA-256 matches the published value). Both variants build; suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:07:46 -05:00
f5561c0dac build(lite): wire macOS --lite packaging in build.sh (M5b)
The mac-release path was mostly ObsidianDragon-hardcoded, so `--lite --mac-release`
would produce a broken bundle. Make it variant-aware, mirroring the linux/win lite
handling that already keys off APP_BASENAME + should_bundle_full_node_assets:

- SDL3 rpath fix, the launcher script + its .bin pair, and CFBundleExecutable now
  follow ${APP_BASENAME} (ObsidianDragonLite), so the bundle's executable resolves.
- Lite variant gets its own CFBundleName/CFBundleDisplayName ("DragonX Wallet Lite"),
  CFBundleIdentifier (is.hush.dragonx.lite), DMG filename (DragonX_Wallet_Lite-…)
  and volume name, so it can coexist with the full-node app.
- Full-node assets (daemon, Sapling params, asmap) were already gated out for lite;
  the lite backend artifact is auto-selected for the macos platform by the existing
  --lite-backend logic, and CMAKE_LITE_ARGS already reaches the mac configure.

Authored + validated on Linux (bash -n; launcher heredoc, plist, and DMG naming
render correctly for the lite variant) but NOT yet built/run — that needs macOS or
osxcross, neither available here. CLAUDE.md updated to reflect the wired-but-unverified
status; remaining M5b is verifying it on a Mac plus CI backend-artifact build + signing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:09:41 -05:00
b3c2282b53 feat(lite): runtime kill-switch + staged-rollout gate (M5b)
Adds a fail-open, local-only gate that decides whether the lite wallet may run,
so a post-release issue can disable it and rollout can be staged — without any
phone-home (privacy posture: no runtime network fetch; the per-install rollout
bucket is a hashed, never-transmitted local id).

- wallet/lite_rollout_policy.{h,cpp}: a pure decision core. Order — emergency env
  kill-switch (absolute) -> local override -> manifest gates (global enable /
  version floor-ceiling / blocklist / staged-rollout permille) -> fail-open allow.
  Plus a JSON manifest loader (missing/invalid -> fail-open) and FNV-1a bucketing.
- Threads the decision through LiteWalletController -> LiteWalletLifecycleService:
  new availability() reason RolloutDisabled blocks create/open/restore and surfaces
  the gate's user-facing message via the lifecycle status.
- App::rebuildLiteWallet() resolves it from: DRAGONX_LITE_KILL_SWITCH (env), the
  lite_rollout setting (auto/force_on/force_off), and a locally-cached manifest at
  <config-dir>/lite_rollout.json. install id generated once via libsodium.
- Settings: persist lite_rollout override + the install id.

A signed remote fetcher can populate the manifest cache later without touching the
policy. Unit-tested (version compare, bucketing, override/env precedence, manifest
gates, staged rollout, loader fail-open, controller integration) and runtime-verified
on Linux (env kill-switch, manifest disable, control sync). Both variants build;
full suite passes; hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:01:08 -05:00
ca14aaddc7 refactor(ui): remove abandoned Material-Design component library + screens layer
~9,988 lines of header-only UI code that no compiled translation unit reached,
verified by transitive include-reachability from every .cpp plus a symbol sweep
(all 28 component classes — Snackbar, Ripple, NavDrawerSpec, TabBarSpec,
TransitionManager, … — had zero references in live code):

- src/ui/material/ component library: the material.h umbrella, components/*
  (app_bar, cards, chips, dialogs, inputs, lists, nav_drawer, progress, slider,
  snackbar, tabs, text_fields), and the animation system (elevation, motion,
  ripple, transitions, app_layout) — 19 headers. Kept the live helpers the app
  actually uses directly: color_theme, colors, type/typography, draw_helpers,
  layout, project_icons, and components/buttons (included by mining_tab).
- src/ui/screens/ layer: main_layout, home_screen, send_screen, etc. — the
  original screen stack and the only consumer of the dead component library.
  The live UI runs through ui/windows/ (34 .cpp) + ui/pages/.
- src/embedded/resources.h: a superseded dragonx::embedded::Resources duplicate;
  the app uses src/resources/embedded_resources.h.

None were in CMakeLists or included by live code, so the build is unaffected.
Both variants build; full test suite passes; source-hygiene check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:31:06 -05:00
a5da5562cf refactor(lite): remove dead backend artifact-contract/resolver scaffold
lite_backend_artifact_{contract,resolver}.{cpp,h} (~1,960 lines) were
app-linked but never invoked: all 14 public entry points
(evaluateLiteBackendArtifactContract/Resolver, evaluateLiteBackendActivation-
Readiness, the resolve*/...Name helpers) had zero callers in the app, the
lite_smoke tool, build scripts, or surviving tests. The real backend load
path (LiteClientBridge::linkedSdxl) uses direct litelib_* externs, and the
DRAGONX_ENABLE_LITE_BACKEND symbol check is done in CMake against the symbols
inventory (FATAL_ERROR on a missing symbol) — not via these C++ files. The
files were saturated with churn markers (disabled / dry-dispatch / scaffold).

- Delete the four artifact files and their 8 CMakeLists references.
- Drop the orphaned test cruft in test_phase4.cpp: the contract include,
  5 type aliases, and 3 never-called helpers (heapConstructPlanResult,
  makeReadyLiteBackendArtifactProvenance, liteBackendArtifactContractHasIssue)
  left over from the already-removed bridge-runtime tests.
- Correct the CLAUDE.md lite-wallet description (it credited these files with
  backend validation that CMake actually performs) and drop the stale
  lite_bridge_runtime mention.

Both variants build; full test suite passes; source-hygiene check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:05:38 -05:00
c676ec8287 refactor(lite): extract owned-string core, drop dead bridge-runtime scaffold
lite_bridge_runtime.{cpp,h} was ~25k lines of dry-dispatch / dynamic-loader
scaffolding that the shipping wallet never used: 0 of its 122 public types
reached the app binary. The only live code on the bridge path was the
owned-string memory-safety helper — LiteClientBridge::linkedSdxl() already
loads the backend via direct litelib_* externs in lite_client_bridge.cpp.

- Extract LiteBridgeOwnedString + liteBridgeRuntimeTakeOwnedString into
  src/wallet/lite_owned_string.{h,cpp} (the copy-before-free / free-once /
  wipe / "Error:"-classify boundary), with the runtime-friend coupling removed.
- Point lite_client_bridge.cpp at the new header.
- Delete lite_bridge_runtime.{cpp,h} and the 16 runtime-only tests +
  their fixtures/aliases in test_phase4.cpp; keep the 5 owned-string tests
  (retargeted) and restore testGeneratedResourceBehavior, which had been
  caught in the runtime-test line range.
- Swap the CMake source/header references.

Both variants build; full test suite passes; source-hygiene check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:56:55 -05:00
f474b0d633 docs(lite): consolidate v2 plan status into CLAUDE.md, archive the plan
The lite-wallet v2 plan was the last tracked lite doc. Fold its still-live
content — current status, remaining M5b work (macOS/CI/signing/rollout), and the
push plan — into a concise "Lite wallet status" section in CLAUDE.md (the
canonical project doc), then move the full milestone plan to docs/_archive/
(untracked) alongside the other lite design docs.

Result: docs/ has no tracked markdown; tracked .md is now just repo essentials
(README, CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, CLAUDE.md). No dangling links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:00:48 -05:00
af252575cf chore: remove dead UI header files (scroll_fade_fbo.h, gpu_mask.h)
Both are header-only, in no CMake target, #included nowhere, and their only
symbols (ScrollFadeRT, DrawScrollFadeMask) are referenced nowhere:
- src/ui/effects/scroll_fade_fbo.h — superseded by the shader-based
  scroll_fade_shader.h (the implementation actually used by settings_page).
- src/ui/material/gpu_mask.h — a GPU blend-mask helper never integrated.

App + test build clean after removal; tests pass; hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:35:18 -05:00
74bd22958a docs: archive stale/dormant-feature markdown out of git tracking
Move 8 dated-snapshot / dormant-feature docs to docs/_archive/ (git-ignored,
kept locally), leaving only repo essentials + the active lite plan tracked:
- docs/codebase-audit-2026-04-27.md, docs/codebase-overview.md — "current as of
  2026-04-27" snapshots, superseded by CLAUDE.md and the v2 plan.
- docs/ui-static-state.md — Phase-9-era UI static-state review snapshot.
- docs/chat-port-feasibility-2026-05-06.md, docs/chat-protocol-spec-2026-05-06.md
  — superseded/old-"Batch"-framing docs for the dormant, gated-OFF chat module.
- tests/fixtures/hushchat/{README,CAPTURE_MANIFEST,IMPORT_CHECKLIST}.md -> docs/
  _archive/hushchat/ — human docs (not tool input) for the dormant chat fixtures;
  the .json fixtures the HushChatFixtureCheck tool globs remain tracked.

These docs only cross-referenced each other (no code/CMake/script refs); no
dangling tracked links remain. Tracked .md (non-libs): 14 -> 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:26:22 -05:00
cd60bded9f docs(lite): archive superseded lite design/planning docs out of git tracking
Consolidate the lite-wallet documentation down to the single active plan
(lite-wallet-implementation-plan-v2-2026-06-04.md). The 8 prior design/planning
docs — the superseded v1 plan, its runtime-promotion-matrix, the two phase2
runtime-bridge plans, and the four backend artifact/signing design docs — are
moved to docs/_archive/ (added to .gitignore), preserving them locally as
reference while decluttering the tracked tree.

The v2 plan's References section is rewritten to be self-contained: it points to
docs/_archive/ for the historical design docs and to the actual shipping
mechanisms (scripts/build-lite-backend-artifact.sh, lite_backend_artifact_*,
lite_bridge_runtime.cpp) so there are no dangling tracked links. No code,
CMake, or scripts referenced these docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:14:56 -05:00
59b8c4da81 docs(lite): record end-of-session implementation status
Summarize the 2026-06-05 session in the v2 plan: M1–M5a + encryption complete,
GUI wired with lite wording, ~3.2k lines cleanup, Linux+Windows packaging
verified, both variants build clean, runtime-verified on Linux. Notes the
remaining M5b infra (macOS/CI/signing/rollout) and the push plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:32:50 -05:00
950d7ace50 feat(lite): startup unlock prompt + real-backend encryption verification
Startup lock screen (soft): once the first refresh reveals the auto-opened wallet
is encrypted+locked, show the unlock modal on launch (reusing renderLiteUnlockPrompt,
one-shot per session). Soft by design — balances stay viewable via viewing keys
while locked, so the user may dismiss and browse read-only; only spending needs
the passphrase.

Real-backend verification: add `lite_smoke --encrypt` (create -> encryptionstatus
-> encrypt -> lock -> unlock, checking flags; passphrase never printed). Running it
against the real SDXL backend showed encrypt LOCKS immediately
(after encrypt: encrypted=1, locked=1) — the backend removes spending keys right
after encrypting. The controller already relays encryptionstatus faithfully (UI is
state-driven, so unaffected), but the fake modeled encrypt->unlocked; corrected the
fake (encrypt -> encrypted+locked) and the test sequence (encrypt -> unlock -> lock
-> decrypt) to match real behavior.

Builds clean, tests pass, hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:53:35 -05:00
d52d3d1b7f feat(lite): send-time unlock prompt for locked encrypted wallets
When the user confirms a send on a locked encrypted lite wallet, show an unlock
modal (passphrase -> unlockWallet) instead of letting the backend reject it with
"Wallet is locked". After unlocking, the user re-confirms the send (the form is
preserved). Balances remain viewable while locked; only spending needs unlock.

- send_tab: the Confirm-and-send button routes to App::requestLiteUnlock() when
  getWalletState().isLocked(), else sends as before.
- App::renderLiteUnlockPrompt(): centered modal, passphrase (Enter submits),
  Unlock/Cancel; the passphrase buffer is sodium-zeroed after every path.

Full-node unaffected (gated on liteWallet()/isLocked()). Builds clean, launches
clean, tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:22:15 -05:00
9569b0ba43 feat(lite): encryption UI — encrypt/unlock/lock/decrypt in Settings
Add a "Security" subsection to Settings → Backup & keys (open wallet only) that
wires the encryption controller methods to the UI:

- Unencrypted wallet: passphrase field + "Encrypt wallet".
- Encrypted + locked: "Unlock" (passphrase) ; Encrypted + unlocked: "Lock now".
- Encrypted: passphrase + "Remove encryption" (decrypt).
- Status line reflects the result; state shown from WalletState.isEncrypted()/
  isLocked() (kept current by the controller's encryptionstatus refresh poll).

Secret hygiene: the passphrase inputs (lite_enc_pass / lite_dec_pass) are
sodium-zeroed immediately after each action and when the wallet closes while the
section was open.

Runtime-checked: app auto-opens a wallet and the new encryptionstatus worker poll
runs clean (no errors); tests pass; hygiene clean.

Follow-ups (not yet): a send-time unlock prompt and a startup lock-screen overlay
for an encrypted+locked wallet (today: unlock via Settings; balances remain
viewable while locked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:55:09 -05:00
50b0419dfe feat(lite): wallet encryption controller layer (encrypt/unlock/lock/decrypt)
Wire the backend passphrase-encryption commands into LiteWalletController:

- encryptWallet / decryptWallet (take passphrase by value, securely wipe it,
  save after), unlockWallet / lockWallet (bring spending keys into/out of
  memory), and encryptionStatus() -> {encrypted, locked}. All return
  failure-safe results; errors arrive as {"error":..} or "Error:" (handled).
- Fold encryptionstatus into refreshModel() (polled every cycle, available even
  mid-sync since it reads local wallet state) and apply it in
  applyLiteRefreshModelToWalletState, so WalletState.isEncrypted()/isLocked()
  track the backend — which gates the existing locked/auto-lock UI.

Backend contracts verified against the SDXL source: encrypt/unlock/decrypt take
the passphrase as the single arg; lock takes none; encryptionstatus returns
{"encrypted","locked"}; ops return {"result":"success"} / {"error":..}.

Tests: testLiteWalletControllerEncryption drives encrypt -> lock -> unlock ->
decrypt via encryptionStatus(), checks empty-passphrase + closed-wallet rejection,
and that the status folds into WalletState. Fake models the state machine.

GUI wiring (encrypt in Settings, unlock prompt / lock action) is the follow-up;
the backend create flow remains unencrypted by default until encrypt is run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:50:53 -05:00
4f7a4fb38e feat(lite): first-run welcome prompt (create / restore)
Replace the bare "land on main UI with a No-wallet overlay" first-run with a
lite welcome modal, shown when no wallet file exists yet (lite_wallet_ present,
not open, walletExists() false):

- "Create new wallet" — one-click createWallet({}); on success, notifies the user
  to back up their recovery phrase and navigates to Settings (Backup & keys),
  where the seed can be revealed/copied via the existing backup UI.
- "Restore from seed" — navigates to Settings (Lite wallet request → Restore).
- "Later" — dismiss for the session.

Routes to the already-built + verified create/restore/backup flows rather than
re-implementing seed display in the modal (no new secret-handling surface).
Dismissed once an action is chosen; never shown again once a wallet exists.
Full-node is unaffected (renderLiteFirstRunPrompt() returns early when
lite_wallet_ is null). English i18n built-ins added.

Verified: fresh-HOME lite launch shows the prompt, clean run + shutdown, no
crash/RPC noise; tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:38:32 -05:00
f511c0d509 fix(lite): skip the full-node first-run wizard in lite builds
isFirstRun() keys off the full-node `blocks/` data dir, which never exists in
lite — so the daemon/blockchain setup wizard (download node, extract blockchain,
daemon status) fired in lite, where none of it applies and it has zero
lite-awareness. Gate the wizard on !isLiteBuild(); lite goes straight to the main
UI, where the "No wallet open — create or open one in Settings" prompt guides new
users to the lite create/open flow. Full-node behavior is unchanged
(isFirstRun() && !isLiteBuild() == isFirstRun() there).

Completes the lite daemon-wording sweep: the other full-node surfaces are already
lite-gated — daemon settings via supportsFullNodeLifecycleActions(), RPC settings
in the isLiteBuild() else-branch, and Console/Peers/Explorer hidden via
isUiSurfaceAvailable.

Verified: true first-run in lite (fresh HOME) no longer starts the wizard; clean
launch + shutdown, no daemon noise. tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:31:27 -05:00
235504657d polish(lite): lite-appropriate wording for no-wallet/connection states
In lite builds there is no daemon, and isConnected() now tracks the lite wallet,
so the full-node "not connected / waiting for daemon" wording was misleading when
no wallet is open. Add two strings (lite_no_wallet, lite_no_wallet_short; English
built-ins, so other languages fall back until translated) and use them in lite:

- receive/send address preview + receive empty-state overlay + send "can't send"
  tooltip + transactions empty state -> "No wallet open [— create or open one in
  Settings]" instead of daemon wording.
- Status bar: the red indicator shows "No wallet open" (not "Disconnected") in
  lite; the P2P peer count is skipped (lite has no peers); and the redundant
  full-node connection-detail line is suppressed (connection_status_ set to
  "Connected"/"" from the lite wallet state).

Full-node wording unchanged (all gated on isLiteBuild()). Build + run clean
(no RPC noise), tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:11:41 -05:00
76c2ac5db8 feat(lite): auto-open existing wallet on startup + gate full-node RPC refreshes
Auto-open: on the first update() tick (kept off init() so a slow
initialize_existing network call can't freeze startup before the window), if a
wallet file exists, open it. initialize_existing needs no passphrase — it loads
the file; a previously-synced + saved wallet resumes from its height (fast)
instead of rescanning from the checkpoint. Adds LiteWalletController::walletExists()
(bridge.walletExists on the connection's chain) + a chainName_ member.

RPC-refresh gating: the earlier connected=walletOpen() fix (so the wallet UI is
enabled in lite) had a side effect — the full-node periodic + per-page RPC
refreshes (mining/balance/peers/txs, and setCurrentPage's immediate refresh)
gate on state_.connected, so they began firing in lite and failing
("X error: Not connected"). Re-gate those on ACTUAL RPC connectivity
(rpc_ && rpc_->isConnected()) instead of the lite proxy. Full-node is unchanged
(state_.connected ⟺ rpc connected there); lite no longer issues any RPC.

Runtime-verified in WSLg with a pre-seeded wallet: app auto-opens (Starting
Mempool + sync begins), and "Not connected" / getMiningInfo / RPC-connect noise
all drop to 0 — a fully clean lite run. tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:29:27 -05:00
89bd21018a fix(lite): don't run the full-node RPC loop in lite; drive isConnected() from the wallet
Runtime monitoring of ObsidianDragonLite (WSLg) showed the full-node RPC connect
state machine running in the lite build — `tryConnect()` fired every ~5s and
failed ("Couldn't connect to server / no daemon"). It's called unconditionally
from the main loop with no lite guard.

Worse than noise: `state_.connected` (App::isConnected()) was therefore ALWAYS
false in lite, and it gates the wallet UI — receive_tab disables the new-address
button + shows "not connected", send_tab disables send, transactions_tab shows
not-connected. So the M3/M4 GUI wiring was effectively unreachable: a lite user
could never generate an address or send, even with an open, synced wallet.

Fix:
- tryConnect() no-ops in lite builds (isLiteBuild()), so no RPC attempts.
- App::update() derives state_.connected from lite_wallet_->walletOpen() each
  frame — a non-blocking proxy for "lite backend operational" (a wallet opens
  only after a successful backend init against the lite server). This enables the
  wallet UI once a wallet is open.

Full-node is unaffected (both branches are runtime-gated: isLiteBuild() is false
and lite_wallet_ is null there).

Verified by re-running the app: RPC connection attempts dropped from 7/30s to 0;
clean launch (GL 4.2) + clean shutdown; tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:11:58 -05:00
9d7054b245 build(lite): enable Windows lite-backend cross-compile (.exe verified)
`build.sh --lite-backend --win-release` now cross-compiles a working
ObsidianDragonLite.exe with the real SDXL backend:

- Artifact platform follows the cross target: when only --win-release is
  requested, auto-select build/lite-backend/windows/ (previously always the host
  artifact, which would link a Linux .a into a Windows .exe).
- Link the Win32 system libs a Rust x86_64-pc-windows-gnu staticlib pulls in
  (rustls/schannel, ring, dirs, std) via DRAGONX_LITE_BACKEND_EXTRA_LIBS. The set
  is rustc's `--print native-static-libs` for the backend (winapi_* shims mapped
  to real mingw import libs); all 21 exist in mingw-w64.

Verified end to end on Linux:
- scripts/build-lite-backend-artifact.sh --platform windows cross-builds the
  backend to x86_64-pc-windows-gnu (~105 MB .a); rustls/ring cross-compile clean
  (no openssl blocker); all required litelib_* symbols present.
- build.sh --lite-backend --win-release -> release/windows/ObsidianDragonLite-
  <ver>.exe (PE32+ GUI x86-64, INCBIN-embedded, ~170 MB) + zip, with the same
  full-node-asset exclusion as Linux.

Not yet done: running the .exe on real Windows (cross-compiled only). Plan
updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 15:45:48 -05:00
70608fcb7a docs(lite): record verified Linux lite release packaging (M5b)
`./build.sh --lite-backend --linux-release` produces a working
ObsidianDragonLite zip + AppImage (SDXL backend linked statically). Verified the
lite bundle excludes all full-node assets (dragonxd, dragonx-cli, sapling
params, asmap.dat) and includes res/ + xmrig (pool mining works in lite). CMake
falls back to FetchContent SDL3 when system SDL3 is absent, so the release build
has no system-SDL3 prerequisite. release/ is gitignored.

Remaining M5b (Windows/macOS packaging, CI artifact build + signing,
kill-switch/rollout) is infra/CI, not locally verifiable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:23:51 -05:00
0c819afdd4 test(lite): smoke-check M4/M5 command shapes against the real backend
Add `lite_smoke --keys`: create a fresh wallet and exercise the M4/M5
spend/backup commands (new-address, export, seed, save) against the real linked
SDXL backend, verifying each response's JSON shape with nlohmann. SECRET-SAFE:
seed and private-key VALUES are never printed — only field presence/shape and
counts (no send/shield, which would broadcast).

Verified live (isolated HOME, throwaway wallet shredded after):
  new z      shape_ok=1            new t      shape_ok=1
  seed       has_seed=1 has_birthday=1 (REDACTED)
  export     is_array=1 count=4 has_private_key=1 (REDACTED)
  save       result_success=1

Confirms the controller's newAddress / exportSeed / exportPrivateKeys / save
parsing matches real backend output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:00:05 -05:00
db4778e6a7 feat(lite): backup & keys UI — export seed/keys + import (Settings)
Add a "Backup & keys" section to the lite Settings page, shown only for an open
wallet, wiring the M4 controller backup/import surface into the GUI:

- "Show seed" / "Show private keys" -> exportSeed() / exportPrivateKeys();
  the revealed secret is displayed read-only (TextWrapped, no extra copies) with
  Copy and "Hide & wipe" controls.
- "Import key" (password input) -> importKey() (auto-detects WIF vs shielded);
  do_import_sk just records the key + saves (no synchronous rescan), so this is
  safe on the UI thread — history appears after the next sync.

Secret hygiene: the revealed-backup buffer is sodium-wiped via
secureWipeLiteSecret on hide, on a new export (overwrite), and if the wallet
closes while revealed; each export also wipes the controller's result copy; the
import input buffer is zeroed immediately after submission.

Lite app + full-node variant build/link clean; controller methods already
covered by testLiteWalletControllerM4; hygiene clean. GUI behavior itself isn't
auto-verifiable here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:40:17 -05:00
eb6114ee19 feat(lite): wire send + new-address GUI to the lite controller (M3/M4)
Route the existing receive/balance/send UI to the lite controller in lite builds,
with no per-tab UI changes — the existing buttons just work:

- App::createNewZAddress / createNewTAddress: lite branch calls
  lite_wallet_->newAddress() (synchronous local key derivation), injects the new
  address into WalletState so the UI selects it next frame, and invokes the
  receive-tab callback. Placed before the full-node !connected guard.
- App::sendTransaction: lite branch builds a LiteSendRequest (DRGX -> zatoshis,
  memo; `from`/`fee` ignored since the backend selects inputs and adds the fee),
  fires the controller's async broadcast, and stashes the send_tab callback.
- App::update: drains takeBroadcastResult() and delivers txid/error to the stored
  callback, so the send_tab's existing "sending.../sent" flow works unchanged.

All branches guard on lite_wallet_ (null in full-node). Verified: lite app +
test suite + full-node variant all build/link clean; hygiene clean.

Backup/import UI (export seed/keys, import) is deferred — it needs new
secret-display UI rather than an existing button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:10:56 -05:00
5d317f6be3 feat(lite): M5a — wallet persistence after sync/send/shield
Verified against the SDXL Rust source that the backend auto-saves only on
new-address / import / rescan; it does NOT save after sync, send, or shield, and
litelib_shutdown merely sets a flag. So without intervention a first sync
(~30 min) and any sent transaction are lost on restart.

The controller now triggers the backend `save` at exactly the right points:
- after the detached `sync` completes — and BEFORE syncDone_ is set, so a
  syncComplete() observer always sees a fully persisted wallet;
- after a successful send / shield (the doSend/doShield cores; skipped on
  failure so a failed broadcast doesn't write);
- a guarded best-effort flush in the destructor, only when syncDone_ and no
  broadcast is in flight, so shutdown never blocks on the wallet lock held by an
  uninterruptible scan or in-progress proving;
- plus a public saveWallet() for explicit/periodic saves.

Wallet-file crash recovery (.dat / .dat.bak rotation) is already handled inside
the backend.

Tests: testLiteWalletControllerM5Persistence proves saves fire after
sync/send/shield and explicit saveWallet(), and do NOT fire on a failed send or
with no wallet open (fake gains a save counter). Plan doc updated; M5b
(packaging/CI/signing/rollout) remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:48:44 -05:00
a677c09984 refactor(lite): drop 4 unused OOP wrapper classes over free functions
Each of these classes wrapped an existing free function with a one-line
delegating method and was never instantiated anywhere (verified: no references
outside their own translation unit, not even within their own .cpp beyond the
definition) — the redundant "wrapper layer" pattern CLAUDE.md warns against:

- LiteWalletLifecycleUiExecutionAdapter      -> executeLiteWalletLifecycleUiRequest
- LiteWalletServerSelectionUiExecutionAdapter -> executeLiteWalletServerSelectionUi
- LiteWalletServerLifecycleReadinessPlanner   -> evaluateLiteWalletServerLifecycleReadiness
- LiteBackendActivationReadinessAdapter       -> evaluateLiteBackendActivationReadiness

The live free functions (the actual entry points used by the UI/runtime) are
unchanged. Both targets build, test suite passes, source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:40:26 -05:00
6611d57147 refactor(lite): remove dead parallel refresh/readiness scaffolding (~3.1k lines)
The lite-wallet tree carried a second, unused refresh+readiness architecture
that never reached the shipping binary — exactly the churn CLAUDE.md warns
against. The live refresh path is controller -> gateway.refresh ->
mapLiteWalletRefreshResult -> applyLiteRefreshModelToWalletState; this parallel
stack was dead weight.

Verified unused (their public types/functions are referenced only within the
cluster), then deleted (8 files / 16 incl. headers):
- lite_wallet_refresh_service            (LiteWalletRefreshService + gateway adapters)
- lite_wallet_app_refresh_coordinator
- lite_wallet_app_refresh_orchestrator
- lite_wallet_refresh_readiness_policy
- lite_wallet_state_apply_plan
- lite_wallet_state_apply_executor
- lite_wallet_sync_app_refresh_integration
- lite_wallet_sync_execution_readiness

Severed three thin couplings into the cluster from live files:
- state_mapper: dropped the dead mapLiteWalletRefreshServiceResult and switched
  its include from refresh_service.h to gateway.h (where the live
  LiteWalletRefreshResult/Bundle DTOs actually live).
- server_lifecycle_readiness: dropped the unused syncLifecycleInput member +
  converter and the sync_app_refresh_integration include.
- artifact_resolver: relocated the three LIVE artifact-input structs
  (LiteWalletSdxlArtifact{Symbols,}Input, LiteWalletLinkedBackendReadinessInput)
  out of sync_execution_readiness.h — their only real consumers — into
  artifact_resolver.h, then dropped the include.

Also removed the dead DRAGONX_LONG_LITE_BATCH CMake machinery (its source var
was empty; on Windows it generated a broken lite_batch90_receipt_plan.cpp that
#included an empty path) and the stale .cpp/.h entries in CMakeLists.

Lite source files: 44 -> 30. Lite + full-node configure, both targets build,
test suite passes, source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:25:02 -05:00
6a4e98b7ed feat(lite): M4 — send/shield/import/export/seed via controller + bridge
Add the spend & backup surface to LiteWalletController, with the real SDXL
backend contracts verified against the Rust source:

- send / shield: ASYNC (detached broadcast thread + takeBroadcastResult() slot,
  mirroring the sync thread's shared-lifetime pattern, since sapling proving can
  take seconds), plus synchronous *Blocking cores for tests. send uses the
  JSON-array form ([{address,amount,memo}]) because litelib_execute passes the
  whole args string as ONE argument (no whitespace split) — the space-separated
  CLI form would never parse. send/shield report failure via {"error":..} in the
  body (NOT an "Error:" prefix), so the result is derived from the parsed JSON.
- importKey: auto-detects transparent WIF (U/5/K/L -> timport) vs shielded key
  (-> import); takes the key by value and securely wipes it before returning.
- exportPrivateKeys / exportSeed: synchronous local reads returning SECRET
  material (flagged: no logging; caller wipes after the user saves the backup).
- broadcast thread is detached in the dtor (captures shared bridge + flag + slot,
  never `this`), so it is safe to outlive the controller.

Tests: testLiteWalletControllerM4 drives send (success / no-recipients /
{"error":..} / async-slot delivery / pre-open rejection), shield, export, seed,
and import (shielded + WIF + pre-open). Fake backend returns the real command
shapes + a g_liteFakeSendFails error toggle.

GUI wiring (send_tab button, backup/import UI) is deferred like the M3 UI hop
(GUI-unverifiable here). Plan doc updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:06:19 -05:00
4b9d6f7db5 fix(lite): rebuild controller on lite-server change (stale-settings audit HIGH)
The LiteWalletController was constructed once at App::init() with the lite
connection settings known at startup; changing the lite server in Settings
persisted to disk but never reached the live controller, so the new server had
no effect until the next launch.

Factor the construction into App::rebuildLiteWallet() and call it after a
successful server-selection save. The rebuild deliberately preserves a live
session: if a wallet is already open (and possibly mid-sync), it no-ops and the
new selection applies on the next controller build, rather than discarding the
open wallet and its uninterruptible in-flight sync.

Closes the last remaining HIGH from the session audit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:32:54 -05:00
043cdc7128 fix(lite): address adversarial audit findings in session's lite work
Re-audited this session's lite-wallet changes (originally written at medium
effort) and fixed the genuine issues found:

- walletReady (open path): litelib_initialize_existing returns the bare string
  "OK", which is NOT valid JSON, so the previous `json::accept(value)` check
  marked a *successful* open as not-ready. Key off a non-empty success response
  instead (the bridge already maps "Error:"/null to failure). Drops the now
  unused nlohmann include.
- sync progress: while the detached sync thread is still running, syncDone_ is
  authoritative — don't surface the backend's transient idle syncstatus
  ({"syncing":"false"} -> parser progress=1.0/complete=true) as a misleading
  100%/done. Force complete=false and zero the bogus 1.0 in the progress model.
- per-address balance: also exclude `pending` outputs (notes/utxos from an
  unconfirmed received tx) so per-address figures match confirmed/available.
- secret wiping: the settings page left the page-local request copies
  (input.request.*Request.{passphrase,seedPhrase}) unwiped, and the
  validation-only fallback path wiped nothing. Replace the single-path memzero
  with an RAII scrubber that wipes both the UI char buffers and the request
  string copies on every return path.
- concurrency: document that concurrent bridge->execute() is intentionally
  unguarded — litelib serializes wallet access internally via
  Arc<RwLock<LightWallet>>, so a C++ mutex is unnecessary and would defeat the
  sync/syncstatus concurrency the design relies on. syncLaunched_ -> atomic.

Tests: fake backend now returns the real init shapes (seed object for
create/restore, bare "OK" for open) and a new open-path case guards the
walletReady regression. Removed an unreliable alloc==freed leak assert from the
thread-bearing controller test (kept in the thread-free bridge test). Also fixed
a stray CMake indent and removed ~220MB of untracked build/debug scratch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:28:37 -05:00
2daea67a1e feat(lite): M3 — new-address generation + sync-indicator confirmation
- LiteWalletController::newAddress(shielded) runs the backend "new" command ("zs"/"R" ->
  do_new_address), parses the ["addr"] response, and returns the new address; the next
  refresh lists it. Fast (local derivation), safe on the UI thread.
- fake_lite_backend returns ["zs1fakenew"]/["R1fakenew"] for "new" by args.
- testLiteWalletControllerNewAddress covers shielded/transparent + no-wallet error.

Also confirmed (no code needed): the sync-progress indicator already works for lite —
balance_tab reads state.sync.* which M2b-3 populates. Per-address balances landed in M2.

Remaining M3 is pure UI wiring (receive_tab button -> newAddress, loading/empty states),
which isn't verifiable without a GUI session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:36:00 -05:00
e6b91ca661 feat(lite): per-address balances from unspent notes/utxos
applyLiteRefreshModelToWalletState now derives each address's balance by summing its unspent
notes/utxos (excluding spent and unconfirmed-spent outputs) instead of the aggregate-only
zeros, so the Receive/Balance UI shows per-address amounts. The notes parser shape is
confirmed against do_list_notes in the backend source.

testLitePerAddressBalances covers the summing + spent-exclusion. Completes M2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:24:36 -05:00
c6e28fc4da test(lite): real-backend shape verification of refresh parsers
lite_smoke: add --restore-recent (restore a throwaway wallet at birthday≈tip) and factor
the data-shape checks (non-blocking commands first). Finding: the backend downloads from a
fixed checkpoint regardless of birthday, so first sync is ~30 min and balance/list block
until synced — a full live data run is impractical.

Verified all refresh parsers against the real backend without a full sync:
- live run: info/addresses/syncstatus parse_ok=1 (addresses z=1/t=6 on a restored wallet).
- via the authoritative Rust source (commands.rs / lightclient.rs):
  - balance do_balance fields match parseLiteBalanceResponse.
  - list do_list_transactions: sends use outgoing_metadata (no top-level address), receives
    use address+amount; parseTransactionRecord already branches correctly.
  - syncstatus was the only mismatch (fixed in the prior commit).

No parser changes needed beyond syncstatus. M2 refresh path verified end-to-end at the
shape level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:13:54 -05:00
268eba6321 fix(lite): gateway refresh degrades gracefully on a failed command
LiteWalletGateway::refresh() aborted the entire refresh on the first command whose bridge
call or parse failed — which turned a single real-backend shape mismatch (e.g. syncstatus)
into a total, empty-everything refresh. Since the balance/addresses/list real shapes are
still unverified and we've already hit shape drift twice, make refresh resilient:

- Run every planned command; assembleLiteWalletRefreshBundle already skips failed results.
- result.ok = any usable data came back (bundle.complete still reflects all-succeeded).
- One command's failure now degrades gracefully — the other sections still populate.

testLiteWalletGatewayRefreshSkipsFailedCommand (fake balance returns invalid JSON) asserts
the refresh still succeeds with addresses/transactions/info populated and balance skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:57:10 -05:00
3119440cd9 fix(lite): non-blocking, non-hanging sync (Finding B)
The backend `sync` command is a blocking, uninterruptible full chain scan (do_sync(true);
does not honor the shutdown flag), and balance/list block until synced. Previously
startSync() ran on the main thread (would freeze wallet creation) and the worker could
block, making the destructor join() hang at shutdown.

Redesign:
- bridge is now std::shared_ptr<LiteClientBridge>, shared with a detached sync thread so
  detaching is safe and litelib_shutdown isn't called while a running sync still holds the
  bridge; the controller's own ref prevents premature shutdown during normal operation.
- startSync() launches the blocking `sync` on a detached thread (non-blocking; never joined).
- refreshModel() gates on syncDone_: while syncing it publishes syncstatus progress only;
  once synced it does the full balance/addresses/list refresh (now fast).
- destructor joins only the fast poll worker and detaches the sync thread -> no hang.
- syncComplete() accessor added.

Tests (deterministic, via a blocking-sync fake; counters made atomic for the detached
thread): testLiteWalletControllerShutdownDoesNotHangDuringSync (destructor returns <1.5s
with sync blocked); refresh/worker tests wait for syncComplete()/a balance-bearing model.
Stable across repeated runs; lite+backend and full-node apps build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:35:26 -05:00
59c55e33f8 fix(lite): parse real syncstatus shapes (idle vs in-progress)
The real backend returns syncstatus as idle {"syncing":"false"} (string) or in-progress
{"syncing":"true","synced_blocks":N,"total_blocks":M} (commands.rs:83-87), but
parseLiteSyncStatusResponse hard-required the block fields and failed whenever the wallet
wasn't actively syncing — so sync/progress never updated in the real app.

- Read "syncing" as a string; require synced_blocks/total_blocks only when syncing=true;
  idle => complete, synced/total 0.
- fake_lite_backend syncstatus now uses the real "syncing":"true" shape.
- testLiteSyncStatusParserRealShapes covers idle, in-progress, and missing-counts-while-syncing.
- Verified against the live backend via lite_smoke --refresh (syncstatus parse_ok=1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:52:42 -05:00
a8b5d2f6a3 feat(lite): M2b-3 — background refresh worker + App::update hook
- LiteWalletController owns a background std::thread worker that, once a wallet is ready,
  refreshes every ~4s and publishes a copyable LiteWalletAppRefreshModel under a mutex.
  Worker auto-starts on lifecycle-ready and is stopped+joined in the destructor. status_
  is written only on the main thread; walletOpen_/syncStarted_ are atomic.
- App::update() calls takeRefreshedModel() and applies it into state_ on the main thread
  (WalletState is non-copyable, so the model crosses the thread boundary, not the state),
  so the existing Balance/Receive/Transactions tabs populate from lite data.
- refreshWalletState() refactored onto refreshModel() (pure, worker-safe).
- testLiteWalletControllerWorkerProducesModel verifies the worker publishes a populated
  model (stable across repeated runs). Builds clean in all configs.

Real-backend smoke (lite_smoke --refresh now runs real output through the parsers) found
two integration bugs, documented in the plan for follow-up:
- syncstatus parser requires synced_blocks/total_blocks but the real idle response is
  {"syncing":"false"} (string), so it fails to parse when not actively syncing.
- the first data query (balance/list) blocks on a full chain sync, which would hang the
  worker's shutdown join — needs a cancel/timeout path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:38:34 -05:00
012341b1a4 feat(lite): M2b-1/2 — shared-bridge refactor + sync/refresh into WalletState
Shared-bridge refactor (litelib is a global singleton; every LiteClientBridge calls
litelib_shutdown() on destruction, so services must not each own one):
- LiteWalletLifecycleService, LiteWalletGateway, LiteSyncService now take a non-owning
  LiteClientBridge*; LiteWalletController owns the single bridge and passes &bridge_.

Sync + controller refresh:
- LiteSyncService::startSync executes the real "sync" command (was a stub).
- LiteWalletController: startSync() (auto-fires when a wallet becomes ready) and
  refreshWalletState(WalletState&) — polls syncstatus, runs gateway.refresh(), maps the
  bundle, applies balances/addresses/transactions/sync into WalletState.

Tests:
- fake_lite_backend.h returns command-shaped JSON (per tests/fixtures/lite/result_parsers.json).
- testLiteWalletControllerRefreshPopulatesState drives the full path against the fake.
- Surfaced + worked around a real integration issue: parseLiteInfoResponse requires
  latest_block_height and the gateway aborts the whole refresh on the first command's
  parse failure (fragile vs partial backend responses; hardening tracked for M2b-3).

Verified: ctest green; lite+backend, full-node, lite-no-backend apps + lite_smoke build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:24:18 -05:00
5586f334a4 feat(lite): real backend integration — controller, M0-M2a wiring, smoke tool, tests
- LiteWalletController (src/wallet/lite_wallet_controller.*): App-owned; runs real
  create/open/restore via the linked SDXL bridge with allowBridgeCalls=true; wipes
  seed/passphrase with sodium_memzero; persists on a ready wallet. M2a:
  applyLiteRefreshModelToWalletState maps a parsed refresh bundle into WalletState
  (zatoshi->DRGX, z/t split, tx typing + confirmations, sync progress).
- App wiring: liteWallet() accessor + init() construction when supportsLiteBackend();
  persist -> settings save.
- settings_page: "Validate" reroutes to the controller for real execution (validation-
  only fallback otherwise); wipes UI secret buffers after submit.
- chain name default -> "main" with load-time migration of legacy "DRAGONX"
  (settings.cpp), preventing the backend "Unknown chain" panic.
- M0: build.sh --lite-backend flag; lite_smoke real-backend tool + CMake targets;
  tests/fake_lite_backend.h deterministic harness.
- Tests (test_phase4): injectable-fake bridge, controller lifecycle, chain-name
  migration, refresh->WalletState mapping; plus the lite test-suite churn-cleanup rewrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:15:44 -05:00
863d015628 feat(lite): lite wallet foundation (inherited working-tree state)
Preserve the previously-uncommitted lite wallet implementation and related dev WIP
under version control:
- src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle,
  sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh
  services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes:
  lifecycle walletReady now parses the response; default chain name -> "main".)
- src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp;
  scripts/build-lite-backend-artifact.sh.
- Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar,
  mining_tab, bootstrap dialog, and version headers captured as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:15:28 -05:00
a78a13edf3 docs(lite): add v2 implementation plan, source-hygiene guard, and CLAUDE.md
- docs/lite-wallet-implementation-plan-v2-2026-06-04.md: vertical-slice plan that
  supersedes the v1 plan (now banner-marked); carries over the inherited artifact/
  signing/phase-2 design docs for reference.
- scripts/check-source-hygiene.sh: pre-commit/CI guard rejecting >80-char filenames
  and chained churn-token names, to stop the deleted "_plan"/"_batch" scaffolding
  from regrowing.
- CLAUDE.md: repository guidance for future sessions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:15:11 -05:00
e95ad50e41 feat(lite): add ObsidianDragonLite build mode and gate full-node features
Add --lite build flow and ObsidianDragonLite target naming, hide full-node pages/features in lite mode, enforce pool-only mining in lite, and include chat port feasibility audit documentation.
2026-05-06 03:42:05 -05:00
975743f754 feat(wallet): persist history and surface pending sends
Add an encrypted SQLite transaction history cache with cached tip metadata and
per-address shielded scan progress so startup and full refreshes avoid
re-scanning every z-address while still invalidating on wallet/address/rescan
changes.

Improve wallet history loading by paging transparent transactions, preserving
cached shielded and sent rows, keeping recent/unconfirmed activity visible, and
classifying mining-address receives. Show z_sendmany opid sends immediately in
History and Overview, pin pending rows through refreshes, and apply optimistic
address/balance debits until opids resolve.

Add timestamped RPC console tracing by source/method without logging params or
results, reduce redundant refresh/RPC calls, and cache Explorer recent block
summaries in SQLite.

Expand focused tests for transaction cache encryption, scan-progress
persistence/invalidation, history preservation, operation-status parsing,
pending send visibility, and Explorer/RPC refresh behavior.
2026-05-05 03:22:14 -05:00
948ef419ac fix(history): keep wallet-created sends visible
Replay cached outgoing viewtransaction entries during transaction refresh so shielded sends created from the wallet remain in the History tab after send tracking is cleared.

Keep incomplete tracked sends retryable, preserve cached send timestamp/confirmation metadata, and emit a send placeholder from gettransaction metadata when viewtransaction enrichment is not yet available.

Add regression coverage for cached sends, retryable empty entries, placeholder sends, and send txid cleanup behavior.
2026-04-30 14:57:37 -05:00
9edab31728 Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed
  refresh results, ordered RPC collectors, applicators, and price parsing.
- Add daemon lifecycle and wallet security workflow helpers while preserving
  App-owned command RPC, decrypt, cancellation, and UI handoff behavior.
- Split balance, console, mining, amount formatting, and async task logic into
  focused modules with expanded Phase 4 test coverage.
- Fix market price loading by triggering price refresh immediately, avoiding
  queue-pressure drops, tracking loading/error state, and adding translations.
- Polish send, explorer, peers, settings, theme/schema, and related tab UI.
- Replace checked-in generated language headers with build-generated resources.
- Document the cleanup audit, UI static-state guidance, and architecture updates.
2026-04-29 12:47:57 -05:00
ee8a08e569 feat(addresses): improve address labeling and view-only handling
- Add expanded address icon picker with search, bottom-aligned actions, and improved modal sizing
- Embed a pickaxe icon font subset and wire it into typography/address icon rendering
- Track view-only shielded addresses and prevent sends from non-spendable z-addresses
- Improve address transfer dialog sizing, max amount handling, and text clipping
- Tune main header layout values in ui.toml
- Update README, codebase overview, and third-party license documentation
2026-04-27 13:54:28 -05:00
ddb810e2f3 fix: drag-to-transfer drop not triggering transfer dialog
s_dropTargetIdx was reset to -1 unconditionally each frame, including
the release frame. Since drop target detection runs in PASS 2 (after
the drop handler), the target was always -1 when checked. Only reset
while mouse button is held so the previous frame's value is preserved.

Also bump version to 1.2.0-rc1 and add release notes.
2026-04-12 19:07:41 -05:00
aa26ab5fbd fix: sidebar nav text overflow for long translations
- Add text scaling for section labels (TOOLS, ADVANCED) in sidebar
- Separate explorer_section key from explorer nav label to fix ALL CAPS
- Shorten long sidebar translations: es/pt settings, pt overview, ru tools/advanced
- Fix explorer translations from ALL CAPS to proper case in all languages
2026-04-12 18:45:48 -05:00
40cec14ebf Add bootstrap download dialog and fix 100 missing translation keys
- New BootstrapDownloadDialog accessible from Settings page
  - Stops daemon before download, prevents auto-restart during bootstrap
  - Confirm/Downloading/Done/Failed states with progress display
  - Mirror support (bootstrap2.dragonx.is)
- Add bootstrap_downloading_ flag to prevent tryConnect() auto-reconnect
- Right-align Download Bootstrap + Setup Wizard buttons in settings
- Add 100 missing i18n keys to all 8 language files (de/es/fr/ja/ko/pt/ru/zh)
  - Includes bootstrap, explorer, mining benchmark, transfer, delete blockchain,
    force quit, address label, and settings section translations
- Update add_missing_translations.py with new translation batch
2026-04-12 18:19:01 -05:00
88d30c1612 feat: modernize address list with drag-transfer, labels, and UX polish
- Rewrite RenderSharedAddressList with two-pass layout architecture
- Add drag-to-transfer: drag address onto another to open transfer dialog
- Add AddressLabelDialog with custom label text and 20-icon picker
- Add AddressTransferDialog with amount input, fee, and balance preview
- Add AddressMeta persistence (label, icon, sortOrder) in settings.json
- Gold favorite border inset 2dp from container edge
- Show hide button on all addresses, not just zero-balance
- Smaller star/hide buttons to clear favorite border
- Semi-transparent dragged row with context-aware tooltip
- Copy-to-clipboard deferred to mouse-up (no copy on drag)
- Themed colors via resolveColor() with CSS variable fallbacks
- Keyboard nav (Up/Down/J/K, Enter to copy, F2 to edit label)
- Add i18n keys for all new UI strings
2026-04-12 17:29:56 -05:00
dbe6546f9f refactor: rewrite sidebar layout with two-pass architecture
Replace fragile Dummy()-based cursor flow with a deterministic two-pass
layout system:
- Pass 1: compute exact Y positions for all elements (pure math)
- Pass 2: render at computed positions using SetCursorScreenPos + draw list

Eliminates the dual-coordinate mismatch that caused persistent centering
and overflow bugs. Height is computed once, not estimated then measured.

Also tune sidebar spacing via ui.toml:
- button-spacing: 4 → 6
- section-gap: 4 → 8
- Add section-label-pad-bottom (4px) below category labels
- bottom-padding: 0 → 4
2026-04-12 16:34:31 -05:00
6e2db50675 fix: accurate sync speed display, add missing i18n keys, native language names
- Fix blk/s calculation that was inflated ~10x due to resetting the
  time baseline every frame instead of only when blocks advanced
- Add decay when no new blocks arrive for 10s so rate doesn't stay stale
- Add 7 missing translation keys (timeout_off/1min/5min/15min/30min/1hour,
  slider_off) to all 8 language files so settings dropdowns translate
- Show language names in native script (中文, Русский, 日本語, 한국어)
2026-04-12 15:12:36 -05:00
d2dccbac05 feat: non-blocking warmup — connect during daemon initialization
Instead of blocking the entire UI with "Activating best chain..." until
the daemon finishes warmup, treat warmup responses as a successful
connection. The wallet now:

- Sets connected=true + warming_up=true when daemon returns RPC -28
- Shows warmup status with block progress in the loading overlay
- Polls getinfo every few seconds to detect warmup completion
- Allows Console, Peers, Settings tabs during warmup
- Shows orange status indicator with warmup message in status bar
- Skips balance/tx/address refresh until warmup completes
- Triggers full data refresh once daemon is ready

Also: fix curl handle/header leak on reconnect, fill in empty
externalDetected error branch, bump version to v1.2.0 in build scripts.
2026-04-12 14:32:57 -05:00
1860e9b277 fix: auto-refresh peers list, show warmup status during daemon startup
- Fix peer timer calling refreshEncryptionState() instead of
  refreshPeerInfo(), so the Network tab now auto-updates every 5s
- Reorder RPC error handling so warmup messages (Loading block index,
  Verifying blocks, etc.) display in the status bar instead of being
  masked by the generic "Waiting for dragonxd" message
2026-04-12 13:43:45 -05:00
648a6c29e0 feat: use DragonX DNS seed nodes, pass -maxconnections to daemon, show sync speed
- Replace hardcoded IP addnodes with node.dragonx.is, node1–4.dragonx.is
  in both daemon launch params and auto-generated DRAGONX.conf
- Add max_connections setting (persisted, default 0 = daemon default);
  passed as -maxconnections= flag to dragonxd on startup
- Show blocks/sec in status bar during sync with exponential smoothing
  (e.g. "Syncing 45.2% (12340 left, 85 blk/s)")
2026-04-12 13:22:22 -05:00
c013038ef7 feat: CJK font rendering, force quit confirmation, settings i18n
- Rebuild CJK font subset (1421 glyphs) and convert CFF→TTF for
  stb_truetype compatibility, fixing Chinese/Japanese/Korean rendering
- Add force quit confirmation dialog with cancel/confirm actions
- Show force quit tooltip immediately on hover (no delay)
- Translate hardcoded English strings in settings dropdowns
  (auto-lock timeouts, slider "Off" labels)
- Fix mojibake en-dashes in 7 translation JSON files
- Add helper scripts: build_cjk_subset, convert_cjk_to_ttf,
  check_font_coverage, fix_mojibake
2026-04-12 10:32:58 -05:00
20cbad687d Redesign benchmark to measure sustained (thermally throttled) hashrate
instead of initial burst performance. Previously the benchmark used a
fixed 20s warmup + 10s peak measurement, which reported inflated
results on thermally constrained hardware (e.g. 179 H/s vs actual
sustained 117 H/s on a MacBook Pro).

- Adaptive warmup with stability detection: mine for at least 90s,
  then compare rolling 10s hashrate windows. Require 3 consecutive
  windows within 5% before declaring thermal equilibrium (cap 300s)
- Average-based measurement: record mean hashrate over 30s instead
  of peak, reflecting real sustained throughput
- Start candidates at half the system cores — lower thread counts
  are rarely optimal and waste time warming up
- Add CoolingDown phase: 5s idle pause between tests so each starts
  from a similar thermal baseline
- Adaptive time estimates: use observed warmup durations from
  completed tests to predict remaining time
- UI shows Stabilizing when waiting for thermal equilibrium past
  the minimum warmup, Cooling during idle pauses"
2026-04-06 13:51:56 -05:00
ddca8b2e43 v1.2.0: UX audit — security fixes, accessibility, and polish
Security (P0):
- Fix sidebar remaining interactive behind lock screen
- Extend auto-lock idle detection to include active widget interactions
- Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase

Blocking UX (P1):
- Add 15s timeout for encryption state check to prevent indefinite loading
- Show restart reason in loading overlay after wallet encryption
- Add Force Quit button on shutdown screen after 10s
- Warn user if embedded daemon fails to start during wizard completion

Polish (P2):
- Use configured explorer URL in Receive tab instead of hardcoded URL
- Increase request memo buffer from 256 to 512 bytes to match Send tab
- Extend notification duration to 5s for critical operations (tx sent,
  wallet encrypted, key import, backup, export)
- Add Reduce Motion accessibility setting (disables page fade + balance lerp)
- Show estimated remaining time during mining thread benchmark
- Add staleness indicator to market price data (warning after 5 min)

New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set,
restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion,
ago, wizard_daemon_start_failed
2026-04-04 19:10:58 -05:00
50e9e7d75e refactor: tab-aware prioritized refresh system
Split monolithic refreshData() into independent sub-functions
(refreshCoreData, refreshAddressData, refreshTransactionData,
refreshEncryptionState) each with its own timer and atomic guard.

Per-category timers replace the single 5s refresh_timer_:
- core_timer_: balance + blockchain info (5s default)
- transaction_timer_: tx list + enrichment (10s default)
- address_timer_: z/t address lists (15s default)
- peer_timer_: encryption state (10s default)

Tab-switching via setCurrentPage() adjusts active intervals so
the current tab's data refreshes faster (e.g. 3s core on Overview,
5s transactions on History) while background categories slow down.

Use fast_worker_ for core data on Overview tab to avoid blocking
behind the main refresh batch.

Bump version to 1.1.2.
2026-04-04 13:05:00 -05:00
d755f6816b refactor: extract AI/agent files into separate repo
ObsidianDragon-agent/ is now a standalone git repo (future submodule)
so AI configuration files are not pushed to the main repository.

- Remove copilot-instructions.md and ARCHITECTURE.md from main tracking
- Remove symlinks from .github/ and docs/
- Add ObsidianDragon-agent/ and .github/ to .gitignore
2026-04-04 11:36:04 -05:00
84d2b9c39d refactor: move AI/agent files into ObsidianDragon-agent/
- copilot-instructions.md → ObsidianDragon-agent/copilot-instructions.md
- ARCHITECTURE.md → ObsidianDragon-agent/ARCHITECTURE.md
- Symlinks at original locations preserve Copilot auto-discovery
2026-04-04 11:29:12 -05:00
27e9a8df26 docs: add ARCHITECTURE.md with project overview
Covers directory layout, threading model, RPC architecture,
connection lifecycle, UI system, build system, and key conventions.
2026-04-04 11:17:21 -05:00
8d51f374cd docs: add copilot-instructions.md and file-level comments
- Create .github/copilot-instructions.md with project coding standards,
  architecture overview, threading model, and key rules for AI sessions
- Add module description comments to app.cpp, rpc_client.cpp, rpc_worker.cpp,
  embedded_daemon.cpp, xmrig_manager.cpp, console_tab.cpp, settings.cpp
- Add ASCII connection state diagram to app_network.cpp
- Remove /.github/ from .gitignore so instructions file is tracked
2026-04-04 11:14:31 -05:00
7ab8f5d82c fix: console not connected when fast-lane RPC still connecting
The console tab was passed fast_rpc_ even before its async connection
completed, causing 'Not connected to daemon' errors despite the main
RPC being connected and sync data flowing. Fall back to the main
rpc_/worker_ until fast_rpc_ reports isConnected().
2026-04-03 11:34:32 -05:00
e4b1b644b3 build: macOS universal binary (arm64+x86_64) with deployment target 11.0
- Set CMAKE_OSX_DEPLOYMENT_TARGET and CMAKE_OSX_ARCHITECTURES before
  project() so they propagate to all FetchContent dependencies (SDL3, etc.)
- build.sh: native mac release builds universal binary, detects and
  rebuilds single-arch libsodium, verifies with lipo, exports
  MACOSX_DEPLOYMENT_TARGET; dev build uses correct build/mac directory
- fetch-libsodium.sh: build arm64 and x86_64 separately then merge with
  lipo on native macOS; fix sha256sum unavailable on macOS (use shasum)
2026-04-03 10:55:07 -05:00
8ef8abeb37 feat: thread benchmark, GPU-aware idle mining, thread scaling fix
- Add pool mining thread benchmark: cycles through thread counts with
  20s warmup + 10s measurement to find optimal setting for CPU
- Add GPU-aware idle detection: GPU utilization >= 10% (video, games)
  treats system as active; toggle in mining tab header (default: on)
  Supports AMD sysfs, NVIDIA nvidia-smi, Intel freq ratio; -1 on macOS
- Fix idle thread scaling: use getRequestedThreads() for immediate
  thread count instead of xmrig API threads_active which lags on restart
- Apply active thread count on initial mining start when user is active
- Skip idle mining adjustments while benchmark is running
- Disable thread grid drag-to-select during benchmark
- Add idle_gpu_aware setting with JSON persistence (default: true)
- Add 7 i18n English strings for benchmark and GPU-aware tooltips
2026-04-01 17:06:05 -05:00
09f876eb60 update build output filenames to include version info 2026-03-25 11:24:21 -05:00
aa3bd4e304 update hardcoded version for mac dmg build 2026-03-25 11:18:03 -05:00
801fa2b96b feat: track shielded send txids via z_viewtransaction
Extract txids from completed z_sendmany operations and store in
send_txids_ so pure shielded sends are discoverable. The network
thread includes them in the enrichment set, calls z_viewtransaction,
caches results in viewtx_cache_, and removes them from send_txids_.
2026-03-25 11:06:09 -05:00
e0bfeb2f29 fix: macOS block index corruption, dbcache auto-sizing, import key rescan height
- Shutdown: 3-phase stop (wait for RPC stop → SIGTERM → SIGKILL) prevents
  LevelDB flush interruption on macOS/APFS that caused full re-sync on restart
- dbcache: auto-detect RAM and set -dbcache to 12.5% (clamped 450-4096 MB)
  on macOS (sysctl), Linux (sysconf), and Windows (GlobalMemoryStatusEx)
- Import key: pass user-entered start height to z_importkey and trigger
  rescanblockchain from that height for t-key imports
- Bump version to 1.1.1
2026-03-25 11:00:14 -05:00
9ed4fbc476 update mac icons 2026-03-19 14:46:33 -05:00
2b3277529e macOS port: build, rendering, daemon, and mining fixes
Build & setup:
- Fix setup.sh and build.sh for macOS (bundle daemon, xmrig, sapling params, asmap.dat into .app)
- Fix CMakeLists.txt libsodium linking for macOS
- Fix incbin.h to use __DATA,__const section on macOS
- Remove vendored libsodium-1.0.18 source tree (use fetch script instead)
- Remove prebuilt-binaries/xmrig (replaced by xmrig-hac)
- Add .DS_Store to .gitignore

Rendering & UI:
- Use GLSL #version 150 and OpenGL 3.2 Core Profile on macOS
- Force dpiScale=1.0 on macOS to fix Retina double-scaling
- Set default window/UI opacity to 100% on Mac/Linux
- Add scroll fade shader guard for macOS GL compatibility
- Add ImGui error recovery around render loop and mining tab

Daemon & bootstrap:
- Fix getDragonXDataDir() to return ~/Library/Application Support/Hush/DRAGONX/ on macOS
- Fix isPortInUse() with connect() fallback (no /proc/net/tcp on macOS)
- Increase daemon watchdog timeout from 3s to 15s
- Add daemon status indicator (colored dot + label) in wizard bootstrap phases

Mining tab:
- Fix EmbeddedDaemon::getMemoryUsageMB() crash on macOS (was using Linux /proc)
- Fix XmrigManager::getMemoryUsageMB() to use ps on macOS instead of /proc
- Restructure RenderMiningTab with wrapper pattern for exception safety
- Fix default pool URL to include port (pool.dragonx.is:3433)
2026-03-19 14:26:04 -05:00
d79d013060 feat: sync thread grid during idle scaling, skip lock screen while pool mining, add paste preview to import key dialog
- Mining tab: sync s_selected_threads with actual thread count when idle
  thread scaling adjusts threads (solo via genproclimit, pool via
  threads_active), skipping sync during user drag
- Auto-lock: bypass lock screen overlay when xmrig pool mining is active
  so the mining UI remains accessible
- Import key dialog: add clipboard hover preview with transparent overlay
  on the input field, inline key type validation next to title (matching
  send tab paste button pattern), configurable via ui.toml
2026-03-19 06:10:46 -05:00
474250bb50 v1.1.0: explorer tab, bootstrap fixes, full theme overlay merge
Explorer tab:
- New block explorer tab with search, chain stats, mempool info,
  recent blocks table, block detail modal with tx expansion
- Sidebar nav entry, i18n strings, ui.toml layout values

Bootstrap fixes:
- Move wizard Done handler into render() — was dead code, preventing
  startEmbeddedDaemon() and tryConnect() from firing post-wizard
- Stop deleting BDB database/ dir during cleanup — caused LSN mismatch
  that salvaged wallet.dat into wallet.{timestamp}.bak
- Add banlist.dat, db.log, .lock to cleanup file list
- Fatal extraction failure for blocks/ and chainstate/ files
- Verification progress: split SHA-256 (0-50%) and MD5 (50-100%)

Theme system:
- Expand overlay merge to apply ALL sections (tabs, dialogs, components,
  screens, flat sections), not just theme+backdrop+effects
- Add screens and security section parsing to UISchema
- Build-time theme expansion via expand_themes.py (CMake + build.sh)

Other:
- Version bump to 1.1.0
- WalletState::clear() resets all fields (sync, daemon info, etc.)
- Sidebar item-height 42 → 36
2026-03-17 18:49:46 -05:00
40dd6d45b2 daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup
- Add startup binary version checking for dragonxd/xmrig
- Display daemon version in UI
- Add idle mining thread count adjustment
- Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard
- Add paste button to import private key dialog with address validation
- Add z-address generation UI feedback (loading indicator)
- Add option to delete blockchain data while preserving wallet.dat
- Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus)
- Fix Windows RPC auth: trim \r from config values, add .cookie fallback
- Fix connection status message during block index loading
- Improve application shutdown to prevent lingering background process
2026-03-17 14:57:12 -05:00
1b97476a54 update version to v1.0.2 2026-03-12 02:29:08 -05:00
d7bc5c638a fix: remove D3D11 debug layer flag that prevented startup on user machines
DRAGONX_DEBUG was defined unconditionally, causing D3D11CreateDevice() to
request the debug layer via D3D11_CREATE_DEVICE_DEBUG. This layer is only
available on machines with the Windows SDK or Graphics Tools installed,
so the call fails with DXGI_ERROR_SDK_COMPONENT_MISSING on regular user
machines — causing the app to silently exit.
2026-03-12 00:13:27 -05:00
39f193a264 fix xmrig bundling issues 2026-03-11 21:14:03 -05:00
4023af9466 fix scrolling bug 2026-03-11 03:15:31 -05:00
853e3e0f17 build: Linux release outputs binaries zip + AppImage, bundle sapling params
- Linux --linux-release now produces both ObsidianDragon-Linux-x64.zip
  (raw binaries) and ObsidianDragon.AppImage (single-file)
- Windows --win-release keeps standalone exe alongside zip with binaries
- Bundle sapling-spend.params and sapling-output.params in Linux release
2026-03-11 01:38:59 -05:00
1bcfcd1c73 ui: add idle delay combo to mining tab
Add inline combo box (30s/1m/2m/5m/10m) next to the idle mining
toggle so users can choose how long to wait before idle mining starts.
2026-03-11 01:38:48 -05:00
6bb35bd3e1 ui: reorganize settings page with collapsible sections
- Rename APPEARANCE section to THEME & LANGUAGE
- Move font scale slider out of effects into main section
- Collapse visual effects into "Advanced Effects..." toggle
- Collapse wallet tools into "Tools & Actions..." toggle
- Remove redundant Tools & Actions divider/section from wallet card
- Add i18n strings: theme_language, advanced_effects, tools_actions
2026-03-11 01:38:40 -05:00
2c5a658ea5 feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
2026-03-11 00:40:50 -05:00
f416ff3d09 Add mine-when-idle, default banlist, and console parsing improvements
Mine-when-idle:
- Auto-start/stop mining based on system idle time detection
- Platform::getSystemIdleSeconds() via XScreenSaver (Linux) / GetLastInputInfo (Win)
- Settings: mine_when_idle toggle + configurable delay (30s–10m)
- Settings page UI with checkbox and delay combo

Console tab:
- Shell-like argument parsing with quote and JSON bracket support
- Pass JSON objects/arrays directly as RPC params
- Fix selection indices when lines are evicted from buffer

Connection & status bar:
- Reduce RPC connect timeout to 1s for localhost fast-fail
- Fast retry timer on daemon startup and external daemon detection
- Show pool mining hashrate in status bar; sidebar badge reflects pool state

UI polish:
- Add logo to About card in settings; expose logo dimensions on App
- Header title offset-y support; adjust content-area margins
- Fix banned peers row cursor position (rawRowPosB.x)

Branding:
- Update copyright to "DragonX Developers" in RC and About section
- Replace logo/icon assets with updated versions

Misc:
- setup.sh: checkout dragonx branch before pulling
- Remove stale prebuilt-binaries/xmrig/.gitkeep
2026-03-07 13:42:31 -06:00
b3a0ce29ed fix: Windows identity, async address creation, mining UI, and chart artifacts
Windows identity:
- Add VERSIONINFO resource (.rc) with ObsidianDragon file description
- Embed application manifest for DPI awareness and shell identity
- Patch libwinpthread/libpthread to remove competing VERSIONINFO
- Set AppUserModelID and HWND property store to override Task Manager cache
- Link patched pthread libs to eliminate "POSIX WinThreads" description

Address creation (+New button):
- Move z_getnewaddress/getnewaddress off UI thread to async worker
- Inject new address into state immediately for instant UI selection
- Trigger background refresh for balance updates

Mining tab:
- Add pool mining dropdown with saved URLs/workers and bookmarks
- Add solo mining log panel from daemon output with chart/log toggle
- Fix toggle button cursor (render after InputTextMultiline)
- Auto-restart miner on pool config change
- Migrate default pool URL to include stratum port

Transactions:
- Sort pending (0-conf) transactions to top of history
- Fall back to timereceived when timestamp is missing

Shutdown:
- Replace blocking sleep_for calls with 100ms polling loops
- Check shutting_down_ flag throughout daemon restart/bootstrap flows
- Reduce daemon stop timeout from 30s to 10s

Other:
- Fix market chart fill artifact (single concave polygon vs per-segment quads)
- Add bootstrap checksum verification state display
- Rename daemon client identifier to ObsidianDragon
2026-03-05 22:43:27 -06:00
e2265b0bdf improve diagnostics, security UX, and network tab refresh
Diagnostics & logging:
- add verbose logging system (VERBOSE_LOGF) with toggle in Settings
- forward app-level log messages to Console tab for in-UI visibility
- add detailed connection attempt logging (attempt #, daemon state,
  config paths, auth failures, port owner identification)
- detect HTTP 401 auth failures and show actionable error messages
- identify port owner process (PID + name) on both Linux and Windows
- demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF
- persist verbose_logging preference in settings.json
- link iphlpapi on Windows for GetExtendedTcpTable

Security & encryption:
- update local encryption state immediately after encryptwallet RPC
  so Settings reflects the change before daemon restarts
- show notifications for encrypt success/failure and PIN skip
- use dedicated RPC client for z_importwallet during decrypt flow
  to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh)
- force full state refresh (addresses, transactions, peers) after
  successful wallet import

Network tab:
- redesign peers refresh button as glass-panel with icon + label,
  matching the mining button style
- add spinning arc animation while peer data is loading
  (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo)
- prevent double-click spam during refresh
- add refresh-button size to ui.toml

Other:
- use fast_rpc_ for rescan polling to avoid blocking on main rpc_
- enable DRAGONX_DEBUG in all build configs (was debug-only)
- setup.sh: pull latest xmrig-hac when repo already exists
2026-03-05 05:26:04 -06:00
9368b945e0 fix text shifting in status bar from font scale changes 2026-03-05 01:29:03 -06:00
eda19ef6d5 improved font scaling text and window adjustment, added ctrl + scroll hotkey for font scaling 2026-03-05 01:22:20 -06:00
7d48936f30 refresh network info instantly when switching to network tab 2026-03-04 15:16:32 -06:00
bc2ef4ffaf feat: RPC caching, background decrypt import, fast-lane peers, mining fix
RPC client:
- Add call() overload with per-call timeout parameter
- z_exportwallet uses 300s, z_importwallet uses 1200s timeout

Decrypt wallet (app_security.cpp, app.cpp):
- Show per-step and overall elapsed timers during decrypt flow
- Reduce dialog to 5 steps; close before key import begins
- Run z_importwallet on detached background thread
- Add pulsing "Importing keys..." status bar indicator
- Report success/failure via notifications instead of dialog

RPC caching (app_network.cpp, app.h):
- Cache z_viewtransaction results in viewtx_cache_ across refresh cycles
- Skip RPC calls for already-cached txids (biggest perf win)
- Build confirmed_tx_cache_ for deeply-confirmed transactions
- Clear all caches on disconnect
- Remove unused refreshTransactions() dead code

Peers (app_network.cpp, peers_tab.cpp):
- Route refreshPeerInfo() through fast_worker_ to avoid head-of-line blocking
- Replace footer "Refresh Peers" button with ICON_MD_REFRESH in toggle header
- Refresh button triggers both peer list and full blockchain data refresh

Mining (mining_tab.cpp):
- Allow pool mining toggle when blockchain is not synced
- Pool mining only needs xmrig, not local daemon sync
2026-03-04 15:12:24 -06:00
0da1657b12 Rename hush→dragonx across wallet codebase
- Rename RESOURCE_HUSHD/HUSH_CLI/HUSH_TX to RESOURCE_DRAGONXD/DRAGONX_CLI/DRAGONX_TX
- Remove unused .bat resource constants (DRAGONXD_BAT, DRAGONX_CLI_BAT)
- Update INCBIN symbols: g_hushd_exe → g_dragonxd_exe, etc.
- Update daemon search paths, removing hush-arrakis-chain fallbacks
- Update process detection (Windows findProcessByName, Linux /proc/comm, macOS pgrep)
- Update build.sh: embed dragonxd.exe/dragonx-cli.exe/dragonx-tx.exe
- Overhaul setup.sh: fix binary names, release paths, add -j passthrough
- Update getDaemonPath/needsDaemonExtraction/hasDaemonAvailable for new names
2026-03-04 03:17:32 -06:00
0d30ebc8e8 setup script improvements, automatically clone xmrig-hac and build for multiple platforms 2026-03-03 01:47:44 -06:00
c778e03f37 update links 2026-03-03 01:20:03 -06:00
0a3f68f1eb change release output names 2026-02-28 15:28:40 -06:00
1a5c4e8744 feat: blockchain rescan via daemon restart + status bar progress
- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
2026-02-28 15:06:35 -06:00
388 changed files with 85448 additions and 29599 deletions

View File

@@ -1,173 +0,0 @@
# Copilot Instructions — DragonX ImGui Wallet
## UI Layout: All values in `ui.toml`
**Every UI layout constant must be defined in `res/themes/ui.toml` and read at runtime via the schema API.** Never hardcode pixel sizes, ratios, rounding values, thicknesses, or spacing constants directly in C++ source files. This is critical for maintainability, theming support, and hot-reload.
### Schema API reference
The singleton is accessed via `schema::UI()` (header: `#include "../schema/ui_schema.h"`).
| Method | Returns | Use for |
|---|---|---|
| `drawElement(section, name)` | `DrawElementStyle` | Custom DrawList layout values (`.size`, `.height`, `.thickness`, `.radius`, `.opacity`) |
| `button(section, name)` | `ButtonStyle` | Button width/height/font |
| `input(section, name)` | `InputStyle` | Input field dimensions |
| `label(section, name)` | `LabelStyle` | Label styling |
| `table(section, name)` | `TableStyle` | Table layout |
| `window(section, name)` | `WindowStyle` | Window/dialog dimensions |
| `combo(section, name)` | `ComboStyle` | Combo box styling |
| `slider(section, name)` | `SliderStyle` | Slider styling |
| `checkbox(section, name)` | `CheckboxStyle` | Checkbox styling |
| `separator(section, name)` | `SeparatorStyle` | Separator/divider styling |
### Section naming convention
Sections use dot-separated paths matching the file/feature:
- `tabs.send`, `tabs.receive`, `tabs.transactions`, `tabs.mining`, `tabs.peers`, `tabs.market` — tab-specific values
- `tabs.balance` — balance/home tab
- `components.main-layout`, `components.settings-page` — shared components
- `dialogs.about`, `dialogs.backup`, etc. — dialog-specific values
- `sidebar` — navigation sidebar
### How to add a new layout value
1. **Add the entry to `res/themes/ui.toml`** under the appropriate section:
```toml
[tabs.example]
my-element = {size = 42.0}
```
2. **Read it in C++** instead of using a literal:
```cpp
// WRONG — hardcoded
float myValue = 42.0f;
// CORRECT — schema-driven
float myValue = schema::UI().drawElement("tabs.example", "my-element").size;
```
3. For values used as min/max pairs with scaling:
```cpp
// WRONG
float h = std::max(18.0f, 24.0f * vScale);
// CORRECT
float h = std::max(
schema::UI().drawElement("tabs.example", "row-min-height").size,
schema::UI().drawElement("tabs.example", "row-height").size * vScale
);
```
### What belongs in `ui.toml`
- Pixel sizes (card heights, icon sizes, bar widths/heights)
- Ratios (column width ratios, max-width ratios)
- Rounding values (`FrameRounding`, corner radius)
- Thickness values (accent bars, chart lines, borders)
- Dot/circle radii
- Fade zones, padding constants
- Min/max dimension bounds
- Font selection (via schema font name strings, resolved with `S.resolveFont()`)
- Colors (via `schema::UI().resolveColor()` or color variable references like `"var(--primary)"`)
- Animation durations (transition times, fade durations, pulse speeds)
- Business logic values (fee amounts, ticker strings, buffer sizes, reward amounts)
### What does NOT belong in `ui.toml`
- Spacing that already goes through `Layout::spacing*()` or `spacing::dp()`
### Legacy system: `UILayout`
`UILayout::instance()` is the older layout system still used for fonts, typography, panels, and global spacing. New layout values should use `schema::UI().drawElement()` instead. Do not add new keys to `UILayout`.
### Validation
After editing `ui.toml`, always validate:
```bash
python3 -c "import toml; toml.load('res/themes/ui.toml'); print('Valid TOML')"
```
Or with the C++ toml++ parser (which is what the app uses):
```bash
cd build && make -j$(nproc)
```
### Build
```bash
# Linux
cd build && make -j$(nproc)
# Windows cross-compile
./build.sh --win-release
```
## Plans
When asked to "create a plan", always create a new markdown document in the `docs/` directory with the plan contents.
## Icons: Use Material Design icon font, never Unicode symbols
**Never use raw Unicode symbols or emoji characters** (e.g. ``, ``, ``, `🔍`, `📬`, `⚠️`, ``) for icons in C++ code. Always use the **Material Design Icons font** via the `ICON_MD_*` defines from `#include "../../embedded/IconsMaterialDesign.h"`.
### Icon font API
| Method | Size | Fallback |
|---|---|---|
| `Type().iconSmall()` | 14px | Body2 |
| `Type().iconMed()` | 18px | Body1 |
| `Type().iconLarge()` | 24px | H5 |
| `Type().iconXL()` | 40px | H3 |
### Correct usage
```cpp
#include "../../embedded/IconsMaterialDesign.h"
// WRONG — raw Unicode symbol
itemSpec.leadingIcon = "↙";
// CORRECT — Material Design icon codepoint
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
// WRONG — emoji for search
searchSpec.leadingIcon = "🔍";
// CORRECT — Material Design icon
searchSpec.leadingIcon = ICON_MD_SEARCH;
// For rendering with icon font directly:
ImGui::PushFont(Type().iconSmall());
ImGui::TextUnformatted(ICON_MD_ARROW_DOWNWARD);
ImGui::PopFont();
```
### Why
Raw Unicode symbols and emoji render inconsistently across platforms and may not be present in the loaded text fonts. The Material Design icon font is always loaded and provides consistent, scalable glyphs on both Linux and Windows.
### Audit for Unicode symbols
Before completing any task that touches UI code, search for and replace any raw Unicode symbols that may have been introduced. Common symbols to look for:
| Unicode | Replacement |
|---------|-------------|
| `` `` | `ICON_MD_PLAY_ARROW` |
| `` `` `` `` | `ICON_MD_STOP` or `ICON_MD_SQUARE` |
| `` `` `` `` | `ICON_MD_FIBER_MANUAL_RECORD` or `ICON_MD_CIRCLE` |
| `` `` `` `` | `ICON_MD_ARROW_UPWARD`, `_DOWNWARD`, `_BACK`, `_FORWARD` |
| `` `` `` `` | `ICON_MD_CALL_RECEIVED`, `_MADE`, etc. |
| `` `` | `ICON_MD_CHECK` |
| `` `` `` | `ICON_MD_CLOSE` |
| `` `⚠️` | `ICON_MD_WARNING` |
| `` `` | `ICON_MD_INFO` |
| `🔍` | `ICON_MD_SEARCH` |
| `📋` | `ICON_MD_CONTENT_COPY` or `ICON_MD_DESCRIPTION` |
| `🛡` `🛡️` | `ICON_MD_SHIELD` |
| `` | `ICON_MD_HOURGLASS_EMPTY` |
| `🔄` `` `` | `ICON_MD_SYNC` or `ICON_MD_REFRESH` |
| `` `⚙️` | `ICON_MD_SETTINGS` |
| `🔒` | `ICON_MD_LOCK` |
| `` `` | `ICON_MD_STAR` or `ICON_MD_STAR_BORDER` |

23
.gitignore vendored
View File

@@ -32,4 +32,25 @@ imgui.ini
*.bak
*.bak*
*.params
asmap.dat
asmap.dat
/external/xmrig-hac
/memory
/todo.md
/.github/
/ObsidianDragon-agent/
# macOS
.DS_Store
# Local-only archive of superseded lite-wallet design/planning docs (untracked)
docs/_archive/
# ed25519 release-signing keys — the secret key must NEVER be committed
*.ed25519.key
*.ed25519.pub.b64
# Lite-backend deps are fetched (or `cargo vendor`-ed locally for offline); not committed.
third_party/silentdragonxlite/lib/vendor/
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
res/ObsidianDragon.manifest

102
CLAUDE.md Normal file
View File

@@ -0,0 +1,102 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
ObsidianDragon is a portable, full-node GUI wallet for DragonX (DRGX), written in C++17 using SDL3 + Dear ImGui (immediate-mode). It drives a `dragonxd` full node over JSON-RPC and can embed/extract the daemon itself. A separate **Lite** variant (`ObsidianDragonLite`) drops the full node and instead talks to an external lite-wallet backend library.
## Build & run
`build.sh` is the single entry point for all builds. `setup.sh` (repo root) installs/validates dependencies.
```bash
./build.sh # Dev build (native, no packaging) -> build/linux/bin/ObsidianDragon
./build.sh --lite # Dev build of the Lite variant -> build/linux/bin/ObsidianDragonLite
./build.sh --clean # Wipe the build dir first
./build.sh --linux-release # Release zip + AppImage -> release/linux/
./build.sh --win-release # Windows cross-compile (mingw-w64) -> release/windows/
./build.sh --mac-release # macOS .app bundle + DMG
./setup.sh --check # Report missing build deps without installing
```
Dev builds use `build/linux/` (or `build/mac/`). To re-build incrementally without re-running CMake config: `cmake --build build/linux -j$(nproc)`.
The wallet connects to the daemon using credentials in `~/.hush/DRAGONX/DRAGONX.conf` (`rpcuser`/`rpcpassword`/`rpcport`). It searches for `dragonxd`/`dragonx-cli` binaries in the **executable's own directory first**, so dropping custom node builds next to the wallet binary overrides the bundled ones.
## Tests
Tests live in `tests/test_phase4.cpp` — a single large translation unit using a custom assertion harness (`EXPECT_TRUE`/`EXPECT_EQ`/`EXPECT_NEAR` macros, one `main()`, exit code = failure count). `include(CTest)` enables `BUILD_TESTING=ON` by default, so the `ObsidianDragonTests` executable is built alongside the app.
```bash
cd build/linux && ctest --output-on-failure # run the suite
./build/linux/bin/ObsidianDragonTests # run the binary directly (same thing)
```
There is no per-test filtering — it is one binary that runs every assertion. The suite exercises the services layer, lite-wallet bridge, and pure helpers (parsers, formatters, model classes) without launching the GUI. Fixtures are under `tests/fixtures/` (path injected as `DRAGONX_TEST_FIXTURE_DIR`).
## Architecture
**Entry & main loop.** `src/main.cpp` owns SDL3 window creation, ImGui/OpenGL(or DX11 on Windows) setup, and the frame loop. The `App` class is the central controller; because it is large it is split across four files that all implement the same class:
- `src/app.cpp` — core lifecycle, the per-frame `render()`, tab dispatch
- `src/app_network.cpp` — RPC orchestration, sync, peers, daemon lifecycle
- `src/app_security.cpp` — encryption, PIN/lock screen, key import/export, backup
- `src/app_wizard.cpp` — first-run wizard
**RPC.** All daemon calls go through `src/rpc/` (`rpc_client`, `connection`, `rpc_worker`). **Never block the main/UI thread with synchronous network I/O — dispatch through `RPCWorker`** (async). `rpc/types.h` holds the shared DTOs.
**Services** (`src/services/`) hold the non-UI state machines that the `App` owns: `NetworkRefreshService` + `RefreshScheduler` (polling/refresh of balance, peers, txs on intervals) and the `WalletSecurity*` controller/workflow stack (encryption & unlock flows).
**Data model** (`src/data/`): `WalletState`, `address_book`, `transaction_history_cache`, `exchange_info`. UI reads from these.
**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `material/` is the design-system layer (the live helpers `color_theme`, `colors`, `type`/`typography`, `draw_helpers`, `layout`, `project_icons`, `components/buttons`), `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic).
**Lite wallet** (`src/wallet/`): the bridge to an external `litelib_*` C-ABI backend. `lite_client_bridge` loads the backend (via direct `litelib_*` externs in `linkedSdxl()`) and owns each Rust string through `lite_owned_string` (copy-before-free / free-once). On top sit `lite_connection_service`, `lite_sync_service`, `lite_result_parsers`, `lite_wallet_gateway`, `lite_wallet_state_mapper`, and `lite_wallet_lifecycle_service`, all driven by `lite_wallet_controller`. The real frontend entry points are `lite_wallet_lifecycle_ui_adapter` and `lite_wallet_server_selection_adapter` (used by `src/ui/pages/settings_page.cpp`); everything else is reachable through them. (The prebuilt-backend symbol check for `DRAGONX_ENABLE_LITE_BACKEND` is done in CMake against the symbols inventory — see below — not in C++.)
> ⚠️ **Do not regrow the `_plan`/`_batch` churn.** This directory previously held ~160 dead `lite_wallet_*_plan` / `*_batch*_receipt_custody_acceptance_confirmation_archive_handoff_*` files (filenames up to 250 chars) — auto-generated scaffolding that never reached the shipping binary. They were deleted. When extending lite-wallet behavior, **edit the named service/bridge/runtime files in place**; never add another "promotion/receipt/custody/handoff/stewardship" wrapper layer. `scripts/check-source-hygiene.sh` (wired as a `.git/hooks/pre-commit` hook) blocks >80-char filenames and chained churn-token names — run it in CI too.
**Chat** (`src/chat/chat_protocol.cpp`): experimental HushChat protocol, compiled in only when `DRAGONX_ENABLE_CHAT=ON`.
## Build variants & feature gating
Variants are selected with CMake options (set by `build.sh` flags), surfaced to C++ as compile definitions:
- `DRAGONX_BUILD_LITE` (`--lite`) → `DRAGONX_LITE_BUILD` define; renames the app to `ObsidianDragonLite` and excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).
- `DRAGONX_ENABLE_LITE_BACKEND` → links a real external lite backend. Requires `--lite`, link mode `imported`, ABI `sdxl-c-v1`, and a symbols inventory file (built by `scripts/build-lite-backend-artifact.sh`); CMake hard-fails if any required `litelib_*` symbol is missing. The backend **source is vendored in-tree** at `third_party/silentdragonxlite/` — the `qtlib` C-ABI wrapper (`lib/`, produces `libsilentdragonxlite.a`) and the `silentdragonxlitelib` core (`silentdragonxlite-cli/lib/`, with `proto/` + `res/`). `build-lite-backend-artifact.sh` defaults `--backend-dir` there, so the lite wallet builds **without** the upstream SilentDragonXLite repo. External build inputs are limited to the **Rust toolchain (rustc/cargo 1.63)** plus two project-controlled sources on `git.dragonx.is`: the librustzcash crates come from the mirror `git.dragonx.is/DragonX/librustzcash` (the 6 `git =` deps in the core `Cargo.toml`, pinned to rev `acff1444…`), and the **Sapling params are not committed** (gitignored) — the build fetches them from the `git.dragonx.is/DragonX/zcash-params` release `sapling-v1` and verifies their SHA-256 before rust-embed bakes them in (`ensure_sapling_params`; override the URL with `SAPLING_PARAMS_BASE_URL`). Other crate deps come from crates.io. For a fully offline build, `cargo vendor` into `third_party/silentdragonxlite/lib/vendor/` and add a `vendored-sources` redirect to `lib/.cargo/config.toml` (the build script symlinks `vendor/` into its prepared dir if present); `vendor/` is gitignored.
- `DRAGONX_ENABLE_CHAT``DRAGONX_ENABLE_CHAT` define gating the chat module.
Guard full-node-only code paths with `#if DRAGONX_LITE_BUILD` / chat code with `DRAGONX_ENABLE_CHAT`.
## Lite wallet status
The Lite variant is **functionally complete and runtime-verified on Linux + Windows** (work lives on branch `cleanup/lite-plan-churn`, **local-only — not pushed yet**):
- **Implemented:** lifecycle (create/open/restore + auto-open on startup), sync, refresh, send / shield / import / export / seed, persistence (the backend does *not* auto-save after sync/send/shield — the controller triggers `save` at those points), and passphrase **encryption** (encrypt/unlock/lock/decrypt + Settings UI + send-time & startup unlock; the backend locks immediately on `encrypt`). All controller-tested against the fake backend (`tests/fake_lite_backend.h`) and smoke-verified against the real SDXL backend via `tools/lite_smoke` (incl. a full sync). GUI is wired end-to-end with lite-appropriate wording; the full-node RPC connect loop / wizard / daemon strings are gated out of lite (lite "online" is derived from `lite_wallet_->walletOpen()`, not RPC).
- **Packaging:** `./build.sh --lite-backend --linux-release` (zip + AppImage, **verified**) and `--win-release` (cross-compiled `.exe`, **verified**; first build the Windows backend artifact with `scripts/build-lite-backend-artifact.sh --platform windows`). macOS `--lite-backend --mac-release` is **wired but not yet verified on this Linux box** (needs macOS/osxcross): the `.app`/launcher/rpath/`CFBundleExecutable` follow `ObsidianDragonLite`, full-node assets are skipped, and the lite variant gets its own `CFBundleName` ("DragonX Wallet Lite"), bundle id (`is.hush.dragonx.lite`), and DMG name so it can coexist with the full-node app. All variants correctly exclude full-node assets.
- **Rollout / kill-switch (implemented):** `wallet/lite_rollout_policy.{h,cpp}` is a pure, fail-open gate (local-only, no network) feeding `LiteWalletLifecycleService::availability()` (new `RolloutDisabled` reason). Inputs: the emergency env var `DRAGONX_LITE_KILL_SWITCH` (absolute — not even `force_on` bypasses it); a `lite_rollout` setting (`auto`/`force_on`/`force_off`); and an optional **locally-cached** manifest at `<config-dir>/lite_rollout.json` (`global_enabled`, `min_version`/`max_version`, `blocked_versions`, `rollout_permille`, `message`) keyed for staged rollout on a hashed, never-transmitted per-install id. A signed remote fetcher can populate that cache later without touching the policy. Resolved in `App::rebuildLiteWallet()`; the disable message surfaces via the lifecycle status. Unit-tested + runtime-verified (env / manifest / control).
- **Remaining (M5b):** verify the wired macOS `--lite` packaging on a Mac/osxcross, CI backend-artifact build + signing.
- **To publish:** rename branch → `feat/lite-wallet`, base the PR on `dev` (the full-node UX is already there), and handle the dormant gated-OFF HushChat content bundled in commit `af06b8b`.
The detailed milestone plan and design history (the v2 plan, backend artifact/ABI/signing design docs, the v1 plan, chat specs, etc.) are kept **untracked** under `docs/_archive/`.
## 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`).
**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)
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.
**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`).
## Versioning
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
## Conventions
- **C++17.** Match the surrounding code's style per file.
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs.
- **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code.
- **i18n:** user-facing strings are translated via `src/util/i18n`; translation JSON lives in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`, English fallback in code). Translation/font helper scripts are in `scripts/` (`gen_*.py`, CJK subset tooling).
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.

View File

@@ -3,12 +3,32 @@
# Released under the GPLv3
cmake_minimum_required(VERSION 3.20)
project(ObsidianDragon
VERSION 1.0.0
# macOS: set deployment target and universal architectures BEFORE project()
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
if(APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
endif()
endif()
project(ObsidianDragon
VERSION 2.0.0
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
set(DRAGONX_VERSION_SUFFIX "")
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
set(DRAGONX_LITE_VERSION "1.0.0")
set(DRAGONX_LITE_VERSION_SUFFIX "")
# C++17 standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -22,6 +42,132 @@ endif()
# Options
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
option(DRAGONX_ENABLE_CHAT "Enable experimental HushChat protocol/UI integration" OFF)
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
set(DRAGONX_LITE_BACKEND_LINK_MODE "imported" CACHE STRING "Lite backend link mode; Phase 1 supports imported only")
set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
if(DRAGONX_BUILD_LITE)
set(DRAGONX_APP_NAME "ObsidianDragonLite")
set(DRAGONX_BINARY_NAME "ObsidianDragonLite")
# NOTE: do NOT FORCE-write DRAGONX_ENABLE_EMBEDDED_DAEMON=OFF into the cache here. A forced
# cache write persists into a later full-node reconfigure of the same build dir and silently
# disables the embedded daemon — the binary still embeds/extracts, but isUsingEmbeddedDaemon()
# returns false, so it "unpacks dragonxd but never starts" (the 1.3.0 regression). It is also
# redundant: makeWalletCapabilities() already forces the embedded-daemon capability off for any
# lite build via `fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
# regardless of this flag. build.sh sets the flag explicitly per variant to defeat stale caches.
set(DRAGONX_APP_VERSION "${DRAGONX_LITE_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_LITE_VERSION_SUFFIX}")
else()
set(DRAGONX_APP_NAME "ObsidianDragon")
set(DRAGONX_BINARY_NAME "ObsidianDragon")
set(DRAGONX_APP_VERSION "${PROJECT_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_VERSION_SUFFIX}")
endif()
# Split the active version into numeric components for the generated header + Windows VERSIONINFO.
string(REPLACE "." ";" _dragonx_ver_parts "${DRAGONX_APP_VERSION}")
list(GET _dragonx_ver_parts 0 DRAGONX_APP_VERSION_MAJOR)
list(GET _dragonx_ver_parts 1 DRAGONX_APP_VERSION_MINOR)
list(GET _dragonx_ver_parts 2 DRAGONX_APP_VERSION_PATCH)
set(DRAGONX_LITE_BACKEND_READY OFF)
if(DRAGONX_ENABLE_LITE_BACKEND)
if(NOT DRAGONX_BUILD_LITE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND is only supported with DRAGONX_BUILD_LITE=ON")
endif()
if(NOT DRAGONX_LITE_BACKEND_LINK_MODE STREQUAL "imported")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LINK_MODE currently supports only 'imported'; runtime dynamic loading is a later bridge-runtime phase")
endif()
if(NOT DRAGONX_LITE_BACKEND_ABI STREQUAL "sdxl-c-v1")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_ABI must be sdxl-c-v1")
endif()
if(NOT DRAGONX_LITE_BACKEND_LIBRARY)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_LIBRARY to point at an SDXL-compatible artifact")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_LIBRARY}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LIBRARY does not exist: ${DRAGONX_LITE_BACKEND_LIBRARY}")
endif()
if(NOT DRAGONX_LITE_BACKEND_SYMBOLS_FILE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_SYMBOLS_FILE generated by scripts/build-lite-backend-artifact.sh")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE does not exist: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
file(STRINGS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}" DRAGONX_LITE_BACKEND_SYMBOL_LINES)
if(NOT DRAGONX_LITE_BACKEND_SYMBOL_LINES)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is empty: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
foreach(DRAGONX_LITE_REQUIRED_SYMBOL IN LISTS DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS)
list(FIND DRAGONX_LITE_BACKEND_SYMBOL_LINES "${DRAGONX_LITE_REQUIRED_SYMBOL}" DRAGONX_LITE_SYMBOL_INDEX)
if(DRAGONX_LITE_SYMBOL_INDEX EQUAL -1)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is missing required symbol: ${DRAGONX_LITE_REQUIRED_SYMBOL}")
endif()
endforeach()
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
endif()
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
endif()
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
endif()
endif()
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
set_target_properties(dragonx_lite_backend PROPERTIES
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
)
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
endif()
set_target_properties(dragonx_lite_backend PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}"
)
endif()
set(DRAGONX_LITE_BACKEND_READY ON)
endif()
include(CTest)
# Output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
@@ -94,6 +240,32 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(tomlplusplus)
# SQLite amalgamation - local Explorer block-summary cache
FetchContent_Declare(
sqlite3
URL https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
URL_HASH SHA256=ea170e73e447703e8359308ca2e4366a3ae0c4304a8665896f068c736781c651
)
FetchContent_GetProperties(sqlite3)
if(NOT sqlite3_POPULATED)
FetchContent_Populate(sqlite3)
endif()
file(GLOB SQLITE3_AMALGAMATION_C CONFIGURE_DEPENDS
${sqlite3_SOURCE_DIR}/sqlite3.c
${sqlite3_SOURCE_DIR}/*/sqlite3.c
)
if(NOT SQLITE3_AMALGAMATION_C)
message(FATAL_ERROR "SQLite amalgamation source not found")
endif()
list(GET SQLITE3_AMALGAMATION_C 0 SQLITE3_SOURCE_FILE)
get_filename_component(SQLITE3_INCLUDE_DIR ${SQLITE3_SOURCE_FILE} DIRECTORY)
add_library(sqlite3_amalgamation STATIC ${SQLITE3_SOURCE_FILE})
target_include_directories(sqlite3_amalgamation PUBLIC ${SQLITE3_INCLUDE_DIR})
target_compile_definitions(sqlite3_amalgamation PRIVATE
SQLITE_THREADSAFE=1
SQLITE_OMIT_LOAD_EXTENSION
)
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
if(WIN32)
# For Windows cross-compilation, fetch and build libcurl statically
@@ -147,6 +319,9 @@ elseif(APPLE)
if(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/include)
elseif(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium/lib/libsodium.a)
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium/lib/libsodium.a)
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium/include)
endif()
else()
# Linux: prefer system libsodium, fall back to local build
@@ -230,21 +405,64 @@ set(APP_SOURCES
src/app_network.cpp
src/app_security.cpp
src/app_wizard.cpp
src/services/network_refresh_service.cpp
src/services/refresh_scheduler.cpp
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/ui/theme.cpp
src/ui/theme_loader.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/material/color_theme.cpp
src/ui/material/typography.cpp
src/ui/notifications.cpp
src/ui/windows/main_window.cpp
src/ui/windows/balance_tab.cpp
src/ui/windows/balance_components.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/balance_tab_helpers.cpp
src/ui/windows/send_tab.cpp
src/ui/windows/receive_tab.cpp
src/ui/windows/transactions_tab.cpp
src/ui/windows/mining_tab.cpp
src/ui/windows/mining_earnings.cpp
src/ui/windows/mining_stats.cpp
src/ui/windows/mining_controls.cpp
src/ui/windows/mining_mode_toggle.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/ui/windows/peers_tab.cpp
src/ui/windows/network_tab.cpp
src/ui/windows/explorer_tab.cpp
src/ui/windows/market_tab.cpp
src/ui/windows/console_tab.cpp
src/ui/windows/console_command_executor.cpp
src/ui/windows/console_command_reference.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
@@ -256,10 +474,8 @@ set(APP_SOURCES
src/ui/windows/shield_dialog.cpp
src/ui/windows/request_payment_dialog.cpp
src/ui/windows/block_info_dialog.cpp
src/ui/windows/import_key_dialog.cpp
src/ui/windows/export_all_keys_dialog.cpp
src/ui/windows/export_transactions_dialog.cpp
src/ui/windows/backup_wallet_dialog.cpp
src/ui/widgets/qr_code.cpp
src/rpc/rpc_client.cpp
src/rpc/rpc_worker.cpp
@@ -268,16 +484,30 @@ set(APP_SOURCES
src/data/address_book.cpp
src/data/exchange_info.cpp
src/util/logger.cpp
src/util/async_task_manager.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/base64.cpp
src/util/single_instance.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/util/platform.cpp
src/util/payment_uri.cpp
src/util/texture_loader.cpp
src/util/noise_texture.cpp
src/daemon/embedded_daemon.cpp
src/daemon/daemon_controller.cpp
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp
src/util/bootstrap.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/pool_stats_service.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
src/util/secure_vault.cpp
src/ui/effects/framebuffer.cpp
src/ui/effects/blur_shader.cpp
@@ -308,19 +538,56 @@ endif()
set(APP_HEADERS
src/app.h
src/services/network_refresh_service.h
src/services/refresh_scheduler.h
src/services/wallet_security_controller.h
src/services/wallet_security_workflow.h
src/services/wallet_security_workflow_executor.h
src/wallet/wallet_capabilities.h
src/wallet/wallet_backend.h
src/wallet/lite_owned_string.h
src/wallet/lite_rollout_policy.h
src/wallet/lite_client_bridge.h
src/wallet/lite_connection_service.h
src/wallet/lite_result_parsers.h
src/wallet/lite_sync_service.h
src/wallet/lite_wallet_gateway.h
src/wallet/lite_wallet_state_mapper.h
src/wallet/lite_wallet_lifecycle_ui_adapter.h
src/wallet/lite_wallet_server_selection_adapter.h
src/wallet/lite_wallet_lifecycle_service.h
src/chat/chat_protocol.h
src/config/version.h
src/data/wallet_state.h
src/data/transaction_history_cache.h
src/ui/theme.h
src/ui/theme_loader.h
src/ui/explorer/explorer_block_cache.h
src/ui/notifications.h
src/ui/windows/main_window.h
src/ui/windows/balance_tab.h
src/ui/windows/balance_address_list.h
src/ui/windows/balance_recent_tx.h
src/ui/windows/balance_tab_helpers.h
src/ui/windows/send_tab.h
src/ui/windows/receive_tab.h
src/ui/windows/transactions_tab.h
src/ui/windows/mining_tab.h
src/ui/windows/mining_benchmark.h
src/ui/windows/mining_pool_panel.h
src/ui/windows/mining_tab_helpers.h
src/ui/windows/peers_tab.h
src/ui/windows/explorer_tab.h
src/ui/windows/market_tab.h
src/ui/windows/console_channel.h
src/ui/windows/console_command_reference.h
src/ui/windows/console_input_model.h
src/ui/windows/console_model.h
src/ui/windows/console_output_model.h
src/ui/windows/console_scroll_controller.h
src/ui/windows/console_selection_controller.h
src/ui/windows/console_tab.h
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h
src/ui/windows/key_export_dialog.h
@@ -331,10 +598,8 @@ set(APP_HEADERS
src/ui/windows/shield_dialog.h
src/ui/windows/request_payment_dialog.h
src/ui/windows/block_info_dialog.h
src/ui/windows/import_key_dialog.h
src/ui/windows/export_all_keys_dialog.h
src/ui/windows/export_transactions_dialog.h
src/ui/windows/backup_wallet_dialog.h
src/ui/widgets/qr_code.h
src/rpc/rpc_client.h
src/rpc/rpc_worker.h
@@ -344,6 +609,8 @@ set(APP_HEADERS
src/data/address_book.h
src/data/exchange_info.h
src/util/logger.h
src/util/async_task_manager.h
src/util/amount_format.h
src/util/base64.h
src/util/single_instance.h
src/util/i18n.h
@@ -351,6 +618,8 @@ set(APP_HEADERS
src/util/payment_uri.h
src/util/secure_vault.h
src/daemon/embedded_daemon.h
src/daemon/daemon_controller.h
src/daemon/lifecycle_adapters.h
src/daemon/xmrig_manager.h
src/ui/effects/framebuffer.h
src/ui/effects/blur_shader.h
@@ -373,11 +642,14 @@ endif()
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
if(WIN32)
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
# Version numbers for the VERSIONINFO resource block
set(DRAGONX_VER_MAJOR 1)
set(DRAGONX_VER_MINOR 0)
set(DRAGONX_VER_PATCH 0)
set(DRAGONX_VERSION "1.0.0")
# Generate manifest with version from project()
configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest.in
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest
@ONLY
)
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
# Generate .rc with version from project()
configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc
${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc
@@ -386,6 +658,15 @@ if(WIN32)
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
endif()
# Generate version values from the single project(VERSION ...) declaration.
# Keep the build-specific app name in the build tree so full/lite configures do
# not rewrite a tracked source header.
configure_file(
${CMAKE_SOURCE_DIR}/src/config/version.h.in
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
@ONLY
)
# Generate INCBIN font embedding source with absolute paths to .ttf files
configure_file(
${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in
@@ -393,6 +674,22 @@ configure_file(
@ONLY
)
# INCBIN uses .incbin assembler directives that reference font files at
# assembly time — CMake doesn't track these implicit dependencies.
# Tell CMake that the generated source depends on the actual font binaries
# so a font file change triggers recompilation.
set_source_files_properties(
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
PROPERTIES OBJECT_DEPENDS
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon
${APP_SOURCES}
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
@@ -403,6 +700,8 @@ add_executable(ObsidianDragon
${WIN_RC_FILE}
)
set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}")
target_include_directories(ObsidianDragon PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/embedded
@@ -422,13 +721,66 @@ target_link_libraries(ObsidianDragon PRIVATE
SDL3::SDL3
nlohmann_json::nlohmann_json
tomlplusplus::tomlplusplus
sqlite3_amalgamation
${CURL_LIBRARIES}
${SODIUM_LIBRARY}
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragon PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
# Real-backend smoke tool (only built when a real lite backend is linked).
add_executable(lite_smoke
tools/lite_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
# Real-backend SEND smoke tool — drives the exact GUI send path (bridge.execute("send", ...)).
add_executable(lite_send_smoke
tools/lite_send_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_send_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_send_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_send_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_send_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
endif()
# Platform-specific settings
if(WIN32)
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi d3d11 dxgi d3dcompiler dcomp)
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
# Hide console window in release builds
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set_target_properties(ObsidianDragon PROPERTIES WIN32_EXECUTABLE TRUE)
@@ -453,7 +805,11 @@ endif()
# Compile definitions
target_compile_definitions(ObsidianDragon PRIVATE
$<$<CONFIG:Debug>:DRAGONX_DEBUG>
DRAGONX_DEBUG
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
)
if(WIN32)
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
@@ -461,6 +817,26 @@ else()
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
endif()
add_executable(HushChatFixtureCheck
tools/hushchat_fixture_check.cpp
src/chat/chat_protocol.cpp
src/chat/chat_fixture_tooling.cpp
)
target_include_directories(HushChatFixtureCheck PRIVATE
${CMAKE_SOURCE_DIR}/src
${SODIUM_INCLUDE_DIR}
)
target_link_libraries(HushChatFixtureCheck PRIVATE
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
target_compile_definitions(HushChatFixtureCheck PRIVATE
DRAGONX_ENABLE_CHAT=0
)
# -----------------------------------------------------------------------------
# Copy resources
# -----------------------------------------------------------------------------
@@ -480,17 +856,44 @@ elseif(EXISTS ${CMAKE_SOURCE_DIR}/../SilentDragonX/res/Ubuntu-R.ttf)
)
endif()
# Copy language files
# Copy language files at BUILD time (not just cmake configure time)
# so edits to res/lang/*.json are picked up by 'make' without re-running cmake.
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
if(LANG_FILES)
find_program(XXD_EXECUTABLE NAMES xxd)
if(NOT XXD_EXECUTABLE)
message(WARNING "xxd not found; runtime language JSON files will be copied, but embedded build/generated/embedded/lang_*.h files will not be regenerated")
endif()
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated/embedded)
foreach(LANG_FILE ${LANG_FILES})
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
configure_file(
${LANG_FILE}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
COPYONLY
add_custom_command(
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${LANG_FILE}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
DEPENDS ${LANG_FILE}
COMMENT "Copying ${LANG_FILENAME}"
)
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
# Also regenerate the embedded header so the binary always has fresh translations
if(XXD_EXECUTABLE)
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
set(LANG_HEADER ${CMAKE_BINARY_DIR}/generated/embedded/lang_${LANG_CODE}.h)
add_custom_command(
OUTPUT ${LANG_HEADER}
COMMAND ${XXD_EXECUTABLE} -i "res/lang/${LANG_FILENAME}" > "${LANG_HEADER}"
DEPENDS ${LANG_FILE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Embedding lang_${LANG_CODE}.h"
)
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
endif()
endforeach()
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
add_dependencies(ObsidianDragon copy_langs)
message(STATUS " Language files: ${LANG_FILES}")
endif()
@@ -502,14 +905,39 @@ embed_resource(
${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h
ui_toml
)
embed_resource(
${CMAKE_SOURCE_DIR}/res/default_banlist.txt
${CMAKE_BINARY_DIR}/generated/default_banlist_embedded.h
default_banlist
)
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
# following the same pattern as daemon embedding.
# Copy theme files at BUILD time (not just cmake configure time)
# so edits to res/themes/*.toml are picked up by 'make' without re-running cmake.
# Expand and copy theme files at BUILD time — skin files get layout sections
# from ui.toml appended automatically so users can see/edit all properties.
# Source skin files stay minimal; the merged output goes to build/bin/res/themes/.
find_package(Python3 QUIET COMPONENTS Interpreter)
if(NOT Python3_FOUND)
find_program(Python3_EXECUTABLE NAMES python3 python)
endif()
file(GLOB THEME_FILES ${CMAKE_SOURCE_DIR}/res/themes/*.toml)
if(THEME_FILES)
if(THEME_FILES AND Python3_EXECUTABLE)
add_custom_command(
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
${CMAKE_SOURCE_DIR}/res/themes
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
DEPENDS ${THEME_FILES} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
COMMENT "Expanding theme files (merging layout from ui.toml)"
)
add_custom_target(copy_themes ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded)
add_dependencies(ObsidianDragon copy_themes)
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
elseif(THEME_FILES)
# Fallback: plain copy if Python is not available
message(WARNING "Python3 not found; copying theme files without expand_themes.py layout merge")
foreach(THEME_FILE ${THEME_FILES})
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
add_custom_command(
@@ -524,7 +952,7 @@ if(THEME_FILES)
endforeach()
add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS})
add_dependencies(ObsidianDragon copy_themes)
message(STATUS " Theme files: ${THEME_FILES}")
message(STATUS " Theme files: ${THEME_FILES} (plain copy, Python not found)")
endif()
# Copy image files (including backgrounds/ subdirectories and logos/)
@@ -559,20 +987,127 @@ install(TARGETS ObsidianDragon
)
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
DESTINATION share/ObsidianDragon
DESTINATION share/${DRAGONX_BINARY_NAME}
OPTIONAL
)
# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
if(BUILD_TESTING)
add_executable(ObsidianDragonTests
tests/test_phase4.cpp
src/services/network_refresh_service.cpp
src/services/refresh_scheduler.cpp
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/util/payment_uri.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/daemon/lifecycle_adapters.cpp
src/rpc/connection.cpp
src/config/settings.cpp
src/resources/embedded_resources.cpp
src/util/secure_vault.cpp
src/util/platform.cpp
src/util/logger.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
${MINIZ_SOURCES}
)
target_include_directories(ObsidianDragonTests PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/resources
${CMAKE_SOURCE_DIR}/libs
${CMAKE_BINARY_DIR}/generated
${IMGUI_DIR}
${SODIUM_INCLUDE_DIR}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
)
target_link_libraries(ObsidianDragonTests PRIVATE
nlohmann_json::nlohmann_json
sqlite3_amalgamation
${SODIUM_LIBRARY}
${CURL_LIBRARIES}
)
target_compile_definitions(ObsidianDragonTests PRIVATE
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragonTests PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
endif()
if(UNIX)
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
endif()
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
endif()
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
message(STATUS "")
message(STATUS "DragonX ImGui Wallet Configuration:")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
message(STATUS " ImGui dir: ${IMGUI_DIR}")
message(STATUS " SDL3 found: ${SDL3_FOUND}")
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
message(STATUS " Lite build: ${DRAGONX_BUILD_LITE}")
message(STATUS " Lite requested: ${DRAGONX_ENABLE_LITE_BACKEND}")
message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
message(STATUS "")

View File

@@ -7,7 +7,7 @@ Thank you for your interest in contributing! This guide will help you get starte
1. Fork the repository
2. Clone your fork and create a branch:
```bash
git clone https://git.hush.is/<your-username>/ObsidianDragon.git
git clone https://git.dragonx.is/<your-username>/ObsidianDragon.git
cd ObsidianDragon
git checkout -b my-feature
```
@@ -67,7 +67,7 @@ Run the wallet and verify:
## Reporting Issues
- Use the [issue tracker](https://git.hush.is/dragonx/ObsidianDragon/issues)
- Use the [issue tracker](https://git.dragonx.is/dragonx/ObsidianDragon/issues)
- Include: OS, wallet version, steps to reproduce, expected vs actual behaviour
- For crashes: include any terminal output

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,6 +1,8 @@
# DragonX Wallet - ImGui Edition
# ObsidianDragon - DragonX Wallet
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
Current pre-release: **1.2.0-rc1**.
![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows%20%7C%20macOS-green.svg)
@@ -9,10 +11,13 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
- **Shielded Transactions**: Full z-address support with encrypted memos
- **Integrated Mining**: CPU mining controls with hashrate monitoring
- **Address Management**: Labels, icons, favorites, hidden addresses, and address-to-address transfers
- **Integrated Mining**: Solo CPU mining plus pool mining through xmrig, with idle-mining controls
- **Explorer Tools**: Block/transaction lookup and bootstrap snapshot download
- **Market Data**: Real-time price charts from CoinGecko
- **QR Codes**: Generate and display QR codes for receiving addresses
- **Multi-language**: i18n support (English, Spanish, more coming)
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
- **Fast Builds**: Compiles in seconds, not minutes
@@ -33,10 +38,10 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
The setup script detects your OS, installs all build dependencies, and validates your environment:
```bash
./scripts/setup.sh # Install core build deps (interactive)
./scripts/setup.sh --check # Just report what's missing
./scripts/setup.sh --all # Core + Windows/macOS cross-compile + Sapling params
./scripts/setup.sh --win # Also install mingw-w64 + libsodium-win
./setup.sh # Install core build deps (interactive)
./setup.sh --check # Just report what's missing
./setup.sh --all # Core + Windows/macOS cross-compile + Sapling params
./setup.sh --win # Also install mingw-w64 + libsodium-win
```
### Manual Prerequisites
@@ -71,12 +76,12 @@ brew install cmake
### Binaries
Download linux and windows binaries of latest releases and place in binary directories:
**DragonX daemon** (https://git.hush.is/dragonx/hush3):
**DragonX daemon** (https://git.dragonx.is/DragonX/dragonx):
- prebuilt-binaries/dragonxd-linux/
- prebuilt-binaries/dragonxd-win/
- prebuilt-binaries/dragonxd-mac/
**xmrig HAC fork** (https://git.hush.is/dragonx/xmrig-hac):
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
- prebuilt-binaries/xmrig-hac/
@@ -84,7 +89,7 @@ Download linux and windows binaries of latest releases and place in binary direc
```
### Clone repository (if not already)
git clone https://git.hush.is/dragonx/ObsidianDragon.git
git clone https://git.dragonx.is/dragonx/ObsidianDragon.git
cd ObsidianDragon/
```
@@ -116,7 +121,8 @@ cd ObsidianDragon/
./ObsidianDragon
```
The wallet will automatically connect to the daemon using credentials from \`~/.hush/DRAGONX/DRAGONX.conf\`.
The wallet will automatically connect to the daemon using credentials from `~/.hush/DRAGONX/DRAGONX.conf`.
### Using Custom Node Binaries
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
@@ -128,12 +134,13 @@ The wallet checks its **own directory first** when looking for DragonX node bina
**Search order:**
1. Wallet executable directory (highest priority)
2. Embedded/extracted daemon (app data directory)
3. System-wide locations (`/usr/local/bin`, `~/hush3/src`, etc.)
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
## Configuration
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`:
Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
```
rpcuser=your_rpc_user
@@ -148,44 +155,46 @@ ObsidianDragon/
├── src/
│ ├── main.cpp # Entry point, SDL/ImGui setup
│ ├── app.cpp/h # Main application class
│ ├── wallet_state.h # Wallet data structures
│ ├── version.h # Version definitions
│ ├── data/ # WalletState, address book, exchange info
│ ├── config/ # Settings persistence and committed/generated version.h
│ ├── ui/
│ │ ├── theme.cpp/h # DragonX theme
│ │ ── windows/ # UI tabs and dialogs
│ │ ├── schema/ # TOML UI schema and skin manager
│ │ ── material/ # Material components, typography, layout
│ │ ├── windows/ # Tabs and dialogs
│ │ └── pages/ # Multi-page screens such as Settings
│ ├── rpc/
│ │ ├── rpc_client.cpp # JSON-RPC client
│ │ └── connection.cpp # Daemon connection
│ ├── config/
│ └── settings.cpp # Settings persistence
│ ├── resources/ # Embedded resource extraction
├── platform/ # Windows DX11/backdrop helpers
│ ├── util/
│ │ ├── i18n.cpp # Internationalization
│ │ └── ...
│ └── daemon/
│ └── embedded_daemon.cpp
├── res/
│ ├── fonts/ # Ubuntu font
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
│ └── lang/ # Translation files
├── libs/
│ └── qrcode/ # QR code generation
├── CMakeLists.txt
├── build-release.sh # Build script
└── create-appimage.sh # AppImage packaging
├── build.sh # Release/cross-platform build script
└── scripts/create-appimage.sh # AppImage packaging
```
## Dependencies
Fetched automatically by CMake (no manual install needed):
Fetched or discovered by CMake:
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
- **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
Bundled in `libs/`:
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`)
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
@@ -202,9 +211,11 @@ Bundled in `libs/`:
## Translation
Current language files live in `res/lang/` as `de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, and `zh` JSON files, with built-in English fallbacks.
To add a new language:
1. Copy \`res/lang/es.json\` to \`res/lang/<code>.json\`
1. Copy `res/lang/es.json` to `res/lang/<code>.json`
2. Translate all strings
3. The language will appear in Settings automatically
@@ -223,4 +234,4 @@ This project is licensed under the GNU General Public License v3 (GPLv3).
- Website: https://dragonx.is
- Explorer: https://explorer.dragonx.is
- Source: https://git.hush.is/dragonx/ObsidianDragon
- Source: https://git.dragonx.is/dragonx/ObsidianDragon

View File

@@ -361,7 +361,22 @@ https://www.apache.org/licenses/LICENSE-2.0
---
## 13. IconFontCppHeaders
## 13. Material Design Icons Pickaxe Subset Font
- **Location:** `res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf`
- **Source:** https://github.com/Templarian/MaterialDesign-Webfont
- **Derived from:** Pictogrammers Material Design Icons webfont (`materialdesignicons-webfont.ttf`)
- **Copyright:** Pictogrammers contributors
- **License:** Apache License 2.0
This bundled font is a local one-glyph subset containing only the MDI pickaxe
icon, remapped onto a BMP private-use codepoint for Dear ImGui compatibility.
The full text of the Apache License 2.0 is available at:
https://www.apache.org/licenses/LICENSE-2.0
---
## 14. IconFontCppHeaders
- **Location:** `src/embedded/IconsMaterialDesign.h`
- **Source:** https://github.com/juliettef/IconFontCppHeaders
@@ -390,7 +405,7 @@ freely, subject to the following restrictions:
---
## 14. Ubuntu Font Family
## 15. Ubuntu Font Family
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
- **Source:** https://design.ubuntu.com/font

593
build.sh
View File

@@ -5,7 +5,7 @@
#
# Usage:
# ./build.sh # Dev build (Linux, debug-friendly)
# ./build.sh --linux-release # Linux release + AppImage
# ./build.sh --linux-release # Linux release (zip + AppImage)
# ./build.sh --win-release # Windows cross-compile (mingw-w64)
# ./build.sh --mac-release # macOS .app bundle + DMG
# ./build.sh --linux-release --win-release # Multiple targets
@@ -20,7 +20,9 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="1.0.0"
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
# parsing — see the APP_BASENAME block below. Placeholder until then.
VERSION=""
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
@@ -41,6 +43,8 @@ DO_DEV=false
DO_LINUX=false
DO_WIN=false
DO_MAC=false
DO_LITE=false
DO_LITE_BACKEND=false
CLEAN=false
BUILD_TYPE="Release"
@@ -51,9 +55,13 @@ DragonX Wallet — Unified Build Script
Usage: $0 [options]
Targets (at least one required, or none for dev build):
--linux-release Linux release build + AppImage -> release/linux/
--linux-release Linux release (zip + AppImage) -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
(auto-discovers build/lite-backend/<platform>/; build it with
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
Build trees are stored under build/{linux,windows,mac}/
@@ -71,9 +79,10 @@ Cross-compiling from Linux:
Examples:
$0 # Quick dev build (Linux)
$0 --linux-release # Linux release + AppImage
$0 --linux-release # Linux release (zip + AppImage)
$0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
$0 --clean --linux-release --win-release # Clean + both
EOF
exit 0
@@ -85,6 +94,8 @@ while [[ $# -gt 0 ]]; do
--linux-release) DO_LINUX=true; shift ;;
--win-release) DO_WIN=true; shift ;;
--mac-release) DO_MAC=true; shift ;;
--lite) DO_LITE=true; shift ;;
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
-c|--clean) CLEAN=true; shift ;;
-d|--debug) BUILD_TYPE="Debug"; shift ;;
-j) JOBS="$2"; shift 2 ;;
@@ -98,6 +109,92 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
DO_DEV=true
fi
APP_BASENAME="ObsidianDragon"
CMAKE_LITE_ARGS=()
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
if $DO_LITE; then
APP_BASENAME="ObsidianDragonLite"
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
info "Lite mode enabled: building ${APP_BASENAME}"
else
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
fi
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
_cml="$SCRIPT_DIR/CMakeLists.txt"
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
if $DO_LITE; then
VERSION="${_lite_ver}${_lite_suffix}"
else
VERSION="${_full_ver}${_full_suffix}"
fi
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
info "Release version: ${VERSION} (${APP_BASENAME})"
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
# override the directory with DRAGONX_LITE_BACKEND_DIR.
if $DO_LITE_BACKEND; then
# Artifact platform follows the cross target when exactly one non-host release is requested,
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
case "$(uname -s)" in
Linux) lb_platform="linux" ;;
Darwin) lb_platform="macos" ;;
*) lb_platform="linux" ;;
esac
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
lb_lib=""
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
done
lb_symbols="$lb_dir/lite-backend-symbols.txt"
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
err "Lite backend artifact not found under: $lb_dir"
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
exit 1
fi
CMAKE_LITE_ARGS+=(
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
)
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
if [[ "$lb_platform" == "windows" ]]; then
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
fi
info "Lite backend enabled ($lb_platform): $lb_lib"
else
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
# (which would then fail the BUILD_LITE=OFF guard in CMake).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
fi
should_bundle_full_node_assets() {
! $DO_LITE
}
# ── Helper: find resource files ──────────────────────────────────────────────
find_sapling_params() {
local dirs=(
@@ -122,10 +219,10 @@ find_sapling_params() {
find_asmap() {
local paths=(
"$SCRIPT_DIR/external/hush3/asmap.dat"
"$SCRIPT_DIR/external/hush3/contrib/asmap/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/external/dragonx/asmap.dat"
"$SCRIPT_DIR/external/dragonx/contrib/asmap/asmap.dat"
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/../asmap.dat"
"$SCRIPT_DIR/asmap.dat"
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
@@ -147,37 +244,37 @@ bundle_linux_daemon() {
local dest="$1"
local found=0
local launcher_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
"$SCRIPT_DIR/../hush-arrakis-chain"
"$SCRIPT_DIR/external/hush3/src/hush-arrakis-chain"
"$HOME/hush3/src/hush-arrakis-chain"
local daemon_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
"$SCRIPT_DIR/../dragonxd"
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${launcher_paths[@]}"; do
for p in "${daemon_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/hush-arrakis-chain"; chmod +x "$dest/hush-arrakis-chain"
info " Bundled hush-arrakis-chain"; found=1; break
cp "$p" "$dest/dragonxd"; chmod +x "$dest/dragonxd"
info " Bundled dragonxd"; found=1; break
fi
done
local hushd_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/hushd"
"$SCRIPT_DIR/../hushd"
"$SCRIPT_DIR/external/hush3/src/hushd"
"$HOME/hush3/src/hushd"
local cli_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonx-cli"
"$SCRIPT_DIR/../dragonx-cli"
"$SCRIPT_DIR/external/dragonx/src/dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${hushd_paths[@]}"; do
for p in "${cli_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/hushd"; chmod +x "$dest/hushd"
info " Bundled hushd"; break
cp "$p" "$dest/dragonx-cli"; chmod +x "$dest/dragonx-cli"
info " Bundled dragonx-cli"; break
fi
done
local dragonxd_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
"$SCRIPT_DIR/../dragonxd"
"$SCRIPT_DIR/external/hush3/src/dragonxd"
"$HOME/hush3/src/dragonxd"
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${dragonxd_paths[@]}"; do
if [[ -f "$p" ]]; then
@@ -197,7 +294,14 @@ bundle_linux_daemon() {
# ═══════════════════════════════════════════════════════════════════════════════
build_dev() {
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
local bd="$SCRIPT_DIR/build/linux"
# Use platform-appropriate build directory
if [[ "$(uname -s)" == "Darwin" ]]; then
local bd="$SCRIPT_DIR/build/mac"
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
local bd="$SCRIPT_DIR/build/linux"
fi
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
@@ -208,17 +312,18 @@ build_dev() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
}
# ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: LINUX — build + strip + bundle daemon + AppImage
# RELEASE: LINUX — build + strip + bundle daemon + zip + AppImage
# ═══════════════════════════════════════════════════════════════════════════════
build_release_linux() {
header "Release: Linux x86_64"
@@ -235,32 +340,64 @@ build_release_linux() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
info "Stripping ..."
strip bin/ObsidianDragon
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
strip "bin/${APP_BASENAME}"
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
# ── Bundle daemon ────────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
if should_bundle_full_node_assets; then
# ── Bundle daemon ────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
else
info "Lite mode: skipping daemon and Sapling/asmap bundling"
fi
# ── Package: release/linux/ ──────────────────────────────────────────────
rm -rf "$out"
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
cp bin/ObsidianDragon "$out/"
[[ -f bin/hush-arrakis-chain ]] && cp bin/hush-arrakis-chain "$out/"
[[ -f bin/hushd ]] && cp bin/hushd "$out/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$out/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$out/"
cp -r bin/res "$out/" 2>/dev/null || true
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
# ── AppImage ─────────────────────────────────────────────────────────────
cp "bin/${APP_BASENAME}" "$dist_dir/"
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
# ── AppImage (single-file) ───────────────────────────────────────────────
info "Creating AppImage ..."
local APPDIR="$bd/AppDir"
rm -rf "$APPDIR"
@@ -269,22 +406,29 @@ build_release_linux() {
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
"$APPDIR/usr/share/ObsidianDragon/res"
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
# Daemon inside AppImage
[[ -f bin/hush-arrakis-chain ]] && cp bin/hush-arrakis-chain "$APPDIR/usr/bin/"
[[ -f bin/hushd ]] && cp bin/hushd "$APPDIR/usr/bin/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/share/ObsidianDragon/"
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/)
# because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<DESK
[Desktop Entry]
Type=Application
Name=DragonX Wallet
Comment=DragonX Cryptocurrency Wallet
Exec=ObsidianDragon
Exec=${APP_BASENAME}
Icon=ObsidianDragon
Categories=Finance;Network;
Terminal=false
@@ -315,14 +459,14 @@ SVG
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
# AppRun
cat > "$APPDIR/AppRun" <<'APPRUN'
cat > "$APPDIR/AppRun" <<APPRUN
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export DRAGONX_RES_PATH="${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
cd "${HERE}/usr/share/ObsidianDragon"
exec "${HERE}/usr/bin/ObsidianDragon" "$@"
SELF=\$(readlink -f "\$0")
HERE=\${SELF%/*}
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
cd "\${HERE}/usr/share/ObsidianDragon"
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
APPRUN
chmod +x "$APPDIR/AppRun"
@@ -350,19 +494,11 @@ APPRUN
local ARCH
ARCH=$(uname -m)
local IMG_NAME="DragonX_Wallet-${VERSION}-${ARCH}.AppImage"
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "$IMG_NAME" 2>/dev/null && {
cp "$IMG_NAME" "$out/"
info "AppImage: $out/$IMG_NAME ($(du -h "$IMG_NAME" | cut -f1))"
} || warn "AppImage creation failed (appimagetool issue) — raw binary still in release/linux/"
# Clean up: keep only AppImage + raw binary in release/linux/
if ls "$out"/*.AppImage 1>/dev/null 2>&1; then
# AppImage succeeded — remove everything except AppImage and the binary
find "$out" -maxdepth 1 -type f ! -name '*.AppImage' ! -name 'ObsidianDragon' -delete
rm -rf "$out/res" 2>/dev/null
fi
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/"
info "Linux release artifacts: $out/"
ls -lh "$out/"
@@ -401,6 +537,22 @@ build_release_win() {
exit 1
fi
# ── Patch libwinpthread + libpthread to remove VERSIONINFO resources ────
# mingw-w64's libwinpthread.a and libpthread.a each ship a version.o
# with their own VERSIONINFO ("POSIX WinThreads for Windows") that
# collides with ours during .rsrc merge, causing Task Manager to show
# the wrong process description.
local PATCHED_LIB_DIR="$bd/patched-lib"
mkdir -p "$PATCHED_LIB_DIR"
for plib in libwinpthread.a libpthread.a; do
local SYS_LIB="/usr/x86_64-w64-mingw32/lib/$plib"
if [[ -f "$SYS_LIB" ]]; then
cp -f "$SYS_LIB" "$PATCHED_LIB_DIR/$plib"
x86_64-w64-mingw32-ar d "$PATCHED_LIB_DIR/$plib" version.o 2>/dev/null || true
info "Patched $plib (removed version.o VERSIONINFO resource)"
fi
done
# ── Toolchain file ───────────────────────────────────────────────────────
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Windows)
@@ -412,7 +564,7 @@ set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -L$PATCHED_LIB_DIR -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
@@ -454,26 +606,48 @@ HDR
# ── Daemon binaries ──────────────────────────────────────────────
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if [[ -d "$DD" && -f "$DD/hushd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in hushd.exe hush-cli.exe hush-tx.exe dragonxd.bat dragonx-cli.bat; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
if should_bundle_full_node_assets; then
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
else
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
else
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
info "Lite mode: skipping embedded daemon binaries"
fi
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
# no miner ("xmrig binary not found" at runtime).
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
local _xtmp; _xtmp=$(mktemp -d)
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
# non-zero if a pattern matches nothing).
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
if [[ -f "$_xtmp/xmrig.exe" ]]; then
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
info " Extracted xmrig.exe from $(basename "$_xz")"
fi
rm -rf "$_xtmp"
fi
fi
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
@@ -517,9 +691,13 @@ HDR
echo "};" >> "$GEN/embedded_data.h"
# ── Overlay themes ───────────────────────────────────────────────
# Expand skin files with layout sections from ui.toml before embedding
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h"
local THEME_STAGE_DIR="$bd/_expanded_themes"
mkdir -p "$THEME_STAGE_DIR"
python3 "$SCRIPT_DIR/scripts/expand_themes.py" "$SCRIPT_DIR/res/themes" "$THEME_STAGE_DIR"
local THEME_TABLE="" THEME_COUNT=0
for tf in "$SCRIPT_DIR/res/themes"/*.toml; do
for tf in "$THEME_STAGE_DIR"/*.toml; do
local tbn=$(basename "$tf")
[[ "$tbn" == "ui.toml" ]] && continue
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
@@ -552,53 +730,57 @@ HDR
cmake "$SCRIPT_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
# ── Package: release/windows/ ────────────────────────────────────────────
rm -rf "$out"
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
local DIST="DragonX-Wallet-Windows-x64"
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp bin/ObsidianDragon.exe "$dist_dir/"
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
for f in dragonxd.bat dragonx-cli.bat hushd.exe hush-cli.exe hush-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
if should_bundle_full_node_assets; then
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
cat > "$dist_dir/README.txt" <<'README'
DragonX Wallet - Windows Edition
================================
# Bundle Sapling params + asmap for the zip distribution
# (The single-file exe has these embedded via INCBIN, but the zip
# needs them on disk so the daemon can find them in its work dir.)
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
else
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
fi
SINGLE-FILE DISTRIBUTION
========================
This wallet is a true single-file executable with all resources embedded.
Just run ObsidianDragon.exe — no additional files needed!
# Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
On first run, the wallet will automatically extract:
- Sapling parameters to %APPDATA%\ZcashParams\
- asmap.dat to %APPDATA%\Hush\DRAGONX\
cp -r bin/res "$dist_dir/" 2>/dev/null || true
For support: https://git.hush.is/hush/ObsidianDragon
README
# Copy single-file exe to release dir
cp bin/ObsidianDragon.exe "$out/"
# ── Single-file exe (all resources embedded) ────────────────────────────
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
# Clean up: keep .zip + single-file exe, remove loose directory
rm -rf "$dist_dir"
fi
rm -rf "$dist_dir"
info "Windows release artifacts: $out/"
ls -lh "$out/"
@@ -694,7 +876,9 @@ build_release_mac() {
fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else
MAC_ARCH=$(uname -m)
# Native macOS: build universal binary (arm64 + x86_64)
MAC_ARCH="universal"
export MACOSX_DEPLOYMENT_TARGET="11.0"
fi
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
@@ -771,41 +955,67 @@ TOOLCHAIN
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
"${CMAKE_LITE_ARGS[@]}"
else
info "Configuring (native) ..."
# Build libsodium as universal if needed
local need_sodium=false
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
need_sodium=true
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
# Rebuild if existing lib is not universal (single-arch won't link)
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
info "Existing libsodium is not universal — rebuilding ..."
rm -rf "$SCRIPT_DIR/libs/libsodium"
need_sodium=true
fi
fi
if $need_sodium; then
info "Building libsodium (universal) ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi
info "Configuring (native universal arm64+x86_64) ..."
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
"${CMAKE_LITE_ARGS[@]}"
fi
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
# Strip — use osxcross strip for cross-builds
if $IS_CROSS; then
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
if [[ -x "$STRIP_CMD" ]]; then
info "Stripping (osxcross) ..."
"$STRIP_CMD" bin/ObsidianDragon
"$STRIP_CMD" "bin/${APP_BASENAME}"
else
warn "osxcross strip not found at $STRIP_CMD — skipping"
fi
else
info "Stripping ..."
strip bin/ObsidianDragon
strip "bin/${APP_BASENAME}"
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info "bin/${APP_BASENAME}"
fi
fi
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
# ── Create .app bundle ───────────────────────────────────────────────────
rm -rf "$out"
mkdir -p "$out"
local APP="$out/ObsidianDragon.app"
local APP="$out/${APP_BASENAME}.app"
local CONTENTS="$APP/Contents"
local MACOS="$CONTENTS/MacOS"
local RESOURCES="$CONTENTS/Resources"
@@ -814,40 +1024,62 @@ TOOLCHAIN
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
# Main binary
cp bin/ObsidianDragon "$MACOS/"
chmod +x "$MACOS/ObsidianDragon"
cp "bin/${APP_BASENAME}" "$MACOS/"
chmod +x "$MACOS/${APP_BASENAME}"
# Resources
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in hush-arrakis-chain hushd hush-cli hush-tx dragonxd; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local launcher_paths=(
"$SCRIPT_DIR/../hush-arrakis-chain"
"$HOME/hush3/src/hush-arrakis-chain"
)
for p in "${launcher_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/hush-arrakis-chain"; chmod +x "$MACOS/hush-arrakis-chain"; info " Bundled hush-arrakis-chain"; break; }
done
local hushd_paths=(
"$SCRIPT_DIR/../hushd"
"$HOME/hush3/src/hushd"
)
for p in "${hushd_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/hushd"; chmod +x "$MACOS/hushd"; info " Bundled hushd"; break; }
done
if should_bundle_full_node_assets; then
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in dragonxd dragonx-cli dragonx-tx; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
for f in sapling-spend.params sapling-output.params; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local daemon_paths=(
"$SCRIPT_DIR/../dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${daemon_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
done
local cli_paths=(
"$SCRIPT_DIR/../dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${cli_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
done
else
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
else
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
fi
# asmap.dat
find_asmap 2>/dev/null && cp "$ASMAP_DAT" "$RESOURCES/asmap.dat" && info " Bundled asmap.dat"
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
if [[ -f "$XMRIG_MAC" ]]; then
cp "$XMRIG_MAC" "$MACOS/xmrig"
chmod +x "$MACOS/xmrig"
info " Bundled xmrig"
else
warn "xmrig not found — mining unavailable in .app"
fi
if should_bundle_full_node_assets; then
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
find_asmap 2>/dev/null && {
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
info " Bundled asmap.dat"
}
fi
# Bundle SDL3 dylib
local sdl_dylib=""
@@ -867,26 +1099,34 @@ TOOLCHAIN
# Fix the rpath so the binary finds SDL3 in Frameworks/
if $IS_CROSS; then
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
else
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
fi
info " Bundled $sdl_name"
fi
# Launcher script (ensures working dir + dylib path)
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
export DYLD_LIBRARY_PATH="$DIR/../Frameworks:$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="$DIR/../Resources/res"
cd "$DIR/../Resources"
exec "$DIR/ObsidianDragon.bin" "$@"
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
cd "\$DIR/../Resources"
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
LAUNCH
chmod +x "$MACOS/ObsidianDragon"
chmod +x "$MACOS/${APP_BASENAME}"
# Info.plist
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
local APP_DISPLAY_NAME="DragonX Wallet"
local APP_BUNDLE_ID="is.hush.dragonx"
if $DO_LITE; then
APP_DISPLAY_NAME="DragonX Wallet Lite"
APP_BUNDLE_ID="is.hush.dragonx.lite"
fi
cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
@@ -894,17 +1134,17 @@ LAUNCH
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>DragonX Wallet</string>
<string>${APP_DISPLAY_NAME}</string>
<key>CFBundleDisplayName</key>
<string>DragonX Wallet</string>
<string>${APP_DISPLAY_NAME}</string>
<key>CFBundleIdentifier</key>
<string>is.hush.dragonx</string>
<string>${APP_BUNDLE_ID}</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleExecutable</key>
<string>ObsidianDragon</string>
<string>${APP_BASENAME}</string>
<key>CFBundleIconFile</key>
<string>ObsidianDragon</string>
<key>CFBundlePackageType</key>
@@ -965,19 +1205,28 @@ PLIST
info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ───────────────────────────────────────────────────────────
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
local DMG_BASENAME="DragonX_Wallet"
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
if command -v create-dmg &>/dev/null; then
# create-dmg (works on macOS; also available on Linux via npm)
info "Creating DMG with create-dmg ..."
create-dmg \
--volname "DragonX Wallet" \
--volname "${APP_DISPLAY_NAME}" \
--volicon "$RESOURCES/ObsidianDragon.icns" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "ObsidianDragon.app" 150 190 \
--icon "${APP_BASENAME}.app" 150 190 \
--app-drop-link 450 190 \
--no-internet-enable \
"$out/$DMG_NAME" \
@@ -992,7 +1241,7 @@ PLIST
mkdir -p "$staging"
cp -a "$APP" "$staging/"
ln -s /Applications "$staging/Applications"
hdiutil create -volname "DragonX Wallet" \
hdiutil create -volname "${APP_DISPLAY_NAME}" \
-srcfolder "$staging" \
-ov -format UDZO \
"$out/$DMG_NAME" 2>/dev/null && {
@@ -1008,7 +1257,7 @@ PLIST
cp -a "$APP" "$staging/"
# Can't create a real symlink to /Applications in an ISO, but the .app
# is the important part — users drag it to Applications manually.
genisoimage -V "DragonX Wallet" \
genisoimage -V "${APP_DISPLAY_NAME}" \
-D -R -apple -no-pad \
-o "$out/$DMG_NAME" \
"$staging" 2>/dev/null && {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -40,7 +40,7 @@ extern "C" {
* read-only segment just like a normal const array would.
*/
#if defined(__APPLE__)
# define INCBIN_SECTION ".const_data"
# define INCBIN_SECTION "__DATA,__const"
# define INCBIN_MANGLE "_"
#else
# define INCBIN_SECTION ".rodata"

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- Application identity —————————————————————————————— -->
<assemblyIdentity
type="win32"
name="DragonX.ObsidianDragon.Wallet"
version="@DRAGONX_APP_VERSION_MAJOR@.@DRAGONX_APP_VERSION_MINOR@.@DRAGONX_APP_VERSION_PATCH@.0"
processorArchitecture="amd64"
/>
<description>ObsidianDragon Wallet</description>
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 / 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -4,6 +4,13 @@
// Use numeric ordinal 1 so LoadIcon(hInst, MAKEINTRESOURCE(1)) finds it.
1 ICON "@OBSIDIAN_ICO_PATH@"
// ---------------------------------------------------------------------------
// Application Manifest — declares DPI awareness, common controls v6,
// UTF-8 code page, and application identity. Without this, Windows may
// fall back to legacy process grouping in Task Manager.
// ---------------------------------------------------------------------------
1 24 "@OBSIDIAN_MANIFEST_PATH@"
// ---------------------------------------------------------------------------
// VERSIONINFO — sets the description shown in Task Manager, Explorer
// "Details" tab, and other Windows tools. Without this, MinGW-w64
@@ -12,8 +19,8 @@
#include <winver.h>
VS_VERSION_INFO VERSIONINFO
FILEVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
PRODUCTVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
FILEVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
PRODUCTVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
@@ -24,14 +31,14 @@ BEGIN
BEGIN
BLOCK "040904B0" // US-English, Unicode
BEGIN
VALUE "CompanyName", "The Hush Developers\0"
VALUE "FileDescription", "ObsidianDragon Wallet\0"
VALUE "FileVersion", "@DRAGONX_VERSION@\0"
VALUE "InternalName", "ObsidianDragon\0"
VALUE "LegalCopyright", "Copyright 2024-2026 The Hush Developers. GPLv3.\0"
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
VALUE "ProductName", "ObsidianDragon\0"
VALUE "ProductVersion", "@DRAGONX_VERSION@\0"
VALUE "CompanyName", "DragonX Developers\0"
VALUE "FileDescription", "@DRAGONX_APP_NAME@ Wallet\0"
VALUE "FileVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
VALUE "InternalName", "@DRAGONX_APP_NAME@\0"
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
VALUE "OriginalFilename", "@DRAGONX_APP_NAME@.exe\0"
VALUE "ProductName", "@DRAGONX_APP_NAME@\0"
VALUE "ProductVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
END
END
BLOCK "VarFileInfo"

10
res/default_banlist.txt Normal file
View File

@@ -0,0 +1,10 @@
# Default Ban List — DragonX Wallet
# IPs listed here are banned automatically when the wallet connects.
# One IP or subnet per line. Comments start with #. Blank lines are ignored.
#
# Examples:
# 192.168.1.100
# 10.0.0.0/8
# 203.0.113.42
#
# Rebuild the wallet after editing this file.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
res/fonts/UbuntuMono-R.ttf Normal file

Binary file not shown.

BIN
res/img/ObsidianDragon.icns Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
name = "Color Pop Dark"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#26262E", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#282836", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
@@ -14,13 +14,13 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--secondary-light = "#FF9CDC"
--background = "#0E0E14"
--surface = "#161620"
--surface-variant = "#20202C"
--surface-variant = "#282836"
--on-primary = "#FFFFFF"
--on-secondary = "#FFFFFF"
--on-background = "#E8E6F0"
--on-surface = "#E8E6F0"
--on-surface-medium = "rgba(232,230,240,0.72)"
--on-surface-disabled = "rgba(232,230,240,0.40)"
--on-surface-medium = "rgba(232,230,240,0.85)"
--on-surface-disabled = "rgba(232,230,240,0.58)"
--error = "#FF5C72"
--on-error = "#000000"
--success = "#3DE8A0"
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--surface-active = "rgba(200,190,240,0.10)"
--glass-button = "rgba(124,108,255,0.08)"
--glass-button-hover = "rgba(124,108,255,0.16)"
--card-border = "rgba(200,190,240,0.10)"
--card-border = "rgba(200,190,240,0.24)"
--text-shadow = "rgba(0,0,0,0.45)"
--input-overlay-text = "rgba(232,230,240,0.25)"
--slider-text = "rgba(232,230,240,0.82)"
@@ -48,20 +48,20 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--tooltip-bg = "rgba(14,14,22,0.94)"
--tooltip-border = "rgba(124,108,255,0.18)"
--glass-fill = "rgba(200,190,240,0.06)"
--glass-border = "rgba(200,190,240,0.10)"
--glass-border = "rgba(124,108,255,0.28)"
--glass-noise-tint = "rgba(124,108,255,0.03)"
--tactile-top = "rgba(200,190,240,0.07)"
--tactile-bottom = "rgba(200,190,240,0.0)"
--hover-overlay = "rgba(124,108,255,0.05)"
--active-overlay = "rgba(124,108,255,0.10)"
--rim-light = "rgba(124,108,255,0.10)"
--rim-light = "rgba(124,108,255,0.16)"
--status-divider = "rgba(200,190,240,0.06)"
--sidebar-hover = "rgba(124,108,255,0.10)"
--sidebar-icon = "rgba(232,230,240,0.45)"
--sidebar-badge = "rgba(232,230,240,1.0)"
--sidebar-divider = "rgba(200,190,240,0.05)"
--chart-line = "rgba(124,108,255,0.12)"
--window-control = "rgba(232,230,240,0.72)"
--window-control = "rgba(232,230,240,0.85)"
--window-control-hover = "rgba(124,108,255,0.12)"
--window-close-hover = "rgba(255,92,114,0.75)"
--spinner-track = "rgba(200,190,240,0.08)"

View File

@@ -19,16 +19,16 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
--on-secondary = "#FFFFFF"
--on-background = "#1E1E2A"
--on-surface = "#1E1E2A"
--on-surface-medium = "rgba(30,30,42,0.72)"
--on-surface-disabled = "rgba(30,30,42,0.38)"
--on-surface-medium = "rgba(30,30,42,0.86)"
--on-surface-disabled = "rgba(30,30,42,0.62)"
--error = "#E0304A"
--on-error = "#FFFFFF"
--success = "#18A860"
--on-success = "#FFFFFF"
--warning = "#E09020"
--on-warning = "#000000"
--divider = "rgba(30,30,60,0.12)"
--outline = "rgba(30,30,60,0.15)"
--divider = "rgba(30,30,60,0.20)"
--outline = "rgba(30,30,60,0.24)"
--scrim = "rgba(0,0,0,0.45)"
--surface-hover = "rgba(96,64,224,0.05)"
--surface-alt = "rgba(96,64,224,0.02)"
@@ -47,14 +47,14 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
--chart-hover-ring = "rgba(96,64,224,0.30)"
--tooltip-bg = "rgba(36,34,52,0.94)"
--tooltip-border = "rgba(96,64,224,0.16)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-border = "rgba(30,30,60,0.10)"
--glass-noise-tint = "rgba(96,64,224,0.02)"
--tactile-top = "rgba(255,255,255,0.40)"
--tactile-bottom = "rgba(255,255,255,0.05)"
--hover-overlay = "rgba(96,64,224,0.04)"
--hover-overlay = "rgba(96,64,224,0.10)"
--active-overlay = "rgba(96,64,224,0.08)"
--rim-light = "rgba(96,64,224,0.06)"
--rim-light = "rgba(30,30,42,0.22)"
--status-divider = "rgba(30,30,60,0.08)"
--sidebar-hover = "rgba(96,64,224,0.07)"
--sidebar-icon = "rgba(30,30,42,0.50)"

View File

@@ -2,7 +2,7 @@
name = "Dark"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2C2C2E", --elevation-3 = "#363638", --elevation-4 = "#404044" }
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2E2E32", --elevation-3 = "#363638", --elevation-4 = "#404044" }
images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
@@ -14,13 +14,13 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--secondary-light = "#9DC5BE"
--background = "#141416"
--surface = "#1A1A1C"
--surface-variant = "#262628"
--surface-variant = "#2E2E32"
--on-primary = "#000000"
--on-secondary = "#000000"
--on-background = "#D0D0D4"
--on-surface = "#D0D0D4"
--on-surface-medium = "rgba(208,208,212,0.75)"
--on-surface-disabled = "rgba(208,208,212,0.45)"
--on-surface-medium = "rgba(208,208,212,0.85)"
--on-surface-disabled = "rgba(208,208,212,0.58)"
--error = "#B07080"
--on-error = "#000000"
--success = "#7AAE7C"
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--surface-active = "rgba(220,220,225,0.08)"
--glass-button = "rgba(220,220,225,0.05)"
--glass-button-hover = "rgba(220,220,225,0.10)"
--card-border = "rgba(220,220,225,0.12)"
--card-border = "rgba(220,220,225,0.24)"
--text-shadow = "rgba(0,0,0,0.40)"
--input-overlay-text = "rgba(208,208,212,0.28)"
--slider-text = "rgba(208,208,212,0.85)"
@@ -48,20 +48,20 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--tooltip-bg = "rgba(18,18,22,0.92)"
--tooltip-border = "rgba(220,220,225,0.10)"
--glass-fill = "rgba(220,220,225,0.07)"
--glass-border = "rgba(220,220,225,0.12)"
--glass-border = "rgba(154,175,200,0.28)"
--glass-noise-tint = "rgba(220,220,225,0.03)"
--tactile-top = "rgba(220,220,225,0.06)"
--tactile-bottom = "rgba(220,220,225,0.0)"
--hover-overlay = "rgba(220,220,225,0.04)"
--active-overlay = "rgba(220,220,225,0.08)"
--rim-light = "rgba(220,220,225,0.08)"
--rim-light = "rgba(220,220,225,0.14)"
--status-divider = "rgba(220,220,225,0.06)"
--sidebar-hover = "rgba(220,220,225,0.08)"
--sidebar-icon = "rgba(220,220,225,0.40)"
--sidebar-badge = "rgba(208,208,212,1.0)"
--sidebar-divider = "rgba(220,220,225,0.05)"
--chart-line = "rgba(220,220,225,0.08)"
--window-control = "rgba(208,208,212,0.75)"
--window-control = "rgba(208,208,212,0.85)"
--window-control-hover = "rgba(220,220,225,0.10)"
--window-close-hover = "rgba(200,50,60,0.70)"
--spinner-track = "rgba(220,220,225,0.08)"

View File

@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#3A2E22"
--on-surface = "#3A2E22"
--on-surface-medium = "rgba(58,46,34,0.68)"
--on-surface-disabled = "rgba(58,46,34,0.38)"
--on-surface-medium = "rgba(58,46,34,0.86)"
--on-surface-disabled = "rgba(58,46,34,0.62)"
--error = "#A0524A"
--on-error = "#FFFFFF"
--success = "#6A8A5C"
--success = "#4E8A42"
--on-success = "#FFFFFF"
--warning = "#C08840"
--on-warning = "#000000"
--divider = "rgba(140,110,70,0.14)"
--outline = "rgba(140,110,70,0.16)"
--divider = "rgba(140,110,70,0.20)"
--outline = "rgba(140,110,70,0.24)"
--scrim = "rgba(30,20,10,0.45)"
--surface-hover = "rgba(176,120,64,0.06)"
--surface-alt = "rgba(176,120,64,0.03)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
--chart-hover-ring = "rgba(176,120,64,0.28)"
--tooltip-bg = "rgba(50,38,24,0.94)"
--tooltip-border = "rgba(176,120,64,0.12)"
--glass-fill = "rgba(255,252,245,0.58)"
--glass-fill = "rgba(255,252,245,0.20)"
--glass-border = "rgba(176,120,64,0.14)"
--glass-noise-tint = "rgba(180,140,80,0.03)"
--tactile-top = "rgba(255,255,248,0.50)"
--tactile-bottom = "rgba(255,255,248,0.08)"
--hover-overlay = "rgba(176,120,64,0.05)"
--hover-overlay = "rgba(176,120,64,0.10)"
--active-overlay = "rgba(176,120,64,0.10)"
--rim-light = "rgba(212,160,108,0.10)"
--rim-light = "rgba(58,46,34,0.22)"
--status-divider = "rgba(176,120,64,0.10)"
--sidebar-hover = "rgba(176,120,64,0.08)"
--sidebar-icon = "rgba(58,46,34,0.50)"

View File

@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#1C1525"
--on-surface = "#1C1525"
--on-surface-medium = "rgba(28,21,37,0.72)"
--on-surface-disabled = "rgba(28,21,37,0.40)"
--on-surface-medium = "rgba(28,21,37,0.86)"
--on-surface-disabled = "rgba(28,21,37,0.62)"
--error = "#C62828"
--on-error = "#FFFFFF"
--success = "#2E7D32"
--on-success = "#FFFFFF"
--warning = "#E65100"
--on-warning = "#000000"
--divider = "rgba(120,80,160,0.12)"
--outline = "rgba(120,80,160,0.14)"
--divider = "rgba(120,80,160,0.20)"
--outline = "rgba(120,80,160,0.24)"
--scrim = "rgba(20,10,30,0.45)"
--surface-hover = "rgba(140,107,175,0.06)"
--surface-alt = "rgba(140,107,175,0.03)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
--chart-hover-ring = "rgba(140,107,175,0.28)"
--tooltip-bg = "rgba(32,24,48,0.94)"
--tooltip-border = "rgba(140,107,175,0.12)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-border = "rgba(140,107,175,0.14)"
--glass-noise-tint = "rgba(180,140,220,0.03)"
--tactile-top = "rgba(255,255,255,0.50)"
--tactile-bottom = "rgba(255,255,255,0.08)"
--hover-overlay = "rgba(140,107,175,0.05)"
--hover-overlay = "rgba(140,107,175,0.10)"
--active-overlay = "rgba(140,107,175,0.10)"
--rim-light = "rgba(180,140,255,0.10)"
--rim-light = "rgba(28,21,37,0.22)"
--status-divider = "rgba(140,107,175,0.10)"
--sidebar-hover = "rgba(140,107,175,0.08)"
--sidebar-icon = "rgba(28,21,37,0.50)"

View File

@@ -19,16 +19,16 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#2A2C30"
--on-surface = "#2A2C30"
--on-surface-medium = "rgba(42,44,48,0.68)"
--on-surface-disabled = "rgba(42,44,48,0.38)"
--on-surface-medium = "rgba(42,44,48,0.86)"
--on-surface-disabled = "rgba(42,44,48,0.62)"
--error = "#8C5A62"
--on-error = "#FFFFFF"
--success = "#5A7E5C"
--success = "#3D7A42"
--on-success = "#FFFFFF"
--warning = "#8A7A52"
--warning = "#9A7A2E"
--on-warning = "#000000"
--divider = "rgba(42,44,48,0.12)"
--outline = "rgba(42,44,48,0.14)"
--divider = "rgba(42,44,48,0.20)"
--outline = "rgba(42,44,48,0.24)"
--scrim = "rgba(0,0,0,0.42)"
--surface-hover = "rgba(42,44,48,0.04)"
--surface-alt = "rgba(42,44,48,0.02)"
@@ -47,14 +47,14 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
--chart-hover-ring = "rgba(42,44,48,0.24)"
--tooltip-bg = "rgba(50,52,58,0.92)"
--tooltip-border = "rgba(42,44,48,0.10)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-border = "rgba(42,44,48,0.10)"
--glass-noise-tint = "rgba(42,44,48,0.015)"
--tactile-top = "rgba(255,255,255,0.35)"
--tactile-bottom = "rgba(255,255,255,0.04)"
--hover-overlay = "rgba(42,44,48,0.04)"
--hover-overlay = "rgba(42,44,48,0.10)"
--active-overlay = "rgba(42,44,48,0.08)"
--rim-light = "rgba(42,44,48,0.06)"
--rim-light = "rgba(42,44,48,0.22)"
--status-divider = "rgba(42,44,48,0.08)"
--sidebar-hover = "rgba(42,44,48,0.05)"
--sidebar-icon = "rgba(42,44,48,0.45)"

View File

@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#2C2A28"
--on-surface = "#2C2A28"
--on-surface-medium = "rgba(44,42,40,0.68)"
--on-surface-disabled = "rgba(44,42,40,0.38)"
--on-surface-medium = "rgba(44,42,40,0.86)"
--on-surface-disabled = "rgba(44,42,40,0.62)"
--error = "#8C5250"
--on-error = "#FFFFFF"
--success = "#5C7A62"
--success = "#3F7A48"
--on-success = "#FFFFFF"
--warning = "#8A7A4C"
--warning = "#9A7A2E"
--on-warning = "#000000"
--divider = "rgba(80,75,68,0.12)"
--outline = "rgba(80,75,68,0.14)"
--divider = "rgba(80,75,68,0.20)"
--outline = "rgba(80,75,68,0.24)"
--scrim = "rgba(20,18,16,0.42)"
--surface-hover = "rgba(110,117,128,0.05)"
--surface-alt = "rgba(110,117,128,0.025)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
--chart-hover-ring = "rgba(110,117,128,0.24)"
--tooltip-bg = "rgba(44,42,40,0.94)"
--tooltip-border = "rgba(110,117,128,0.10)"
--glass-fill = "rgba(255,255,254,0.62)"
--glass-fill = "rgba(255,255,254,0.20)"
--glass-border = "rgba(110,117,128,0.10)"
--glass-noise-tint = "rgba(80,75,68,0.02)"
--tactile-top = "rgba(255,255,255,0.45)"
--tactile-bottom = "rgba(255,255,255,0.06)"
--hover-overlay = "rgba(110,117,128,0.04)"
--hover-overlay = "rgba(110,117,128,0.10)"
--active-overlay = "rgba(110,117,128,0.08)"
--rim-light = "rgba(180,175,168,0.10)"
--rim-light = "rgba(44,42,40,0.22)"
--status-divider = "rgba(110,117,128,0.08)"
--sidebar-hover = "rgba(110,117,128,0.06)"
--sidebar-icon = "rgba(44,42,40,0.48)"

View File

@@ -2,25 +2,25 @@
name = "Obsidian"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#1C1625", --elevation-3 = "#211A2C", --elevation-4 = "#261E33" }
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#252030", --elevation-3 = "#2D2838", --elevation-4 = "#353040" }
images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
--primary = "#AB47BC"
--primary-variant = "#8E24AA"
--primary-light = "#CE93D8"
--secondary = "#B388FF"
--secondary-variant = "#7C4DFF"
--secondary-light = "#D1C4E9"
--primary = "#9A6BA8"
--primary-variant = "#7A4888"
--primary-light = "#C5A8CC"
--secondary = "#A898D8"
--secondary-variant = "#8074C8"
--secondary-light = "#CCC4D9"
--background = "#0A0810"
--surface = "#110E18"
--surface-variant = "#1C1625"
--surface-variant = "#252030"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#E8E0F0"
--on-surface = "#E8E0F0"
--on-surface-medium = "rgba(232,224,240,0.75)"
--on-surface-disabled = "rgba(232,224,240,0.45)"
--on-surface-medium = "rgba(232,224,240,0.85)"
--on-surface-disabled = "rgba(232,224,240,0.58)"
--error = "#CF6679"
--on-error = "#000000"
--success = "#81C784"
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--surface-active = "rgba(200,180,255,0.10)"
--glass-button = "rgba(200,180,255,0.06)"
--glass-button-hover = "rgba(200,180,255,0.12)"
--card-border = "rgba(200,180,255,0.14)"
--card-border = "rgba(200,180,255,0.26)"
--text-shadow = "rgba(0,0,0,0.50)"
--input-overlay-text = "rgba(232,224,240,0.30)"
--slider-text = "rgba(232,224,240,0.85)"
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--tooltip-bg = "rgba(14,11,20,0.92)"
--tooltip-border = "rgba(200,180,255,0.12)"
--glass-fill = "rgba(200,180,255,0.08)"
--glass-border = "rgba(200,180,255,0.14)"
--glass-border = "rgba(154,107,168,0.30)"
--glass-noise-tint = "rgba(200,180,255,0.03)"
--tactile-top = "rgba(200,180,255,0.06)"
--tactile-bottom = "rgba(200,180,255,0.0)"
--hover-overlay = "rgba(200,180,255,0.05)"
--active-overlay = "rgba(200,180,255,0.10)"
--rim-light = "rgba(200,180,255,0.08)"
--rim-light = "rgba(200,180,255,0.14)"
--status-divider = "rgba(200,180,255,0.08)"
--sidebar-hover = "rgba(200,180,255,0.10)"
--sidebar-icon = "rgba(200,180,255,0.42)"
@@ -65,19 +65,19 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--window-control-hover = "rgba(200,180,255,0.12)"
--window-close-hover = "rgba(232,17,35,0.78)"
--spinner-track = "rgba(200,180,255,0.10)"
--spinner-active = "rgba(179,136,255,0.85)"
--spinner-active = "rgba(168,152,216,0.85)"
--shutdown-panel-bg = "rgba(10,8,16,0.90)"
--shutdown-panel-border = "rgba(200,180,255,0.07)"
--ram-bar-app = "#AB47BC"
--ram-bar-app = "#9A6BA8"
--ram-bar-system = "rgba(255,255,255,0.18)"
--accent-total = "#CE93D8"
--accent-shielded = "#80CBC4"
--accent-transparent = "#FFAB91"
--accent-action = "#AB47BC"
--accent-market = "#80CBC4"
--accent-portfolio = "#B388FF"
--toast-info-accent = "#AB47BC"
--toast-info-text = "#CE93D8"
--accent-total = "#C5A8CC"
--accent-shielded = "#8AB8B4"
--accent-transparent = "#D8B0A0"
--accent-action = "#9A6BA8"
--accent-market = "#8AB8B4"
--accent-portfolio = "#A898D8"
--toast-info-accent = "#9A6BA8"
--toast-info-text = "#C5A8CC"
--toast-success-accent = "rgba(50,180,80,1.0)"
--toast-success-text = "rgba(180,255,180,1.0)"
--toast-warning-accent = "rgba(204,166,50,1.0)"
@@ -86,10 +86,10 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--toast-error-text = "rgba(255,153,153,1.0)"
--snackbar-bg = "rgba(40,35,55,0.95)"
--snackbar-text = "rgba(232,224,240,0.87)"
--snackbar-action = "rgba(179,136,255,1.0)"
--snackbar-action-hover = "rgba(206,147,216,1.0)"
--snackbar-action = "rgba(168,152,216,1.0)"
--snackbar-action-hover = "rgba(197,168,204,1.0)"
--switch-track-off = "rgba(200,180,255,0.12)"
--switch-track-on = "rgba(171,71,188,0.50)"
--switch-track-on = "rgba(154,107,168,0.50)"
--switch-thumb-off = "#B0A0C0"
--switch-thumb-on = "#E8E0F0"
--control-shadow = "rgba(0,0,0,0.24)"
@@ -144,18 +144,18 @@ gradient-border-enabled = { size = 1.0 }
gradient-border-speed = { size = 0.12 }
gradient-border-thickness = { size = 1.5 }
gradient-border-alpha = { size = 0.55 }
gradient-border-color-a = { color = "#CE93D8" }
gradient-border-color-b = { color = "#3F51B5" }
gradient-border-color-a = { color = "#C5A8CC" }
gradient-border-color-b = { color = "#6878A8" }
ember-rise-enabled = { size = 0.0 }
# Shader-like viewport overlay — deep indigo crystal atmosphere
viewport-wash-enabled = { size = 1.0 }
viewport-wash-alpha = { size = 0.05 }
viewport-wash-tl = { color = "#4A148C" }
viewport-wash-tr = { color = "#1A237E" }
viewport-wash-bl = { color = "#311B92" }
viewport-wash-br = { color = "#6A1B9A" }
viewport-wash-tl = { color = "#3A2860" }
viewport-wash-tr = { color = "#2A3058" }
viewport-wash-bl = { color = "#302860" }
viewport-wash-br = { color = "#4A3068" }
viewport-wash-rotate = { size = 0.015 }
viewport-wash-pulse = { size = 0.0 }
viewport-wash-pulse-depth = { size = 0.0 }

View File

@@ -20,7 +20,7 @@ spacing-tokens = { xs = 2.0, sm = 4.0, md = 8.0, lg = 12.0, xl = 16.0, xxl = 24.
name = "DragonX"
author = "DanS"
dark = true
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#201410", --elevation-3 = "#261914", --elevation-4 = "#2C1E18" }
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#281C16", --elevation-3 = "#30241C", --elevation-4 = "#382C22" }
images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
@@ -32,13 +32,13 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--secondary-light = "#FF9E40"
--background = "#0C0606"
--surface = "#120A08"
--surface-variant = "#201410"
--surface-variant = "#281C16"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#F0E0D8"
--on-surface = "#F0E0D8"
--on-surface-medium = "rgba(240,224,216,0.7)"
--on-surface-disabled = "rgba(240,224,216,0.44)"
--on-surface-medium = "rgba(240,224,216,0.85)"
--on-surface-disabled = "rgba(240,224,216,0.58)"
--error = "#FF5252"
--on-error = "#000000"
--success = "#81C784"
@@ -66,13 +66,13 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--tooltip-bg = "rgba(12,8,6,0.92)"
--tooltip-border = "rgba(255,180,140,0.12)"
--glass-fill = "rgba(255,180,140,0.08)"
--glass-border = "rgba(255,180,140,0.13)"
--glass-border = "rgba(211,47,47,0.26)"
--glass-noise-tint = "rgba(255,180,140,0.03)"
--tactile-top = "rgba(255,180,140,0.06)"
--tactile-bottom = "rgba(255,180,140,0.0)"
--hover-overlay = "rgba(255,180,140,0.05)"
--active-overlay = "rgba(255,180,140,0.10)"
--rim-light = "rgba(255,180,140,0.08)"
--rim-light = "rgba(255,180,140,0.14)"
--status-divider = "rgba(255,180,140,0.08)"
--sidebar-hover = "rgba(255,180,140,0.10)"
--sidebar-icon = "rgba(255,180,140,0.36)"
@@ -485,12 +485,12 @@ suggestion-trunc-len = { size = 50 }
fee-rounding = { size = 10.0 }
amount-bar-max-btn-width = { size = 80.0 }
amount-bar-height = { size = 22.0 }
confirm-popup-max-width = { size = 420.0 }
confirm-popup-max-width = { size = 560.0 }
confirm-addr-card-height = { size = 28.0 }
confirm-amount-card-height = { size = 70.0 }
confirm-row-step = { size = 16.0 }
confirm-val-col-x = { size = 90.0 }
confirm-usd-col-x = { size = 80.0 }
confirm-amount-card-height = { size = 96.0 }
confirm-row-step = { size = 24.0 }
confirm-val-col-x = { size = 150.0 }
confirm-usd-col-x = { size = 116.0 }
progress-card-height = { size = 36.0 }
progress-card-height-txid = { size = 52.0 }
progress-card-pad-x = { size = 12.0 }
@@ -516,10 +516,10 @@ error-icon-inset = { size = 20.0 }
error-btn-rounding = { size = 4.0 }
progress-glass-rounding-ratio = { size = 0.75 }
confirm-addr-card-min-height = { size = 24.0 }
confirm-val-col-min-x = { size = 70.0 }
confirm-usd-col-min-x = { size = 60.0 }
confirm-amount-card-min-height = { size = 54.0 }
confirm-row-step-min = { size = 12.0 }
confirm-val-col-min-x = { size = 112.0 }
confirm-usd-col-min-x = { size = 92.0 }
confirm-amount-card-min-height = { size = 82.0 }
confirm-row-step-min = { size = 20.0 }
action-btn-min-height = { size = 26.0 }
recent-icon-min-size = { size = 3.5 }
recent-icon-size = { size = 5.0 }
@@ -566,7 +566,7 @@ txid-trunc-len = { size = 14 }
txid-label-x-offset = { size = 20.0 }
txid-copy-btn-right-offset = { size = 50.0 }
txid-copy-btn-y-offset = { size = 2.0 }
confirm-popup-width-ratio = { size = 0.85 }
confirm-popup-width-ratio = { size = 0.92 }
confirm-glass-rounding-ratio = { size = 0.75 }
confirm-addr-trunc-len = { size = 48 }
confirm-divider-thickness = { size = 1.0 }
@@ -766,7 +766,7 @@ control-card-min-height = { size = 60.0 }
active-cell-border-thickness = { size = 1.5 }
cell-border-thickness = { size = 1.0 }
button-icon-y-ratio = { size = 0.42 }
button-label-y-ratio = { size = 0.78 }
button-label-y-ratio = { size = 0.72 }
chart-line-thickness = { size = 1.5 }
details-card-min-height = { size = 50.0 }
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
@@ -779,8 +779,11 @@ pool-url-input = { width = 300.0, height = 28.0 }
pool-worker-input = { width = 200.0, height = 28.0 }
log-panel-height = { size = 120.0 }
log-panel-min = { size = 60.0 }
idle-row-height = { size = 28.0 }
idle-combo-width = { size = 64.0 }
[tabs.peers]
refresh-button = { size = 110.0 }
table-min-height = 150.0
table-height-ratio = 0.45
version-column-width = 150.0
@@ -807,6 +810,7 @@ dir-pill-padding = { size = 4.0 }
dir-pill-y-offset = { size = 1.0 }
dir-pill-y-bottom = { size = 3.0 }
dir-pill-rounding = { size = 3.0 }
seed-badge-padding = { size = 3.0 }
tls-badge-min-width = { size = 20.0 }
tls-badge-width = { size = 28.0 }
tls-badge-rounding = { size = 3.0 }
@@ -870,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
accent-stripe-left-offset = { size = 0.0 }
accent-stripe-width = { size = 4.0 }
accent-stripe-rounding = { size = 1.5 }
chart-y-axis-min-padding = { size = 40.0 }
chart-y-axis-min-padding = { size = 54.0 }
chart-y-axis-padding = { size = 70.0 }
chart-dot-min-radius = { size = 1.5 }
chart-dot-radius = { size = 2.0 }
@@ -890,8 +894,21 @@ pair-bar-arrow-size = { size = 28.0 }
exchange-combo-width = { size = 180.0 }
exchange-top-gap = { size = 0.0 }
[tabs.explorer]
search-input-width = { size = 400.0 }
search-button-width = { size = 100.0 }
search-bar-height = { size = 32.0 }
row-height = { size = 32.0 }
row-rounding = { size = 4.0 }
tx-row-height = { size = 28.0 }
label-column = { size = 160.0 }
detail-modal-width = { size = 700.0 }
detail-max-height = { size = 600.0 }
scroll-fade-zone = { size = 24.0 }
[tabs.console]
input-area-padding = 8.0
bg-darken-alpha = { size = 110.0 } # black overlay alpha (0-255) for the terminal-dark output + input
output-line-spacing = 2.0
output = { line-spacing = 2 }
scroll-multiplier = { size = 3.0 }
@@ -914,7 +931,7 @@ scanline-speed = { size = 40.0 }
scanline-height = { size = 36.0 }
scanline-alpha = { size = 8.0 }
scanline-gap = { size = 2.0 }
scanline-line-alpha = { size = 4.0 }
scanline-line-alpha = { size = 2.0 }
scanline-glow-spread = { size = 4.0 }
scanline-glow-intensity = { size = 0.6 }
scanline-glow-color = { size = 255.0 }
@@ -1152,6 +1169,8 @@ key-input = { height = 150 }
rescan-height-input = { width = 100 }
import-button = { width = 120, font = "button" }
close-button = { width = 100, font = "button" }
paste-preview-alpha = { size = 0.3 }
paste-preview-max-chars = { size = 200 }
[components]
@@ -1200,6 +1219,9 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
fill-alpha = { size = 12.0 }
noise-alpha = { size = 14.0 }
[components.overlay-dialog]
confirm-btn-height = { size = 40.0 }
[components.qr-code]
module-scale = { size = 4 }
border-modules = { size = 2 }
@@ -1212,9 +1234,10 @@ width = { size = 140.0 }
collapsed-width = { size = 64.0 }
collapse-anim-speed = { size = 10.0 }
auto-collapse-threshold = { size = 800.0 }
section-gap = { size = 4.0 }
section-gap = { size = 8.0 }
section-label-pad-left = { size = 16.0 }
item-height = { size = 42.0 }
section-label-pad-bottom = { size = 4.0 }
item-height = { size = 36.0 }
item-pad-x = { size = 8.0 }
min-height = { size = 360.0 }
margin-top = { size = -12 }
@@ -1230,8 +1253,8 @@ icon-half-size = { size = 7.0 }
icon-label-gap = { size = 8.0 }
badge-radius-dot = { size = 4.0 }
badge-radius-number = { size = 8.0 }
button-spacing = { size = 4.0 }
bottom-padding = { size = 0.0 }
button-spacing = { size = 6.0 }
bottom-padding = { size = 4.0 }
exit-icon-gap = { size = 4.0 }
cutout-shadow-alpha = { size = 55 }
cutout-highlight-alpha = { size = 8 }
@@ -1247,8 +1270,8 @@ inset-shadow-fade-ratio = { size = 5.0 }
[components.content-area]
padding-x = 0.0
padding-y = 0.0
margin-top = { size = 4.0 }
margin-bottom = { size = -40.0 }
margin-top = { size = 6.0 }
margin-bottom = { size = -39.0 }
edge-fade-zone = { size = 0.0 }
[components.content-area.window]
@@ -1260,10 +1283,10 @@ page-fade-speed = { size = 8.0 }
collapse-hysteresis = { size = 60.0 }
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
coin-icon = { icon = "logos/logo_dragonx_128.png" }
header-title = { font = "subtitle1", size = 14.0, pad-x = 16.0, pad-y = 12.0, logo-gap = 8.0, opacity = 0.7 }
header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
[components.main-window.window]
padding = [12, 38]
padding = [12, 36]
[components.shutdown]
content-height = { height = 120.0 }
@@ -1305,8 +1328,13 @@ wallet-btn-padding = { size = 24.0 }
rpc-label-min-width = { size = 70.0 }
rpc-label-width = { size = 85.0 }
security-combo-width = { size = 120.0 }
node-grid-breakpoint = { size = 900.0 }
port-input-min-width = { size = 60.0 }
port-input-width-ratio = { size = 0.4 }
idle-combo-width = { size = 64.0 }
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
# card's actual height (aspect-preserved) and capped to this * aspect in width.
about-logo-size = { size = 150.0 }
[components.main-layout]
app-bar-height = { size = 64.0 }
@@ -1357,6 +1385,17 @@ summary = { min-width = 280.0, max-width = 400.0, width-ratio = 0.32, min-height
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
[dialog]
width-default = 480.0
width-lg = 600.0
width-xl = 660.0
min-width = 280.0
form-width = 400.0
action-width = 100.0
action-gap = 8.0
max-height-ratio = 0.94
compact-bottom-ratio = 0.64
[button]
min-width = 180.0
width = 140.0
@@ -1489,7 +1528,6 @@ title = { font = "h5" }
input = { width = 320.0, height = 40.0 }
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
error-text = { font = "caption" }
backdrop-alpha = { opacity = 0.0 }
mode-toggle = { font = "caption" }
# ---------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,801 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ABI_VERSION="sdxl-c-v1"
LINK_MODE="imported"
BACKEND_DIR="$PROJECT_ROOT/third_party/silentdragonxlite/lib"
BACKEND_SOURCE_DIR=""
BUILD_BACKEND_DIR=""
BACKEND_DEPENDENCY_DIR=""
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=false
OUT_DIR="$PROJECT_ROOT/build/lite-backend"
PLATFORM=""
RUST_TARGET=""
CARGO_TARGET_DIR_VALUE="${CARGO_TARGET_DIR:-}"
ARTIFACT_PATH=""
BUILD_ARTIFACT=true
BUILDER="${DRAGONX_LITE_BACKEND_BUILDER:-local}"
JOBS="${JOBS:-}"
SOURCE_DATE_EPOCH_VALUE="${SOURCE_DATE_EPOCH:-}"
REPRODUCIBLE=false
SIGNATURE_REQUIRED=false
SIGNATURE_FILE=""
SIGNATURE_FORMAT=""
SIGNATURE_VERIFICATION_TOOL=""
SIGNATURE_VERIFICATION_COMMAND=""
SIGNATURE_KEY_FINGERPRINT=""
SIGNATURE_CERTIFICATE_IDENTITY=""
SIGNATURE_CERTIFICATE_ISSUER=""
SIGNATURE_TRANSPARENCY_LOG_URL=""
SIGNATURE_VERIFIED_SHA256=""
SIGNATURE_POLICY_NAME="dragonx-lite-backend-signature-policy-v1"
SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE=true
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
SIGNATURE_METADATA_PROVIDED=false
SIGNATURE_VERIFICATION_PERFORMED=false
SIGNATURE_VERIFICATION_STATUS="not-provided"
SIGNATURE_FILE_SHA256=""
REQUIRED_SYMBOLS=(
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
EXTRA_CARGO_ARGS=()
EXTRA_REMAP_PATH_PREFIXES=()
usage() {
cat <<EOF
Build or inventory the SDXL-compatible lite backend artifact.
Usage: $0 [options]
Options:
--platform linux|windows|macos Artifact platform. Defaults to host platform.
--rust-target TRIPLE Cargo target triple for cross builds.
--cargo-target-dir PATH Isolated Cargo target directory for clean builds.
--backend-dir PATH SilentDragonXLite/lib source directory.
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
--out-dir PATH Output directory for copied artifact and metadata.
--artifact PATH Inventory an existing artifact instead of building.
--no-build Do not run cargo; requires --artifact.
--reproducible Add deterministic Rust path remaps for clean builds.
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
--builder NAME Redacted builder/provenance label. Default: local.
--signature-required Fail if verified signature metadata is not supplied.
--signature-file PATH Existing sidecar signature file to record.
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
--signature-verification-tool T Verification tool and version used by the release builder.
--signature-verification-command C
Verification command already run by the release builder.
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
--signature-certificate-identity ID
Reviewed certificate identity, when applicable.
--signature-certificate-issuer I
Reviewed certificate issuer, when applicable.
--signature-transparency-log-url URL
Transparency log entry, when applicable.
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
-j, --jobs N Cargo parallel jobs.
--cargo-arg ARG Extra argument forwarded to cargo build.
-h, --help Show this help.
Outputs:
<out>/<platform>/<artifact>
<out>/<platform>/lite-backend-symbols.txt
<out>/<platform>/lite-backend-artifact-manifest.json
The script captures symbols, checksums, and optional read-only signature
verification metadata only. It does not load the library, resolve function
pointers, call SDXL, sign, upload, or publish artifacts.
EOF
}
info() { printf '[lite-backend] %s\n' "$*"; }
warn() { printf '[lite-backend] warning: %s\n' "$*" >&2; }
die() { printf '[lite-backend] ERROR: %s\n' "$*" >&2; exit 1; }
absolute_path() {
local path="$1"
if [[ "$path" = /* ]]; then
printf '%s\n' "$path"
else
printf '%s/%s\n' "$PWD" "$path"
fi
}
host_platform() {
case "$(uname -s)" in
Linux) printf 'linux\n' ;;
Darwin) printf 'macos\n' ;;
MINGW*|MSYS*|CYGWIN*) printf 'windows\n' ;;
*) die "unsupported host platform: $(uname -s)" ;;
esac
}
normalize_platform() {
case "$1" in
linux|Linux) printf 'linux\n' ;;
windows|win|Win|Windows) printf 'windows\n' ;;
macos|mac|darwin|Darwin) printf 'macos\n' ;;
"") host_platform ;;
*) die "unsupported platform: $1" ;;
esac
}
while [[ $# -gt 0 ]]; do
case "$1" in
--platform)
[[ $# -ge 2 ]] || die "--platform requires a value"
PLATFORM="$(normalize_platform "$2")"
shift 2
;;
--rust-target)
[[ $# -ge 2 ]] || die "--rust-target requires a value"
RUST_TARGET="$2"
shift 2
;;
--cargo-target-dir)
[[ $# -ge 2 ]] || die "--cargo-target-dir requires a value"
CARGO_TARGET_DIR_VALUE="$(absolute_path "$2")"
shift 2
;;
--backend-dir)
[[ $# -ge 2 ]] || die "--backend-dir requires a value"
BACKEND_DIR="$(absolute_path "$2")"
shift 2
;;
--silentdragonxlitelib-dir)
[[ $# -ge 2 ]] || die "--silentdragonxlitelib-dir requires a value"
BACKEND_DEPENDENCY_DIR="$(absolute_path "$2")"
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=true
shift 2
;;
--out-dir)
[[ $# -ge 2 ]] || die "--out-dir requires a value"
OUT_DIR="$(absolute_path "$2")"
shift 2
;;
--artifact)
[[ $# -ge 2 ]] || die "--artifact requires a value"
ARTIFACT_PATH="$(absolute_path "$2")"
BUILD_ARTIFACT=false
shift 2
;;
--no-build)
BUILD_ARTIFACT=false
shift
;;
--reproducible)
REPRODUCIBLE=true
shift
;;
--remap-path-prefix)
[[ $# -ge 2 ]] || die "--remap-path-prefix requires FROM=TO"
[[ "$2" == *=* ]] || die "--remap-path-prefix requires FROM=TO"
EXTRA_REMAP_PATH_PREFIXES+=("$2")
shift 2
;;
--builder)
[[ $# -ge 2 ]] || die "--builder requires a value"
BUILDER="$2"
shift 2
;;
--signature-required)
SIGNATURE_REQUIRED=true
shift
;;
--signature-file|--signature-path)
[[ $# -ge 2 ]] || die "$1 requires a value"
SIGNATURE_FILE="$(absolute_path "$2")"
shift 2
;;
--signature-format)
[[ $# -ge 2 ]] || die "--signature-format requires a value"
SIGNATURE_FORMAT="$2"
shift 2
;;
--signature-verification-tool|--signature-tool)
[[ $# -ge 2 ]] || die "$1 requires a value"
SIGNATURE_VERIFICATION_TOOL="$2"
shift 2
;;
--signature-verification-command)
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
SIGNATURE_VERIFICATION_COMMAND="$2"
shift 2
;;
--signature-key-fingerprint)
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
SIGNATURE_KEY_FINGERPRINT="$2"
shift 2
;;
--signature-certificate-identity)
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
SIGNATURE_CERTIFICATE_IDENTITY="$2"
shift 2
;;
--signature-certificate-issuer)
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
SIGNATURE_CERTIFICATE_ISSUER="$2"
shift 2
;;
--signature-transparency-log-url)
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
shift 2
;;
--signature-verified-sha256)
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
SIGNATURE_VERIFIED_SHA256="$2"
shift 2
;;
-j|--jobs)
[[ $# -ge 2 ]] || die "--jobs requires a value"
JOBS="$2"
shift 2
;;
--cargo-arg)
[[ $# -ge 2 ]] || die "--cargo-arg requires a value"
EXTRA_CARGO_ARGS+=("$2")
shift 2
;;
-h|--help)
usage
exit 0
;;
*) die "unknown option: $1" ;;
esac
done
PLATFORM="$(normalize_platform "$PLATFORM")"
BACKEND_SOURCE_DIR="$BACKEND_DIR"
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
if [[ "$PLATFORM" == "windows" && -z "$RUST_TARGET" ]]; then
RUST_TARGET="x86_64-pc-windows-gnu"
fi
if [[ "$PLATFORM" == "macos" && -z "$RUST_TARGET" && "$(host_platform)" != "macos" ]]; then
die "macOS artifacts require --rust-target when not running on macOS"
fi
if [[ "$BUILD_ARTIFACT" == false && -z "$ARTIFACT_PATH" ]]; then
die "--no-build requires --artifact"
fi
backend_dependency_path_from_cargo() {
local cargo_toml="$1"
awk '
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
original = $0
path = $0
sub(/.*path[[:space:]]*=[[:space:]]*"/, "", path)
sub(/".*/, "", path)
if (path != original) print path
exit
}
' "$cargo_toml"
}
canonical_dependency_path() {
local path="$1"
if [[ -d "$path" ]]; then
(cd "$path" && pwd -P)
else
absolute_path "$path"
fi
}
validate_backend_dependency_source() {
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] || return
if [[ ! -f "$BACKEND_DEPENDENCY_DIR/Cargo.toml" ]]; then
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
die "Cargo.toml not found in $BACKEND_DEPENDENCY_DIR"
fi
warn "Cargo.toml not found in silentdragonxlitelib source: $BACKEND_DEPENDENCY_DIR"
return
fi
if ! grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*"silentdragonxlitelib"' "$BACKEND_DEPENDENCY_DIR/Cargo.toml"; then
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
die "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
fi
warn "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
fi
}
# Ensure the Sapling proving params are present in the core crate (rust-embed bakes them in at build
# time). They are the fixed Zcash trusted-setup output — not buildable — so fetch + verify them from
# git.dragonx.is when absent. Override the source with SAPLING_PARAMS_BASE_URL.
SAPLING_PARAMS_BASE_URL="${SAPLING_PARAMS_BASE_URL:-https://git.dragonx.is/DragonX/zcash-params/releases/download/sapling-v1}"
ensure_sapling_params() {
local dir="$1"
[[ -n "$dir" ]] || return 0
mkdir -p "$dir"
local specs=(
"sapling-spend.params:8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
"sapling-output.params:2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
)
local spec name want path got
for spec in "${specs[@]}"; do
name="${spec%%:*}"; want="${spec##*:}"; path="$dir/$name"
if [[ -f "$path" ]] && [[ "$(compute_sha256 "$path")" == "$want" ]]; then
info "sapling param present and verified: $name"
continue
fi
info "fetching $name from $SAPLING_PARAMS_BASE_URL"
curl -fsSL "$SAPLING_PARAMS_BASE_URL/$name" -o "$path" || die "failed to download sapling param: $name"
got="$(compute_sha256 "$path")"
[[ "$got" == "$want" ]] || { rm -f "$path"; die "sapling param $name sha256 mismatch (got $got, want $want)"; }
info "downloaded and verified $name"
done
}
prepare_backend_source() {
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == false ]]; then
if [[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]]; then
local configured_dependency_path
configured_dependency_path="$(backend_dependency_path_from_cargo "$BACKEND_SOURCE_DIR/Cargo.toml")"
if [[ -n "$configured_dependency_path" ]]; then
if [[ "$configured_dependency_path" = /* ]]; then
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$configured_dependency_path")"
warn "backend Cargo.toml uses an absolute silentdragonxlitelib path; use --silentdragonxlitelib-dir for portable builders"
else
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$BACKEND_SOURCE_DIR/$configured_dependency_path")"
info "using relative silentdragonxlitelib dependency at $BACKEND_DEPENDENCY_DIR"
fi
validate_backend_dependency_source
fi
fi
return
fi
[[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BACKEND_SOURCE_DIR"
validate_backend_dependency_source
[[ "$BACKEND_DEPENDENCY_DIR" != *\"* ]] || die "--silentdragonxlitelib-dir path cannot contain a double quote"
local prepared_root="$OUT_DIR/.prepared-backend/$PLATFORM"
[[ "$prepared_root" == */.prepared-backend/* ]] || die "refusing unsafe prepared backend path: $prepared_root"
rm -rf "$prepared_root"
mkdir -p "$prepared_root"
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
# "vendor" relative to the build root, so expose it inside the prepared root too.
[[ -d "$BACKEND_SOURCE_DIR/vendor" ]] && ln -s "$BACKEND_SOURCE_DIR/vendor" "$prepared_root/vendor"
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
awk -v replacement="$replacement" '
BEGIN { replaced = 0 }
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
print replacement
replaced = 1
next
}
{ print }
END { if (replaced != 1) exit 42 }
' "$BACKEND_SOURCE_DIR/Cargo.toml" > "$prepared_root/Cargo.toml" \
|| die "failed to prepare backend Cargo.toml with portable silentdragonxlitelib path"
BUILD_BACKEND_DIR="$prepared_root"
info "prepared backend source at $BUILD_BACKEND_DIR with silentdragonxlitelib from $BACKEND_DEPENDENCY_DIR"
}
prepare_backend_source
artifact_kind() {
local name="${1##*/}"
case "$name" in
*.a|*.lib) printf 'static-library\n' ;;
*.so|*.dylib|*.dll) printf 'shared-library\n' ;;
*) printf 'unknown\n' ;;
esac
}
cargo_output_candidates() {
local cargo_target_root="$BUILD_BACKEND_DIR/target"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
fi
local base="$cargo_target_root/release"
if [[ -n "$RUST_TARGET" ]]; then
base="$cargo_target_root/$RUST_TARGET/release"
fi
case "$PLATFORM" in
linux)
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.so"
;;
windows)
printf '%s\n' "$base/silentdragonxlite.lib" "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.dll"
;;
macos)
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.dylib" "$base/silentdragonxlite.dylib"
;;
esac
}
source_revision_for() {
local dir="$1"
local revision_file
for revision_file in "$dir/DRAGONX_SOURCE_REVISION" "$dir/../DRAGONX_SOURCE_REVISION"; do
if [[ -f "$revision_file" ]]; then
sed -n '1p' "$revision_file"
return
fi
done
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$dir" rev-parse HEAD 2>/dev/null || printf 'unknown'
else
printf 'unknown'
fi
}
default_source_date_epoch() {
if git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$PROJECT_ROOT" log -1 --format=%ct 2>/dev/null || printf '0'
else
printf '0'
fi
}
append_rustflag() {
local rustflag="$1"
if [[ -n "${RUSTFLAGS:-}" ]]; then
export RUSTFLAGS="${RUSTFLAGS} ${rustflag}"
else
export RUSTFLAGS="$rustflag"
fi
}
append_rust_path_remap() {
local from_path="$1"
local to_path="$2"
[[ -n "$from_path" && -n "$to_path" ]] || return
append_rustflag "--remap-path-prefix=${from_path}=${to_path}"
}
apply_reproducible_rustflags() {
local cargo_target_root="$BUILD_BACKEND_DIR/target"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
fi
append_rust_path_remap "$PROJECT_ROOT" "/dragonx-project"
append_rust_path_remap "$BACKEND_SOURCE_DIR" "/dragonx-lite-backend"
if [[ "$BUILD_BACKEND_DIR" != "$BACKEND_SOURCE_DIR" ]]; then
append_rust_path_remap "$BUILD_BACKEND_DIR" "/dragonx-lite-backend"
fi
append_rust_path_remap "$BACKEND_DEPENDENCY_DIR" "/dragonx-lite-backend-dependency"
for path_remap in "${EXTRA_REMAP_PATH_PREFIXES[@]}"; do
append_rustflag "--remap-path-prefix=${path_remap}"
done
local cargo_home="${CARGO_HOME:-}"
if [[ -z "$cargo_home" && -n "${HOME:-}" ]]; then
cargo_home="$HOME/.cargo"
fi
if [[ -n "$cargo_home" && -d "$cargo_home" ]]; then
append_rust_path_remap "$cargo_home" "/cargo-home"
fi
append_rust_path_remap "$cargo_target_root" "/dragonx-lite-cargo-target"
}
build_with_cargo() {
command -v cargo >/dev/null 2>&1 || die "cargo was not found"
[[ -f "$BUILD_BACKEND_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BUILD_BACKEND_DIR"
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
fi
export CARGO_INCREMENTAL=0
export SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH_VALUE"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
export CARGO_TARGET_DIR="$CARGO_TARGET_DIR_VALUE"
fi
if [[ "$REPRODUCIBLE" == true ]]; then
apply_reproducible_rustflags
fi
if [[ "$PLATFORM" == "windows" && -d "$BUILD_BACKEND_DIR/libsodium-mingw" ]]; then
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
fi
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] && ensure_sapling_params "$BACKEND_DEPENDENCY_DIR/zcash-params"
local cargo_cmd=(cargo build --locked --lib --release)
if [[ -n "$RUST_TARGET" ]]; then
cargo_cmd+=(--target "$RUST_TARGET")
fi
if [[ -n "$JOBS" ]]; then
cargo_cmd+=(-j "$JOBS")
fi
cargo_cmd+=("${EXTRA_CARGO_ARGS[@]}")
info "building backend in $BUILD_BACKEND_DIR"
(cd "$BUILD_BACKEND_DIR" && "${cargo_cmd[@]}")
while IFS= read -r candidate; do
if [[ -f "$candidate" ]]; then
ARTIFACT_PATH="$candidate"
return
fi
done < <(cargo_output_candidates)
die "cargo finished, but no expected backend artifact was found under $BUILD_BACKEND_DIR/target"
}
select_nm_tool() {
if [[ "$PLATFORM" == "windows" ]] && command -v x86_64-w64-mingw32-nm >/dev/null 2>&1; then
printf 'x86_64-w64-mingw32-nm\n'
return
fi
if command -v llvm-nm >/dev/null 2>&1; then
printf 'llvm-nm\n'
return
fi
if command -v nm >/dev/null 2>&1; then
printf 'nm\n'
return
fi
die "no symbol inventory tool found; install nm, llvm-nm, or x86_64-w64-mingw32-nm"
}
compute_sha256() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
else
die "sha256sum or shasum is required"
fi
}
json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/\\n}"
value="${value//$'\r'/}"
value="${value//$'\t'/\\t}"
printf '"%s"' "$value"
}
json_array() {
local first=true
printf '['
for value in "$@"; do
if [[ "$first" == true ]]; then
first=false
else
printf ','
fi
json_escape "$value"
done
printf ']'
}
json_array_from_file() {
local file="$1"
local values=()
if [[ -f "$file" ]]; then
mapfile -t values < "$file"
fi
json_array "${values[@]}"
}
signature_metadata_requested() {
[[ "$SIGNATURE_REQUIRED" == true || \
-n "$SIGNATURE_FILE" || \
-n "$SIGNATURE_FORMAT" || \
-n "$SIGNATURE_VERIFICATION_TOOL" || \
-n "$SIGNATURE_VERIFICATION_COMMAND" || \
-n "$SIGNATURE_KEY_FINGERPRINT" || \
-n "$SIGNATURE_CERTIFICATE_IDENTITY" || \
-n "$SIGNATURE_CERTIFICATE_ISSUER" || \
-n "$SIGNATURE_TRANSPARENCY_LOG_URL" || \
-n "$SIGNATURE_VERIFIED_SHA256" ]]
}
validate_signature_metadata() {
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
if [[ "$SIGNATURE_REQUIRED" == true ]]; then
SIGNATURE_REQUIRED_MANIFEST_VALUE=true
fi
if ! signature_metadata_requested; then
return
fi
[[ -n "$SIGNATURE_FILE" ]] || die "signature metadata requires --signature-file"
[[ -f "$SIGNATURE_FILE" ]] || die "signature file does not exist: $SIGNATURE_FILE"
[[ -n "$SIGNATURE_FORMAT" ]] || die "signature metadata requires --signature-format"
case "$SIGNATURE_FORMAT" in
minisign|gpg|sigstore|external|other) ;;
*) die "unsupported --signature-format: $SIGNATURE_FORMAT" ;;
esac
[[ -n "$SIGNATURE_VERIFICATION_TOOL" ]] || die "signature metadata requires --signature-verification-tool"
[[ -n "$SIGNATURE_VERIFIED_SHA256" ]] || die "signature metadata requires --signature-verified-sha256"
[[ "$SIGNATURE_VERIFIED_SHA256" == "$SHA256_DIGEST" ]] || die "signature verified SHA-256 does not match artifact SHA-256"
if [[ -z "$SIGNATURE_KEY_FINGERPRINT" && -z "$SIGNATURE_CERTIFICATE_IDENTITY" ]]; then
die "signature metadata requires --signature-key-fingerprint or --signature-certificate-identity"
fi
SIGNATURE_METADATA_PROVIDED=true
SIGNATURE_VERIFICATION_PERFORMED=true
SIGNATURE_VERIFICATION_STATUS="verified"
SIGNATURE_FILE_SHA256="$(compute_sha256 "$SIGNATURE_FILE")"
}
if [[ "$BUILD_ARTIFACT" == true ]]; then
build_with_cargo
fi
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
fi
[[ -f "$ARTIFACT_PATH" ]] || die "artifact not found: $ARTIFACT_PATH"
KIND="$(artifact_kind "$ARTIFACT_PATH")"
[[ "$KIND" != "unknown" ]] || die "artifact kind is unsupported: $ARTIFACT_PATH"
PLATFORM_OUT_DIR="$OUT_DIR/$PLATFORM"
mkdir -p "$PLATFORM_OUT_DIR"
ARTIFACT_NAME="$(basename "$ARTIFACT_PATH")"
ARTIFACT_OUTPUT="$PLATFORM_OUT_DIR/$ARTIFACT_NAME"
if [[ "$(absolute_path "$ARTIFACT_PATH")" != "$(absolute_path "$ARTIFACT_OUTPUT")" ]]; then
cp -p "$ARTIFACT_PATH" "$ARTIFACT_OUTPUT"
fi
SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.txt"
RAW_SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.raw.txt"
NM_TOOL="$(select_nm_tool)"
info "capturing exported symbols with $NM_TOOL"
if ! "$NM_TOOL" -g --defined-only "$ARTIFACT_OUTPUT" > "$RAW_SYMBOLS_FILE" 2> "$PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"; then
die "symbol inventory failed; see $PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"
fi
awk '{print $NF}' "$RAW_SYMBOLS_FILE" \
| sed 's/^_//' \
| grep -E '^(litelib_[A-Za-z0-9_]*|blake3_PW)$' \
| sort -u > "$SYMBOLS_FILE" || true
[[ -s "$SYMBOLS_FILE" ]] || die "no SDXL C ABI symbols were found in $ARTIFACT_OUTPUT"
MISSING_SYMBOLS=()
for required in "${REQUIRED_SYMBOLS[@]}"; do
if ! grep -Fxq "$required" "$SYMBOLS_FILE"; then
MISSING_SYMBOLS+=("$required")
fi
done
if [[ ${#MISSING_SYMBOLS[@]} -ne 0 ]]; then
printf '%s\n' "${MISSING_SYMBOLS[@]}" > "$PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
die "artifact is missing required symbols; see $PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
fi
SHA256_DIGEST="$(compute_sha256 "$ARTIFACT_OUTPUT")"
validate_signature_metadata
ARTIFACT_SIZE_BYTES="$(wc -c < "$ARTIFACT_OUTPUT" | tr -d ' ')"
PROJECT_REVISION="$(source_revision_for "$PROJECT_ROOT")"
BACKEND_REVISION="$(source_revision_for "$BACKEND_SOURCE_DIR")"
BACKEND_DEPENDENCY_REVISION=""
if [[ -n "$BACKEND_DEPENDENCY_DIR" ]]; then
BACKEND_DEPENDENCY_REVISION="$(source_revision_for "$BACKEND_DEPENDENCY_DIR")"
fi
ARTIFACT_SET_ID="$PLATFORM-${SHA256_DIGEST:0:16}"
REPRODUCIBLE_MANIFEST_VALUE=false
if [[ "$BUILD_ARTIFACT" == true && "$REPRODUCIBLE" == true ]]; then
REPRODUCIBLE_MANIFEST_VALUE=true
fi
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=false
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=true
fi
FILE_DESCRIPTION="unknown"
if command -v file >/dev/null 2>&1; then
FILE_DESCRIPTION="$(file -b "$ARTIFACT_OUTPUT")"
fi
MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
{
printf '{\n'
printf ' "schema": "dragonx.lite.backend-artifact.v1",\n'
printf ' "generated_by": "scripts/build-lite-backend-artifact.sh",\n'
printf ' "read_only_inventory": true,\n'
printf ' "artifact_mutation_requested": false,\n'
printf ' "upload_requested": false,\n'
printf ' "signing_requested": false,\n'
printf ' "publication_requested": false,\n'
printf ' "signature_verification": {\n'
printf ' "policy_name": '; json_escape "$SIGNATURE_POLICY_NAME"; printf ',\n'
printf ' "policy_defined": %s,\n' "$SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE"
printf ' "required_for_release": %s,\n' "$SIGNATURE_REQUIRED_MANIFEST_VALUE"
printf ' "metadata_read_only": true,\n'
printf ' "metadata_provided": %s,\n' "$SIGNATURE_METADATA_PROVIDED"
printf ' "verification_performed": %s,\n' "$SIGNATURE_VERIFICATION_PERFORMED"
printf ' "verification_status": '; json_escape "$SIGNATURE_VERIFICATION_STATUS"; printf ',\n'
printf ' "signature_format": '; json_escape "$SIGNATURE_FORMAT"; printf ',\n'
printf ' "signature_path": '; json_escape "$SIGNATURE_FILE"; printf ',\n'
printf ' "signature_file_sha256": '; json_escape "$SIGNATURE_FILE_SHA256"; printf ',\n'
printf ' "verification_tool": '; json_escape "$SIGNATURE_VERIFICATION_TOOL"; printf ',\n'
printf ' "verification_command": '; json_escape "$SIGNATURE_VERIFICATION_COMMAND"; printf ',\n'
printf ' "key_fingerprint": '; json_escape "$SIGNATURE_KEY_FINGERPRINT"; printf ',\n'
printf ' "certificate_identity": '; json_escape "$SIGNATURE_CERTIFICATE_IDENTITY"; printf ',\n'
printf ' "certificate_issuer": '; json_escape "$SIGNATURE_CERTIFICATE_ISSUER"; printf ',\n'
printf ' "transparency_log_url": '; json_escape "$SIGNATURE_TRANSPARENCY_LOG_URL"; printf ',\n'
printf ' "verified_artifact_sha256": '; json_escape "$SIGNATURE_VERIFIED_SHA256"; printf '\n'
printf ' },\n'
printf ' "abi_version": '; json_escape "$ABI_VERSION"; printf ',\n'
printf ' "link_mode": '; json_escape "$LINK_MODE"; printf ',\n'
printf ' "platform": '; json_escape "$PLATFORM"; printf ',\n'
printf ' "rust_target": '; json_escape "$RUST_TARGET"; printf ',\n'
printf ' "artifact": {\n'
printf ' "path": '; json_escape "$ARTIFACT_OUTPUT"; printf ',\n'
printf ' "kind": '; json_escape "$KIND"; printf ',\n'
printf ' "size_bytes": %s,\n' "$ARTIFACT_SIZE_BYTES"
printf ' "sha256": '; json_escape "$SHA256_DIGEST"; printf ',\n'
printf ' "file_description": '; json_escape "$FILE_DESCRIPTION"; printf '\n'
printf ' },\n'
printf ' "symbol_inventory": {\n'
printf ' "tool": '; json_escape "$NM_TOOL"; printf ',\n'
printf ' "symbols_path": '; json_escape "$SYMBOLS_FILE"; printf ',\n'
printf ' "raw_symbols_path": '; json_escape "$RAW_SYMBOLS_FILE"; printf ',\n'
printf ' "required_symbols": '; json_array "${REQUIRED_SYMBOLS[@]}"; printf ',\n'
printf ' "exported_symbols": '; json_array_from_file "$SYMBOLS_FILE"; printf ',\n'
printf ' "missing_required_symbols": []\n'
printf ' },\n'
printf ' "provenance": {\n'
printf ' "owner_ready": true,\n'
printf ' "metadata_provided": true,\n'
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
printf ' "portable_dependency_override": %s,\n' "$PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE"
printf ' "silentdragonxlitelib_source": '; json_escape "$BACKEND_DEPENDENCY_DIR"; printf ',\n'
printf ' "builder": '; json_escape "$BUILDER"; printf ',\n'
printf ' "source_revision": '; json_escape "$BACKEND_REVISION"; printf ',\n'
printf ' "silentdragonxlitelib_revision": '; json_escape "$BACKEND_DEPENDENCY_REVISION"; printf ',\n'
printf ' "project_revision": '; json_escape "$PROJECT_REVISION"; printf ',\n'
printf ' "artifact_set_id": '; json_escape "$ARTIFACT_SET_ID"; printf ',\n'
printf ' "source_date_epoch": '; json_escape "$SOURCE_DATE_EPOCH_VALUE"; printf ',\n'
printf ' "reproducible": %s,\n' "$REPRODUCIBLE_MANIFEST_VALUE"
printf ' "redacted": true\n'
printf ' }\n'
printf '}\n'
} > "$MANIFEST_FILE"
info "artifact: $ARTIFACT_OUTPUT"
info "symbols: $SYMBOLS_FILE"
info "manifest: $MANIFEST_FILE"
info "sha256: $SHA256_DIGEST"
cat <<EOF
CMake configure example:
cmake -S "$PROJECT_ROOT" -B "$PROJECT_ROOT/build/lite" \\
-DDRAGONX_BUILD_LITE=ON \\
-DDRAGONX_ENABLE_LITE_BACKEND=ON \\
-DDRAGONX_LITE_BACKEND_LIBRARY="$ARTIFACT_OUTPUT" \\
-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE="$SYMBOLS_FILE" \\
-DDRAGONX_LITE_BACKEND_MANIFEST="$MANIFEST_FILE" \\
-DDRAGONX_LITE_BACKEND_LINK_MODE=$LINK_MODE \\
-DDRAGONX_LITE_BACKEND_ABI=$ABI_VERSION
EOF

131
scripts/build_cjk_subset.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Build a NotoSansCJK subset font containing all characters used by
the zh, ja, and ko translation files, plus common CJK punctuation
and symbols.
Usage:
python3 scripts/build_cjk_subset.py
Requires: pip install fonttools brotli
"""
import json
import os
from fontTools.ttLib import TTFont
from fontTools import subset as ftsubset
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
# Collect all characters used in CJK translation files
needed = set()
for lang in ['zh', 'ja', 'ko']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
for v in data.values():
if isinstance(v, str):
for c in v:
cp = ord(c)
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
needed.add(cp)
# Also add common CJK ranges that future translations might use:
# - CJK punctuation and symbols (3000-303F)
# - Hiragana (3040-309F)
# - Katakana (30A0-30FF)
# - Bopomofo (3100-312F)
# - CJK quotation marks, brackets
for cp in range(0x3000, 0x3100):
needed.add(cp)
for cp in range(0x3100, 0x3130):
needed.add(cp)
# Fullwidth ASCII variants (commonly mixed in CJK text)
for cp in range(0xFF01, 0xFF5F):
needed.add(cp)
print(f"Total non-ASCII characters to include: {len(needed)}")
# Check which of these the source font supports
font = TTFont(SOURCE_FONT)
cmap = font.getBestCmap()
supportable = needed & set(cmap.keys())
unsupported = needed - set(cmap.keys())
print(f"Supported by source font: {len(supportable)}")
if unsupported:
print(f"Not in source font (will use fallback): {len(unsupported)}")
for cp in sorted(unsupported)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
# Build the subset using pyftsubset CLI-style API
args = [
SOURCE_FONT,
f'--output-file={OUTPUT_FONT}',
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
'--no-hinting',
'--desubroutinize',
]
ftsubset.main(args)
# Convert CFF outlines to TrueType (glyf) outlines.
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib import newTable
tmp_otf = OUTPUT_FONT + '.tmp.otf'
os.rename(OUTPUT_FONT, tmp_otf)
conv = TTFont(tmp_otf)
if 'CFF ' in conv:
print("Converting CFF -> TrueType outlines...")
glyphOrder = conv.getGlyphOrder()
glyphSet = conv.getGlyphSet()
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception:
glyf_table.glyphs[gname] = TTGlyph()
del conv['CFF ']
if 'VORG' in conv:
del conv['VORG']
conv['glyf'] = glyf_table
conv['loca'] = loca_table
conv['head'].indexToLocFormat = 1
if 'maxp' in conv:
conv['maxp'].version = 0x00010000
conv.sfntVersion = "\x00\x01\x00\x00"
conv.save(OUTPUT_FONT)
conv.close()
os.remove(tmp_otf)
size = os.path.getsize(OUTPUT_FONT)
print(f"\nOutput: {OUTPUT_FONT}")
print(f"Size: {size / 1024:.0f} KB")
# Verify
verify = TTFont(OUTPUT_FONT)
verify_cmap = set(verify.getBestCmap().keys())
still_missing = needed - verify_cmap
print(f"Verified glyphs in subset: {len(verify_cmap)}")
if still_missing:
# These are chars not in the source font - expected for some Hangul/Hiragana
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
for cp in sorted(still_missing)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
else:
print("All needed characters are covered!")

56
scripts/check-source-hygiene.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# Source-tree hygiene guard.
#
# Blocks two failure modes that an AI coding session previously introduced in
# src/wallet/ (the lite-wallet "_plan"/"_batch" churn): pathologically long
# filenames (which also break the Windows MAX_PATH 260-char limit during the
# cross-build) and the runaway "receipt/custody/handoff/stewardship" naming
# explosion where each session wrapped the previous artifact in one more layer.
#
# Usage:
# scripts/check-source-hygiene.sh # check working-tree src/
# scripts/check-source-hygiene.sh --staged # check staged files (pre-commit)
#
# Install as a git pre-commit hook:
# ln -sf ../../scripts/check-source-hygiene.sh .git/hooks/pre-commit
# # (the hook invokes it with --staged automatically when named pre-commit)
set -euo pipefail
MAX_LEN=80
# Naming-explosion tokens. Two or more chained in one basename is the smell.
CHURN_RE='receipt|custody|handoff|stewardship|promotion_activation|acceptance_confirmation|archive_handoff|post_closure'
mode="${1:-}"
if [[ "$mode" == "--staged" || "$(basename "$0")" == "pre-commit" ]]; then
mapfile -t files < <(git diff --cached --name-only --diff-filter=AR | grep -E '\.(cpp|h|hpp|cc)$' || true)
else
mapfile -t files < <(git ls-files 'src/**/*.cpp' 'src/**/*.h' 2>/dev/null; \
find src -type f \( -name '*.cpp' -o -name '*.h' \) 2>/dev/null)
# de-dup
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u)
fi
fail=0
for f in "${files[@]}"; do
[[ -z "$f" ]] && continue
base="$(basename "$f")"
len=${#base}
if (( len > MAX_LEN )); then
echo "✗ filename too long ($len > $MAX_LEN chars): $f" >&2
fail=1
fi
# count distinct churn tokens in the basename ( || true: grep exits 1 on no match)
n=$(printf '%s' "$base" | grep -oE "$CHURN_RE" | sort -u | wc -l || true)
if (( n >= 2 )); then
echo "✗ runaway naming pattern ($n churn tokens) — refactor in place, don't add a layer: $f" >&2
fail=1
fi
done
if (( fail )); then
echo "" >&2
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
exit 1
fi
echo "source hygiene OK (${#files[@]} files checked)"

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Check which characters in translation files fall outside the font glyph ranges."""
import json
import unicodedata
import glob
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
# Glyph ranges from typography.cpp (regular font + CJK merge)
RANGES = [
# Regular font ranges
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
(0x0100, 0x024F), # Latin Extended-A + B
(0x0370, 0x03FF), # Greek and Coptic
(0x0400, 0x04FF), # Cyrillic
(0x0500, 0x052F), # Cyrillic Supplement
(0x2000, 0x206F), # General Punctuation
(0x2190, 0x21FF), # Arrows
(0x2200, 0x22FF), # Mathematical Operators
(0x2600, 0x26FF), # Miscellaneous Symbols
# CJK ranges
(0x2E80, 0x2FDF), # CJK Radicals
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
(0x3100, 0x312F), # Bopomofo
(0x31F0, 0x31FF), # Katakana Extensions
(0x3400, 0x4DBF), # CJK Extension A
(0x4E00, 0x9FFF), # CJK Unified Ideographs
(0xAC00, 0xD7AF), # Hangul Syllables
(0xFF00, 0xFFEF), # Fullwidth Forms
]
def in_ranges(cp):
return any(lo <= cp <= hi for lo, hi in RANGES)
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
lang = os.path.basename(path).replace('.json', '')
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
missing = {}
for key, val in data.items():
if not isinstance(val, str):
continue
for c in val:
cp = ord(c)
if cp > 0x7F and not in_ranges(cp):
if c not in missing:
missing[c] = []
missing[c].append(key)
if missing:
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
for c in sorted(missing, key=lambda x: ord(x)):
cp = ord(c)
name = unicodedata.name(c, 'UNKNOWN')
keys = missing[c][:3]
key_str = ', '.join(keys)
if len(missing[c]) > 3:
key_str += f' (+{len(missing[c])-3} more)'
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
else:
print(f"=== {lang}.json: OK (all characters covered) ===")

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Check which characters needed by translations are missing from bundled fonts."""
import json
import os
from fontTools.ttLib import TTFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
# Load font cmaps
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
cjk_cmap = set(cjk.getBestCmap().keys())
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
combined = cjk_cmap | ubuntu_cmap
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
print(f"Combined: {len(combined)}")
print()
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
needed = set()
for v in data.values():
if isinstance(v, str):
for c in v:
needed.add(ord(c))
missing = sorted(needed - combined)
if missing:
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
for cp in missing[:20]:
c = chr(cp)
print(f" U+{cp:04X} {c}")
if len(missing) > 20:
print(f" ... and {len(missing) - 20} more")
else:
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
so we need glyf-based TrueType outlines instead.
Two approaches:
1. Direct CFF->TTF conversion via cu2qu (fontTools)
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
This script tries approach 1 first, falls back to approach 2.
"""
import os
import sys
import json
import glob
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
def get_needed_codepoints():
"""Collect all unique codepoints from CJK translation files."""
codepoints = set()
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
with open(lang_file, "r", encoding="utf-8") as f:
data = json.load(f)
for value in data.values():
if isinstance(value, str):
for ch in value:
cp = ord(ch)
# Include CJK + Hangul + fullwidth + CJK symbols/kana
if cp >= 0x2E80:
codepoints.add(cp)
return codepoints
def convert_cff_to_ttf():
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
from fontTools.ttLib import TTFont
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
print(f"Loading {SRC_OTF}...")
font = TTFont(SRC_OTF)
# Verify it's CFF
if "CFF " not in font:
print("Font is not CFF, skipping conversion")
return False
cff = font["CFF "]
top = cff.cff.topDictIndex[0]
print(f"ROS: {getattr(top, 'ROS', None)}")
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
glyphOrder = font.getGlyphOrder()
print(f"Glyphs: {len(glyphOrder)}")
# Use fontTools' built-in otf2ttf if available
try:
from fontTools.otf2ttf import otf_to_ttf
otf_to_ttf(font)
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
except ImportError:
pass
# Manual conversion using cu2qu
print("Using manual CFF->TTF conversion with cu2qu...")
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.pointPen import SegmentToPointPen
from fontTools import ttLib
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
import struct
# Get glyph set
glyphSet = font.getGlyphSet()
# Create new glyf table
from fontTools.ttLib import newTable
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
max_error = 1.0 # em-units tolerance for cubic->quadratic
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception as e:
# Fallback: empty glyph
glyf_table.glyphs[gname] = TTGlyph()
# Replace CFF with glyf
del font["CFF "]
if "VORG" in font:
del font["VORG"]
font["glyf"] = glyf_table
font["loca"] = loca_table
# Add required tables for TTF
# head table needs indexToLocFormat
font["head"].indexToLocFormat = 1 # long format
# Create maxp for TrueType
if "maxp" in font:
font["maxp"].version = 0x00010000
# Update sfntVersion
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def download_and_subset():
"""Download NotoSansSC-Regular.ttf and subset it."""
import urllib.request
from fontTools.ttLib import TTFont
from fontTools import subset
# Google Fonts provides static TTF files
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
# Actually, we want TTF. Let's try the variable font approach.
# Or better: use google-fonts API for static TTF
# NotoSansSC static TTF from Google Fonts CDN
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
if not os.path.exists(tmp_font):
print(f"Downloading NotoSansSC-Regular.ttf...")
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
# This is a TTC (font collection), too large.
# Use the OTF we already have and convert it.
return False
print(f"Using {tmp_font}")
font = TTFont(tmp_font)
cmap = font.getBestCmap()
print(f"Source has {len(cmap)} cmap entries")
needed = get_needed_codepoints()
print(f"Need {len(needed)} CJK codepoints")
# Subset
subsetter = subset.Subsetter()
subsetter.populate(unicodes=needed)
subsetter.subset(font)
font.save(DST_TTF)
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def verify_result():
"""Verify the output TTF has glyf outlines and correct characters."""
from fontTools.ttLib import TTFont
font = TTFont(DST_TTF)
cmap = font.getBestCmap()
print(f"\n--- Verification ---")
print(f"Format: {font.sfntVersion!r}")
print(f"Has glyf: {'glyf' in font}")
print(f"Has CFF: {'CFF ' in font}")
print(f"Cmap entries: {len(cmap)}")
# Check key characters
test_chars = {
"": 0x5386, "": 0x53F2, # Chinese: history
"": 0x6982, "": 0x8FF0, # Chinese: overview
"": 0x8BBE, "": 0x7F6E, # Chinese: settings
}
for name, cp in test_chars.items():
status = "YES" if cp in cmap else "NO"
print(f" {name} (U+{cp:04X}): {status}")
size = os.path.getsize(DST_TTF)
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
font.close()
if __name__ == "__main__":
print("=== CJK Font CFF -> TTF Converter ===\n")
if convert_cff_to_ttf():
verify_result()
else:
print("Direct conversion failed, trying download approach...")
if download_and_subset():
verify_result()
else:
print("ERROR: Could not convert font")
sys.exit(1)

View File

@@ -8,7 +8,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
APPDIR="${BUILD_DIR}/AppDir"
VERSION="1.0.0"
VERSION="1.2.0"
# Colors
GREEN='\033[0;32m'

122
scripts/expand_themes.py Normal file
View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
Build-time theme expander — merges layout sections from ui.toml into skin files.
Called by CMake during build. Reads source skin files + ui.toml, writes merged
output files to the build directory. Source files are never modified.
Usage:
python3 expand_themes.py <source_themes_dir> <output_themes_dir>
For each .toml file in source_themes_dir (except ui.toml), the script:
1. Copies the skin file contents (theme/palette/backdrop/effects)
2. Appends all layout sections from ui.toml (fonts, tabs, components, etc.)
3. Writes the merged result to output_themes_dir/<filename>
ui.toml itself is copied unchanged.
"""
import re
import sys
import os
# Sections to SKIP when extracting from ui.toml (theme-specific, already in skins)
SKIP_SECTIONS = {"theme", "theme.palette", "backdrop", "effects"}
def extract_layout_sections(ui_toml_path):
"""Extract non-theme sections from ui.toml as a string."""
sections = []
current_section = None
current_lines = []
section_re = re.compile(r'^\[{1,2}([^\]]+)\]{1,2}\s*$')
with open(ui_toml_path, 'r') as f:
for line in f:
m = section_re.match(line.strip())
if m:
if current_section is not None or current_lines:
sections.append((current_section, current_lines))
current_section = m.group(1).strip()
current_lines = [line]
else:
current_lines.append(line)
if current_section is not None or current_lines:
sections.append((current_section, current_lines))
layout_parts = []
for section_name, lines in sections:
if section_name is None:
# Preamble: only include top-level key=value lines
kv_lines = [l for l in lines
if l.strip() and not l.strip().startswith('#') and '=' in l]
if kv_lines:
layout_parts.append(''.join(kv_lines))
continue
if section_name in SKIP_SECTIONS:
continue
layout_parts.append(''.join(lines))
return '\n'.join(layout_parts)
def main():
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <source_themes_dir> <output_themes_dir>")
sys.exit(1)
src_dir = sys.argv[1]
out_dir = sys.argv[2]
ui_toml = os.path.join(src_dir, "ui.toml")
if not os.path.exists(ui_toml):
print(f"ERROR: ui.toml not found at {ui_toml}")
sys.exit(1)
os.makedirs(out_dir, exist_ok=True)
layout_content = extract_layout_sections(ui_toml)
separator = (
"\n# ===========================================================================\n"
"# Layout & Component Properties\n"
"# All values below can be customized per-theme. Edit and save to see\n"
"# changes reflected in the app in real time via hot-reload.\n"
"# ===========================================================================\n\n"
)
for fname in sorted(os.listdir(src_dir)):
if not fname.endswith('.toml'):
continue
src_path = os.path.join(src_dir, fname)
dst_path = os.path.join(out_dir, fname)
if fname == "ui.toml":
# Copy ui.toml unchanged
with open(src_path, 'r') as f:
content = f.read()
with open(dst_path, 'w') as f:
f.write(content)
print(f" COPY {fname}")
continue
# Skin file — append layout sections
with open(src_path, 'r') as f:
skin = f.read()
if not skin.endswith('\n'):
skin += '\n'
merged = skin + separator + layout_content
if not merged.endswith('\n'):
merged += '\n'
with open(dst_path, 'w') as f:
f.write(merged)
lines = merged.count('\n')
print(f" MERGE {fname}{lines} lines")
print("Done.")
if __name__ == "__main__":
main()

View File

@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
curl -fSL -o "$TARBALL" "$SODIUM_URL"
fi
# Verify checksum
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
# Verify checksum (sha256sum on Linux, shasum on macOS)
if command -v sha256sum &>/dev/null; then
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
elif command -v shasum &>/dev/null; then
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
fi
# ── Extract ─────────────────────────────────────────────────────────────────
if [[ ! -d "$SRC_DIR" ]]; then
@@ -77,7 +85,7 @@ case "$TARGET" in
mac)
# Cross-compile for macOS via osxcross
if [[ -z "${OSXCROSS:-}" ]]; then
for try in "$HOME/osxcross" "/opt/osxcross" "$PROJECT_DIR/osxcross"; do
for try in "$PROJECT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross" "$PROJECT_DIR/osxcross"; do
[[ -d "$try/target" ]] && OSXCROSS="$try" && break
done
fi
@@ -115,6 +123,69 @@ case "$TARGET" in
;;
esac
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
IS_MACOS_NATIVE=false
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
IS_MACOS_NATIVE=true
fi
if $IS_MACOS_NATIVE; then
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
export MACOSX_DEPLOYMENT_TARGET="11.0"
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
for ARCH in arm64 x86_64; do
echo "[fetch-libsodium] Building for $ARCH..."
cd "$SRC_DIR"
make clean 2>/dev/null || true
make distclean 2>/dev/null || true
if [[ "$ARCH" == "arm64" ]]; then
ARCH_INSTALL="$INSTALL_ARM64"
HOST_TRIPLE="aarch64-apple-darwin"
else
ARCH_INSTALL="$INSTALL_X86_64"
HOST_TRIPLE="x86_64-apple-darwin"
fi
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
./configure \
--prefix="$ARCH_INSTALL" \
--disable-shared \
--enable-static \
--with-pic \
--host="$HOST_TRIPLE" \
CFLAGS="$ARCH_CFLAGS" \
LDFLAGS="-arch $ARCH" \
> /dev/null
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
make install > /dev/null
done
# Merge with lipo
echo "[fetch-libsodium] Creating universal binary with lipo..."
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
lipo -create \
"$INSTALL_ARM64/lib/libsodium.a" \
"$INSTALL_X86_64/lib/libsodium.a" \
-output "$INSTALL_DIR/lib/libsodium.a"
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
# Clean up per-arch builds
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
cd "$PROJECT_DIR"
rm -rf "$SRC_DIR"
rm -f "$TARBALL"
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
lipo -info "$INSTALL_DIR/lib/libsodium.a"
exit 0
fi
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
./configure "${CONFIGURE_ARGS[@]}" > /dev/null

36
scripts/fix_mojibake.py Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
import os
import glob
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
MOJIBAKE_FIXES = {
'\u00e2\u0080\u0093': '\u2013', # en dash
'\u00e2\u0080\u0094': '\u2014', # em dash
'\u00e2\u0080\u0099': '\u2019', # right single quote
'\u00e2\u0080\u009c': '\u201c', # left double quote
'\u00e2\u0080\u009d': '\u201d', # right double quote
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
}
total_fixed = 0
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
with open(path, 'r', encoding='utf-8') as f:
raw = f.read()
original = raw
for bad, good in MOJIBAKE_FIXES.items():
if bad in raw:
count = raw.count(bad)
raw = raw.replace(bad, good)
lang = os.path.basename(path)
print(f" {lang}: fixed {count} x {repr(good)}")
total_fixed += count
if raw != original:
with open(path, 'w', encoding='utf-8') as f:
f.write(raw)
print(f"\nTotal fixes: {total_fixed}")

35
scripts/gen-lite-checkpoints.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Generate SDXL lite-wallet mainnet checkpoint entries from a fully-synced dragonxd.
# Each entry is (height,"blockhash","serialized_sapling_tree") in checkpoints.rs format.
# Fills the 1,770,000 -> tip gap so wallets reseed close to their birthday on rescan,
# bounding the (divergence-prone) compact-block replay span. Usage:
# scripts/gen-lite-checkpoints.sh [start] [step] > /tmp/new_checkpoints.txt
set -euo pipefail
CLI=${DRAGONX_CLI:-/home/d/dragonx/src/dragonx-cli}
START=${1:-1770000}
STEP=${2:-10000}
tip=$("$CLI" getblockcount)
end=$(( (tip / STEP) * STEP ))
# Sanity: confirm the method reproduces a KNOWN checkpoint tree before trusting it.
ref_hash=$("$CLI" getblockhash 1760000 | tr -d '"[:space:]')
ref_tree=$("$CLI" getblockmerkletree 1760000 | tr -d '"[:space:]')
expect_hash="0000545a45b8d4ee4e4b423cb1ea74d67e3a04c320c6ea2f59ee06c08f91a117"
if [ "$ref_hash" != "$expect_hash" ]; then
echo "ABORT: getblockhash 1760000 = $ref_hash != known $expect_hash" >&2; exit 1
fi
echo "# self-check: 1760000 hash matches; tree len=${#ref_tree}" >&2
n=0
h=$START
while [ "$h" -le "$end" ]; do
hash=$("$CLI" getblockhash "$h" | tr -d '"[:space:]')
tree=$("$CLI" getblockmerkletree "$h" | tr -d '"[:space:]')
if [ -z "$hash" ] || [ -z "$tree" ]; then echo "ABORT: empty hash/tree at $h" >&2; exit 1; fi
printf '\t(%s,"%s",\n\t\t"%s"\n\t),\n' "$h" "$hash" "$tree"
n=$((n+1))
h=$((h+STEP))
done
echo "# generated $n checkpoints from $START to $end (tip=$tip)" >&2

645
scripts/gen_de.py Normal file
View File

@@ -0,0 +1,645 @@
#!/usr/bin/env python3
"""Generate German (de) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24h Änderung",
"24h_volume": "24h Volumen",
"about": "Über",
"about_block_explorer": "Block-Explorer",
"about_block_height": "Blockhöhe:",
"about_build_date": "Erstellungsdatum:",
"about_build_type": "Build-Typ:",
"about_chain": "Chain:",
"about_connections": "Verbindungen:",
"about_credits": "Danksagungen",
"about_daemon": "Daemon:",
"about_debug": "Debug",
"about_dragonx": "Über ObsidianDragon",
"about_edition": "ImGui Edition",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "Lizenz",
"about_license_text": "Diese Software wird unter der GNU General Public License v3 (GPLv3) veröffentlicht. Sie dürfen diese Software gemäß den Lizenzbedingungen frei verwenden, modifizieren und verbreiten.",
"about_peers_count": "%zu Peers",
"about_release": "Release",
"about_title": "Über ObsidianDragon",
"about_version": "Version:",
"about_website": "Webseite",
"acrylic": "Acryl",
"add": "Hinzufügen",
"address": "Adresse",
"address_book_add": "Adresse hinzufügen",
"address_book_add_new": "Neue hinzufügen",
"address_book_added": "Adresse zum Buch hinzugefügt",
"address_book_count": "%zu Adressen gespeichert",
"address_book_deleted": "Eintrag gelöscht",
"address_book_edit": "Adresse bearbeiten",
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
"address_book_exists": "Adresse existiert bereits im Buch",
"address_book_title": "Adressbuch",
"address_book_update_failed": "Aktualisierung fehlgeschlagen - Adresse könnte doppelt sein",
"address_book_updated": "Adresse aktualisiert",
"address_copied": "Adresse in Zwischenablage kopiert",
"address_details": "Adressdetails",
"address_label": "Adresse:",
"address_upper": "ADRESSE",
"address_url": "Adress-URL",
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
"advanced": "ERWEITERT",
"all_filter": "Alle",
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
"amount": "Betrag",
"amount_details": "BETRAGSDETAILS",
"amount_exceeds_balance": "Betrag übersteigt Guthaben",
"amount_label": "Betrag:",
"appearance": "ERSCHEINUNGSBILD",
"auto_shield": "Mining automatisch abschirmen",
"available": "Verfügbar",
"backup_backing_up": "Sicherung läuft...",
"backup_create": "Sicherung erstellen",
"backup_created": "Wallet-Sicherung erstellt",
"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_destination": "Sicherungsziel:",
"backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher",
"backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten",
"backup_tip_test": "Testen Sie regelmäßig die Wiederherstellung aus der Sicherung",
"backup_tips": "Tipps:",
"backup_title": "Wallet sichern",
"backup_wallet": "Wallet sichern...",
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
"balance": "Guthaben",
"balance_layout": "Guthaben-Layout",
"ban": "Sperren",
"banned_peers": "Gesperrte Peers",
"block": "Block",
"block_bits": "Bits:",
"block_click_next": "Klicken für nächsten Block",
"block_click_prev": "Klicken für vorherigen Block",
"block_explorer": "Block-Explorer",
"block_get_info": "Block-Info abrufen",
"block_hash": "Block-Hash:",
"block_height": "Blockhöhe:",
"block_info_title": "Block-Informationen",
"block_merkle_root": "Merkle-Root:",
"block_nav_next": "Weiter >>",
"block_nav_prev": "<< Zurück",
"block_next": "Nächster Block:",
"block_previous": "Vorheriger Block:",
"block_size": "Größe:",
"block_timestamp": "Zeitstempel:",
"block_transactions": "Transaktionen:",
"blockchain_syncing": "Blockchain synchronisiert (%.1f%%)... Guthaben könnten ungenau sein.",
"cancel": "Abbrechen",
"characters": "Zeichen",
"clear": "Leeren",
"clear_all_bans": "Alle Sperren aufheben",
"clear_form_confirm": "Alle Formularfelder leeren?",
"clear_request": "Anfrage leeren",
"click_copy_address": "Klicken zum Kopieren der Adresse",
"click_copy_uri": "Klicken zum Kopieren der URI",
"close": "Schließen",
"conf_count": "%d Best.",
"confirm_and_send": "Bestätigen & Senden",
"confirm_send": "Senden bestätigen",
"confirm_transaction": "Transaktion bestätigen",
"confirmations": "Bestätigungen",
"confirmations_display": "%d Bestätigungen | %s",
"confirmed": "Bestätigt",
"connected": "Verbunden",
"connected_peers": "Verbundene Peers",
"connecting": "Verbinde...",
"console": "Konsole",
"console_auto_scroll": "Automatisch scrollen",
"console_available_commands": "Verfügbare Befehle:",
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
"console_clear": "Leeren",
"console_clear_console": "Konsole leeren",
"console_cleared": "Konsole geleert",
"console_click_commands": "Befehle oben klicken zum Einfügen",
"console_click_insert": "Klicken zum Einfügen",
"console_click_insert_params": "Klicken zum Einfügen mit Parametern",
"console_close": "Schließen",
"console_commands": "Befehle",
"console_common_rpc": "Häufige RPC-Befehle:",
"console_completions": "Vervollständigungen:",
"console_connected": "Verbunden mit Daemon",
"console_copy_all": "Alles kopieren",
"console_copy_selected": "Kopieren",
"console_daemon": "Daemon",
"console_daemon_error": "Daemon-Fehler!",
"console_daemon_started": "Daemon gestartet",
"console_daemon_stopped": "Daemon gestoppt",
"console_disconnected": "Vom Daemon getrennt",
"console_errors": "Fehler",
"console_filter_hint": "Ausgabe filtern...",
"console_help_clear": " clear - Konsole leeren",
"console_help_getbalance": " getbalance - Transparentes Guthaben anzeigen",
"console_help_getblockcount": " getblockcount - Aktuelle Blockhöhe anzeigen",
"console_help_getinfo": " getinfo - Knoteninformationen anzeigen",
"console_help_getmininginfo": " getmininginfo - Mining-Status anzeigen",
"console_help_getpeerinfo": " getpeerinfo - Verbundene Peers anzeigen",
"console_help_gettotalbalance": " gettotalbalance - Gesamtguthaben anzeigen",
"console_help_help": " help - Diese Hilfe anzeigen",
"console_help_setgenerate": " setgenerate - Mining steuern",
"console_help_stop": " stop - Daemon stoppen",
"console_line_count": "%zu Zeilen",
"console_new_lines": "%d neue Zeilen",
"console_no_daemon": "Kein Daemon",
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
"console_rpc_reference": "RPC-Befehlsreferenz",
"console_scanline": "Konsolen-Scanline",
"console_search_commands": "Befehle suchen...",
"console_select_all": "Alles auswählen",
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
"console_show_errors_only": "Nur Fehler anzeigen",
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
"console_showing_lines": "Zeige %zu von %zu Zeilen",
"console_starting_node": "Knoten wird gestartet...",
"console_status_error": "Fehler",
"console_status_running": "Läuft",
"console_status_starting": "Startet",
"console_status_stopped": "Gestoppt",
"console_status_stopping": "Stoppt",
"console_status_unknown": "Unbekannt",
"console_tab_completion": "Tab zur Vervollständigung",
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
"console_zoom_in": "Vergrößern",
"console_zoom_out": "Verkleinern",
"copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren",
"copy_to_clipboard": "In Zwischenablage kopieren",
"copy_txid": "TxID kopieren",
"copy_uri": "URI kopieren",
"current_price": "Aktueller Preis",
"custom_fees": "Benutzerdefinierte Gebühren",
"dark": "Dunkel",
"date": "Datum",
"date_label": "Datum:",
"delete": "Löschen",
"difficulty": "Schwierigkeit",
"disconnected": "Getrennt",
"dismiss": "Verwerfen",
"display": "Anzeige",
"dragonx_green": "DragonX (Grün)",
"edit": "Bearbeiten",
"error": "Fehler",
"est_time_to_block": "Gesch. Zeit bis Block",
"exit": "Beenden",
"explorer": "EXPLORER",
"export": "Exportieren",
"export_csv": "CSV exportieren",
"export_keys_btn": "Schlüssel exportieren",
"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_z": "Z-Adressen einschließen (abgeschirmt)",
"export_keys_options": "Export-Optionen:",
"export_keys_success": "Schlüssel erfolgreich exportiert",
"export_keys_title": "Alle privaten Schlüssel exportieren",
"export_private_key": "Privaten Schlüssel exportieren",
"export_tx_count": "%zu Transaktionen als CSV exportieren.",
"export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden",
"export_tx_none": "Keine Transaktionen zum Exportieren",
"export_tx_success": "Transaktionen erfolgreich exportiert",
"export_tx_title": "Transaktionen als CSV exportieren",
"export_viewing_key": "Betrachtungsschlüssel exportieren",
"failed_create_shielded": "Abgeschirmte Adresse konnte nicht erstellt werden",
"failed_create_transparent": "Transparente Adresse konnte nicht erstellt werden",
"fee": "Gebühr",
"fee_high": "Hoch",
"fee_label": "Gebühr:",
"fee_low": "Niedrig",
"fee_normal": "Normal",
"fetch_prices": "Preise abrufen",
"file": "Datei",
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
"font_scale": "Schriftgröße",
"from": "Von",
"from_upper": "VON",
"full_details": "Alle Details",
"general": "Allgemein",
"go_to_receive": "Zum Empfangen",
"height": "Höhe",
"help": "Hilfe",
"hide": "Ausblenden",
"history": "Verlauf",
"immature_type": "Unreif",
"import": "Importieren",
"import_key_btn": "Schlüssel importieren",
"import_key_formats": "Unterstützte Schlüsselformate:",
"import_key_full_rescan": "(0 = vollständiger Rescan)",
"import_key_label": "Privater Schlüssel:",
"import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden",
"import_key_rescan": "Blockchain nach Import neu scannen",
"import_key_start_height": "Starthöhe:",
"import_key_success": "Schlüssel erfolgreich importiert",
"import_key_t_format": "T-Adresse WIF private Schlüssel",
"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_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_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
"import_private_key": "Privaten Schlüssel importieren...",
"invalid_address": "Ungültiges Adressformat",
"ip_address": "IP-Adresse",
"keep": "Behalten",
"keep_daemon": "Daemon weiterlaufen lassen",
"key_export_fetching": "Schlüssel wird aus Wallet abgerufen...",
"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_reveal": "Schlüssel anzeigen",
"key_export_viewing_key": "Betrachtungsschlüssel:",
"key_export_viewing_warning": "Dieser Betrachtungsschlüssel ermöglicht es anderen, Ihre eingehenden Transaktionen und Ihr Guthaben zu sehen, aber NICHT Ihre Gelder auszugeben. Teilen Sie ihn nur mit vertrauenswürdigen Parteien.",
"label": "Bezeichnung:",
"language": "Sprache",
"light": "Hell",
"loading": "Laden...",
"loading_addresses": "Adressen werden geladen...",
"local_hashrate": "Lokale Hashrate",
"low_spec_mode": "Energiesparmodus",
"market": "Markt",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"market_24h_volume": "24H VOLUMEN",
"market_6h": "6h",
"market_attribution": "Preisdaten von NonKYC",
"market_btc_price": "BTC PREIS",
"market_cap": "Marktkapitalisierung",
"market_no_history": "Kein Preisverlauf verfügbar",
"market_no_price": "Keine Preisdaten",
"market_now": "Jetzt",
"market_pct_shielded": "%.0f%% Abgeschirmt",
"market_portfolio": "PORTFOLIO",
"market_price_unavailable": "Preisdaten nicht verfügbar",
"market_refresh_price": "Preisdaten aktualisieren",
"market_trade_on": "Handeln auf %s",
"mature": "Reif",
"max": "Max",
"memo": "Memo (optional, verschlüsselt)",
"memo_label": "Memo:",
"memo_optional": "MEMO (OPTIONAL)",
"memo_upper": "MEMO",
"memo_z_only": "Hinweis: Memos sind nur beim Senden an abgeschirmte (z) Adressen verfügbar",
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
"merge_funds": "Gelder zusammenführen",
"merge_started": "Zusammenführung gestartet",
"merge_title": "An Adresse zusammenführen",
"mine_when_idle": "Im Leerlauf minen",
"mined": "gemined",
"mined_filter": "Gemined",
"mined_type": "Gemined",
"mined_upper": "GEMINED",
"miner_fee": "Miner-Gebühr",
"mining": "Mining",
"mining_active": "Aktiv",
"mining_address_copied": "Mining-Adresse kopiert",
"mining_all_time": "Gesamt",
"mining_already_saved": "Pool-URL bereits gespeichert",
"mining_block_copied": "Block-Hash kopiert",
"mining_chart_1m_ago": "vor 1m",
"mining_chart_5m_ago": "vor 5m",
"mining_chart_now": "Jetzt",
"mining_click": "Klicken",
"mining_click_copy_address": "Klicken zum Kopieren der Adresse",
"mining_click_copy_block": "Klicken zum Kopieren des Block-Hash",
"mining_click_copy_difficulty": "Klicken zum Kopieren der Schwierigkeit",
"mining_connected": "Verbunden",
"mining_connecting": "Verbinde...",
"mining_control": "Mining-Steuerung",
"mining_difficulty_copied": "Schwierigkeit kopiert",
"mining_est_block": "Gesch. Block",
"mining_est_daily": "Gesch. täglich",
"mining_filter_all": "Alle",
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
"mining_filter_tip_solo": "Nur Solo-Einnahmen anzeigen",
"mining_idle_off_tooltip": "Leerlauf-Mining aktivieren",
"mining_idle_on_tooltip": "Leerlauf-Mining deaktivieren",
"mining_local_hashrate": "Lokale Hashrate",
"mining_mine": "Minen",
"mining_mining_addr": "Mining-Adr.",
"mining_network": "Netzwerk",
"mining_no_blocks_yet": "Noch keine Blöcke gefunden",
"mining_no_payouts_yet": "Noch keine Pool-Auszahlungen",
"mining_no_saved_addresses": "Keine gespeicherten Adressen",
"mining_no_saved_pools": "Keine gespeicherten Pools",
"mining_off": "Mining ist AUS",
"mining_on": "Mining ist AN",
"mining_open_in_explorer": "Im Explorer öffnen",
"mining_payout_address": "Auszahlungsadresse",
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
"mining_pool": "Pool",
"mining_pool_hashrate": "Pool-Hashrate",
"mining_pool_url": "Pool-URL",
"mining_recent_blocks": "LETZTE BLÖCKE",
"mining_recent_payouts": "LETZTE POOL-AUSZAHLUNGEN",
"mining_remove": "Entfernen",
"mining_reset_defaults": "Standardwerte zurücksetzen",
"mining_save_payout_address": "Auszahlungsadresse speichern",
"mining_save_pool_url": "Pool-URL speichern",
"mining_saved_addresses": "Gespeicherte Adressen:",
"mining_saved_pools": "Gespeicherte Pools:",
"mining_shares": "Shares",
"mining_show_chart": "Diagramm",
"mining_show_log": "Protokoll",
"mining_solo": "Solo",
"mining_starting": "Startet...",
"mining_starting_tooltip": "Miner startet...",
"mining_statistics": "Mining-Statistiken",
"mining_stop": "Stopp",
"mining_stop_solo_for_pool": "Solo-Mining stoppen bevor Pool-Mining gestartet wird",
"mining_stop_solo_for_pool_settings": "Solo-Mining stoppen um Pool-Einstellungen zu ändern",
"mining_stopping": "Stoppt...",
"mining_stopping_tooltip": "Miner stoppt...",
"mining_syncing_tooltip": "Blockchain synchronisiert...",
"mining_threads": "Mining-Threads",
"mining_to_save": "zum Speichern",
"mining_today": "Heute",
"mining_uptime": "Laufzeit",
"mining_yesterday": "Gestern",
"network": "Netzwerk",
"network_fee": "NETZWERKGEBÜHR",
"network_hashrate": "Netzwerk-Hashrate",
"new": "+ Neu",
"new_shielded_created": "Neue abgeschirmte Adresse erstellt",
"new_t_address": "Neue T-Adresse",
"new_t_transparent": "Neue t-Adresse (Transparent)",
"new_transparent_created": "Neue transparente Adresse erstellt",
"new_z_address": "Neue Z-Adresse",
"new_z_shielded": "Neue z-Adresse (Abgeschirmt)",
"no_addresses": "Keine Adressen gefunden. Erstellen Sie eine mit den Schaltflächen oben.",
"no_addresses_available": "Keine Adressen verfügbar",
"no_addresses_match": "Keine Adressen passen zum Filter",
"no_addresses_with_balance": "Keine Adressen mit Guthaben",
"no_matching": "Keine passenden Transaktionen",
"no_recent_receives": "Keine kürzlichen Empfänge",
"no_recent_sends": "Keine kürzlichen Sendungen",
"no_transactions": "Keine Transaktionen gefunden",
"node": "KNOTEN",
"node_security": "KNOTEN & SICHERHEIT",
"noise": "Rauschen",
"not_connected": "Nicht mit Daemon verbunden...",
"not_connected_to_daemon": "Nicht mit Daemon verbunden",
"notes": "Notizen",
"notes_optional": "Notizen (optional):",
"output_filename": "Ausgabedateiname:",
"overview": "Übersicht",
"paste": "Einfügen",
"paste_from_clipboard": "Aus Zwischenablage einfügen",
"pay_from": "Zahlen von",
"payment_request": "ZAHLUNGSANFRAGE",
"payment_request_copied": "Zahlungsanfrage kopiert",
"payment_uri_copied": "Zahlungs-URI kopiert",
"peers": "Peers",
"peers_avg_ping": "Durchschn. Ping",
"peers_ban_24h": "Peer 24h sperren",
"peers_ban_score": "Sperr-Score: %d",
"peers_banned": "Gesperrt",
"peers_banned_count": "Gesperrt: %d",
"peers_best_block": "Bester Block",
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Blöcke",
"peers_blocks_left": "%d Blöcke übrig",
"peers_clear_all_bans": "Alle Sperren aufheben",
"peers_click_copy": "Klicken zum Kopieren",
"peers_connected": "Verbunden",
"peers_connected_count": "Verbunden: %d",
"peers_copy_ip": "IP kopieren",
"peers_dir_in": "Ein",
"peers_dir_out": "Aus",
"peers_hash_copied": "Hash kopiert",
"peers_hashrate": "Hashrate",
"peers_in_out": "Ein/Aus",
"peers_longest": "Längste",
"peers_longest_chain": "Längste Chain",
"peers_memory": "Speicher",
"peers_no_banned": "Keine gesperrten Peers",
"peers_no_connected": "Keine verbundenen Peers",
"peers_no_tls": "Kein TLS",
"peers_notarized": "Notarisiert",
"peers_p2p_port": "P2P-Port",
"peers_protocol": "Protokoll",
"peers_received": "Empfangen",
"peers_refresh": "Aktualisieren",
"peers_refresh_tooltip": "Peer-Liste aktualisieren",
"peers_refreshing": "Aktualisiere...",
"peers_sent": "Gesendet",
"peers_tt_id": "ID: %d",
"peers_tt_received": "Empfangen: %s",
"peers_tt_sent": "Gesendet: %s",
"peers_tt_services": "Dienste: %s",
"peers_tt_start_height": "Starthöhe: %d",
"peers_tt_synced": "Synchronisiert H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "Entsperren",
"peers_upper": "PEERS",
"peers_version": "Version",
"pending": "Ausstehend",
"ping": "Ping",
"price_chart": "Preisdiagramm",
"qr_code": "QR-Code",
"qr_failed": "QR-Code konnte nicht generiert werden",
"qr_title": "QR-Code",
"qr_unavailable": "QR nicht verfügbar",
"receive": "Empfangen",
"received": "empfangen",
"received_filter": "Empfangen",
"received_label": "Empfangen",
"received_upper": "EMPFANGEN",
"receiving_addresses": "Ihre Empfangsadressen",
"recent_received": "KÜRZLICH EMPFANGEN",
"recent_sends": "KÜRZLICH GESENDET",
"recipient": "EMPFÄNGER",
"recv_type": "Empf.",
"refresh": "Aktualisieren",
"refresh_now": "Jetzt aktualisieren",
"report_bug": "Fehler melden",
"request_amount": "Betrag (optional):",
"request_copy_uri": "URI kopieren",
"request_description": "Erstellen Sie eine Zahlungsanfrage, die andere scannen oder kopieren können. Der QR-Code enthält Ihre Adresse und optionalen Betrag/Memo.",
"request_label": "Bezeichnung (optional):",
"request_memo": "Memo (optional):",
"request_payment": "Zahlung anfordern",
"request_payment_uri": "Zahlungs-URI:",
"request_receive_address": "Empfangsadresse:",
"request_select_address": "Adresse auswählen...",
"request_shielded_addrs": "-- Abgeschirmte Adressen --",
"request_title": "Zahlung anfordern",
"request_transparent_addrs": "-- Transparente Adressen --",
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
"rescan": "Neu scannen",
"reset_to_defaults": "Standardwerte zurücksetzen",
"review_send": "Senden prüfen",
"rpc_host": "RPC-Host",
"rpc_pass": "Passwort",
"rpc_port": "Port",
"rpc_user": "Benutzername",
"save": "Speichern",
"save_settings": "Einstellungen speichern",
"save_z_transactions": "Z-Tx in Tx-Liste speichern",
"search_placeholder": "Suchen...",
"security": "SICHERHEIT",
"select_address": "Adresse auswählen...",
"select_receiving_address": "Empfangsadresse auswählen...",
"select_source_address": "Quelladresse auswählen...",
"send": "Senden",
"send_amount": "Betrag",
"send_amount_details": "BETRAGSDETAILS",
"send_amount_upper": "BETRAG",
"send_clear_fields": "Alle Formularfelder leeren?",
"send_copy_error": "Fehler kopieren",
"send_dismiss": "Verwerfen",
"send_error_copied": "Fehler in Zwischenablage kopiert",
"send_error_prefix": "Fehler: %s",
"send_exceeds_available": "Übersteigt verfügbar (%.8f)",
"send_fee": "Gebühr",
"send_fee_high": "Hoch",
"send_fee_low": "Niedrig",
"send_fee_normal": "Normal",
"send_form_restored": "Formular wiederhergestellt",
"send_from_this_address": "Von dieser Adresse senden",
"send_go_to_receive": "Zum Empfangen",
"send_keep": "Behalten",
"send_network_fee": "NETZWERKGEBÜHR",
"send_no_balance": "Kein Guthaben",
"send_no_recent": "Keine kürzlichen Sendungen",
"send_recent_sends": "KÜRZLICH GESENDET",
"send_recipient": "EMPFÄNGER",
"send_select_source": "Quelladresse auswählen...",
"send_sending_from": "SENDEN VON",
"send_submitting": "Transaktion wird übermittelt...",
"send_switch_to_receive": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
"send_to": "Senden an",
"send_tooltip_enter_amount": "Geben Sie einen Betrag zum Senden ein",
"send_tooltip_exceeds_balance": "Betrag übersteigt verfügbares Guthaben",
"send_tooltip_in_progress": "Transaktion bereits in Bearbeitung",
"send_tooltip_invalid_address": "Geben Sie eine gültige Empfängeradresse ein",
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
"send_total": "Gesamt",
"send_transaction": "Transaktion senden",
"send_tx_failed": "Transaktion fehlgeschlagen",
"send_tx_sent": "Transaktion gesendet!",
"send_tx_success": "Transaktion erfolgreich gesendet!",
"send_txid_copied": "TxID in Zwischenablage kopiert",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "Gültige abgeschirmte Adresse",
"send_valid_transparent": "Gültige transparente Adresse",
"send_wallet_empty": "Ihre Wallet ist leer",
"send_yes_clear": "Ja, leeren",
"sending": "Transaktion wird gesendet",
"sending_from": "SENDEN VON",
"sent": "gesendet",
"sent_filter": "Gesendet",
"sent_type": "Gesendet",
"sent_upper": "GESENDET",
"settings": "Einstellungen",
"setup_wizard": "Einrichtungsassistent",
"share": "Teilen",
"shield_check_status": "Status prüfen",
"shield_completed": "Vorgang erfolgreich abgeschlossen!",
"shield_description": "Schirmen Sie Ihre Mining-Belohnungen ab, indem Sie Coinbase-Ausgaben von transparenten Adressen an eine abgeschirmte Adresse senden. Dies verbessert die Privatsphäre, indem Ihre Mining-Einkünfte verborgen werden.",
"shield_from_address": "Von Adresse:",
"shield_funds": "Gelder abschirmen",
"shield_in_progress": "Vorgang läuft...",
"shield_max_utxos": "Max. UTXOs pro Vorgang",
"shield_merge_done": "Abschirmung/Zusammenführung abgeschlossen!",
"shield_select_z": "z-Adresse auswählen...",
"shield_started": "Abschirmvorgang gestartet",
"shield_title": "Coinbase-Belohnungen abschirmen",
"shield_to_address": "An Adresse (Abgeschirmt):",
"shield_utxo_limit": "UTXO-Limit:",
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
"shielded": "Abgeschirmt",
"shielded_to": "ABGESCHIRMT AN",
"shielded_type": "Abgeschirmt",
"show": "Anzeigen",
"show_qr_code": "QR-Code anzeigen",
"showing_transactions": "Zeige %d\xe2\x80\x93%d von %d Transaktionen (gesamt: %zu)",
"simple_background": "Einfacher Hintergrund",
"start_mining": "Mining starten",
"status": "Status",
"stop_external": "Externen Daemon stoppen",
"stop_mining": "Mining stoppen",
"submitting_transaction": "Transaktion wird übermittelt...",
"success": "Erfolg",
"summary": "Zusammenfassung",
"syncing": "Synchronisiere...",
"t_addresses": "T-Adressen",
"test_connection": "Testen",
"theme": "Design",
"theme_effects": "Design-Effekte",
"time_days_ago": "vor %d Tagen",
"time_hours_ago": "vor %d Stunden",
"time_minutes_ago": "vor %d Minuten",
"time_seconds_ago": "vor %d Sekunden",
"to": "An",
"to_upper": "AN",
"tools": "WERKZEUGE",
"total": "Gesamt",
"transaction_id": "TRANSAKTIONS-ID",
"transaction_sent": "Transaktion erfolgreich gesendet",
"transaction_sent_msg": "Transaktion gesendet!",
"transaction_url": "Transaktions-URL",
"transactions": "Transaktionen",
"transactions_upper": "TRANSAKTIONEN",
"transparent": "Transparent",
"tx_confirmations": "%d Bestätigungen",
"tx_details_title": "Transaktionsdetails",
"tx_from_address": "Von Adresse:",
"tx_id_label": "Transaktions-ID:",
"tx_immature": "UNREIF",
"tx_mined": "GEMINED",
"tx_received": "EMPFANGEN",
"tx_sent": "GESENDET",
"tx_to_address": "An Adresse:",
"tx_view_explorer": "Im Explorer anzeigen",
"txs_count": "%d Txs",
"type": "Typ",
"ui_opacity": "UI-Transparenz",
"unban": "Entsperren",
"unconfirmed": "Unbestätigt",
"undo_clear": "Leeren rückgängig",
"unknown": "Unbekannt",
"use_embedded_daemon": "Eingebetteten dragonxd verwenden",
"use_tor": "Tor verwenden",
"validate_btn": "Validieren",
"validate_description": "Geben Sie eine DragonX-Adresse ein, um zu prüfen, ob sie gültig ist und ob sie zu dieser Wallet gehört.",
"validate_invalid": "UNGÜLTIG",
"validate_is_mine": "Diese Wallet besitzt diese Adresse",
"validate_not_mine": "Nicht im Besitz dieser Wallet",
"validate_ownership": "Eigentum:",
"validate_results": "Ergebnisse:",
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
"validate_status": "Status:",
"validate_title": "Adresse validieren",
"validate_transparent_type": "Transparent (t-Adresse)",
"validate_type": "Typ:",
"validate_valid": "GÜLTIG",
"validating": "Validiere...",
"verbose_logging": "Ausführliches Logging",
"version": "Version",
"view": "Ansicht",
"view_details": "Details anzeigen",
"view_on_explorer": "Im Explorer anzeigen",
"waiting_for_daemon": "Warte auf Daemon-Verbindung...",
"wallet": "WALLET",
"wallet_empty": "Ihre Wallet ist leer",
"wallet_empty_hint": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
"warning": "Warnung",
"warning_upper": "WARNUNG!",
"website": "Webseite",
"window_opacity": "Fenster-Transparenz",
"yes_clear": "Ja, leeren",
"your_addresses": "Ihre Adressen",
"z_addresses": "Z-Adressen",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "de.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} German translations to {os.path.abspath(out)}")

665
scripts/gen_es.py Normal file
View File

@@ -0,0 +1,665 @@
#!/usr/bin/env python3
"""Generate complete Spanish (es.json) translations for ObsidianDragon wallet."""
import json
es = {
# ---- Navigation & Tabs ----
"overview": "Resumen",
"balance": "Saldo",
"send": "Enviar",
"receive": "Recibir",
"transactions": "Transacciones",
"history": "Historial",
"mining": "Minería",
"peers": "Nodos",
"market": "Mercado",
"settings": "Configuración",
"console": "Consola",
"tools": "HERRAMIENTAS",
"advanced": "AVANZADO",
"network": "Red",
# ---- Settings sections ----
"appearance": "APARIENCIA",
"wallet": "CARTERA",
"node_security": "NODO Y SEGURIDAD",
"node": "NODO",
"security": "SEGURIDAD",
"explorer": "EXPLORADOR",
"about": "Acerca de",
"backup_data": "RESPALDO Y DATOS",
"general": "General",
# ---- Settings options ----
"balance_layout": "Diseño de Saldo",
"low_spec_mode": "Modo bajo rendimiento",
"simple_background": "Fondo simple",
"console_scanline": "Líneas de consola",
"theme_effects": "Efectos de tema",
"language": "Idioma",
"save_z_transactions": "Guardar Z-tx en lista",
"allow_custom_fees": "Permitir comisiones personalizadas",
"custom_fees": "Comisiones personalizadas",
"auto_shield": "Auto-proteger minería",
"fetch_prices": "Obtener precios",
"use_tor": "Usar Tor",
"font_scale": "Escala de fuente",
"keep_daemon": "Mantener daemon activo",
"stop_external": "Detener daemon externo",
"mine_when_idle": "Minar en reposo",
"verbose_logging": "Registro detallado",
"acrylic": "Acrílico",
"noise": "Ruido",
"ui_opacity": "Opacidad de UI",
"window_opacity": "Opacidad de ventana",
# ---- Settings buttons ----
"save_settings": "Guardar Configuración",
"reset_to_defaults": "Restablecer Valores",
"report_bug": "Reportar Error",
"website": "Sitio Web",
"setup_wizard": "Asistente de Configuración",
"rescan": "Re-escanear",
"test_connection": "Probar",
# ---- Settings fields ----
"rpc_host": "Host RPC",
"rpc_port": "Puerto",
"rpc_user": "Usuario",
"rpc_pass": "Contraseña",
"transaction_url": "URL de Transacción",
"address_url": "URL de Dirección",
"block_explorer": "Explorador de Bloques",
# ---- Common actions ----
"add": "Agregar",
"edit": "Editar",
"delete": "Eliminar",
"cancel": "Cancelar",
"close": "Cerrar",
"clear": "Limpiar",
"copy": "Copiar",
"paste": "Pegar",
"save": "Guardar",
"refresh": "Actualizar",
"export": "Exportar",
"import": "Importar",
"show": "Mostrar",
"hide": "Ocultar",
"share": "Compartir",
"confirm_and_send": "Confirmar y Enviar",
"confirm_send": "Confirmar Envío",
"confirm_transaction": "Confirmar Transacción",
"review_send": "Revisar Envío",
"copy_address": "Copiar Dirección Completa",
"copy_to_clipboard": "Copiar al Portapapeles",
"paste_from_clipboard": "Pegar del Portapapeles",
"copy_txid": "Copiar TxID",
"copy_uri": "Copiar URI",
"copy_error": "Copiar Error",
"search_placeholder": "Buscar...",
"exit": "Salir",
"help": "Ayuda",
"file": "Archivo",
"display": "Pantalla",
"new": "+ Nuevo",
"dismiss": "Descartar",
"keep": "Mantener",
"yes_clear": "Sí, Limpiar",
"undo_clear": "Deshacer Limpieza",
# ---- Common labels ----
"address": "Dirección",
"address_label": "Dirección:",
"amount": "Cantidad",
"amount_label": "Cantidad:",
"date": "Fecha",
"date_label": "Fecha:",
"fee": "Comisión",
"fee_label": "Comisión:",
"label": "Etiqueta:",
"memo": "Memo (opcional, encriptado)",
"memo_label": "Memo:",
"notes": "Notas",
"notes_optional": "Notas (opcional):",
"total": "Total",
"from": "Desde",
"to_upper": "PARA",
"from_upper": "DESDE",
"max": "Máximo",
"characters": "caracteres",
"ping": "Ping",
"height": "Altura",
"block": "Bloque",
"available": "Disponible",
"unknown": "Desconocido",
"loading": "Cargando...",
"validating": "Validando...",
"warning_upper": "¡ADVERTENCIA!",
"output_filename": "Nombre del archivo:",
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
"light": "Claro",
"dark": "Oscuro",
# ---- Status ----
"connected": "Conectado",
"disconnected": "Desconectado",
"connecting": "Conectando...",
"confirmed": "Confirmada",
"confirmations": "Confirmaciones",
"not_connected_to_daemon": "No conectado al daemon",
"waiting_for_daemon": "Esperando conexión al daemon...",
"blockchain_syncing": "Sincronizando blockchain (%.1f%%)... Los saldos pueden ser inexactos.",
"error": "Error",
"success": "Éxito",
"warning": "Advertencia",
# ---- Time ----
"time_days_ago": "hace %d días",
"time_hours_ago": "hace %d horas",
"time_minutes_ago": "hace %d minutos",
"time_seconds_ago": "hace %d segundos",
# ---- Transaction types/filters ----
"sent_type": "Enviado",
"sent_filter": "Enviado",
"sent_upper": "ENVIADO",
"received_label": "Recibido",
"received_filter": "Recibido",
"received_upper": "RECIBIDO",
"mined_type": "Minado",
"mined_filter": "Minado",
"mined_upper": "MINADO",
"immature_type": "Inmaduro",
"mature": "Maduro",
"recv_type": "Recibido",
"all_filter": "Todos",
"shielded_type": "Protegido",
# ---- Balance / Overview ----
"address_upper": "DIRECCIÓN",
"address_details": "Detalles de Dirección",
"amount_details": "DETALLES DE CANTIDAD",
"transactions_upper": "TRANSACCIONES",
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
"wallet_empty": "Tu cartera está vacía",
"wallet_empty_hint": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
"loading_addresses": "Cargando direcciones...",
"no_addresses_match": "No hay direcciones que coincidan con el filtro",
"no_addresses_with_balance": "No hay direcciones con saldo",
"click_copy_address": "Clic para copiar dirección",
"click_copy_uri": "Clic para copiar URI",
"address_copied": "Dirección copiada al portapapeles",
"about_dragonx": "Acerca de DragonX",
"dragonx_green": "DragonX (Verde)",
# ---- Transactions tab ----
"no_transactions": "No se encontraron transacciones",
"no_matching": "No hay transacciones coincidentes",
"showing_transactions": "Mostrando %d\u2013%d de %d transacciones (total: %zu)",
"conf_count": "%d conf",
"confirmations_display": "%d confirmaciones | %s",
"txs_count": "%d txs",
"view_details": "Ver Detalles",
"full_details": "Detalles Completos",
"export_csv": "Exportar CSV",
"transaction_id": "ID DE TRANSACCIÓN",
# ---- Receive tab ----
"select_receiving_address": "Seleccionar dirección de recepción...",
"payment_request": "SOLICITUD DE PAGO",
"recent_received": "RECIBIDOS RECIENTES",
"no_recent_receives": "No hay recepciones recientes",
"qr_unavailable": "QR no disponible",
"clear_request": "Limpiar Solicitud",
"clear_form_confirm": "¿Limpiar todos los campos del formulario?",
"payment_request_copied": "Solicitud de pago copiada",
"payment_uri_copied": "URI de pago copiada",
"failed_create_shielded": "Error al crear dirección protegida",
"failed_create_transparent": "Error al crear dirección transparente",
"new_shielded_created": "Nueva dirección protegida creada",
"new_transparent_created": "Nueva dirección transparente creada",
# ---- Send tab ----
"send_sending_from": "ENVIANDO DESDE",
"send_select_source": "Seleccionar dirección de origen...",
"send_no_balance": "Sin saldo",
"send_recipient": "DESTINATARIO",
"send_amount_upper": "CANTIDAD",
"send_amount": "Cantidad",
"send_amount_details": "DETALLES DE CANTIDAD",
"send_fee": "Comisión",
"send_fee_low": "Baja",
"send_fee_normal": "Normal",
"send_fee_high": "Alta",
"send_network_fee": "COMISIÓN DE RED",
"send_total": "Total",
"send_recent_sends": "ENVÍOS RECIENTES",
"send_no_recent": "No hay envíos recientes",
"send_clear_fields": "¿Limpiar todos los campos del formulario?",
"send_yes_clear": "Sí, Limpiar",
"send_keep": "Mantener",
"send_form_restored": "Formulario restaurado",
"send_valid_shielded": "Dirección protegida válida",
"send_valid_transparent": "Dirección transparente válida",
"send_exceeds_available": "Excede disponible (%.8f)",
"send_submitting": "Enviando transacción...",
"send_tx_sent": "¡Transacción enviada!",
"send_tx_success": "¡Transacción enviada exitosamente!",
"send_tx_failed": "Error en la transacción",
"send_error_prefix": "Error: %s",
"send_error_copied": "Error copiado al portapapeles",
"send_copy_error": "Copiar Error",
"send_dismiss": "Descartar",
"send_txid_copied": "TxID copiado al portapapeles",
"send_txid_label": "TxID: %s",
"send_go_to_receive": "Ir a Recibir",
"send_wallet_empty": "Tu cartera está vacía",
"send_switch_to_receive": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
"send_tooltip_invalid_address": "Ingresa una dirección de destinatario válida",
"send_tooltip_enter_amount": "Ingresa una cantidad a enviar",
"send_tooltip_exceeds_balance": "La cantidad excede el saldo disponible",
"send_tooltip_not_connected": "No conectado al daemon",
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
"send_tooltip_in_progress": "Transacción ya en progreso",
"sending_from": "ENVIANDO DESDE",
"select_source_address": "Seleccionar dirección de origen...",
"recipient": "DESTINATARIO",
"memo_optional": "MEMO (OPCIONAL)",
"memo_upper": "MEMO",
"network_fee": "COMISIÓN DE RED",
"fee_low": "Baja",
"fee_normal": "Normal",
"fee_high": "Alta",
"recent_sends": "ENVÍOS RECIENTES",
"no_recent_sends": "No hay envíos recientes",
"shielded_to": "PROTEGIDA PARA",
"submitting_transaction": "Enviando transacción...",
"transaction_sent_msg": "¡Transacción enviada!",
"amount_exceeds_balance": "La cantidad excede el saldo",
# ---- Mining tab ----
"mining_solo": "Solo",
"mining_pool": "Pool",
"mining_pool_url": "URL del Pool",
"mining_pool_hashrate": "Hashrate del Pool",
"mining_local_hashrate": "Hashrate Local",
"mining_payout_address": "Dirección de Pago",
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
"mining_mine": "Minar",
"mining_stop": "Detener",
"mining_starting": "Iniciando...",
"mining_stopping": "Deteniendo...",
"mining_active": "Activo",
"mining_connected": "Conectado",
"mining_connecting": "Conectando...",
"mining_network": "Red",
"mining_shares": "Shares",
"mining_uptime": "Tiempo activo",
"mining_mining_addr": "Dir. Minería",
"mining_est_block": "Bloque Est.",
"mining_est_daily": "Diario Est.",
"mining_today": "Hoy",
"mining_yesterday": "Ayer",
"mining_all_time": "Todo el Tiempo",
"mining_recent_blocks": "BLOQUES RECIENTES",
"mining_recent_payouts": "PAGOS DE POOL RECIENTES",
"mining_no_blocks_yet": "Aún no se han encontrado bloques",
"mining_no_payouts_yet": "Aún no hay pagos del pool",
"mining_show_chart": "Gráfico",
"mining_show_log": "Registro",
"mining_chart_now": "Ahora",
"mining_chart_start": "Inicio",
"mining_chart_1m_ago": "hace 1m",
"mining_chart_5m_ago": "hace 5m",
"mining_save_pool_url": "Guardar URL del pool",
"mining_save_payout_address": "Guardar dirección de pago",
"mining_saved_pools": "Pools Guardados:",
"mining_saved_addresses": "Direcciones Guardadas:",
"mining_no_saved_pools": "No hay pools guardados",
"mining_no_saved_addresses": "No hay direcciones guardadas",
"mining_already_saved": "URL del pool ya guardada",
"mining_remove": "Eliminar",
"mining_reset_defaults": "Restablecer Valores",
"mining_click": "Clic",
"mining_to_save": "para guardar",
"mining_click_copy_address": "Clic para copiar dirección",
"mining_click_copy_block": "Clic para copiar hash de bloque",
"mining_click_copy_difficulty": "Clic para copiar dificultad",
"mining_address_copied": "Dirección de minería copiada",
"mining_block_copied": "Hash de bloque copiado",
"mining_difficulty_copied": "Dificultad copiada",
"mining_open_in_explorer": "Abrir en explorador",
"mining_starting_tooltip": "El minero está iniciando...",
"mining_stopping_tooltip": "El minero está deteniéndose...",
"mining_syncing_tooltip": "El blockchain está sincronizando...",
"mining_idle_on_tooltip": "Desactivar minería en reposo",
"mining_idle_off_tooltip": "Activar minería en reposo",
"mining_stop_solo_for_pool": "Detener minería solo antes de iniciar minería en pool",
"mining_stop_solo_for_pool_settings": "Detener minería solo para cambiar configuración del pool",
"mining_filter_all": "Todos",
"mining_filter_tip_all": "Mostrar todas las ganancias",
"mining_filter_tip_solo": "Mostrar solo ganancias solo",
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
"local_hashrate": "Tasa Hash Local",
"est_time_to_block": "Tiempo Est. al Bloque",
"difficulty": "Dificultad",
"current_price": "Precio Actual",
"market_cap": "Cap. de Mercado",
# ---- Peers tab ----
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Bloques",
"peers_connected": "Conectados",
"peers_connected_count": "Conectados: %d",
"peers_banned": "Bloqueados",
"peers_banned_count": "Bloqueados: %d",
"peers_upper": "NODOS",
"peers_avg_ping": "Ping Prom.",
"peers_best_block": "Mejor Bloque",
"peers_hashrate": "Hashrate",
"peers_longest": "Más Larga",
"peers_longest_chain": "Cadena Más Larga",
"peers_memory": "Memoria",
"peers_notarized": "Notarizado",
"peers_p2p_port": "Puerto P2P",
"peers_protocol": "Protocolo",
"peers_version": "Versión",
"peers_in_out": "Ent/Sal",
"peers_dir_in": "Ent",
"peers_dir_out": "Sal",
"peers_received": "Recibido",
"peers_sent": "Enviado",
"peers_refresh": "Actualizar",
"peers_refresh_tooltip": "Actualizar lista de nodos",
"peers_refreshing": "Actualizando...",
"peers_no_connected": "No hay nodos conectados",
"peers_no_banned": "No hay nodos bloqueados",
"peers_ban_24h": "Bloquear Nodo 24h",
"peers_unban": "Desbloquear",
"peers_clear_all_bans": "Limpiar Todos los Bloqueos",
"peers_copy_ip": "Copiar IP",
"peers_click_copy": "Clic para copiar",
"peers_hash_copied": "Hash copiado",
"peers_no_tls": "Sin TLS",
"peers_blocks_left": "%d bloques restantes",
"peers_ban_score": "Puntuación: %d",
"peers_tt_id": "ID: %d",
"peers_tt_sent": "Enviado: %s",
"peers_tt_received": "Recibido: %s",
"peers_tt_services": "Servicios: %s",
"peers_tt_start_height": "Altura Inicial: %d",
"peers_tt_synced": "Sinc H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"connected_peers": "Nodos Conectados",
"banned_peers": "Nodos Bloqueados",
"ban": "Bloquear",
"clear_all_bans": "Limpiar Todos los Bloqueos",
"ip_address": "Dirección IP",
# ---- Market tab ----
"market_btc_price": "PRECIO BTC",
"market_24h_volume": "VOLUMEN 24H",
"market_portfolio": "PORTAFOLIO",
"market_pct_shielded": "%.0f%% Protegido",
"market_attribution": "Datos de precios de NonKYC",
"market_no_price": "Sin datos de precio",
"market_no_history": "No hay historial de precios disponible",
"market_price_unavailable": "Datos de precio no disponibles",
"market_refresh_price": "Actualizar datos de precio",
"market_trade_on": "Operar en %s",
"market_now": "Ahora",
"market_6h": "6h",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"24h_change": "Cambio 24h",
"24h_volume": "Volumen 24h",
# ---- Console tab ----
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
"console_type_help": "Escribe 'help' para ver los comandos disponibles",
"console_available_commands": "Comandos disponibles:",
"console_common_rpc": "Comandos RPC comunes:",
"console_rpc_reference": "Referencia de Comandos RPC",
"console_auto_scroll": "Auto-desplazamiento",
"console_clear": "Limpiar",
"console_clear_console": "Limpiar Consola",
"console_cleared": "Consola limpiada",
"console_commands": "Comandos",
"console_completions": "Completaciones:",
"console_tab_completion": "Tab para completar",
"console_connected": "Conectado al daemon",
"console_disconnected": "Desconectado del daemon",
"console_not_connected": "Error: No conectado al daemon",
"console_no_daemon": "Sin daemon",
"console_daemon": "Daemon",
"console_daemon_error": "¡Error del daemon!",
"console_daemon_started": "Daemon iniciado",
"console_daemon_stopped": "Daemon detenido",
"console_errors": "Errores",
"console_filter_hint": "Filtrar salida...",
"console_search_commands": "Buscar comandos...",
"console_copy_all": "Copiar Todo",
"console_copy_selected": "Copiar",
"console_select_all": "Seleccionar Todo",
"console_zoom_in": "Acercar",
"console_zoom_out": "Alejar",
"console_show_daemon_output": "Mostrar salida del daemon",
"console_show_errors_only": "Mostrar solo errores",
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
"console_capturing_output": "Capturando salida del daemon...",
"console_starting_node": "Iniciando nodo...",
"console_line_count": "%zu líneas",
"console_new_lines": "%d nuevas líneas",
"console_showing_lines": "Mostrando %zu de %zu líneas",
"console_click_commands": "Clic en los comandos de arriba para insertarlos",
"console_click_insert": "Clic para insertar",
"console_click_insert_params": "Clic para insertar con parámetros",
"console_close": "Cerrar",
"console_status_running": "Ejecutando",
"console_status_stopped": "Detenido",
"console_status_starting": "Iniciando",
"console_status_stopping": "Deteniendo",
"console_status_error": "Error",
"console_status_unknown": "Desconocido",
"console_help_help": " help - Mostrar este mensaje de ayuda",
"console_help_getinfo": " getinfo - Mostrar información del nodo",
"console_help_getblockcount": " getblockcount - Mostrar altura actual del bloque",
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
"console_help_getmininginfo": " getmininginfo - Mostrar estado de minería",
"console_help_getpeerinfo": " getpeerinfo - Mostrar nodos conectados",
"console_help_setgenerate": " setgenerate - Controlar minería",
"console_help_stop": " stop - Detener el daemon",
"console_help_clear": " clear - Limpiar la consola",
# ---- About dialog ----
"about_title": "Acerca de ObsidianDragon",
"about_edition": "Edición ImGui",
"about_version": "Versión:",
"about_imgui": "ImGui:",
"about_build_date": "Fecha de Compilación:",
"about_build_type": "Tipo de Compilación:",
"about_debug": "Depuración",
"about_release": "Producción",
"about_daemon": "Daemon:",
"about_chain": "Cadena:",
"about_block_height": "Altura de Bloque:",
"about_connections": "Conexiones:",
"about_peers_count": "%zu nodos",
"about_credits": "Créditos",
"about_license": "Licencia",
"about_license_text": "Este software se distribuye bajo la Licencia Pública General de GNU v3 (GPLv3). Usted es libre de usar, modificar y distribuir este software bajo los términos de la licencia.",
"about_website": "Sitio Web",
"about_github": "GitHub",
"about_block_explorer": "Explorador de Bloques",
# ---- Address Book dialog ----
"address_book_title": "Libreta de Direcciones",
"address_book_add_new": "Agregar Nueva",
"address_book_add": "Agregar Dirección",
"address_book_edit": "Editar Dirección",
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
"address_book_count": "%zu direcciones guardadas",
"address_book_deleted": "Entrada eliminada",
"address_book_added": "Dirección agregada a la libreta",
"address_book_exists": "La dirección ya existe en la libreta",
"address_book_updated": "Dirección actualizada",
"address_book_update_failed": "Error al actualizar - la dirección puede estar duplicada",
# ---- Backup dialog ----
"backup_title": "Respaldar Cartera",
"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_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
"backup_create": "Crear Respaldo",
"backup_created": "Respaldo de cartera creado",
"backup_backing_up": "Respaldando...",
"backup_tips": "Consejos:",
"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_test": "Prueba restaurar desde el respaldo periódicamente",
"backup_wallet": "Respaldar Cartera...",
# ---- Block Info dialog ----
"block_info_title": "Información del Bloque",
"block_height": "Altura del Bloque:",
"block_get_info": "Obtener Info del Bloque",
"block_hash": "Hash del Bloque:",
"block_timestamp": "Fecha y Hora:",
"block_transactions": "Transacciones:",
"block_size": "Tamaño:",
"block_bits": "Bits:",
"block_merkle_root": "Raíz Merkle:",
"block_previous": "Bloque Anterior:",
"block_next": "Bloque Siguiente:",
"block_click_prev": "Clic para ver bloque anterior",
"block_click_next": "Clic para ver bloque siguiente",
"block_nav_prev": "<< Anterior",
"block_nav_next": "Siguiente >>",
# ---- Export Keys dialog ----
"export_keys_title": "Exportar Todas las Claves Privadas",
"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_options": "Opciones de exportación:",
"export_keys_include_z": "Incluir direcciones Z (protegidas)",
"export_keys_include_t": "Incluir direcciones T (transparentes)",
"export_keys_btn": "Exportar Claves",
"export_keys_success": "Claves exportadas exitosamente",
"export_private_key": "Exportar Clave Privada",
"export_viewing_key": "Exportar Clave de Vista",
# ---- Export Transactions dialog ----
"export_tx_title": "Exportar Transacciones a CSV",
"export_tx_count": "Exportar %zu transacciones a archivo CSV.",
"export_tx_none": "No hay transacciones para exportar",
"export_tx_file_fail": "Error al crear archivo CSV",
"export_tx_success": "Transacciones exportadas exitosamente",
# ---- Import Key dialog ----
"import_key_title": "Importar Clave Privada",
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
"import_key_label": "Clave(s) Privada(s):",
"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_btn": "Importar Clave(s)",
"import_key_no_valid": "No se encontraron claves válidas en la entrada",
"import_key_success": "Claves importadas exitosamente",
"import_key_rescan": "Re-escanear blockchain después de importar",
"import_key_start_height": "Altura inicial:",
"import_key_full_rescan": "(0 = re-escaneo completo)",
"import_key_formats": "Formatos de clave soportados:",
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
"import_key_t_format": "Claves privadas WIF de direcciones T",
"import_private_key": "Importar Clave Privada...",
"invalid_address": "Formato de dirección inválido",
# ---- Key Export dialog ----
"key_export_private_key": "Clave Privada:",
"key_export_viewing_key": "Clave de Vista:",
"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_viewing_warning": "Esta clave de vista permite a otros ver tus transacciones entrantes y saldo, pero NO gastar tus fondos. Comparte solo con personas de confianza.",
"key_export_fetching": "Obteniendo clave de la cartera...",
"key_export_reveal": "Revelar Clave",
# ---- QR dialog ----
"qr_title": "Código QR",
"qr_failed": "Error al generar código QR",
# ---- Request Payment dialog ----
"request_title": "Solicitar Pago",
"request_description": "Genera una solicitud de pago que otros pueden escanear o copiar. El código QR contiene tu dirección y cantidad/memo opcionales.",
"request_receive_address": "Dirección de Recepción:",
"request_select_address": "Seleccionar dirección...",
"request_shielded_addrs": "-- Direcciones Protegidas --",
"request_transparent_addrs": "-- Direcciones Transparentes --",
"request_amount": "Cantidad (opcional):",
"request_label": "Etiqueta (opcional):",
"request_memo": "Memo (opcional):",
"request_payment_uri": "URI de Pago:",
"request_copy_uri": "Copiar URI",
"request_uri_copied": "URI de pago copiada al portapapeles",
# ---- Shield dialog ----
"shield_title": "Proteger Recompensas de Coinbase",
"shield_description": "Protege tus recompensas de minería enviando salidas coinbase de direcciones transparentes a una dirección protegida. Esto mejora la privacidad ocultando tus ingresos de minería.",
"shield_from_address": "Dirección de Origen:",
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
"shield_to_address": "Dirección Destino (Protegida):",
"shield_select_z": "Seleccionar dirección z...",
"shield_utxo_limit": "Límite UTXO:",
"shield_max_utxos": "UTXOs máximos por operación",
"shield_funds": "Proteger Fondos",
"shield_started": "Operación de protección iniciada",
"shield_check_status": "Verificar Estado",
"shield_completed": "¡Operación completada exitosamente!",
"shield_merge_done": "¡Protección/fusión completada!",
"shield_in_progress": "Operación en progreso...",
"merge_title": "Fusionar a Dirección",
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
"merge_funds": "Fusionar Fondos",
"merge_started": "Operación de fusión iniciada",
# ---- Transaction Details dialog ----
"tx_details_title": "Detalles de Transacción",
"tx_received": "RECIBIDO",
"tx_sent": "ENVIADO",
"tx_mined": "MINADO",
"tx_immature": "INMADURO",
"tx_confirmations": "%d confirmaciones",
"tx_id_label": "ID de Transacción:",
"tx_to_address": "Dirección Destino:",
"tx_from_address": "Dirección Origen:",
"tx_view_explorer": "Ver en Explorador",
"pending": "Pendiente",
# ---- Validate Address dialog ----
"validate_title": "Validar Dirección",
"validate_description": "Ingresa una dirección DragonX para verificar si es válida y si pertenece a esta cartera.",
"validate_btn": "Validar",
"validate_results": "Resultados:",
"validate_status": "Estado:",
"validate_valid": "VÁLIDA",
"validate_invalid": "INVÁLIDA",
"validate_type": "Tipo:",
"validate_ownership": "Propiedad:",
"validate_is_mine": "Esta cartera es dueña de esta dirección",
"validate_not_mine": "No es propiedad de esta cartera",
"validate_shielded_type": "Protegida (dirección z)",
"validate_transparent_type": "Transparente (dirección t)",
# ---- Misc ----
"transaction_sent": "Transacción enviada exitosamente",
}
# Load existing to preserve anything we might have missed
import os
existing_path = os.path.join(os.path.dirname(__file__), '..', 'res', 'lang', 'es.json')
out_path = existing_path
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(dict(sorted(es.items())), f, indent=4, ensure_ascii=False)
f.write('\n')
print(f"Wrote {len(es)} Spanish translations to {out_path}")

646
scripts/gen_fr.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate French (fr) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "Variation 24h",
"24h_volume": "Volume 24h",
"about": "À propos",
"about_block_explorer": "Explorateur de blocs",
"about_block_height": "Hauteur de bloc :",
"about_build_date": "Date de compilation :",
"about_build_type": "Type de build :",
"about_chain": "Chaîne :",
"about_connections": "Connexions :",
"about_credits": "Crédits",
"about_daemon": "Daemon :",
"about_debug": "Débogage",
"about_dragonx": "À propos d'ObsidianDragon",
"about_edition": "Édition ImGui",
"about_github": "GitHub",
"about_imgui": "ImGui :",
"about_license": "Licence",
"about_license_text": "Ce logiciel est publié sous la licence publique générale GNU v3 (GPLv3). Vous êtes libre d'utiliser, de modifier et de distribuer ce logiciel selon les termes de la licence.",
"about_peers_count": "%zu pairs",
"about_release": "Version",
"about_title": "À propos d'ObsidianDragon",
"about_version": "Version :",
"about_website": "Site web",
"acrylic": "Acrylique",
"add": "Ajouter",
"address": "Adresse",
"address_book_add": "Ajouter une adresse",
"address_book_add_new": "Ajouter",
"address_book_added": "Adresse ajoutée au carnet",
"address_book_count": "%zu adresses enregistrées",
"address_book_deleted": "Entrée supprimée",
"address_book_edit": "Modifier l'adresse",
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
"address_book_exists": "L'adresse existe déjà dans le carnet",
"address_book_title": "Carnet d'adresses",
"address_book_update_failed": "Échec de la mise à jour - l'adresse est peut-être en double",
"address_book_updated": "Adresse mise à jour",
"address_copied": "Adresse copiée dans le presse-papiers",
"address_details": "Détails de l'adresse",
"address_label": "Adresse :",
"address_upper": "ADRESSE",
"address_url": "URL de l'adresse",
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
"advanced": "AVANCÉ",
"all_filter": "Tout",
"allow_custom_fees": "Autoriser les frais personnalisés",
"amount": "Montant",
"amount_details": "DÉTAILS DU MONTANT",
"amount_exceeds_balance": "Le montant dépasse le solde",
"amount_label": "Montant :",
"appearance": "APPARENCE",
"auto_shield": "Auto-blindage du minage",
"available": "Disponible",
"backup_backing_up": "Sauvegarde en cours...",
"backup_create": "Créer une sauvegarde",
"backup_created": "Sauvegarde du portefeuille créée",
"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_destination": "Destination de sauvegarde :",
"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_test": "Testez périodiquement la restauration à partir de la sauvegarde",
"backup_tips": "Conseils :",
"backup_title": "Sauvegarder le portefeuille",
"backup_wallet": "Sauvegarder le portefeuille...",
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
"balance": "Solde",
"balance_layout": "Disposition du solde",
"ban": "Bannir",
"banned_peers": "Pairs bannis",
"block": "Bloc",
"block_bits": "Bits :",
"block_click_next": "Cliquez pour voir le bloc suivant",
"block_click_prev": "Cliquez pour voir le bloc précédent",
"block_explorer": "Explorateur de blocs",
"block_get_info": "Obtenir les infos du bloc",
"block_hash": "Hash du bloc :",
"block_height": "Hauteur du bloc :",
"block_info_title": "Informations sur le bloc",
"block_merkle_root": "Racine de Merkle :",
"block_nav_next": "Suivant >>",
"block_nav_prev": "<< Précédent",
"block_next": "Bloc suivant :",
"block_previous": "Bloc précédent :",
"block_size": "Taille :",
"block_timestamp": "Horodatage :",
"block_transactions": "Transactions :",
"blockchain_syncing": "Synchronisation de la blockchain (%.1f%%)... Les soldes peuvent être inexacts.",
"cancel": "Annuler",
"characters": "caractères",
"clear": "Effacer",
"clear_all_bans": "Lever tous les bannissements",
"clear_form_confirm": "Effacer tous les champs du formulaire ?",
"clear_request": "Effacer la demande",
"click_copy_address": "Cliquez pour copier l'adresse",
"click_copy_uri": "Cliquez pour copier l'URI",
"close": "Fermer",
"conf_count": "%d conf.",
"confirm_and_send": "Confirmer & Envoyer",
"confirm_send": "Confirmer l'envoi",
"confirm_transaction": "Confirmer la transaction",
"confirmations": "Confirmations",
"confirmations_display": "%d confirmations | %s",
"confirmed": "Confirmé",
"connected": "Connecté",
"connected_peers": "Pairs connectés",
"connecting": "Connexion...",
"console": "Console",
"console_auto_scroll": "Défilement auto",
"console_available_commands": "Commandes disponibles :",
"console_capturing_output": "Capture de la sortie du daemon...",
"console_clear": "Effacer",
"console_clear_console": "Effacer la console",
"console_cleared": "Console effacée",
"console_click_commands": "Cliquez sur les commandes ci-dessus pour les insérer",
"console_click_insert": "Cliquez pour insérer",
"console_click_insert_params": "Cliquez pour insérer avec paramètres",
"console_close": "Fermer",
"console_commands": "Commandes",
"console_common_rpc": "Commandes RPC courantes :",
"console_completions": "Complétions :",
"console_connected": "Connecté au daemon",
"console_copy_all": "Tout copier",
"console_copy_selected": "Copier",
"console_daemon": "Daemon",
"console_daemon_error": "Erreur du daemon !",
"console_daemon_started": "Daemon démarré",
"console_daemon_stopped": "Daemon arrêté",
"console_disconnected": "Déconnecté du daemon",
"console_errors": "Erreurs",
"console_filter_hint": "Filtrer la sortie...",
"console_help_clear": " clear - Effacer la console",
"console_help_getbalance": " getbalance - Afficher le solde transparent",
"console_help_getblockcount": " getblockcount - Afficher la hauteur de bloc actuelle",
"console_help_getinfo": " getinfo - Afficher les infos du nœud",
"console_help_getmininginfo": " getmininginfo - Afficher le statut du minage",
"console_help_getpeerinfo": " getpeerinfo - Afficher les pairs connectés",
"console_help_gettotalbalance": " gettotalbalance - Afficher le solde total",
"console_help_help": " help - Afficher ce message d'aide",
"console_help_setgenerate": " setgenerate - Contrôler le minage",
"console_help_stop": " stop - Arrêter le daemon",
"console_line_count": "%zu lignes",
"console_new_lines": "%d nouvelles lignes",
"console_no_daemon": "Pas de daemon",
"console_not_connected": "Erreur : Non connecté au daemon",
"console_rpc_reference": "Référence des commandes RPC",
"console_scanline": "Scanline de la console",
"console_search_commands": "Rechercher des commandes...",
"console_select_all": "Tout sélectionner",
"console_show_daemon_output": "Afficher la sortie du daemon",
"console_show_errors_only": "Afficher uniquement les erreurs",
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
"console_showing_lines": "Affichage de %zu sur %zu lignes",
"console_starting_node": "Démarrage du nœud...",
"console_status_error": "Erreur",
"console_status_running": "En cours",
"console_status_starting": "Démarrage",
"console_status_stopped": "Arrêté",
"console_status_stopping": "Arrêt",
"console_status_unknown": "Inconnu",
"console_tab_completion": "Tab pour compléter",
"console_type_help": "Tapez 'help' pour les commandes disponibles",
"console_welcome": "Bienvenue dans la console ObsidianDragon",
"console_zoom_in": "Agrandir",
"console_zoom_out": "Réduire",
"copy": "Copier",
"copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur",
"copy_to_clipboard": "Copier dans le presse-papiers",
"copy_txid": "Copier le TxID",
"copy_uri": "Copier l'URI",
"current_price": "Prix actuel",
"custom_fees": "Frais personnalisés",
"dark": "Sombre",
"date": "Date",
"date_label": "Date :",
"delete": "Supprimer",
"difficulty": "Difficulté",
"disconnected": "Déconnecté",
"dismiss": "Ignorer",
"display": "Affichage",
"dragonx_green": "DragonX (Vert)",
"edit": "Modifier",
"error": "Erreur",
"est_time_to_block": "Temps est. par bloc",
"exit": "Quitter",
"explorer": "EXPLORATEUR",
"export": "Exporter",
"export_csv": "Exporter en CSV",
"export_keys_btn": "Exporter les clés",
"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_z": "Inclure les adresses Z (blindées)",
"export_keys_options": "Options d'exportation :",
"export_keys_success": "Clés exportées avec succès",
"export_keys_title": "Exporter toutes les clés privées",
"export_private_key": "Exporter la clé privée",
"export_tx_count": "Exporter %zu transactions en fichier CSV.",
"export_tx_file_fail": "Impossible de créer le fichier CSV",
"export_tx_none": "Aucune transaction à exporter",
"export_tx_success": "Transactions exportées avec succès",
"export_tx_title": "Exporter les transactions en CSV",
"export_viewing_key": "Exporter la clé de visualisation",
"failed_create_shielded": "Échec de la création de l'adresse blindée",
"failed_create_transparent": "Échec de la création de l'adresse transparente",
"fee": "Frais",
"fee_high": "Élevés",
"fee_label": "Frais :",
"fee_low": "Faibles",
"fee_normal": "Normal",
"fetch_prices": "Récupérer les prix",
"file": "Fichier",
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
"font_scale": "Taille de police",
"from": "De",
"from_upper": "DE",
"full_details": "Tous les détails",
"general": "Général",
"go_to_receive": "Aller à Recevoir",
"height": "Hauteur",
"help": "Aide",
"hide": "Masquer",
"history": "Historique",
"immature_type": "Immature",
"import": "Importer",
"import_key_btn": "Importer clé(s)",
"import_key_formats": "Formats de clés pris en charge :",
"import_key_full_rescan": "(0 = rescan complet)",
"import_key_label": "Clé(s) privée(s) :",
"import_key_no_valid": "Aucune clé valide trouvée dans l'entrée",
"import_key_rescan": "Re-scanner la blockchain après l'importation",
"import_key_start_height": "Hauteur de départ :",
"import_key_success": "Clés importées avec succès",
"import_key_t_format": "Clés privées WIF d'adresses T",
"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_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_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
"import_private_key": "Importer une clé privée...",
"invalid_address": "Format d'adresse invalide",
"ip_address": "Adresse IP",
"keep": "Conserver",
"keep_daemon": "Garder le daemon en marche",
"key_export_fetching": "Récupération de la clé depuis le portefeuille...",
"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_reveal": "Révéler la clé",
"key_export_viewing_key": "Clé de visualisation :",
"key_export_viewing_warning": "Cette clé de visualisation permet à d'autres de voir vos transactions entrantes et votre solde, mais PAS de dépenser vos fonds. Ne la partagez qu'avec des personnes de confiance.",
"label": "Libellé :",
"language": "Langue",
"light": "Clair",
"loading": "Chargement...",
"loading_addresses": "Chargement des adresses...",
"local_hashrate": "Hashrate local",
"low_spec_mode": "Mode économie",
"market": "Marché",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"market_24h_volume": "VOLUME 24H",
"market_6h": "6h",
"market_attribution": "Données de prix de NonKYC",
"market_btc_price": "PRIX BTC",
"market_cap": "Capitalisation",
"market_no_history": "Aucun historique de prix disponible",
"market_no_price": "Pas de données de prix",
"market_now": "Maintenant",
"market_pct_shielded": "%.0f%% Blindé",
"market_portfolio": "PORTEFEUILLE",
"market_price_unavailable": "Données de prix indisponibles",
"market_refresh_price": "Actualiser les données de prix",
"market_trade_on": "Échanger sur %s",
"mature": "Mature",
"max": "Max",
"memo": "Mémo (optionnel, chiffré)",
"memo_label": "Mémo :",
"memo_optional": "MÉMO (OPTIONNEL)",
"memo_upper": "MÉMO",
"memo_z_only": "Note : Les mémos ne sont disponibles que lors de l'envoi vers des adresses blindées (z)",
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
"merge_funds": "Fusionner les fonds",
"merge_started": "Opération de fusion démarrée",
"merge_title": "Fusionner vers une adresse",
"mine_when_idle": "Miner au repos",
"mined": "miné",
"mined_filter": "Miné",
"mined_type": "Miné",
"mined_upper": "MINÉ",
"miner_fee": "Frais de mineur",
"mining": "Minage",
"mining_active": "Actif",
"mining_address_copied": "Adresse de minage copiée",
"mining_all_time": "Tout le temps",
"mining_already_saved": "URL du pool déjà enregistrée",
"mining_block_copied": "Hash du bloc copié",
"mining_chart_1m_ago": "il y a 1m",
"mining_chart_5m_ago": "il y a 5m",
"mining_chart_now": "Maintenant",
"mining_chart_start": "Début",
"mining_click": "Cliquer",
"mining_click_copy_address": "Cliquez pour copier l'adresse",
"mining_click_copy_block": "Cliquez pour copier le hash du bloc",
"mining_click_copy_difficulty": "Cliquez pour copier la difficulté",
"mining_connected": "Connecté",
"mining_connecting": "Connexion...",
"mining_control": "Contrôle du minage",
"mining_difficulty_copied": "Difficulté copiée",
"mining_est_block": "Bloc est.",
"mining_est_daily": "Est. quotidien",
"mining_filter_all": "Tout",
"mining_filter_tip_all": "Afficher tous les gains",
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
"mining_filter_tip_solo": "Afficher uniquement les gains solo",
"mining_idle_off_tooltip": "Activer le minage au repos",
"mining_idle_on_tooltip": "Désactiver le minage au repos",
"mining_local_hashrate": "Hashrate local",
"mining_mine": "Miner",
"mining_mining_addr": "Adr. minage",
"mining_network": "Réseau",
"mining_no_blocks_yet": "Aucun bloc trouvé pour l'instant",
"mining_no_payouts_yet": "Aucun paiement de pool pour l'instant",
"mining_no_saved_addresses": "Aucune adresse enregistrée",
"mining_no_saved_pools": "Aucun pool enregistré",
"mining_off": "Le minage est DÉSACTIVÉ",
"mining_on": "Le minage est ACTIVÉ",
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
"mining_payout_address": "Adresse de paiement",
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
"mining_pool": "Pool",
"mining_pool_hashrate": "Hashrate du pool",
"mining_pool_url": "URL du pool",
"mining_recent_blocks": "BLOCS RÉCENTS",
"mining_recent_payouts": "PAIEMENTS DE POOL RÉCENTS",
"mining_remove": "Supprimer",
"mining_reset_defaults": "Réinitialiser les paramètres",
"mining_save_payout_address": "Enregistrer l'adresse de paiement",
"mining_save_pool_url": "Enregistrer l'URL du pool",
"mining_saved_addresses": "Adresses enregistrées :",
"mining_saved_pools": "Pools enregistrés :",
"mining_shares": "Parts",
"mining_show_chart": "Graphique",
"mining_show_log": "Journal",
"mining_solo": "Solo",
"mining_starting": "Démarrage...",
"mining_starting_tooltip": "Le mineur démarre...",
"mining_statistics": "Statistiques de minage",
"mining_stop": "Arrêter",
"mining_stop_solo_for_pool": "Arrêtez le minage solo avant de démarrer le minage en pool",
"mining_stop_solo_for_pool_settings": "Arrêtez le minage solo pour modifier les paramètres du pool",
"mining_stopping": "Arrêt...",
"mining_stopping_tooltip": "Le mineur s'arrête...",
"mining_syncing_tooltip": "La blockchain se synchronise...",
"mining_threads": "Threads de minage",
"mining_to_save": "pour enregistrer",
"mining_today": "Aujourd'hui",
"mining_uptime": "Temps de fonctionnement",
"mining_yesterday": "Hier",
"network": "Réseau",
"network_fee": "FRAIS RÉSEAU",
"network_hashrate": "Hashrate du réseau",
"new": "+ Nouveau",
"new_shielded_created": "Nouvelle adresse blindée créée",
"new_t_address": "Nouvelle adresse T",
"new_t_transparent": "Nouvelle adresse t (Transparente)",
"new_transparent_created": "Nouvelle adresse transparente créée",
"new_z_address": "Nouvelle adresse Z",
"new_z_shielded": "Nouvelle adresse z (Blindée)",
"no_addresses": "Aucune adresse trouvée. Créez-en une avec les boutons ci-dessus.",
"no_addresses_available": "Aucune adresse disponible",
"no_addresses_match": "Aucune adresse ne correspond au filtre",
"no_addresses_with_balance": "Aucune adresse avec solde",
"no_matching": "Aucune transaction correspondante",
"no_recent_receives": "Aucune réception récente",
"no_recent_sends": "Aucun envoi récent",
"no_transactions": "Aucune transaction trouvée",
"node": "NŒUD",
"node_security": "NŒUD & SÉCURITÉ",
"noise": "Bruit",
"not_connected": "Non connecté au daemon...",
"not_connected_to_daemon": "Non connecté au daemon",
"notes": "Notes",
"notes_optional": "Notes (optionnel) :",
"output_filename": "Nom du fichier de sortie :",
"overview": "Aperçu",
"paste": "Coller",
"paste_from_clipboard": "Coller depuis le presse-papiers",
"pay_from": "Payer depuis",
"payment_request": "DEMANDE DE PAIEMENT",
"payment_request_copied": "Demande de paiement copiée",
"payment_uri_copied": "URI de paiement copiée",
"peers": "Pairs",
"peers_avg_ping": "Ping moyen",
"peers_ban_24h": "Bannir le pair 24h",
"peers_ban_score": "Score de ban : %d",
"peers_banned": "Bannis",
"peers_banned_count": "Bannis : %d",
"peers_best_block": "Meilleur bloc",
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Blocs",
"peers_blocks_left": "%d blocs restants",
"peers_clear_all_bans": "Lever tous les bannissements",
"peers_click_copy": "Cliquez pour copier",
"peers_connected": "Connectés",
"peers_connected_count": "Connectés : %d",
"peers_copy_ip": "Copier l'IP",
"peers_dir_in": "Ent.",
"peers_dir_out": "Sort.",
"peers_hash_copied": "Hash copié",
"peers_hashrate": "Hashrate",
"peers_in_out": "Ent./Sort.",
"peers_longest": "Plus longue",
"peers_longest_chain": "Plus longue chaîne",
"peers_memory": "Mémoire",
"peers_no_banned": "Aucun pair banni",
"peers_no_connected": "Aucun pair connecté",
"peers_no_tls": "Pas de TLS",
"peers_notarized": "Notarisé",
"peers_p2p_port": "Port P2P",
"peers_protocol": "Protocole",
"peers_received": "Reçu",
"peers_refresh": "Actualiser",
"peers_refresh_tooltip": "Actualiser la liste des pairs",
"peers_refreshing": "Actualisation...",
"peers_sent": "Envoyé",
"peers_tt_id": "ID : %d",
"peers_tt_received": "Reçu : %s",
"peers_tt_sent": "Envoyé : %s",
"peers_tt_services": "Services : %s",
"peers_tt_start_height": "Hauteur de départ : %d",
"peers_tt_synced": "Synchronisé H/B : %d/%d",
"peers_tt_tls_cipher": "TLS : %s",
"peers_unban": "Débannir",
"peers_upper": "PAIRS",
"peers_version": "Version",
"pending": "En attente",
"ping": "Ping",
"price_chart": "Graphique des prix",
"qr_code": "Code QR",
"qr_failed": "Échec de la génération du code QR",
"qr_title": "Code QR",
"qr_unavailable": "QR indisponible",
"receive": "Recevoir",
"received": "reçu",
"received_filter": "Reçu",
"received_label": "Reçu",
"received_upper": "REÇU",
"receiving_addresses": "Vos adresses de réception",
"recent_received": "REÇUS RÉCENTS",
"recent_sends": "ENVOIS RÉCENTS",
"recipient": "DESTINATAIRE",
"recv_type": "Reçu",
"refresh": "Actualiser",
"refresh_now": "Actualiser maintenant",
"report_bug": "Signaler un bug",
"request_amount": "Montant (optionnel) :",
"request_copy_uri": "Copier l'URI",
"request_description": "Générez une demande de paiement que d'autres peuvent scanner ou copier. Le code QR contient votre adresse et un montant/mémo optionnel.",
"request_label": "Libellé (optionnel) :",
"request_memo": "Mémo (optionnel) :",
"request_payment": "Demander un paiement",
"request_payment_uri": "URI de paiement :",
"request_receive_address": "Adresse de réception :",
"request_select_address": "Sélectionner une adresse...",
"request_shielded_addrs": "-- Adresses blindées --",
"request_title": "Demander un paiement",
"request_transparent_addrs": "-- Adresses transparentes --",
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
"rescan": "Re-scanner",
"reset_to_defaults": "Réinitialiser les paramètres",
"review_send": "Vérifier l'envoi",
"rpc_host": "Hôte RPC",
"rpc_pass": "Mot de passe",
"rpc_port": "Port",
"rpc_user": "Nom d'utilisateur",
"save": "Enregistrer",
"save_settings": "Enregistrer les paramètres",
"save_z_transactions": "Enregistrer les Z-tx dans la liste",
"search_placeholder": "Rechercher...",
"security": "SÉCURITÉ",
"select_address": "Sélectionner une adresse...",
"select_receiving_address": "Sélectionner une adresse de réception...",
"select_source_address": "Sélectionner une adresse source...",
"send": "Envoyer",
"send_amount": "Montant",
"send_amount_details": "DÉTAILS DU MONTANT",
"send_amount_upper": "MONTANT",
"send_clear_fields": "Effacer tous les champs du formulaire ?",
"send_copy_error": "Copier l'erreur",
"send_dismiss": "Ignorer",
"send_error_copied": "Erreur copiée dans le presse-papiers",
"send_error_prefix": "Erreur : %s",
"send_exceeds_available": "Dépasse le disponible (%.8f)",
"send_fee": "Frais",
"send_fee_high": "Élevés",
"send_fee_low": "Faibles",
"send_fee_normal": "Normal",
"send_form_restored": "Formulaire restauré",
"send_from_this_address": "Envoyer depuis cette adresse",
"send_go_to_receive": "Aller à Recevoir",
"send_keep": "Conserver",
"send_network_fee": "FRAIS RÉSEAU",
"send_no_balance": "Pas de solde",
"send_no_recent": "Aucun envoi récent",
"send_recent_sends": "ENVOIS RÉCENTS",
"send_recipient": "DESTINATAIRE",
"send_select_source": "Sélectionner une adresse source...",
"send_sending_from": "ENVOI DEPUIS",
"send_submitting": "Soumission de la transaction...",
"send_switch_to_receive": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
"send_to": "Envoyer à",
"send_tooltip_enter_amount": "Entrez un montant à envoyer",
"send_tooltip_exceeds_balance": "Le montant dépasse le solde disponible",
"send_tooltip_in_progress": "Transaction déjà en cours",
"send_tooltip_invalid_address": "Entrez une adresse de destinataire valide",
"send_tooltip_not_connected": "Non connecté au daemon",
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
"send_total": "Total",
"send_transaction": "Envoyer la transaction",
"send_tx_failed": "Transaction échouée",
"send_tx_sent": "Transaction envoyée !",
"send_tx_success": "Transaction envoyée avec succès !",
"send_txid_copied": "TxID copié dans le presse-papiers",
"send_txid_label": "TxID : %s",
"send_valid_shielded": "Adresse blindée valide",
"send_valid_transparent": "Adresse transparente valide",
"send_wallet_empty": "Votre portefeuille est vide",
"send_yes_clear": "Oui, effacer",
"sending": "Envoi de la transaction",
"sending_from": "ENVOI DEPUIS",
"sent": "envoyé",
"sent_filter": "Envoyé",
"sent_type": "Envoyé",
"sent_upper": "ENVOYÉ",
"settings": "Paramètres",
"setup_wizard": "Assistant de configuration",
"share": "Partager",
"shield_check_status": "Vérifier le statut",
"shield_completed": "Opération terminée avec succès !",
"shield_description": "Blindez vos récompenses de minage en envoyant les sorties coinbase des adresses transparentes vers une adresse blindée. Cela améliore la confidentialité en masquant vos revenus de minage.",
"shield_from_address": "Depuis l'adresse :",
"shield_funds": "Blinder les fonds",
"shield_in_progress": "Opération en cours...",
"shield_max_utxos": "UTXOs max par opération",
"shield_merge_done": "Blindage/fusion terminé !",
"shield_select_z": "Sélectionner une z-adresse...",
"shield_started": "Opération de blindage démarrée",
"shield_title": "Blinder les récompenses coinbase",
"shield_to_address": "Vers l'adresse (blindée) :",
"shield_utxo_limit": "Limite UTXO :",
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
"shielded": "Blindé",
"shielded_to": "BLINDÉ VERS",
"shielded_type": "Blindé",
"show": "Afficher",
"show_qr_code": "Afficher le code QR",
"showing_transactions": "Affichage %d\xe2\x80\x93%d sur %d transactions (total : %zu)",
"simple_background": "Arrière-plan simple",
"start_mining": "Démarrer le minage",
"status": "Statut",
"stop_external": "Arrêter le daemon externe",
"stop_mining": "Arrêter le minage",
"submitting_transaction": "Soumission de la transaction...",
"success": "Succès",
"summary": "Résumé",
"syncing": "Synchronisation...",
"t_addresses": "Adresses T",
"test_connection": "Tester",
"theme": "Thème",
"theme_effects": "Effets de thème",
"time_days_ago": "il y a %d jours",
"time_hours_ago": "il y a %d heures",
"time_minutes_ago": "il y a %d minutes",
"time_seconds_ago": "il y a %d secondes",
"to": "À",
"to_upper": "À",
"tools": "OUTILS",
"total": "Total",
"transaction_id": "ID DE TRANSACTION",
"transaction_sent": "Transaction envoyée avec succès",
"transaction_sent_msg": "Transaction envoyée !",
"transaction_url": "URL de transaction",
"transactions": "Transactions",
"transactions_upper": "TRANSACTIONS",
"transparent": "Transparent",
"tx_confirmations": "%d confirmations",
"tx_details_title": "Détails de la transaction",
"tx_from_address": "Adresse d'origine :",
"tx_id_label": "ID de transaction :",
"tx_immature": "IMMATURE",
"tx_mined": "MINÉ",
"tx_received": "REÇU",
"tx_sent": "ENVOYÉ",
"tx_to_address": "Adresse de destination :",
"tx_view_explorer": "Voir dans l'explorateur",
"txs_count": "%d txs",
"type": "Type",
"ui_opacity": "Opacité de l'interface",
"unban": "Débannir",
"unconfirmed": "Non confirmé",
"undo_clear": "Annuler l'effacement",
"unknown": "Inconnu",
"use_embedded_daemon": "Utiliser le dragonxd intégré",
"use_tor": "Utiliser Tor",
"validate_btn": "Valider",
"validate_description": "Entrez une adresse DragonX pour vérifier si elle est valide et si elle appartient à ce portefeuille.",
"validate_invalid": "INVALIDE",
"validate_is_mine": "Ce portefeuille possède cette adresse",
"validate_not_mine": "N'appartient pas à ce portefeuille",
"validate_ownership": "Propriété :",
"validate_results": "Résultats :",
"validate_shielded_type": "Blindée (z-adresse)",
"validate_status": "Statut :",
"validate_title": "Valider l'adresse",
"validate_transparent_type": "Transparente (t-adresse)",
"validate_type": "Type :",
"validate_valid": "VALIDE",
"validating": "Validation...",
"verbose_logging": "Journalisation détaillée",
"version": "Version",
"view": "Afficher",
"view_details": "Voir les détails",
"view_on_explorer": "Voir dans l'explorateur",
"waiting_for_daemon": "En attente de la connexion au daemon...",
"wallet": "PORTEFEUILLE",
"wallet_empty": "Votre portefeuille est vide",
"wallet_empty_hint": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
"warning": "Attention",
"warning_upper": "ATTENTION !",
"website": "Site web",
"window_opacity": "Opacité de la fenêtre",
"yes_clear": "Oui, effacer",
"your_addresses": "Vos adresses",
"z_addresses": "Adresses Z",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "fr.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} French translations to {os.path.abspath(out)}")

646
scripts/gen_ja.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Japanese (ja) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24時間変動",
"24h_volume": "24時間出来高",
"about": "概要",
"about_block_explorer": "ブロックエクスプローラー",
"about_block_height": "ブロック高:",
"about_build_date": "ビルド日:",
"about_build_type": "ビルドタイプ:",
"about_chain": "チェーン:",
"about_connections": "接続数:",
"about_credits": "クレジット",
"about_daemon": "デーモン:",
"about_debug": "デバッグ",
"about_dragonx": "ObsidianDragonについて",
"about_edition": "ImGui エディション",
"about_github": "GitHub",
"about_imgui": "ImGui",
"about_license": "ライセンス",
"about_license_text": "本ソフトウェアはGNU General Public License v3 (GPLv3)の下で公開されています。ライセンス条項に従い、自由に使用、変更、配布できます。",
"about_peers_count": "%zu ピア",
"about_release": "リリース",
"about_title": "ObsidianDragonについて",
"about_version": "バージョン:",
"about_website": "ウェブサイト",
"acrylic": "アクリル",
"add": "追加",
"address": "アドレス",
"address_book_add": "アドレスを追加",
"address_book_add_new": "新規追加",
"address_book_added": "アドレスをアドレス帳に追加しました",
"address_book_count": "%zu 件のアドレスを保存済み",
"address_book_deleted": "エントリを削除しました",
"address_book_edit": "アドレスを編集",
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
"address_book_exists": "アドレスは既にアドレス帳に存在します",
"address_book_title": "アドレス帳",
"address_book_update_failed": "更新に失敗しました — アドレスが重複している可能性があります",
"address_book_updated": "アドレスを更新しました",
"address_copied": "アドレスをクリップボードにコピーしました",
"address_details": "アドレス詳細",
"address_label": "アドレス:",
"address_upper": "アドレス",
"address_url": "アドレスURL",
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
"advanced": "詳細設定",
"all_filter": "すべて",
"allow_custom_fees": "カスタム手数料を許可",
"amount": "金額",
"amount_details": "金額の詳細",
"amount_exceeds_balance": "金額が残高を超えています",
"amount_label": "金額:",
"appearance": "外観",
"auto_shield": "マイニング自動シールド",
"available": "利用可能",
"backup_backing_up": "バックアップ中...",
"backup_create": "バックアップを作成",
"backup_created": "ウォレットのバックアップを作成しました",
"backup_data": "バックアップとデータ",
"backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。",
"backup_destination": "バックアップ先:",
"backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存",
"backup_tip_multiple": "異なる場所に複数のバックアップを作成",
"backup_tip_test": "定期的にバックアップからの復元をテスト",
"backup_tips": "ヒント:",
"backup_title": "ウォレットのバックアップ",
"backup_wallet": "ウォレットをバックアップ...",
"backup_wallet_not_found": "警告予想される場所にwallet.datが見つかりません",
"balance": "残高",
"balance_layout": "残高レイアウト",
"ban": "ブロック",
"banned_peers": "ブロック済みピア",
"block": "ブロック",
"block_bits": "ビット:",
"block_click_next": "クリックして次のブロックを表示",
"block_click_prev": "クリックして前のブロックを表示",
"block_explorer": "ブロックエクスプローラー",
"block_get_info": "ブロック情報を取得",
"block_hash": "ブロックハッシュ:",
"block_height": "ブロック高:",
"block_info_title": "ブロック情報",
"block_merkle_root": "マークルルート:",
"block_nav_next": "次へ >>",
"block_nav_prev": "<< 前へ",
"block_next": "次のブロック:",
"block_previous": "前のブロック:",
"block_size": "サイズ:",
"block_timestamp": "タイムスタンプ:",
"block_transactions": "トランザクション:",
"blockchain_syncing": "ブロックチェーン同期中 (%.1f%%)... 残高が不正確な場合があります。",
"cancel": "キャンセル",
"characters": "文字",
"clear": "クリア",
"clear_all_bans": "すべてのブロックを解除",
"clear_form_confirm": "すべてのフォームフィールドをクリアしますか?",
"clear_request": "リクエストをクリア",
"click_copy_address": "クリックしてアドレスをコピー",
"click_copy_uri": "クリックしてURIをコピー",
"close": "閉じる",
"conf_count": "%d 確認",
"confirm_and_send": "確認して送金",
"confirm_send": "送金を確認",
"confirm_transaction": "取引を確認",
"confirmations": "確認数",
"confirmations_display": "%d 確認 | %s",
"confirmed": "確認済み",
"connected": "接続済み",
"connected_peers": "接続中のピア",
"connecting": "接続中...",
"console": "コンソール",
"console_auto_scroll": "自動スクロール",
"console_available_commands": "利用可能なコマンド:",
"console_capturing_output": "デーモン出力をキャプチャ中...",
"console_clear": "クリア",
"console_clear_console": "コンソールをクリア",
"console_cleared": "コンソールをクリアしました",
"console_click_commands": "上のコマンドをクリックして挿入",
"console_click_insert": "クリックして挿入",
"console_click_insert_params": "クリックしてパラメータ付きで挿入",
"console_close": "閉じる",
"console_commands": "コマンド",
"console_common_rpc": "一般的なRPCコマンド",
"console_completions": "補完:",
"console_connected": "デーモンに接続済み",
"console_copy_all": "すべてコピー",
"console_copy_selected": "コピー",
"console_daemon": "デーモン",
"console_daemon_error": "デーモンエラー!",
"console_daemon_started": "デーモンが起動しました",
"console_daemon_stopped": "デーモンが停止しました",
"console_disconnected": "デーモンから切断されました",
"console_errors": "エラー",
"console_filter_hint": "出力をフィルタ...",
"console_help_clear": " clear - コンソールをクリア",
"console_help_getbalance": " getbalance - 透明残高を表示",
"console_help_getblockcount": " getblockcount - 現在のブロック高を表示",
"console_help_getinfo": " getinfo - ノード情報を表示",
"console_help_getmininginfo": " getmininginfo - マイニング状況を表示",
"console_help_getpeerinfo": " getpeerinfo - 接続中のピアを表示",
"console_help_gettotalbalance": " gettotalbalance - 合計残高を表示",
"console_help_help": " help - このヘルプを表示",
"console_help_setgenerate": " setgenerate - マイニングを制御",
"console_help_stop": " stop - デーモンを停止",
"console_line_count": "%zu 行",
"console_new_lines": "%d 新しい行",
"console_no_daemon": "デーモンなし",
"console_not_connected": "エラー:デーモンに接続されていません",
"console_rpc_reference": "RPCコマンドリファレンス",
"console_scanline": "コンソールスキャンライン",
"console_search_commands": "コマンドを検索...",
"console_select_all": "すべて選択",
"console_show_daemon_output": "デーモン出力を表示",
"console_show_errors_only": "エラーのみ表示",
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
"console_showing_lines": "%zu / %zu 行を表示中",
"console_starting_node": "ノードを起動中...",
"console_status_error": "エラー",
"console_status_running": "実行中",
"console_status_starting": "起動中",
"console_status_stopped": "停止済み",
"console_status_stopping": "停止中",
"console_status_unknown": "不明",
"console_tab_completion": "Tabで補完",
"console_type_help": "'help'と入力して利用可能なコマンドを表示",
"console_welcome": "ObsidianDragonコンソールへようこそ",
"console_zoom_in": "拡大",
"console_zoom_out": "縮小",
"copy": "コピー",
"copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー",
"copy_to_clipboard": "クリップボードにコピー",
"copy_txid": "TxIDをコピー",
"copy_uri": "URIをコピー",
"current_price": "現在の価格",
"custom_fees": "カスタム手数料",
"dark": "ダーク",
"date": "日付",
"date_label": "日付:",
"delete": "削除",
"difficulty": "難易度",
"disconnected": "切断済み",
"dismiss": "閉じる",
"display": "表示",
"dragonx_green": "DragonXグリーン",
"edit": "編集",
"error": "エラー",
"est_time_to_block": "予測ブロック時間",
"exit": "終了",
"explorer": "エクスプローラー",
"export": "エクスポート",
"export_csv": "CSVエクスポート",
"export_keys_btn": "鍵をエクスポート",
"export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。",
"export_keys_include_t": "Tアドレスを含める透明",
"export_keys_include_z": "Zアドレスを含めるシールド",
"export_keys_options": "エクスポートオプション:",
"export_keys_success": "鍵のエクスポートに成功しました",
"export_keys_title": "すべての秘密鍵をエクスポート",
"export_private_key": "秘密鍵をエクスポート",
"export_tx_count": "%zu件の取引をCSVファイルにエクスポート。",
"export_tx_file_fail": "CSVファイルの作成に失敗しました",
"export_tx_none": "エクスポートする取引がありません",
"export_tx_success": "取引のエクスポートに成功しました",
"export_tx_title": "取引をCSVにエクスポート",
"export_viewing_key": "閲覧鍵をエクスポート",
"failed_create_shielded": "シールドアドレスの作成に失敗しました",
"failed_create_transparent": "透明アドレスの作成に失敗しました",
"fee": "手数料",
"fee_high": "高い",
"fee_label": "手数料:",
"fee_low": "低い",
"fee_normal": "通常",
"fetch_prices": "価格を取得",
"file": "ファイル",
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
"font_scale": "フォントサイズ",
"from": "送信元",
"from_upper": "送信元",
"full_details": "詳細情報",
"general": "一般",
"go_to_receive": "受信へ移動",
"height": "高さ",
"help": "ヘルプ",
"hide": "非表示",
"history": "履歴",
"immature_type": "未成熟",
"import": "インポート",
"import_key_btn": "鍵をインポート",
"import_key_formats": "サポートされる鍵形式:",
"import_key_full_rescan": "0 = 完全再スキャン)",
"import_key_label": "秘密鍵:",
"import_key_no_valid": "入力に有効な鍵が見つかりません",
"import_key_rescan": "インポート後にブロックチェーンを再スキャン",
"import_key_start_height": "開始高:",
"import_key_success": "鍵のインポートに成功しました",
"import_key_t_format": "TアドレスWIF秘密鍵",
"import_key_title": "秘密鍵をインポート",
"import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。",
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
"import_private_key": "秘密鍵をインポート...",
"invalid_address": "無効なアドレス形式",
"ip_address": "IPアドレス",
"keep": "保持",
"keep_daemon": "デーモンを実行し続ける",
"key_export_fetching": "ウォレットから鍵を取得中...",
"key_export_private_key": "秘密鍵:",
"key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。",
"key_export_reveal": "鍵を表示",
"key_export_viewing_key": "閲覧鍵:",
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
"label": "ラベル:",
"language": "言語",
"light": "ライト",
"loading": "読み込み中...",
"loading_addresses": "アドレスを読み込み中...",
"local_hashrate": "ローカルハッシュレート",
"low_spec_mode": "省電力モード",
"market": "市場",
"market_12h": "12時間",
"market_18h": "18時間",
"market_24h": "24時間",
"market_24h_volume": "24時間出来高",
"market_6h": "6時間",
"market_attribution": "価格データNonKYC提供",
"market_btc_price": "BTC価格",
"market_cap": "時価総額",
"market_no_history": "価格履歴がありません",
"market_no_price": "価格データなし",
"market_now": "現在",
"market_pct_shielded": "%.0f%% シールド済み",
"market_portfolio": "ポートフォリオ",
"market_price_unavailable": "価格データが利用できません",
"market_refresh_price": "価格データを更新",
"market_trade_on": "%s で取引",
"mature": "成熟済み",
"max": "最大",
"memo": "メモ(任意、暗号化)",
"memo_label": "メモ:",
"memo_optional": "メモ(任意)",
"memo_upper": "メモ",
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
"merge_funds": "資金を統合",
"merge_started": "統合操作を開始しました",
"merge_title": "アドレスに統合",
"mine_when_idle": "アイドル時にマイニング",
"mined": "採掘済み",
"mined_filter": "採掘済み",
"mined_type": "採掘済み",
"mined_upper": "採掘済み",
"miner_fee": "マイナー手数料",
"mining": "マイニング",
"mining_active": "アクティブ",
"mining_address_copied": "マイニングアドレスをコピーしました",
"mining_all_time": "全期間",
"mining_already_saved": "プールURLは既に保存済みです",
"mining_block_copied": "ブロックハッシュをコピーしました",
"mining_chart_1m_ago": "1分前",
"mining_chart_5m_ago": "5分前",
"mining_chart_now": "現在",
"mining_chart_start": "開始",
"mining_click": "クリック",
"mining_click_copy_address": "クリックしてアドレスをコピー",
"mining_click_copy_block": "クリックしてブロックハッシュをコピー",
"mining_click_copy_difficulty": "クリックして難易度をコピー",
"mining_connected": "接続済み",
"mining_connecting": "接続中...",
"mining_control": "マイニング制御",
"mining_difficulty_copied": "難易度をコピーしました",
"mining_est_block": "予測ブロック",
"mining_est_daily": "予測日収",
"mining_filter_all": "すべて",
"mining_filter_tip_all": "すべての収益を表示",
"mining_filter_tip_pool": "プール収益のみ表示",
"mining_filter_tip_solo": "ソロ収益のみ表示",
"mining_idle_off_tooltip": "アイドルマイニングを有効にする",
"mining_idle_on_tooltip": "アイドルマイニングを無効にする",
"mining_local_hashrate": "ローカルハッシュレート",
"mining_mine": "マイニング",
"mining_mining_addr": "マイニングアドレス",
"mining_network": "ネットワーク",
"mining_no_blocks_yet": "まだブロックが見つかっていません",
"mining_no_payouts_yet": "まだプール支払いがありません",
"mining_no_saved_addresses": "保存されたアドレスがありません",
"mining_no_saved_pools": "保存されたプールがありません",
"mining_off": "マイニングはオフです",
"mining_on": "マイニングはオンです",
"mining_open_in_explorer": "エクスプローラーで開く",
"mining_payout_address": "支払いアドレス",
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
"mining_pool": "プール",
"mining_pool_hashrate": "プールハッシュレート",
"mining_pool_url": "プールURL",
"mining_recent_blocks": "最近のブロック",
"mining_recent_payouts": "最近のプール支払い",
"mining_remove": "削除",
"mining_reset_defaults": "デフォルトにリセット",
"mining_save_payout_address": "支払いアドレスを保存",
"mining_save_pool_url": "プールURLを保存",
"mining_saved_addresses": "保存済みアドレス:",
"mining_saved_pools": "保存済みプール:",
"mining_shares": "シェア",
"mining_show_chart": "チャート",
"mining_show_log": "ログ",
"mining_solo": "ソロ",
"mining_starting": "起動中...",
"mining_starting_tooltip": "マイナーを起動中...",
"mining_statistics": "マイニング統計",
"mining_stop": "停止",
"mining_stop_solo_for_pool": "プールマイニングを開始する前にソロマイニングを停止してください",
"mining_stop_solo_for_pool_settings": "プール設定を変更するにはソロマイニングを停止してください",
"mining_stopping": "停止中...",
"mining_stopping_tooltip": "マイナーを停止中...",
"mining_syncing_tooltip": "ブロックチェーン同期中...",
"mining_threads": "マイニングスレッド",
"mining_to_save": "保存する",
"mining_today": "今日",
"mining_uptime": "稼働時間",
"mining_yesterday": "昨日",
"network": "ネットワーク",
"network_fee": "ネットワーク手数料",
"network_hashrate": "ネットワークハッシュレート",
"new": "+ 新規",
"new_shielded_created": "新しいシールドアドレスを作成しました",
"new_t_address": "新しいTアドレス",
"new_t_transparent": "新しいtアドレス透明",
"new_transparent_created": "新しい透明アドレスを作成しました",
"new_z_address": "新しいZアドレス",
"new_z_shielded": "新しいzアドレスシールド",
"no_addresses": "アドレスが見つかりません。上のボタンを使用して作成してください。",
"no_addresses_available": "利用可能なアドレスがありません",
"no_addresses_match": "フィルタに一致するアドレスがありません",
"no_addresses_with_balance": "残高のあるアドレスがありません",
"no_matching": "一致する取引がありません",
"no_recent_receives": "最近の受信がありません",
"no_recent_sends": "最近の送信がありません",
"no_transactions": "取引が見つかりません",
"node": "ノード",
"node_security": "ノードとセキュリティ",
"noise": "ノイズ",
"not_connected": "デーモンに未接続...",
"not_connected_to_daemon": "デーモンに未接続",
"notes": "メモ",
"notes_optional": "メモ(任意):",
"output_filename": "出力ファイル名:",
"overview": "概要",
"paste": "貼り付け",
"paste_from_clipboard": "クリップボードから貼り付け",
"pay_from": "支払い元",
"payment_request": "支払い請求",
"payment_request_copied": "支払い請求をコピーしました",
"payment_uri_copied": "支払いURIをコピーしました",
"peers": "ピア",
"peers_avg_ping": "平均Ping",
"peers_ban_24h": "ピアを24時間ブロック",
"peers_ban_score": "ブロックスコア:%d",
"peers_banned": "ブロック済み",
"peers_banned_count": "ブロック済み:%d",
"peers_best_block": "最良ブロック",
"peers_blockchain": "ブロックチェーン",
"peers_blocks": "ブロック",
"peers_blocks_left": "残り %d ブロック",
"peers_clear_all_bans": "すべてのブロックを解除",
"peers_click_copy": "クリックしてコピー",
"peers_connected": "接続済み",
"peers_connected_count": "接続済み:%d",
"peers_copy_ip": "IPをコピー",
"peers_dir_in": "",
"peers_dir_out": "",
"peers_hash_copied": "ハッシュをコピーしました",
"peers_hashrate": "ハッシュレート",
"peers_in_out": "入/出",
"peers_longest": "最長",
"peers_longest_chain": "最長チェーン",
"peers_memory": "メモリ",
"peers_no_banned": "ブロック済みピアなし",
"peers_no_connected": "接続済みピアなし",
"peers_no_tls": "TLSなし",
"peers_notarized": "公証済み",
"peers_p2p_port": "P2Pポート",
"peers_protocol": "プロトコル",
"peers_received": "受信",
"peers_refresh": "更新",
"peers_refresh_tooltip": "ピアリストを更新",
"peers_refreshing": "更新中...",
"peers_sent": "送信",
"peers_tt_id": "ID%d",
"peers_tt_received": "受信:%s",
"peers_tt_sent": "送信:%s",
"peers_tt_services": "サービス:%s",
"peers_tt_start_height": "開始高:%d",
"peers_tt_synced": "同期済み H/B%d/%d",
"peers_tt_tls_cipher": "TLS%s",
"peers_unban": "ブロック解除",
"peers_upper": "ピア",
"peers_version": "バージョン",
"pending": "保留中",
"ping": "Ping",
"price_chart": "価格チャート",
"qr_code": "QRコード",
"qr_failed": "QRコードの生成に失敗しました",
"qr_title": "QRコード",
"qr_unavailable": "QR利用不可",
"receive": "受信",
"received": "受信済み",
"received_filter": "受信済み",
"received_label": "受信済み",
"received_upper": "受信済み",
"receiving_addresses": "あなたの受信アドレス",
"recent_received": "最近の受信",
"recent_sends": "最近の送信",
"recipient": "受取人",
"recv_type": "受信",
"refresh": "更新",
"refresh_now": "今すぐ更新",
"report_bug": "バグを報告",
"request_amount": "金額(任意):",
"request_copy_uri": "URIをコピー",
"request_description": "他の人がスキャンまたはコピーできる支払い請求を生成します。QRコードにはアドレスとオプションの金額/メモが含まれます。",
"request_label": "ラベル(任意):",
"request_memo": "メモ(任意):",
"request_payment": "支払いを請求",
"request_payment_uri": "支払いURI",
"request_receive_address": "受信アドレス:",
"request_select_address": "アドレスを選択...",
"request_shielded_addrs": "-- シールドアドレス --",
"request_title": "支払いを請求",
"request_transparent_addrs": "-- 透明アドレス --",
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
"rescan": "再スキャン",
"reset_to_defaults": "デフォルトにリセット",
"review_send": "送金を確認",
"rpc_host": "RPCホスト",
"rpc_pass": "パスワード",
"rpc_port": "ポート",
"rpc_user": "ユーザー名",
"save": "保存",
"save_settings": "設定を保存",
"save_z_transactions": "Z取引を取引リストに保存",
"search_placeholder": "検索...",
"security": "セキュリティ",
"select_address": "アドレスを選択...",
"select_receiving_address": "受信アドレスを選択...",
"select_source_address": "送信元アドレスを選択...",
"send": "送金",
"send_amount": "金額",
"send_amount_details": "金額の詳細",
"send_amount_upper": "金額",
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
"send_copy_error": "エラーをコピー",
"send_dismiss": "閉じる",
"send_error_copied": "エラーをクリップボードにコピーしました",
"send_error_prefix": "エラー:%s",
"send_exceeds_available": "利用可能額を超過 (%.8f)",
"send_fee": "手数料",
"send_fee_high": "高い",
"send_fee_low": "低い",
"send_fee_normal": "通常",
"send_form_restored": "フォームが復元されました",
"send_from_this_address": "このアドレスから送金",
"send_go_to_receive": "受信へ移動",
"send_keep": "保持",
"send_network_fee": "ネットワーク手数料",
"send_no_balance": "残高なし",
"send_no_recent": "最近の送信なし",
"send_recent_sends": "最近の送信",
"send_recipient": "受取人",
"send_select_source": "送信元アドレスを選択...",
"send_sending_from": "送信元",
"send_submitting": "取引を送信中...",
"send_switch_to_receive": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
"send_to": "送金先",
"send_tooltip_enter_amount": "送金額を入力してください",
"send_tooltip_exceeds_balance": "金額が利用可能残高を超えています",
"send_tooltip_in_progress": "取引は既に進行中です",
"send_tooltip_invalid_address": "有効な受取人アドレスを入力してください",
"send_tooltip_not_connected": "デーモンに未接続",
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
"send_total": "合計",
"send_transaction": "取引を送信",
"send_tx_failed": "取引に失敗しました",
"send_tx_sent": "取引を送信しました!",
"send_tx_success": "取引の送信に成功しました!",
"send_txid_copied": "TxIDをクリップボードにコピーしました",
"send_txid_label": "TxID%s",
"send_valid_shielded": "有効なシールドアドレス",
"send_valid_transparent": "有効な透明アドレス",
"send_wallet_empty": "ウォレットは空です",
"send_yes_clear": "はい、クリア",
"sending": "取引を送信中",
"sending_from": "送信元",
"sent": "送信済み",
"sent_filter": "送信済み",
"sent_type": "送信済み",
"sent_upper": "送信済み",
"settings": "設定",
"setup_wizard": "セットアップウィザード",
"share": "共有",
"shield_check_status": "ステータスを確認",
"shield_completed": "操作が正常に完了しました!",
"shield_description": "透明アドレスのcoinbase出力をシールドアドレスに送信して、マイニング報酬をシールドします。マイニング収入を隠すことでプライバシーが向上します。",
"shield_from_address": "送信元アドレス:",
"shield_funds": "資金をシールド",
"shield_in_progress": "操作進行中...",
"shield_max_utxos": "1回の操作あたりの最大UTXO数",
"shield_merge_done": "シールド/統合が完了しました!",
"shield_select_z": "zアドレスを選択...",
"shield_started": "シールド操作を開始しました",
"shield_title": "Coinbase報酬をシールド",
"shield_to_address": "送信先アドレス(シールド):",
"shield_utxo_limit": "UTXO制限",
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
"shielded": "シールド",
"shielded_to": "シールド先",
"shielded_type": "シールド",
"show": "表示",
"show_qr_code": "QRコードを表示",
"showing_transactions": "%d\xe2\x80\x93%d / %d 件の取引を表示中(合計:%zu",
"simple_background": "シンプル背景",
"start_mining": "マイニング開始",
"status": "ステータス",
"stop_external": "外部デーモンを停止",
"stop_mining": "マイニング停止",
"submitting_transaction": "取引を送信中...",
"success": "成功",
"summary": "概要",
"syncing": "同期中...",
"t_addresses": "Tアドレス",
"test_connection": "テスト",
"theme": "テーマ",
"theme_effects": "テーマ効果",
"time_days_ago": "%d日前",
"time_hours_ago": "%d時間前",
"time_minutes_ago": "%d分前",
"time_seconds_ago": "%d秒前",
"to": "宛先",
"to_upper": "宛先",
"tools": "ツール",
"total": "合計",
"transaction_id": "取引ID",
"transaction_sent": "取引の送信に成功しました",
"transaction_sent_msg": "取引を送信しました!",
"transaction_url": "取引URL",
"transactions": "取引",
"transactions_upper": "取引",
"transparent": "透明",
"tx_confirmations": "%d 確認",
"tx_details_title": "取引の詳細",
"tx_from_address": "送信元アドレス:",
"tx_id_label": "取引ID",
"tx_immature": "未成熟",
"tx_mined": "採掘済み",
"tx_received": "受信済み",
"tx_sent": "送信済み",
"tx_to_address": "送信先アドレス:",
"tx_view_explorer": "エクスプローラーで表示",
"txs_count": "%d",
"type": "タイプ",
"ui_opacity": "UI透明度",
"unban": "ブロック解除",
"unconfirmed": "未確認",
"undo_clear": "クリアを元に戻す",
"unknown": "不明",
"use_embedded_daemon": "内蔵dragonxdを使用",
"use_tor": "Torを使用",
"validate_btn": "検証",
"validate_description": "DragonXアドレスを入力して、有効かどうか、そしてこのウォレットに属しているかどうかを確認します。",
"validate_invalid": "無効",
"validate_is_mine": "このウォレットがこのアドレスを所有しています",
"validate_not_mine": "このウォレットに属していません",
"validate_ownership": "所有者:",
"validate_results": "結果:",
"validate_shielded_type": "シールドzアドレス",
"validate_status": "ステータス:",
"validate_title": "アドレスを検証",
"validate_transparent_type": "透明tアドレス",
"validate_type": "タイプ:",
"validate_valid": "有効",
"validating": "検証中...",
"verbose_logging": "詳細ログ",
"version": "バージョン",
"view": "表示",
"view_details": "詳細を表示",
"view_on_explorer": "エクスプローラーで表示",
"waiting_for_daemon": "デーモン接続を待機中...",
"wallet": "ウォレット",
"wallet_empty": "ウォレットは空です",
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
"warning": "警告",
"warning_upper": "警告!",
"website": "ウェブサイト",
"window_opacity": "ウィンドウ透明度",
"yes_clear": "はい、クリア",
"your_addresses": "あなたのアドレス",
"z_addresses": "Zアドレス",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ja.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Japanese translations to {os.path.abspath(out)}")

646
scripts/gen_ko.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Korean (ko) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24시간 변동",
"24h_volume": "24시간 거래량",
"about": "정보",
"about_block_explorer": "블록 탐색기",
"about_block_height": "블록 높이:",
"about_build_date": "빌드 날짜:",
"about_build_type": "빌드 유형:",
"about_chain": "체인:",
"about_connections": "연결:",
"about_credits": "크레딧",
"about_daemon": "데몬:",
"about_debug": "디버그",
"about_dragonx": "ObsidianDragon 정보",
"about_edition": "ImGui 에디션",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "라이선스",
"about_license_text": "본 소프트웨어는 GNU General Public License v3 (GPLv3) 하에 배포됩니다. 라이선스 조건에 따라 자유롭게 사용, 수정 및 배포할 수 있습니다.",
"about_peers_count": "%zu 피어",
"about_release": "릴리스",
"about_title": "ObsidianDragon 정보",
"about_version": "버전:",
"about_website": "웹사이트",
"acrylic": "아크릴",
"add": "추가",
"address": "주소",
"address_book_add": "주소 추가",
"address_book_add_new": "새로 추가",
"address_book_added": "주소록에 주소를 추가했습니다",
"address_book_count": "저장된 주소 %zu개",
"address_book_deleted": "항목이 삭제되었습니다",
"address_book_edit": "주소 편집",
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
"address_book_exists": "주소가 이미 주소록에 있습니다",
"address_book_title": "주소록",
"address_book_update_failed": "업데이트 실패 — 주소가 중복될 수 있습니다",
"address_book_updated": "주소가 업데이트되었습니다",
"address_copied": "주소가 클립보드에 복사되었습니다",
"address_details": "주소 상세",
"address_label": "주소:",
"address_upper": "주소",
"address_url": "주소 URL",
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
"advanced": "고급 설정",
"all_filter": "전체",
"allow_custom_fees": "사용자 정의 수수료 허용",
"amount": "금액",
"amount_details": "금액 상세",
"amount_exceeds_balance": "금액이 잔액을 초과합니다",
"amount_label": "금액:",
"appearance": "외관",
"auto_shield": "채굴 자동 차폐",
"available": "사용 가능",
"backup_backing_up": "백업 중...",
"backup_create": "백업 생성",
"backup_created": "지갑 백업이 생성되었습니다",
"backup_data": "백업 및 데이터",
"backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.",
"backup_destination": "백업 위치:",
"backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장",
"backup_tip_multiple": "서로 다른 위치에 여러 백업 생성",
"backup_tip_test": "정기적으로 백업 복원 테스트",
"backup_tips": "팁:",
"backup_title": "지갑 백업",
"backup_wallet": "지갑 백업...",
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
"balance": "잔액",
"balance_layout": "잔액 레이아웃",
"ban": "차단",
"banned_peers": "차단된 피어",
"block": "블록",
"block_bits": "비트:",
"block_click_next": "클릭하여 다음 블록 보기",
"block_click_prev": "클릭하여 이전 블록 보기",
"block_explorer": "블록 탐색기",
"block_get_info": "블록 정보 조회",
"block_hash": "블록 해시:",
"block_height": "블록 높이:",
"block_info_title": "블록 정보",
"block_merkle_root": "머클 루트:",
"block_nav_next": "다음 >>",
"block_nav_prev": "<< 이전",
"block_next": "다음 블록:",
"block_previous": "이전 블록:",
"block_size": "크기:",
"block_timestamp": "타임스탬프:",
"block_transactions": "트랜잭션:",
"blockchain_syncing": "블록체인 동기화 중 (%.1f%%)... 잔액이 정확하지 않을 수 있습니다.",
"cancel": "취소",
"characters": "문자",
"clear": "지우기",
"clear_all_bans": "모든 차단 해제",
"clear_form_confirm": "모든 양식 필드를 지우시겠습니까?",
"clear_request": "요청 지우기",
"click_copy_address": "클릭하여 주소 복사",
"click_copy_uri": "클릭하여 URI 복사",
"close": "닫기",
"conf_count": "%d 확인",
"confirm_and_send": "확인 후 전송",
"confirm_send": "전송 확인",
"confirm_transaction": "거래 확인",
"confirmations": "확인 수",
"confirmations_display": "%d 확인 | %s",
"confirmed": "확인됨",
"connected": "연결됨",
"connected_peers": "연결된 피어",
"connecting": "연결 중...",
"console": "콘솔",
"console_auto_scroll": "자동 스크롤",
"console_available_commands": "사용 가능한 명령어:",
"console_capturing_output": "데몬 출력 캡처 중...",
"console_clear": "지우기",
"console_clear_console": "콘솔 지우기",
"console_cleared": "콘솔이 지워졌습니다",
"console_click_commands": "위의 명령어를 클릭하여 삽입",
"console_click_insert": "클릭하여 삽입",
"console_click_insert_params": "클릭하여 매개변수와 함께 삽입",
"console_close": "닫기",
"console_commands": "명령어",
"console_common_rpc": "일반 RPC 명령어:",
"console_completions": "자동 완성:",
"console_connected": "데몬에 연결됨",
"console_copy_all": "모두 복사",
"console_copy_selected": "복사",
"console_daemon": "데몬",
"console_daemon_error": "데몬 오류!",
"console_daemon_started": "데몬이 시작되었습니다",
"console_daemon_stopped": "데몬이 중지되었습니다",
"console_disconnected": "데몬 연결이 끊어졌습니다",
"console_errors": "오류",
"console_filter_hint": "출력 필터...",
"console_help_clear": " clear - 콘솔 지우기",
"console_help_getbalance": " getbalance - 투명 잔액 표시",
"console_help_getblockcount": " getblockcount - 현재 블록 높이 표시",
"console_help_getinfo": " getinfo - 노드 정보 표시",
"console_help_getmininginfo": " getmininginfo - 채굴 상태 표시",
"console_help_getpeerinfo": " getpeerinfo - 연결된 피어 표시",
"console_help_gettotalbalance": " gettotalbalance - 총 잔액 표시",
"console_help_help": " help - 도움말 표시",
"console_help_setgenerate": " setgenerate - 채굴 제어",
"console_help_stop": " stop - 데몬 중지",
"console_line_count": "%zu줄",
"console_new_lines": "%d 새 줄",
"console_no_daemon": "데몬 없음",
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
"console_rpc_reference": "RPC 명령어 참조",
"console_scanline": "콘솔 스캔라인",
"console_search_commands": "명령어 검색...",
"console_select_all": "모두 선택",
"console_show_daemon_output": "데몬 출력 표시",
"console_show_errors_only": "오류만 표시",
"console_show_rpc_ref": "RPC 명령어 참조 표시",
"console_showing_lines": "%zu / %zu줄 표시 중",
"console_starting_node": "노드 시작 중...",
"console_status_error": "오류",
"console_status_running": "실행 중",
"console_status_starting": "시작 중",
"console_status_stopped": "중지됨",
"console_status_stopping": "중지 중",
"console_status_unknown": "알 수 없음",
"console_tab_completion": "Tab으로 자동 완성",
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
"console_zoom_in": "확대",
"console_zoom_out": "축소",
"copy": "복사",
"copy_address": "전체 주소 복사",
"copy_error": "오류 복사",
"copy_to_clipboard": "클립보드에 복사",
"copy_txid": "TxID 복사",
"copy_uri": "URI 복사",
"current_price": "현재 가격",
"custom_fees": "사용자 정의 수수료",
"dark": "다크",
"date": "날짜",
"date_label": "날짜:",
"delete": "삭제",
"difficulty": "난이도",
"disconnected": "연결 끊김",
"dismiss": "닫기",
"display": "디스플레이",
"dragonx_green": "DragonX(그린)",
"edit": "편집",
"error": "오류",
"est_time_to_block": "예상 블록 시간",
"exit": "종료",
"explorer": "탐색기",
"export": "내보내기",
"export_csv": "CSV 내보내기",
"export_keys_btn": "키 내보내기",
"export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.",
"export_keys_include_t": "T 주소 포함 (투명)",
"export_keys_include_z": "Z 주소 포함 (차폐)",
"export_keys_options": "내보내기 옵션:",
"export_keys_success": "키 내보내기 성공",
"export_keys_title": "모든 개인 키 내보내기",
"export_private_key": "개인 키 내보내기",
"export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.",
"export_tx_file_fail": "CSV 파일 생성 실패",
"export_tx_none": "내보낼 거래가 없습니다",
"export_tx_success": "거래 내보내기 성공",
"export_tx_title": "거래를 CSV로 내보내기",
"export_viewing_key": "조회 키 내보내기",
"failed_create_shielded": "차폐 주소 생성 실패",
"failed_create_transparent": "투명 주소 생성 실패",
"fee": "수수료",
"fee_high": "높음",
"fee_label": "수수료:",
"fee_low": "낮음",
"fee_normal": "보통",
"fetch_prices": "가격 조회",
"file": "파일",
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
"font_scale": "글꼴 크기",
"from": "보낸 곳",
"from_upper": "보낸 곳",
"full_details": "전체 세부 정보",
"general": "일반",
"go_to_receive": "수신으로 이동",
"height": "높이",
"help": "도움말",
"hide": "숨기기",
"history": "내역",
"immature_type": "미성숙",
"import": "가져오기",
"import_key_btn": "키 가져오기",
"import_key_formats": "지원되는 키 형식:",
"import_key_full_rescan": "(0 = 전체 재스캔)",
"import_key_label": "개인 키:",
"import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다",
"import_key_rescan": "가져오기 후 블록체인 재스캔",
"import_key_start_height": "시작 높이:",
"import_key_success": "키 가져오기 성공",
"import_key_t_format": "T 주소 WIF 개인 키",
"import_key_title": "개인 키 가져오기",
"import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.",
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
"import_private_key": "개인 키 가져오기...",
"invalid_address": "잘못된 주소 형식",
"ip_address": "IP 주소",
"keep": "유지",
"keep_daemon": "데몬 계속 실행",
"key_export_fetching": "지갑에서 키를 가져오는 중...",
"key_export_private_key": "개인 키:",
"key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.",
"key_export_reveal": "키 표시",
"key_export_viewing_key": "조회 키:",
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
"label": "라벨:",
"language": "언어",
"light": "라이트",
"loading": "로딩 중...",
"loading_addresses": "주소 로딩 중...",
"local_hashrate": "로컬 해시레이트",
"low_spec_mode": "저사양 모드",
"market": "시장",
"market_12h": "12시간",
"market_18h": "18시간",
"market_24h": "24시간",
"market_24h_volume": "24시간 거래량",
"market_6h": "6시간",
"market_attribution": "가격 데이터: NonKYC 제공",
"market_btc_price": "BTC 가격",
"market_cap": "시가총액",
"market_no_history": "가격 내역 없음",
"market_no_price": "가격 데이터 없음",
"market_now": "현재",
"market_pct_shielded": "%.0f%% 차폐됨",
"market_portfolio": "포트폴리오",
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
"market_refresh_price": "가격 데이터 새로고침",
"market_trade_on": "%s에서 거래",
"mature": "성숙됨",
"max": "최대",
"memo": "메모 (선택, 암호화)",
"memo_label": "메모:",
"memo_optional": "메모 (선택)",
"memo_upper": "메모",
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
"merge_funds": "자금 통합",
"merge_started": "통합 작업이 시작되었습니다",
"merge_title": "주소로 통합",
"mine_when_idle": "유휴 시 채굴",
"mined": "채굴됨",
"mined_filter": "채굴됨",
"mined_type": "채굴됨",
"mined_upper": "채굴됨",
"miner_fee": "채굴 수수료",
"mining": "채굴",
"mining_active": "활성",
"mining_address_copied": "채굴 주소가 복사되었습니다",
"mining_all_time": "전체 기간",
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
"mining_block_copied": "블록 해시가 복사되었습니다",
"mining_chart_1m_ago": "1분 전",
"mining_chart_5m_ago": "5분 전",
"mining_chart_now": "현재",
"mining_chart_start": "시작",
"mining_click": "클릭",
"mining_click_copy_address": "클릭하여 주소 복사",
"mining_click_copy_block": "클릭하여 블록 해시 복사",
"mining_click_copy_difficulty": "클릭하여 난이도 복사",
"mining_connected": "연결됨",
"mining_connecting": "연결 중...",
"mining_control": "채굴 제어",
"mining_difficulty_copied": "난이도가 복사되었습니다",
"mining_est_block": "예상 블록",
"mining_est_daily": "예상 일일 수익",
"mining_filter_all": "전체",
"mining_filter_tip_all": "모든 수익 표시",
"mining_filter_tip_pool": "풀 수익만 표시",
"mining_filter_tip_solo": "솔로 수익만 표시",
"mining_idle_off_tooltip": "유휴 채굴 활성화",
"mining_idle_on_tooltip": "유휴 채굴 비활성화",
"mining_local_hashrate": "로컬 해시레이트",
"mining_mine": "채굴",
"mining_mining_addr": "채굴 주소",
"mining_network": "네트워크",
"mining_no_blocks_yet": "아직 블록을 찾지 못했습니다",
"mining_no_payouts_yet": "아직 풀 지급이 없습니다",
"mining_no_saved_addresses": "저장된 주소 없음",
"mining_no_saved_pools": "저장된 풀 없음",
"mining_off": "채굴이 꺼져 있습니다",
"mining_on": "채굴이 켜져 있습니다",
"mining_open_in_explorer": "탐색기에서 열기",
"mining_payout_address": "지급 주소",
"mining_payout_tooltip": "채굴 보상 수신 주소",
"mining_pool": "",
"mining_pool_hashrate": "풀 해시레이트",
"mining_pool_url": "풀 URL",
"mining_recent_blocks": "최근 블록",
"mining_recent_payouts": "최근 풀 지급",
"mining_remove": "제거",
"mining_reset_defaults": "기본값으로 재설정",
"mining_save_payout_address": "지급 주소 저장",
"mining_save_pool_url": "풀 URL 저장",
"mining_saved_addresses": "저장된 주소:",
"mining_saved_pools": "저장된 풀:",
"mining_shares": "셰어",
"mining_show_chart": "차트",
"mining_show_log": "로그",
"mining_solo": "솔로",
"mining_starting": "시작 중...",
"mining_starting_tooltip": "채굴기 시작 중...",
"mining_statistics": "채굴 통계",
"mining_stop": "중지",
"mining_stop_solo_for_pool": "풀 채굴을 시작하려면 솔로 채굴을 먼저 중지하세요",
"mining_stop_solo_for_pool_settings": "풀 설정을 변경하려면 솔로 채굴을 중지하세요",
"mining_stopping": "중지 중...",
"mining_stopping_tooltip": "채굴기 중지 중...",
"mining_syncing_tooltip": "블록체인 동기화 중...",
"mining_threads": "채굴 스레드",
"mining_to_save": "저장하려면",
"mining_today": "오늘",
"mining_uptime": "가동 시간",
"mining_yesterday": "어제",
"network": "네트워크",
"network_fee": "네트워크 수수료",
"network_hashrate": "네트워크 해시레이트",
"new": "+ 새로 만들기",
"new_shielded_created": "새 차폐 주소가 생성되었습니다",
"new_t_address": "새 T 주소",
"new_t_transparent": "새 t 주소 (투명)",
"new_transparent_created": "새 투명 주소가 생성되었습니다",
"new_z_address": "새 Z 주소",
"new_z_shielded": "새 z 주소 (차폐)",
"no_addresses": "주소가 없습니다. 위의 버튼을 사용하여 생성하세요.",
"no_addresses_available": "사용 가능한 주소 없음",
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
"no_matching": "일치하는 거래가 없습니다",
"no_recent_receives": "최근 수신 내역 없음",
"no_recent_sends": "최근 전송 내역 없음",
"no_transactions": "거래 내역이 없습니다",
"node": "노드",
"node_security": "노드 및 보안",
"noise": "노이즈",
"not_connected": "데몬에 연결되지 않음...",
"not_connected_to_daemon": "데몬에 연결되지 않음",
"notes": "메모",
"notes_optional": "메모 (선택):",
"output_filename": "출력 파일명:",
"overview": "개요",
"paste": "붙여넣기",
"paste_from_clipboard": "클립보드에서 붙여넣기",
"pay_from": "보낼 곳",
"payment_request": "결제 요청",
"payment_request_copied": "결제 요청이 복사되었습니다",
"payment_uri_copied": "결제 URI가 복사되었습니다",
"peers": "피어",
"peers_avg_ping": "평균 Ping",
"peers_ban_24h": "피어 24시간 차단",
"peers_ban_score": "차단 점수: %d",
"peers_banned": "차단됨",
"peers_banned_count": "차단됨: %d",
"peers_best_block": "최고 블록",
"peers_blockchain": "블록체인",
"peers_blocks": "블록",
"peers_blocks_left": "남은 블록: %d",
"peers_clear_all_bans": "모든 차단 해제",
"peers_click_copy": "클릭하여 복사",
"peers_connected": "연결됨",
"peers_connected_count": "연결됨: %d",
"peers_copy_ip": "IP 복사",
"peers_dir_in": "수신",
"peers_dir_out": "송신",
"peers_hash_copied": "해시가 복사되었습니다",
"peers_hashrate": "해시레이트",
"peers_in_out": "수신/송신",
"peers_longest": "최장",
"peers_longest_chain": "최장 체인",
"peers_memory": "메모리",
"peers_no_banned": "차단된 피어 없음",
"peers_no_connected": "연결된 피어 없음",
"peers_no_tls": "TLS 없음",
"peers_notarized": "공증됨",
"peers_p2p_port": "P2P 포트",
"peers_protocol": "프로토콜",
"peers_received": "수신됨",
"peers_refresh": "새로고침",
"peers_refresh_tooltip": "피어 목록 새로고침",
"peers_refreshing": "새로고침 중...",
"peers_sent": "전송됨",
"peers_tt_id": "ID: %d",
"peers_tt_received": "수신: %s",
"peers_tt_sent": "전송: %s",
"peers_tt_services": "서비스: %s",
"peers_tt_start_height": "시작 높이: %d",
"peers_tt_synced": "동기화 H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "차단 해제",
"peers_upper": "피어",
"peers_version": "버전",
"pending": "대기 중",
"ping": "Ping",
"price_chart": "가격 차트",
"qr_code": "QR 코드",
"qr_failed": "QR 코드 생성 실패",
"qr_title": "QR 코드",
"qr_unavailable": "QR 사용 불가",
"receive": "수신",
"received": "수신됨",
"received_filter": "수신됨",
"received_label": "수신됨",
"received_upper": "수신됨",
"receiving_addresses": "수신 주소",
"recent_received": "최근 수신",
"recent_sends": "최근 전송",
"recipient": "수신자",
"recv_type": "수신",
"refresh": "새로고침",
"refresh_now": "지금 새로고침",
"report_bug": "버그 신고",
"request_amount": "금액 (선택):",
"request_copy_uri": "URI 복사",
"request_description": "다른 사람이 스캔하거나 복사할 수 있는 결제 요청을 생성합니다. QR 코드에는 주소와 선택적 금액/메모가 포함됩니다.",
"request_label": "라벨 (선택):",
"request_memo": "메모 (선택):",
"request_payment": "결제 요청",
"request_payment_uri": "결제 URI:",
"request_receive_address": "수신 주소:",
"request_select_address": "주소 선택...",
"request_shielded_addrs": "-- 차폐 주소 --",
"request_title": "결제 요청",
"request_transparent_addrs": "-- 투명 주소 --",
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
"rescan": "재스캔",
"reset_to_defaults": "기본값으로 재설정",
"review_send": "전송 검토",
"rpc_host": "RPC 호스트",
"rpc_pass": "비밀번호",
"rpc_port": "포트",
"rpc_user": "사용자명",
"save": "저장",
"save_settings": "설정 저장",
"save_z_transactions": "Z 거래를 거래 목록에 저장",
"search_placeholder": "검색...",
"security": "보안",
"select_address": "주소 선택...",
"select_receiving_address": "수신 주소 선택...",
"select_source_address": "보낼 주소 선택...",
"send": "전송",
"send_amount": "금액",
"send_amount_details": "금액 상세",
"send_amount_upper": "금액",
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
"send_copy_error": "오류 복사",
"send_dismiss": "닫기",
"send_error_copied": "오류가 클립보드에 복사되었습니다",
"send_error_prefix": "오류: %s",
"send_exceeds_available": "사용 가능 금액 초과 (%.8f)",
"send_fee": "수수료",
"send_fee_high": "높음",
"send_fee_low": "낮음",
"send_fee_normal": "보통",
"send_form_restored": "양식이 복원되었습니다",
"send_from_this_address": "이 주소에서 전송",
"send_go_to_receive": "수신으로 이동",
"send_keep": "유지",
"send_network_fee": "네트워크 수수료",
"send_no_balance": "잔액 없음",
"send_no_recent": "최근 전송 없음",
"send_recent_sends": "최근 전송",
"send_recipient": "수신자",
"send_select_source": "보낼 주소 선택...",
"send_sending_from": "보내는 곳",
"send_submitting": "거래 제출 중...",
"send_switch_to_receive": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
"send_to": "받는 곳",
"send_tooltip_enter_amount": "전송할 금액을 입력하세요",
"send_tooltip_exceeds_balance": "금액이 사용 가능 잔액을 초과합니다",
"send_tooltip_in_progress": "거래가 이미 진행 중입니다",
"send_tooltip_invalid_address": "유효한 수신자 주소를 입력하세요",
"send_tooltip_not_connected": "데몬에 연결되지 않음",
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
"send_total": "합계",
"send_transaction": "거래 전송",
"send_tx_failed": "거래 실패",
"send_tx_sent": "거래가 전송되었습니다!",
"send_tx_success": "거래 전송 성공!",
"send_txid_copied": "TxID가 클립보드에 복사되었습니다",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "유효한 차폐 주소",
"send_valid_transparent": "유효한 투명 주소",
"send_wallet_empty": "지갑이 비어 있습니다",
"send_yes_clear": "예, 지우기",
"sending": "거래 전송 중",
"sending_from": "보내는 곳",
"sent": "전송됨",
"sent_filter": "전송됨",
"sent_type": "전송됨",
"sent_upper": "전송됨",
"settings": "설정",
"setup_wizard": "설정 마법사",
"share": "공유",
"shield_check_status": "상태 확인",
"shield_completed": "작업이 성공적으로 완료되었습니다!",
"shield_description": "투명 주소의 코인베이스 출력을 차폐 주소로 전송하여 채굴 보상을 차폐합니다. 채굴 수입을 숨겨 프라이버시가 향상됩니다.",
"shield_from_address": "보내는 주소:",
"shield_funds": "자금 차폐",
"shield_in_progress": "작업 진행 중...",
"shield_max_utxos": "작업당 최대 UTXO 수",
"shield_merge_done": "차폐/통합이 완료되었습니다!",
"shield_select_z": "z 주소 선택...",
"shield_started": "차폐 작업이 시작되었습니다",
"shield_title": "코인베이스 보상 차폐",
"shield_to_address": "받는 주소 (차폐):",
"shield_utxo_limit": "UTXO 제한:",
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
"shielded": "차폐",
"shielded_to": "차폐 대상",
"shielded_type": "차폐",
"show": "표시",
"show_qr_code": "QR 코드 표시",
"showing_transactions": "%d\xe2\x80\x93%d / %d건의 거래 표시 중 (총: %zu)",
"simple_background": "단순 배경",
"start_mining": "채굴 시작",
"status": "상태",
"stop_external": "외부 데몬 중지",
"stop_mining": "채굴 중지",
"submitting_transaction": "거래 제출 중...",
"success": "성공",
"summary": "요약",
"syncing": "동기화 중...",
"t_addresses": "T 주소",
"test_connection": "테스트",
"theme": "테마",
"theme_effects": "테마 효과",
"time_days_ago": "%d일 전",
"time_hours_ago": "%d시간 전",
"time_minutes_ago": "%d분 전",
"time_seconds_ago": "%d초 전",
"to": "받는 곳",
"to_upper": "받는 곳",
"tools": "도구",
"total": "합계",
"transaction_id": "거래 ID",
"transaction_sent": "거래 전송 성공",
"transaction_sent_msg": "거래가 전송되었습니다!",
"transaction_url": "거래 URL",
"transactions": "거래",
"transactions_upper": "거래",
"transparent": "투명",
"tx_confirmations": "%d 확인",
"tx_details_title": "거래 상세",
"tx_from_address": "보낸 주소:",
"tx_id_label": "거래 ID:",
"tx_immature": "미성숙",
"tx_mined": "채굴됨",
"tx_received": "수신됨",
"tx_sent": "전송됨",
"tx_to_address": "받는 주소:",
"tx_view_explorer": "탐색기에서 보기",
"txs_count": "%d",
"type": "유형",
"ui_opacity": "UI 투명도",
"unban": "차단 해제",
"unconfirmed": "미확인",
"undo_clear": "지우기 취소",
"unknown": "알 수 없음",
"use_embedded_daemon": "내장 dragonxd 사용",
"use_tor": "Tor 사용",
"validate_btn": "검증",
"validate_description": "DragonX 주소를 입력하여 유효한지 그리고 이 지갑에 속하는지 확인합니다.",
"validate_invalid": "유효하지 않음",
"validate_is_mine": "이 지갑이 이 주소를 소유합니다",
"validate_not_mine": "이 지갑에 속하지 않음",
"validate_ownership": "소유자:",
"validate_results": "결과:",
"validate_shielded_type": "차폐 (z 주소)",
"validate_status": "상태:",
"validate_title": "주소 검증",
"validate_transparent_type": "투명 (t 주소)",
"validate_type": "유형:",
"validate_valid": "유효함",
"validating": "검증 중...",
"verbose_logging": "상세 로깅",
"version": "버전",
"view": "보기",
"view_details": "상세 보기",
"view_on_explorer": "탐색기에서 보기",
"waiting_for_daemon": "데몬 연결 대기 중...",
"wallet": "지갑",
"wallet_empty": "지갑이 비어 있습니다",
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
"warning": "경고",
"warning_upper": "경고!",
"website": "웹사이트",
"window_opacity": "창 투명도",
"yes_clear": "예, 지우기",
"your_addresses": "내 주소",
"z_addresses": "Z 주소",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ko.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Korean translations to {os.path.abspath(out)}")

646
scripts/gen_pt.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Portuguese (pt) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "Variação 24h",
"24h_volume": "Volume 24h",
"about": "Sobre",
"about_block_explorer": "Explorador de Blocos",
"about_block_height": "Altura do Bloco:",
"about_build_date": "Data de Compilação:",
"about_build_type": "Tipo de Build:",
"about_chain": "Chain:",
"about_connections": "Conexões:",
"about_credits": "Créditos",
"about_daemon": "Daemon:",
"about_debug": "Depuração",
"about_dragonx": "Sobre o ObsidianDragon",
"about_edition": "Edição ImGui",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "Licença",
"about_license_text": "Este software é disponibilizado sob a Licença Pública Geral GNU v3 (GPLv3). Você é livre para usar, modificar e distribuir este software sob os termos da licença.",
"about_peers_count": "%zu pares",
"about_release": "Versão",
"about_title": "Sobre o ObsidianDragon",
"about_version": "Versão:",
"about_website": "Website",
"acrylic": "Acrílico",
"add": "Adicionar",
"address": "Endereço",
"address_book_add": "Adicionar Endereço",
"address_book_add_new": "Adicionar Novo",
"address_book_added": "Endereço adicionado ao livro",
"address_book_count": "%zu endereços salvos",
"address_book_deleted": "Entrada excluída",
"address_book_edit": "Editar Endereço",
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
"address_book_exists": "Endereço já existe no livro",
"address_book_title": "Livro de Endereços",
"address_book_update_failed": "Falha na atualização - endereço pode ser duplicado",
"address_book_updated": "Endereço atualizado",
"address_copied": "Endereço copiado para a área de transferência",
"address_details": "Detalhes do Endereço",
"address_label": "Endereço:",
"address_upper": "ENDEREÇO",
"address_url": "URL do Endereço",
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
"advanced": "AVANÇADO",
"all_filter": "Todos",
"allow_custom_fees": "Permitir taxas personalizadas",
"amount": "Valor",
"amount_details": "DETALHES DO VALOR",
"amount_exceeds_balance": "Valor excede o saldo",
"amount_label": "Valor:",
"appearance": "APARÊNCIA",
"auto_shield": "Auto-blindar mineração",
"available": "Disponível",
"backup_backing_up": "Fazendo backup...",
"backup_create": "Criar Backup",
"backup_created": "Backup da carteira criado",
"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_destination": "Destino do backup:",
"backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem",
"backup_tip_multiple": "Crie múltiplos backups em diferentes locais",
"backup_tip_test": "Teste a restauração do backup periodicamente",
"backup_tips": "Dicas:",
"backup_title": "Backup da Carteira",
"backup_wallet": "Fazer Backup da Carteira...",
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
"balance": "Saldo",
"balance_layout": "Layout do Saldo",
"ban": "Banir",
"banned_peers": "Pares Banidos",
"block": "Bloco",
"block_bits": "Bits:",
"block_click_next": "Clique para ver o próximo bloco",
"block_click_prev": "Clique para ver o bloco anterior",
"block_explorer": "Explorador de Blocos",
"block_get_info": "Obter Info do Bloco",
"block_hash": "Hash do Bloco:",
"block_height": "Altura do Bloco:",
"block_info_title": "Informações do Bloco",
"block_merkle_root": "Raiz Merkle:",
"block_nav_next": "Próximo >>",
"block_nav_prev": "<< Anterior",
"block_next": "Próximo Bloco:",
"block_previous": "Bloco Anterior:",
"block_size": "Tamanho:",
"block_timestamp": "Carimbo de Data:",
"block_transactions": "Transações:",
"blockchain_syncing": "Blockchain sincronizando (%.1f%%)... Os saldos podem ser imprecisos.",
"cancel": "Cancelar",
"characters": "caracteres",
"clear": "Limpar",
"clear_all_bans": "Remover Todos os Banimentos",
"clear_form_confirm": "Limpar todos os campos do formulário?",
"clear_request": "Limpar Solicitação",
"click_copy_address": "Clique para copiar o endereço",
"click_copy_uri": "Clique para copiar a URI",
"close": "Fechar",
"conf_count": "%d conf.",
"confirm_and_send": "Confirmar & Enviar",
"confirm_send": "Confirmar Envio",
"confirm_transaction": "Confirmar Transação",
"confirmations": "Confirmações",
"confirmations_display": "%d confirmações | %s",
"confirmed": "Confirmado",
"connected": "Conectado",
"connected_peers": "Pares Conectados",
"connecting": "Conectando...",
"console": "Console",
"console_auto_scroll": "Rolagem automática",
"console_available_commands": "Comandos disponíveis:",
"console_capturing_output": "Capturando saída do daemon...",
"console_clear": "Limpar",
"console_clear_console": "Limpar Console",
"console_cleared": "Console limpo",
"console_click_commands": "Clique nos comandos acima para inseri-los",
"console_click_insert": "Clique para inserir",
"console_click_insert_params": "Clique para inserir com parâmetros",
"console_close": "Fechar",
"console_commands": "Comandos",
"console_common_rpc": "Comandos RPC comuns:",
"console_completions": "Completações:",
"console_connected": "Conectado ao daemon",
"console_copy_all": "Copiar Tudo",
"console_copy_selected": "Copiar",
"console_daemon": "Daemon",
"console_daemon_error": "Erro do daemon!",
"console_daemon_started": "Daemon iniciado",
"console_daemon_stopped": "Daemon parado",
"console_disconnected": "Desconectado do daemon",
"console_errors": "Erros",
"console_filter_hint": "Filtrar saída...",
"console_help_clear": " clear - Limpar o console",
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
"console_help_getblockcount": " getblockcount - Mostrar altura atual do bloco",
"console_help_getinfo": " getinfo - Mostrar informações do nó",
"console_help_getmininginfo": " getmininginfo - Mostrar status da mineração",
"console_help_getpeerinfo": " getpeerinfo - Mostrar pares conectados",
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
"console_help_help": " help - Mostrar esta mensagem de ajuda",
"console_help_setgenerate": " setgenerate - Controlar mineração",
"console_help_stop": " stop - Parar o daemon",
"console_line_count": "%zu linhas",
"console_new_lines": "%d novas linhas",
"console_no_daemon": "Sem daemon",
"console_not_connected": "Erro: Não conectado ao daemon",
"console_rpc_reference": "Referência de Comandos RPC",
"console_scanline": "Scanline do console",
"console_search_commands": "Pesquisar comandos...",
"console_select_all": "Selecionar Tudo",
"console_show_daemon_output": "Mostrar saída do daemon",
"console_show_errors_only": "Mostrar apenas erros",
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
"console_showing_lines": "Mostrando %zu de %zu linhas",
"console_starting_node": "Iniciando nó...",
"console_status_error": "Erro",
"console_status_running": "Em execução",
"console_status_starting": "Iniciando",
"console_status_stopped": "Parado",
"console_status_stopping": "Parando",
"console_status_unknown": "Desconhecido",
"console_tab_completion": "Tab para completar",
"console_type_help": "Digite 'help' para comandos disponíveis",
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
"console_zoom_in": "Aumentar zoom",
"console_zoom_out": "Diminuir zoom",
"copy": "Copiar",
"copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro",
"copy_to_clipboard": "Copiar para Área de Transferência",
"copy_txid": "Copiar TxID",
"copy_uri": "Copiar URI",
"current_price": "Preço Atual",
"custom_fees": "Taxas personalizadas",
"dark": "Escuro",
"date": "Data",
"date_label": "Data:",
"delete": "Excluir",
"difficulty": "Dificuldade",
"disconnected": "Desconectado",
"dismiss": "Dispensar",
"display": "Exibição",
"dragonx_green": "DragonX (Verde)",
"edit": "Editar",
"error": "Erro",
"est_time_to_block": "Tempo Est. por Bloco",
"exit": "Sair",
"explorer": "EXPLORADOR",
"export": "Exportar",
"export_csv": "Exportar CSV",
"export_keys_btn": "Exportar Chaves",
"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_z": "Incluir endereços Z (blindados)",
"export_keys_options": "Opções de exportação:",
"export_keys_success": "Chaves exportadas com sucesso",
"export_keys_title": "Exportar Todas as Chaves Privadas",
"export_private_key": "Exportar Chave Privada",
"export_tx_count": "Exportar %zu transações para arquivo CSV.",
"export_tx_file_fail": "Falha ao criar arquivo CSV",
"export_tx_none": "Nenhuma transação para exportar",
"export_tx_success": "Transações exportadas com sucesso",
"export_tx_title": "Exportar Transações para CSV",
"export_viewing_key": "Exportar Chave de Visualização",
"failed_create_shielded": "Falha ao criar endereço blindado",
"failed_create_transparent": "Falha ao criar endereço transparente",
"fee": "Taxa",
"fee_high": "Alta",
"fee_label": "Taxa:",
"fee_low": "Baixa",
"fee_normal": "Normal",
"fetch_prices": "Buscar preços",
"file": "Arquivo",
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
"font_scale": "Escala da Fonte",
"from": "De",
"from_upper": "DE",
"full_details": "Detalhes Completos",
"general": "Geral",
"go_to_receive": "Ir para Receber",
"height": "Altura",
"help": "Ajuda",
"hide": "Ocultar",
"history": "Histórico",
"immature_type": "Imaturo",
"import": "Importar",
"import_key_btn": "Importar Chave(s)",
"import_key_formats": "Formatos de chave suportados:",
"import_key_full_rescan": "(0 = rescan completo)",
"import_key_label": "Chave(s) Privada(s):",
"import_key_no_valid": "Nenhuma chave válida encontrada na entrada",
"import_key_rescan": "Reescanear blockchain após importação",
"import_key_start_height": "Altura inicial:",
"import_key_success": "Chaves importadas com sucesso",
"import_key_t_format": "Chaves privadas WIF de endereços T",
"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_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
"import_private_key": "Importar Chave Privada...",
"invalid_address": "Formato de endereço inválido",
"ip_address": "Endereço IP",
"keep": "Manter",
"keep_daemon": "Manter daemon em execução",
"key_export_fetching": "Buscando chave da carteira...",
"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_reveal": "Revelar Chave",
"key_export_viewing_key": "Chave de Visualização:",
"key_export_viewing_warning": "Esta chave de visualização permite que outros vejam suas transações recebidas e saldo, mas NÃO gastem seus fundos. Compartilhe apenas com partes confiáveis.",
"label": "Rótulo:",
"language": "Idioma",
"light": "Claro",
"loading": "Carregando...",
"loading_addresses": "Carregando endereços...",
"local_hashrate": "Hashrate Local",
"low_spec_mode": "Modo econômico",
"market": "Mercado",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"market_24h_volume": "VOLUME 24H",
"market_6h": "6h",
"market_attribution": "Dados de preço do NonKYC",
"market_btc_price": "PREÇO BTC",
"market_cap": "Capitalização",
"market_no_history": "Nenhum histórico de preços disponível",
"market_no_price": "Sem dados de preço",
"market_now": "Agora",
"market_pct_shielded": "%.0f%% Blindado",
"market_portfolio": "PORTFÓLIO",
"market_price_unavailable": "Dados de preço indisponíveis",
"market_refresh_price": "Atualizar dados de preço",
"market_trade_on": "Negociar no %s",
"mature": "Maduro",
"max": "Máx",
"memo": "Memo (opcional, criptografado)",
"memo_label": "Memo:",
"memo_optional": "MEMO (OPCIONAL)",
"memo_upper": "MEMO",
"memo_z_only": "Nota: Memos só estão disponíveis ao enviar para endereços blindados (z)",
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
"merge_funds": "Fundir Fundos",
"merge_started": "Operação de fusão iniciada",
"merge_title": "Fundir para Endereço",
"mine_when_idle": "Minerar quando ocioso",
"mined": "minerado",
"mined_filter": "Minerado",
"mined_type": "Minerado",
"mined_upper": "MINERADO",
"miner_fee": "Taxa de Minerador",
"mining": "Mineração",
"mining_active": "Ativo",
"mining_address_copied": "Endereço de mineração copiado",
"mining_all_time": "Todo o Tempo",
"mining_already_saved": "URL do pool já salva",
"mining_block_copied": "Hash do bloco copiado",
"mining_chart_1m_ago": "1m atrás",
"mining_chart_5m_ago": "5m atrás",
"mining_chart_now": "Agora",
"mining_chart_start": "Início",
"mining_click": "Clique",
"mining_click_copy_address": "Clique para copiar o endereço",
"mining_click_copy_block": "Clique para copiar o hash do bloco",
"mining_click_copy_difficulty": "Clique para copiar a dificuldade",
"mining_connected": "Conectado",
"mining_connecting": "Conectando...",
"mining_control": "Controle de Mineração",
"mining_difficulty_copied": "Dificuldade copiada",
"mining_est_block": "Bloco Est.",
"mining_est_daily": "Est. Diário",
"mining_filter_all": "Todos",
"mining_filter_tip_all": "Mostrar todos os ganhos",
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
"mining_filter_tip_solo": "Mostrar apenas ganhos solo",
"mining_idle_off_tooltip": "Ativar mineração ociosa",
"mining_idle_on_tooltip": "Desativar mineração ociosa",
"mining_local_hashrate": "Hashrate Local",
"mining_mine": "Minerar",
"mining_mining_addr": "End. Mineração",
"mining_network": "Rede",
"mining_no_blocks_yet": "Nenhum bloco encontrado ainda",
"mining_no_payouts_yet": "Nenhum pagamento de pool ainda",
"mining_no_saved_addresses": "Nenhum endereço salvo",
"mining_no_saved_pools": "Nenhum pool salvo",
"mining_off": "Mineração está DESLIGADA",
"mining_on": "Mineração está LIGADA",
"mining_open_in_explorer": "Abrir no explorador",
"mining_payout_address": "Endereço de Pagamento",
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
"mining_pool": "Pool",
"mining_pool_hashrate": "Hashrate do Pool",
"mining_pool_url": "URL do Pool",
"mining_recent_blocks": "BLOCOS RECENTES",
"mining_recent_payouts": "PAGAMENTOS DE POOL RECENTES",
"mining_remove": "Remover",
"mining_reset_defaults": "Redefinir Padrões",
"mining_save_payout_address": "Salvar endereço de pagamento",
"mining_save_pool_url": "Salvar URL do pool",
"mining_saved_addresses": "Endereços Salvos:",
"mining_saved_pools": "Pools Salvos:",
"mining_shares": "Shares",
"mining_show_chart": "Gráfico",
"mining_show_log": "Log",
"mining_solo": "Solo",
"mining_starting": "Iniciando...",
"mining_starting_tooltip": "Minerador está iniciando...",
"mining_statistics": "Estatísticas de Mineração",
"mining_stop": "Parar",
"mining_stop_solo_for_pool": "Pare a mineração solo antes de iniciar a mineração em pool",
"mining_stop_solo_for_pool_settings": "Pare a mineração solo para alterar as configurações do pool",
"mining_stopping": "Parando...",
"mining_stopping_tooltip": "Minerador está parando...",
"mining_syncing_tooltip": "Blockchain está sincronizando...",
"mining_threads": "Threads de Mineração",
"mining_to_save": "para salvar",
"mining_today": "Hoje",
"mining_uptime": "Tempo Ativo",
"mining_yesterday": "Ontem",
"network": "Rede",
"network_fee": "TAXA DA REDE",
"network_hashrate": "Hashrate da Rede",
"new": "+ Novo",
"new_shielded_created": "Novo endereço blindado criado",
"new_t_address": "Novo Endereço T",
"new_t_transparent": "Novo endereço t (Transparente)",
"new_transparent_created": "Novo endereço transparente criado",
"new_z_address": "Novo Endereço Z",
"new_z_shielded": "Novo endereço z (Blindado)",
"no_addresses": "Nenhum endereço encontrado. Crie um usando os botões acima.",
"no_addresses_available": "Nenhum endereço disponível",
"no_addresses_match": "Nenhum endereço corresponde ao filtro",
"no_addresses_with_balance": "Nenhum endereço com saldo",
"no_matching": "Nenhuma transação correspondente",
"no_recent_receives": "Nenhum recebimento recente",
"no_recent_sends": "Nenhum envio recente",
"no_transactions": "Nenhuma transação encontrada",
"node": "",
"node_security": "NÓ & SEGURANÇA",
"noise": "Ruído",
"not_connected": "Não conectado ao daemon...",
"not_connected_to_daemon": "Não conectado ao daemon",
"notes": "Notas",
"notes_optional": "Notas (opcional):",
"output_filename": "Nome do arquivo de saída:",
"overview": "Visão Geral",
"paste": "Colar",
"paste_from_clipboard": "Colar da Área de Transferência",
"pay_from": "Pagar de",
"payment_request": "SOLICITAÇÃO DE PAGAMENTO",
"payment_request_copied": "Solicitação de pagamento copiada",
"payment_uri_copied": "URI de pagamento copiada",
"peers": "Pares",
"peers_avg_ping": "Ping Médio",
"peers_ban_24h": "Banir Par 24h",
"peers_ban_score": "Score de Ban: %d",
"peers_banned": "Banidos",
"peers_banned_count": "Banidos: %d",
"peers_best_block": "Melhor Bloco",
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Blocos",
"peers_blocks_left": "%d blocos restantes",
"peers_clear_all_bans": "Remover Todos os Banimentos",
"peers_click_copy": "Clique para copiar",
"peers_connected": "Conectados",
"peers_connected_count": "Conectados: %d",
"peers_copy_ip": "Copiar IP",
"peers_dir_in": "Ent.",
"peers_dir_out": "Saí.",
"peers_hash_copied": "Hash copiado",
"peers_hashrate": "Hashrate",
"peers_in_out": "Ent./Saí.",
"peers_longest": "Mais longa",
"peers_longest_chain": "Chain Mais Longa",
"peers_memory": "Memória",
"peers_no_banned": "Nenhum par banido",
"peers_no_connected": "Nenhum par conectado",
"peers_no_tls": "Sem TLS",
"peers_notarized": "Notarizado",
"peers_p2p_port": "Porta P2P",
"peers_protocol": "Protocolo",
"peers_received": "Recebido",
"peers_refresh": "Atualizar",
"peers_refresh_tooltip": "Atualizar lista de pares",
"peers_refreshing": "Atualizando...",
"peers_sent": "Enviado",
"peers_tt_id": "ID: %d",
"peers_tt_received": "Recebido: %s",
"peers_tt_sent": "Enviado: %s",
"peers_tt_services": "Serviços: %s",
"peers_tt_start_height": "Altura Inicial: %d",
"peers_tt_synced": "Sincronizado H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "Desbanir",
"peers_upper": "PARES",
"peers_version": "Versão",
"pending": "Pendente",
"ping": "Ping",
"price_chart": "Gráfico de Preços",
"qr_code": "Código QR",
"qr_failed": "Falha ao gerar código QR",
"qr_title": "Código QR",
"qr_unavailable": "QR indisponível",
"receive": "Receber",
"received": "recebido",
"received_filter": "Recebido",
"received_label": "Recebido",
"received_upper": "RECEBIDO",
"receiving_addresses": "Seus Endereços de Recebimento",
"recent_received": "RECEBIDOS RECENTES",
"recent_sends": "ENVIOS RECENTES",
"recipient": "DESTINATÁRIO",
"recv_type": "Receb.",
"refresh": "Atualizar",
"refresh_now": "Atualizar Agora",
"report_bug": "Reportar Bug",
"request_amount": "Valor (opcional):",
"request_copy_uri": "Copiar URI",
"request_description": "Gere uma solicitação de pagamento que outros podem escanear ou copiar. O código QR contém seu endereço e valor/memo opcionais.",
"request_label": "Rótulo (opcional):",
"request_memo": "Memo (opcional):",
"request_payment": "Solicitar Pagamento",
"request_payment_uri": "URI de Pagamento:",
"request_receive_address": "Endereço de Recebimento:",
"request_select_address": "Selecionar endereço...",
"request_shielded_addrs": "-- Endereços Blindados --",
"request_title": "Solicitar Pagamento",
"request_transparent_addrs": "-- Endereços Transparentes --",
"request_uri_copied": "URI de pagamento copiada para a área de transferência",
"rescan": "Reescanear",
"reset_to_defaults": "Redefinir Padrões",
"review_send": "Revisar Envio",
"rpc_host": "Host RPC",
"rpc_pass": "Senha",
"rpc_port": "Porta",
"rpc_user": "Usuário",
"save": "Salvar",
"save_settings": "Salvar Configurações",
"save_z_transactions": "Salvar Z-tx na lista de tx",
"search_placeholder": "Pesquisar...",
"security": "SEGURANÇA",
"select_address": "Selecionar endereço...",
"select_receiving_address": "Selecionar endereço de recebimento...",
"select_source_address": "Selecionar endereço de origem...",
"send": "Enviar",
"send_amount": "Valor",
"send_amount_details": "DETALHES DO VALOR",
"send_amount_upper": "VALOR",
"send_clear_fields": "Limpar todos os campos do formulário?",
"send_copy_error": "Copiar Erro",
"send_dismiss": "Dispensar",
"send_error_copied": "Erro copiado para a área de transferência",
"send_error_prefix": "Erro: %s",
"send_exceeds_available": "Excede o disponível (%.8f)",
"send_fee": "Taxa",
"send_fee_high": "Alta",
"send_fee_low": "Baixa",
"send_fee_normal": "Normal",
"send_form_restored": "Formulário restaurado",
"send_from_this_address": "Enviar deste endereço",
"send_go_to_receive": "Ir para Receber",
"send_keep": "Manter",
"send_network_fee": "TAXA DA REDE",
"send_no_balance": "Sem saldo",
"send_no_recent": "Nenhum envio recente",
"send_recent_sends": "ENVIOS RECENTES",
"send_recipient": "DESTINATÁRIO",
"send_select_source": "Selecionar endereço de origem...",
"send_sending_from": "ENVIANDO DE",
"send_submitting": "Enviando transação...",
"send_switch_to_receive": "Mude para Receber para obter seu endereço e começar a receber fundos.",
"send_to": "Enviar para",
"send_tooltip_enter_amount": "Digite um valor para enviar",
"send_tooltip_exceeds_balance": "Valor excede o saldo disponível",
"send_tooltip_in_progress": "Transação já em andamento",
"send_tooltip_invalid_address": "Digite um endereço de destinatário válido",
"send_tooltip_not_connected": "Não conectado ao daemon",
"send_tooltip_select_source": "Selecione primeiro um endereço de origem",
"send_tooltip_syncing": "Aguarde a sincronização da blockchain",
"send_total": "Total",
"send_transaction": "Enviar Transação",
"send_tx_failed": "Transação falhou",
"send_tx_sent": "Transação enviada!",
"send_tx_success": "Transação enviada com sucesso!",
"send_txid_copied": "TxID copiado para a área de transferência",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "Endereço blindado válido",
"send_valid_transparent": "Endereço transparente válido",
"send_wallet_empty": "Sua carteira está vazia",
"send_yes_clear": "Sim, Limpar",
"sending": "Enviando transação",
"sending_from": "ENVIANDO DE",
"sent": "enviado",
"sent_filter": "Enviado",
"sent_type": "Enviado",
"sent_upper": "ENVIADO",
"settings": "Configurações",
"setup_wizard": "Assistente de Configuração",
"share": "Compartilhar",
"shield_check_status": "Verificar Status",
"shield_completed": "Operação concluída com sucesso!",
"shield_description": "Blinde suas recompensas de mineração enviando saídas coinbase de endereços transparentes para um endereço blindado. Isso melhora a privacidade ocultando sua renda de mineração.",
"shield_from_address": "Do Endereço:",
"shield_funds": "Blindar Fundos",
"shield_in_progress": "Operação em andamento...",
"shield_max_utxos": "Máx. UTXOs por operação",
"shield_merge_done": "Blindagem/fusão concluída!",
"shield_select_z": "Selecionar z-endereço...",
"shield_started": "Operação de blindagem iniciada",
"shield_title": "Blindar Recompensas Coinbase",
"shield_to_address": "Para Endereço (Blindado):",
"shield_utxo_limit": "Limite UTXO:",
"shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes",
"shielded": "Blindado",
"shielded_to": "BLINDADO PARA",
"shielded_type": "Blindado",
"show": "Mostrar",
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d\xe2\x80\x93%d de %d transações (total: %zu)",
"simple_background": "Fundo simples",
"start_mining": "Iniciar Mineração",
"status": "Status",
"stop_external": "Parar daemon externo",
"stop_mining": "Parar Mineração",
"submitting_transaction": "Enviando transação...",
"success": "Sucesso",
"summary": "Resumo",
"syncing": "Sincronizando...",
"t_addresses": "Endereços T",
"test_connection": "Testar",
"theme": "Tema",
"theme_effects": "Efeitos de tema",
"time_days_ago": "%d dias",
"time_hours_ago": "%d horas",
"time_minutes_ago": "%d minutos",
"time_seconds_ago": "%d segundos",
"to": "Para",
"to_upper": "PARA",
"tools": "FERRAMENTAS",
"total": "Total",
"transaction_id": "ID DA TRANSAÇÃO",
"transaction_sent": "Transação enviada com sucesso",
"transaction_sent_msg": "Transação enviada!",
"transaction_url": "URL da Transação",
"transactions": "Transações",
"transactions_upper": "TRANSAÇÕES",
"transparent": "Transparente",
"tx_confirmations": "%d confirmações",
"tx_details_title": "Detalhes da Transação",
"tx_from_address": "Endereço de Origem:",
"tx_id_label": "ID da Transação:",
"tx_immature": "IMATURO",
"tx_mined": "MINERADO",
"tx_received": "RECEBIDO",
"tx_sent": "ENVIADO",
"tx_to_address": "Endereço de Destino:",
"tx_view_explorer": "Ver no Explorador",
"txs_count": "%d txs",
"type": "Tipo",
"ui_opacity": "Opacidade da Interface",
"unban": "Desbanir",
"unconfirmed": "Não confirmado",
"undo_clear": "Desfazer Limpeza",
"unknown": "Desconhecido",
"use_embedded_daemon": "Usar dragonxd integrado",
"use_tor": "Usar Tor",
"validate_btn": "Validar",
"validate_description": "Digite um endereço DragonX para verificar se é válido e se pertence a esta carteira.",
"validate_invalid": "INVÁLIDO",
"validate_is_mine": "Esta carteira possui este endereço",
"validate_not_mine": "Não pertence a esta carteira",
"validate_ownership": "Propriedade:",
"validate_results": "Resultados:",
"validate_shielded_type": "Blindado (z-endereço)",
"validate_status": "Status:",
"validate_title": "Validar Endereço",
"validate_transparent_type": "Transparente (t-endereço)",
"validate_type": "Tipo:",
"validate_valid": "VÁLIDO",
"validating": "Validando...",
"verbose_logging": "Log detalhado",
"version": "Versão",
"view": "Visualizar",
"view_details": "Ver Detalhes",
"view_on_explorer": "Ver no Explorador",
"waiting_for_daemon": "Aguardando conexão com o daemon...",
"wallet": "CARTEIRA",
"wallet_empty": "Sua carteira está vazia",
"wallet_empty_hint": "Mude para Receber para obter seu endereço e começar a receber fundos.",
"warning": "Aviso",
"warning_upper": "AVISO!",
"website": "Website",
"window_opacity": "Opacidade da Janela",
"yes_clear": "Sim, Limpar",
"your_addresses": "Seus Endereços",
"z_addresses": "Endereços Z",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "pt.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Portuguese translations to {os.path.abspath(out)}")

646
scripts/gen_ru.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Russian (ru) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "Изменение за 24ч",
"24h_volume": "Объём за 24ч",
"about": "О программе",
"about_block_explorer": "Обозреватель блоков",
"about_block_height": "Высота блока:",
"about_build_date": "Дата сборки:",
"about_build_type": "Тип сборки:",
"about_chain": "Цепочка:",
"about_connections": "Подключения:",
"about_credits": "Благодарности",
"about_daemon": "Daemon:",
"about_debug": "Отладка",
"about_dragonx": "Об ObsidianDragon",
"about_edition": "Редакция ImGui",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "Лицензия",
"about_license_text": "Это программное обеспечение выпущено под лицензией GNU General Public License v3 (GPLv3). Вы можете свободно использовать, изменять и распространять это ПО в соответствии с условиями лицензии.",
"about_peers_count": "%zu узлов",
"about_release": "Релиз",
"about_title": "Об ObsidianDragon",
"about_version": "Версия:",
"about_website": "Веб-сайт",
"acrylic": "Акрил",
"add": "Добавить",
"address": "Адрес",
"address_book_add": "Добавить адрес",
"address_book_add_new": "Добавить новый",
"address_book_added": "Адрес добавлен в книгу",
"address_book_count": "%zu адресов сохранено",
"address_book_deleted": "Запись удалена",
"address_book_edit": "Редактировать адрес",
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
"address_book_exists": "Адрес уже существует в книге",
"address_book_title": "Адресная книга",
"address_book_update_failed": "Не удалось обновить — адрес может быть дубликатом",
"address_book_updated": "Адрес обновлён",
"address_copied": "Адрес скопирован в буфер обмена",
"address_details": "Детали адреса",
"address_label": "Адрес:",
"address_upper": "АДРЕС",
"address_url": "URL адреса",
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
"advanced": "РАСШИРЕННЫЕ",
"all_filter": "Все",
"allow_custom_fees": "Разрешить пользовательские комиссии",
"amount": "Сумма",
"amount_details": "ДЕТАЛИ СУММЫ",
"amount_exceeds_balance": "Сумма превышает баланс",
"amount_label": "Сумма:",
"appearance": "ВНЕШНИЙ ВИД",
"auto_shield": "Авто-экранирование майнинга",
"available": "Доступно",
"backup_backing_up": "Создание резервной копии...",
"backup_create": "Создать резервную копию",
"backup_created": "Резервная копия кошелька создана",
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
"backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.",
"backup_destination": "Место сохранения:",
"backup_tip_external": "Храните резервные копии на внешних дисках или в облаке",
"backup_tip_multiple": "Создавайте несколько копий в разных местах",
"backup_tip_test": "Периодически проверяйте восстановление из резервной копии",
"backup_tips": "Советы:",
"backup_title": "Резервное копирование кошелька",
"backup_wallet": "Создать резервную копию...",
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
"balance": "Баланс",
"balance_layout": "Макет баланса",
"ban": "Заблокировать",
"banned_peers": "Заблокированные узлы",
"block": "Блок",
"block_bits": "Биты:",
"block_click_next": "Нажмите для следующего блока",
"block_click_prev": "Нажмите для предыдущего блока",
"block_explorer": "Обозреватель блоков",
"block_get_info": "Получить информацию о блоке",
"block_hash": "Хэш блока:",
"block_height": "Высота блока:",
"block_info_title": "Информация о блоке",
"block_merkle_root": "Корень Меркла:",
"block_nav_next": "Далее >>",
"block_nav_prev": "<< Назад",
"block_next": "Следующий блок:",
"block_previous": "Предыдущий блок:",
"block_size": "Размер:",
"block_timestamp": "Временная метка:",
"block_transactions": "Транзакции:",
"blockchain_syncing": "Синхронизация блокчейна (%.1f%%)... Балансы могут быть неточными.",
"cancel": "Отмена",
"characters": "символов",
"clear": "Очистить",
"clear_all_bans": "Снять все блокировки",
"clear_form_confirm": "Очистить все поля формы?",
"clear_request": "Очистить запрос",
"click_copy_address": "Нажмите, чтобы скопировать адрес",
"click_copy_uri": "Нажмите, чтобы скопировать URI",
"close": "Закрыть",
"conf_count": "%d подтв.",
"confirm_and_send": "Подтвердить и отправить",
"confirm_send": "Подтвердить отправку",
"confirm_transaction": "Подтвердить транзакцию",
"confirmations": "Подтверждения",
"confirmations_display": "%d подтверждений | %s",
"confirmed": "Подтверждено",
"connected": "Подключено",
"connected_peers": "Подключённые узлы",
"connecting": "Подключение...",
"console": "Консоль",
"console_auto_scroll": "Авто-прокрутка",
"console_available_commands": "Доступные команды:",
"console_capturing_output": "Захват вывода daemon...",
"console_clear": "Очистить",
"console_clear_console": "Очистить консоль",
"console_cleared": "Консоль очищена",
"console_click_commands": "Нажмите на команды выше, чтобы вставить их",
"console_click_insert": "Нажмите для вставки",
"console_click_insert_params": "Нажмите для вставки с параметрами",
"console_close": "Закрыть",
"console_commands": "Команды",
"console_common_rpc": "Частые RPC-команды:",
"console_completions": "Дополнения:",
"console_connected": "Подключено к daemon",
"console_copy_all": "Копировать всё",
"console_copy_selected": "Копировать",
"console_daemon": "Daemon",
"console_daemon_error": "Ошибка daemon!",
"console_daemon_started": "Daemon запущен",
"console_daemon_stopped": "Daemon остановлен",
"console_disconnected": "Отключено от daemon",
"console_errors": "Ошибки",
"console_filter_hint": "Фильтр вывода...",
"console_help_clear": " clear - Очистить консоль",
"console_help_getbalance": " getbalance - Показать прозрачный баланс",
"console_help_getblockcount": " getblockcount - Показать текущую высоту блока",
"console_help_getinfo": " getinfo - Показать информацию об узле",
"console_help_getmininginfo": " getmininginfo - Показать статус майнинга",
"console_help_getpeerinfo": " getpeerinfo - Показать подключённые узлы",
"console_help_gettotalbalance": " gettotalbalance - Показать общий баланс",
"console_help_help": " help - Показать эту справку",
"console_help_setgenerate": " setgenerate - Управление майнингом",
"console_help_stop": " stop - Остановить daemon",
"console_line_count": "%zu строк",
"console_new_lines": "%d новых строк",
"console_no_daemon": "Нет daemon",
"console_not_connected": "Ошибка: Не подключено к daemon",
"console_rpc_reference": "Справочник RPC-команд",
"console_scanline": "Скан-линия консоли",
"console_search_commands": "Поиск команд...",
"console_select_all": "Выбрать всё",
"console_show_daemon_output": "Показать вывод daemon",
"console_show_errors_only": "Показать только ошибки",
"console_show_rpc_ref": "Показать справочник RPC-команд",
"console_showing_lines": "Показано %zu из %zu строк",
"console_starting_node": "Запуск узла...",
"console_status_error": "Ошибка",
"console_status_running": "Работает",
"console_status_starting": "Запуск",
"console_status_stopped": "Остановлен",
"console_status_stopping": "Остановка",
"console_status_unknown": "Неизвестно",
"console_tab_completion": "Tab для дополнения",
"console_type_help": "Введите 'help' для списка команд",
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
"console_zoom_in": "Увеличить",
"console_zoom_out": "Уменьшить",
"copy": "Копировать",
"copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку",
"copy_to_clipboard": "Копировать в буфер обмена",
"copy_txid": "Копировать TxID",
"copy_uri": "Копировать URI",
"current_price": "Текущая цена",
"custom_fees": "Пользовательские комиссии",
"dark": "Тёмная",
"date": "Дата",
"date_label": "Дата:",
"delete": "Удалить",
"difficulty": "Сложность",
"disconnected": "Отключено",
"dismiss": "Отклонить",
"display": "Отображение",
"dragonx_green": "DragonX (Зелёная)",
"edit": "Редактировать",
"error": "Ошибка",
"est_time_to_block": "Расч. время до блока",
"exit": "Выход",
"explorer": "ОБОЗРЕВАТЕЛЬ",
"export": "Экспорт",
"export_csv": "Экспорт в CSV",
"export_keys_btn": "Экспорт ключей",
"export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.",
"export_keys_include_t": "Включить T-адреса (прозрачные)",
"export_keys_include_z": "Включить Z-адреса (экранированные)",
"export_keys_options": "Параметры экспорта:",
"export_keys_success": "Ключи успешно экспортированы",
"export_keys_title": "Экспорт всех приватных ключей",
"export_private_key": "Экспорт приватного ключа",
"export_tx_count": "Экспортировать %zu транзакций в файл CSV.",
"export_tx_file_fail": "Не удалось создать файл CSV",
"export_tx_none": "Нет транзакций для экспорта",
"export_tx_success": "Транзакции успешно экспортированы",
"export_tx_title": "Экспорт транзакций в CSV",
"export_viewing_key": "Экспорт ключа просмотра",
"failed_create_shielded": "Не удалось создать экранированный адрес",
"failed_create_transparent": "Не удалось создать прозрачный адрес",
"fee": "Комиссия",
"fee_high": "Высокая",
"fee_label": "Комиссия:",
"fee_low": "Низкая",
"fee_normal": "Обычная",
"fetch_prices": "Получить цены",
"file": "Файл",
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
"font_scale": "Масштаб шрифта",
"from": "От",
"from_upper": "ОТ",
"full_details": "Полные детали",
"general": "Общие",
"go_to_receive": "Перейти к получению",
"height": "Высота",
"help": "Справка",
"hide": "Скрыть",
"history": "История",
"immature_type": "Незрелая",
"import": "Импорт",
"import_key_btn": "Импорт ключей",
"import_key_formats": "Поддерживаемые форматы ключей:",
"import_key_full_rescan": "(0 = полное сканирование)",
"import_key_label": "Приватный ключ(и):",
"import_key_no_valid": "В введённых данных не найдено действительных ключей",
"import_key_rescan": "Пересканировать блокчейн после импорта",
"import_key_start_height": "Начальная высота:",
"import_key_success": "Ключи успешно импортированы",
"import_key_t_format": "Приватные ключи WIF для T-адресов",
"import_key_title": "Импорт приватного ключа",
"import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.",
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
"import_private_key": "Импорт приватного ключа...",
"invalid_address": "Неверный формат адреса",
"ip_address": "IP-адрес",
"keep": "Сохранить",
"keep_daemon": "Оставить daemon работающим",
"key_export_fetching": "Получение ключа из кошелька...",
"key_export_private_key": "Приватный ключ:",
"key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.",
"key_export_reveal": "Показать ключ",
"key_export_viewing_key": "Ключ просмотра:",
"key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.",
"label": "Метка:",
"language": "Язык",
"light": "Светлая",
"loading": "Загрузка...",
"loading_addresses": "Загрузка адресов...",
"local_hashrate": "Локальный хешрейт",
"low_spec_mode": "Режим экономии",
"market": "Рынок",
"market_12h": "12ч",
"market_18h": "18ч",
"market_24h": "24ч",
"market_24h_volume": "ОБЪЁМ 24Ч",
"market_6h": "",
"market_attribution": "Данные о ценах с NonKYC",
"market_btc_price": "ЦЕНА BTC",
"market_cap": "Рыночная капитализация",
"market_no_history": "Нет истории цен",
"market_no_price": "Нет данных о ценах",
"market_now": "Сейчас",
"market_pct_shielded": "%.0f%% Экранировано",
"market_portfolio": "ПОРТФЕЛЬ",
"market_price_unavailable": "Данные о ценах недоступны",
"market_refresh_price": "Обновить данные о ценах",
"market_trade_on": "Торговать на %s",
"mature": "Зрелая",
"max": "Макс",
"memo": "Заметка (необязательно, зашифровано)",
"memo_label": "Заметка:",
"memo_optional": "ЗАМЕТКА (НЕОБЯЗАТЕЛЬНО)",
"memo_upper": "ЗАМЕТКА",
"memo_z_only": "Примечание: Заметки доступны только при отправке на экранированные (z) адреса",
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
"merge_funds": "Объединить средства",
"merge_started": "Операция объединения начата",
"merge_title": "Объединить на адрес",
"mine_when_idle": "Майнить в простое",
"mined": "добыто",
"mined_filter": "Добытые",
"mined_type": "Добытая",
"mined_upper": "ДОБЫТО",
"miner_fee": "Комиссия майнера",
"mining": "Майнинг",
"mining_active": "Активен",
"mining_address_copied": "Адрес майнинга скопирован",
"mining_all_time": "За всё время",
"mining_already_saved": "URL пула уже сохранён",
"mining_block_copied": "Хэш блока скопирован",
"mining_chart_1m_ago": "1м назад",
"mining_chart_5m_ago": "5м назад",
"mining_chart_now": "Сейчас",
"mining_chart_start": "Старт",
"mining_click": "Нажмите",
"mining_click_copy_address": "Нажмите, чтобы скопировать адрес",
"mining_click_copy_block": "Нажмите, чтобы скопировать хэш блока",
"mining_click_copy_difficulty": "Нажмите, чтобы скопировать сложность",
"mining_connected": "Подключено",
"mining_connecting": "Подключение...",
"mining_control": "Управление майнингом",
"mining_difficulty_copied": "Сложность скопирована",
"mining_est_block": "Расч. блок",
"mining_est_daily": "Расч. за день",
"mining_filter_all": "Все",
"mining_filter_tip_all": "Показать все доходы",
"mining_filter_tip_pool": "Показать только доходы пула",
"mining_filter_tip_solo": "Показать только доходы соло",
"mining_idle_off_tooltip": "Включить майнинг в простое",
"mining_idle_on_tooltip": "Отключить майнинг в простое",
"mining_local_hashrate": "Локальный хешрейт",
"mining_mine": "Майнить",
"mining_mining_addr": "Адрес майн.",
"mining_network": "Сеть",
"mining_no_blocks_yet": "Блоки пока не найдены",
"mining_no_payouts_yet": "Выплат пула пока нет",
"mining_no_saved_addresses": "Нет сохранённых адресов",
"mining_no_saved_pools": "Нет сохранённых пулов",
"mining_off": "Майнинг ВЫКЛЮЧЕН",
"mining_on": "Майнинг ВКЛЮЧЁН",
"mining_open_in_explorer": "Открыть в обозревателе",
"mining_payout_address": "Адрес выплат",
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
"mining_pool": "Пул",
"mining_pool_hashrate": "Хешрейт пула",
"mining_pool_url": "URL пула",
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
"mining_recent_payouts": "ПОСЛЕДНИЕ ВЫПЛАТЫ ПУЛА",
"mining_remove": "Удалить",
"mining_reset_defaults": "Сбросить настройки",
"mining_save_payout_address": "Сохранить адрес выплат",
"mining_save_pool_url": "Сохранить URL пула",
"mining_saved_addresses": "Сохранённые адреса:",
"mining_saved_pools": "Сохранённые пулы:",
"mining_shares": "Шары",
"mining_show_chart": "График",
"mining_show_log": "Журнал",
"mining_solo": "Соло",
"mining_starting": "Запуск...",
"mining_starting_tooltip": "Майнер запускается...",
"mining_statistics": "Статистика майнинга",
"mining_stop": "Стоп",
"mining_stop_solo_for_pool": "Остановите соло-майнинг перед запуском пул-майнинга",
"mining_stop_solo_for_pool_settings": "Остановите соло-майнинг для изменения настроек пула",
"mining_stopping": "Остановка...",
"mining_stopping_tooltip": "Майнер останавливается...",
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
"mining_threads": "Потоки майнинга",
"mining_to_save": "для сохранения",
"mining_today": "Сегодня",
"mining_uptime": "Время работы",
"mining_yesterday": "Вчера",
"network": "Сеть",
"network_fee": "СЕТЕВАЯ КОМИССИЯ",
"network_hashrate": "Хешрейт сети",
"new": "+ Новый",
"new_shielded_created": "Создан новый экранированный адрес",
"new_t_address": "Новый T-адрес",
"new_t_transparent": "Новый t-адрес (Прозрачный)",
"new_transparent_created": "Создан новый прозрачный адрес",
"new_z_address": "Новый Z-адрес",
"new_z_shielded": "Новый z-адрес (Экранированный)",
"no_addresses": "Адреса не найдены. Создайте один, используя кнопки выше.",
"no_addresses_available": "Нет доступных адресов",
"no_addresses_match": "Нет адресов, соответствующих фильтру",
"no_addresses_with_balance": "Нет адресов с балансом",
"no_matching": "Нет подходящих транзакций",
"no_recent_receives": "Нет недавних получений",
"no_recent_sends": "Нет недавних отправлений",
"no_transactions": "Транзакции не найдены",
"node": "УЗЕЛ",
"node_security": "УЗЕЛ И БЕЗОПАСНОСТЬ",
"noise": "Шум",
"not_connected": "Не подключено к daemon...",
"not_connected_to_daemon": "Не подключено к daemon",
"notes": "Заметки",
"notes_optional": "Заметки (необязательно):",
"output_filename": "Имя выходного файла:",
"overview": "Обзор",
"paste": "Вставить",
"paste_from_clipboard": "Вставить из буфера обмена",
"pay_from": "Оплатить с",
"payment_request": "ЗАПРОС НА ОПЛАТУ",
"payment_request_copied": "Запрос на оплату скопирован",
"payment_uri_copied": "URI платежа скопирован",
"peers": "Узлы",
"peers_avg_ping": "Средний пинг",
"peers_ban_24h": "Заблокировать узел на 24ч",
"peers_ban_score": "Очки блокировки: %d",
"peers_banned": "Заблокированные",
"peers_banned_count": "Заблокировано: %d",
"peers_best_block": "Лучший блок",
"peers_blockchain": "БЛОКЧЕЙН",
"peers_blocks": "Блоки",
"peers_blocks_left": "Осталось %d блоков",
"peers_clear_all_bans": "Снять все блокировки",
"peers_click_copy": "Нажмите, чтобы скопировать",
"peers_connected": "Подключено",
"peers_connected_count": "Подключено: %d",
"peers_copy_ip": "Копировать IP",
"peers_dir_in": "Вх.",
"peers_dir_out": "Исх.",
"peers_hash_copied": "Хэш скопирован",
"peers_hashrate": "Хешрейт",
"peers_in_out": "Вх./Исх.",
"peers_longest": "Длиннейшая",
"peers_longest_chain": "Длиннейшая цепь",
"peers_memory": "Память",
"peers_no_banned": "Нет заблокированных узлов",
"peers_no_connected": "Нет подключённых узлов",
"peers_no_tls": "Без TLS",
"peers_notarized": "Нотаризован",
"peers_p2p_port": "P2P-порт",
"peers_protocol": "Протокол",
"peers_received": "Получено",
"peers_refresh": "Обновить",
"peers_refresh_tooltip": "Обновить список узлов",
"peers_refreshing": "Обновление...",
"peers_sent": "Отправлено",
"peers_tt_id": "ID: %d",
"peers_tt_received": "Получено: %s",
"peers_tt_sent": "Отправлено: %s",
"peers_tt_services": "Сервисы: %s",
"peers_tt_start_height": "Начальная высота: %d",
"peers_tt_synced": "Синхронизировано В/Б: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "Разблокировать",
"peers_upper": "УЗЛЫ",
"peers_version": "Версия",
"pending": "Ожидание",
"ping": "Пинг",
"price_chart": "График цен",
"qr_code": "QR-код",
"qr_failed": "Не удалось сгенерировать QR-код",
"qr_title": "QR-код",
"qr_unavailable": "QR недоступен",
"receive": "Получить",
"received": "получено",
"received_filter": "Получено",
"received_label": "Получено",
"received_upper": "ПОЛУЧЕНО",
"receiving_addresses": "Ваши адреса для получения",
"recent_received": "НЕДАВНО ПОЛУЧЕНО",
"recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
"recipient": "ПОЛУЧАТЕЛЬ",
"recv_type": "Получ.",
"refresh": "Обновить",
"refresh_now": "Обновить сейчас",
"report_bug": "Сообщить об ошибке",
"request_amount": "Сумма (необязательно):",
"request_copy_uri": "Копировать URI",
"request_description": "Создайте запрос на оплату, который другие могут отсканировать или скопировать. QR-код содержит ваш адрес и опциональную сумму/заметку.",
"request_label": "Метка (необязательно):",
"request_memo": "Заметка (необязательно):",
"request_payment": "Запрос оплаты",
"request_payment_uri": "URI платежа:",
"request_receive_address": "Адрес получения:",
"request_select_address": "Выбрать адрес...",
"request_shielded_addrs": "-- Экранированные адреса --",
"request_title": "Запрос оплаты",
"request_transparent_addrs": "-- Прозрачные адреса --",
"request_uri_copied": "URI платежа скопирован в буфер обмена",
"rescan": "Пересканировать",
"reset_to_defaults": "Сбросить настройки",
"review_send": "Проверить отправку",
"rpc_host": "RPC-хост",
"rpc_pass": "Пароль",
"rpc_port": "Порт",
"rpc_user": "Имя пользователя",
"save": "Сохранить",
"save_settings": "Сохранить настройки",
"save_z_transactions": "Сохранять Z-tx в списке транзакций",
"search_placeholder": "Поиск...",
"security": "БЕЗОПАСНОСТЬ",
"select_address": "Выбрать адрес...",
"select_receiving_address": "Выбрать адрес получения...",
"select_source_address": "Выбрать адрес-источник...",
"send": "Отправить",
"send_amount": "Сумма",
"send_amount_details": "ДЕТАЛИ СУММЫ",
"send_amount_upper": "СУММА",
"send_clear_fields": "Очистить все поля формы?",
"send_copy_error": "Копировать ошибку",
"send_dismiss": "Отклонить",
"send_error_copied": "Ошибка скопирована в буфер обмена",
"send_error_prefix": "Ошибка: %s",
"send_exceeds_available": "Превышает доступное (%.8f)",
"send_fee": "Комиссия",
"send_fee_high": "Высокая",
"send_fee_low": "Низкая",
"send_fee_normal": "Обычная",
"send_form_restored": "Форма восстановлена",
"send_from_this_address": "Отправить с этого адреса",
"send_go_to_receive": "Перейти к получению",
"send_keep": "Сохранить",
"send_network_fee": "СЕТЕВАЯ КОМИССИЯ",
"send_no_balance": "Нет баланса",
"send_no_recent": "Нет недавних отправлений",
"send_recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
"send_recipient": "ПОЛУЧАТЕЛЬ",
"send_select_source": "Выбрать адрес-источник...",
"send_sending_from": "ОТПРАВКА С",
"send_submitting": "Отправка транзакции...",
"send_switch_to_receive": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
"send_to": "Отправить на",
"send_tooltip_enter_amount": "Введите сумму для отправки",
"send_tooltip_exceeds_balance": "Сумма превышает доступный баланс",
"send_tooltip_in_progress": "Транзакция уже выполняется",
"send_tooltip_invalid_address": "Введите действительный адрес получателя",
"send_tooltip_not_connected": "Не подключено к daemon",
"send_tooltip_select_source": "Сначала выберите адрес-источник",
"send_tooltip_syncing": "Дождитесь синхронизации блокчейна",
"send_total": "Итого",
"send_transaction": "Отправить транзакцию",
"send_tx_failed": "Транзакция не удалась",
"send_tx_sent": "Транзакция отправлена!",
"send_tx_success": "Транзакция успешно отправлена!",
"send_txid_copied": "TxID скопирован в буфер обмена",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "Действительный экранированный адрес",
"send_valid_transparent": "Действительный прозрачный адрес",
"send_wallet_empty": "Ваш кошелёк пуст",
"send_yes_clear": "Да, очистить",
"sending": "Отправка транзакции",
"sending_from": "ОТПРАВКА С",
"sent": "отправлено",
"sent_filter": "Отправлено",
"sent_type": "Отправлено",
"sent_upper": "ОТПРАВЛЕНО",
"settings": "Настройки",
"setup_wizard": "Мастер настройки",
"share": "Поделиться",
"shield_check_status": "Проверить статус",
"shield_completed": "Операция успешно завершена!",
"shield_description": "Экранируйте вознаграждения за майнинг, отправив coinbase-выходы с прозрачных адресов на экранированный адрес. Это улучшает конфиденциальность, скрывая ваш доход от майнинга.",
"shield_from_address": "С адреса:",
"shield_funds": "Экранировать средства",
"shield_in_progress": "Операция выполняется...",
"shield_max_utxos": "Макс. UTXO за операцию",
"shield_merge_done": "Экранирование/объединение завершено!",
"shield_select_z": "Выбрать z-адрес...",
"shield_started": "Операция экранирования начата",
"shield_title": "Экранировать вознаграждения coinbase",
"shield_to_address": "На адрес (экранированный):",
"shield_utxo_limit": "Лимит UTXO:",
"shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов",
"shielded": "Экранированный",
"shielded_to": "ЭКРАНИРОВАНО НА",
"shielded_type": "Экранированный",
"show": "Показать",
"show_qr_code": "Показать QR-код",
"showing_transactions": "Показано %d\xe2\x80\x93%d из %d транзакций (всего: %zu)",
"simple_background": "Простой фон",
"start_mining": "Начать майнинг",
"status": "Статус",
"stop_external": "Остановить внешний daemon",
"stop_mining": "Остановить майнинг",
"submitting_transaction": "Отправка транзакции...",
"success": "Успешно",
"summary": "Итоги",
"syncing": "Синхронизация...",
"t_addresses": "T-адреса",
"test_connection": "Тест",
"theme": "Тема",
"theme_effects": "Эффекты темы",
"time_days_ago": "%d дней назад",
"time_hours_ago": "%d часов назад",
"time_minutes_ago": "%d минут назад",
"time_seconds_ago": "%d секунд назад",
"to": "Кому",
"to_upper": "КОМУ",
"tools": "ИНСТРУМЕНТЫ",
"total": "Итого",
"transaction_id": "ID ТРАНЗАКЦИИ",
"transaction_sent": "Транзакция успешно отправлена",
"transaction_sent_msg": "Транзакция отправлена!",
"transaction_url": "URL транзакции",
"transactions": "Транзакции",
"transactions_upper": "ТРАНЗАКЦИИ",
"transparent": "Прозрачный",
"tx_confirmations": "%d подтверждений",
"tx_details_title": "Детали транзакции",
"tx_from_address": "Адрес отправителя:",
"tx_id_label": "ID транзакции:",
"tx_immature": "НЕЗРЕЛАЯ",
"tx_mined": "ДОБЫТА",
"tx_received": "ПОЛУЧЕНО",
"tx_sent": "ОТПРАВЛЕНО",
"tx_to_address": "Адрес получателя:",
"tx_view_explorer": "Посмотреть в обозревателе",
"txs_count": "%d тр.",
"type": "Тип",
"ui_opacity": "Прозрачность интерфейса",
"unban": "Разблокировать",
"unconfirmed": "Не подтверждено",
"undo_clear": "Отменить очистку",
"unknown": "Неизвестно",
"use_embedded_daemon": "Использовать встроенный dragonxd",
"use_tor": "Использовать Tor",
"validate_btn": "Проверить",
"validate_description": "Введите адрес DragonX, чтобы проверить его действительность и принадлежность к этому кошельку.",
"validate_invalid": "НЕДЕЙСТВИТЕЛЕН",
"validate_is_mine": "Этот кошелёк владеет этим адресом",
"validate_not_mine": "Не принадлежит этому кошельку",
"validate_ownership": "Принадлежность:",
"validate_results": "Результаты:",
"validate_shielded_type": "Экранированный (z-адрес)",
"validate_status": "Статус:",
"validate_title": "Проверить адрес",
"validate_transparent_type": "Прозрачный (t-адрес)",
"validate_type": "Тип:",
"validate_valid": "ДЕЙСТВИТЕЛЕН",
"validating": "Проверка...",
"verbose_logging": "Подробное логирование",
"version": "Версия",
"view": "Просмотр",
"view_details": "Подробнее",
"view_on_explorer": "Посмотреть в обозревателе",
"waiting_for_daemon": "Ожидание подключения к daemon...",
"wallet": "КОШЕЛЁК",
"wallet_empty": "Ваш кошелёк пуст",
"wallet_empty_hint": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
"warning": "Предупреждение",
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
"website": "Веб-сайт",
"window_opacity": "Прозрачность окна",
"yes_clear": "Да, очистить",
"your_addresses": "Ваши адреса",
"z_addresses": "Z-адреса",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ru.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Russian translations to {os.path.abspath(out)}")

646
scripts/gen_zh.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Chinese Simplified (zh) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24小时变化",
"24h_volume": "24小时交易量",
"about": "关于",
"about_block_explorer": "区块浏览器",
"about_block_height": "区块高度:",
"about_build_date": "构建日期:",
"about_build_type": "构建类型:",
"about_chain": "链:",
"about_connections": "连接数:",
"about_credits": "致谢",
"about_daemon": "守护进程:",
"about_debug": "调试",
"about_dragonx": "关于 ObsidianDragon",
"about_edition": "ImGui 版本",
"about_github": "GitHub",
"about_imgui": "ImGui",
"about_license": "许可证",
"about_license_text": "本软件根据 GNU 通用公共许可证 v3 (GPLv3) 发布。您可以根据许可证条款自由使用、修改和分发本软件。",
"about_peers_count": "%zu 个节点",
"about_release": "发布版",
"about_title": "关于 ObsidianDragon",
"about_version": "版本:",
"about_website": "网站",
"acrylic": "亚克力",
"add": "添加",
"address": "地址",
"address_book_add": "添加地址",
"address_book_add_new": "添加新地址",
"address_book_added": "地址已添加到通讯录",
"address_book_count": "已保存 %zu 个地址",
"address_book_deleted": "条目已删除",
"address_book_edit": "编辑地址",
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
"address_book_exists": "地址已存在于通讯录中",
"address_book_title": "地址簿",
"address_book_update_failed": "更新失败——地址可能重复",
"address_book_updated": "地址已更新",
"address_copied": "地址已复制到剪贴板",
"address_details": "地址详情",
"address_label": "地址:",
"address_upper": "地址",
"address_url": "地址 URL",
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
"advanced": "高级",
"all_filter": "全部",
"allow_custom_fees": "允许自定义手续费",
"amount": "金额",
"amount_details": "金额详情",
"amount_exceeds_balance": "金额超过余额",
"amount_label": "金额:",
"appearance": "外观",
"auto_shield": "自动屏蔽挖矿",
"available": "可用",
"backup_backing_up": "正在备份...",
"backup_create": "创建备份",
"backup_created": "钱包备份已创建",
"backup_data": "备份与数据",
"backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。",
"backup_destination": "备份目标:",
"backup_tip_external": "将备份存储在外部驱动器或云存储中",
"backup_tip_multiple": "在不同位置创建多个备份",
"backup_tip_test": "定期测试从备份恢复",
"backup_tips": "提示:",
"backup_title": "备份钱包",
"backup_wallet": "备份钱包...",
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
"balance": "余额",
"balance_layout": "余额布局",
"ban": "封禁",
"banned_peers": "已封禁节点",
"block": "区块",
"block_bits": "比特:",
"block_click_next": "点击查看下一个区块",
"block_click_prev": "点击查看上一个区块",
"block_explorer": "区块浏览器",
"block_get_info": "获取区块信息",
"block_hash": "区块哈希:",
"block_height": "区块高度:",
"block_info_title": "区块信息",
"block_merkle_root": "默克尔根:",
"block_nav_next": "下一个 >>",
"block_nav_prev": "<< 上一个",
"block_next": "下一个区块:",
"block_previous": "上一个区块:",
"block_size": "大小:",
"block_timestamp": "时间戳:",
"block_transactions": "交易:",
"blockchain_syncing": "区块链同步中 (%.1f%%)... 余额可能不准确。",
"cancel": "取消",
"characters": "字符",
"clear": "清除",
"clear_all_bans": "解除所有封禁",
"clear_form_confirm": "清除所有表单字段?",
"clear_request": "清除请求",
"click_copy_address": "点击复制地址",
"click_copy_uri": "点击复制 URI",
"close": "关闭",
"conf_count": "%d 确认",
"confirm_and_send": "确认并发送",
"confirm_send": "确认发送",
"confirm_transaction": "确认交易",
"confirmations": "确认数",
"confirmations_display": "%d 次确认 | %s",
"confirmed": "已确认",
"connected": "已连接",
"connected_peers": "已连接节点",
"connecting": "连接中...",
"console": "控制台",
"console_auto_scroll": "自动滚动",
"console_available_commands": "可用命令:",
"console_capturing_output": "正在捕获守护进程输出...",
"console_clear": "清除",
"console_clear_console": "清除控制台",
"console_cleared": "控制台已清除",
"console_click_commands": "点击上方命令以插入",
"console_click_insert": "点击插入",
"console_click_insert_params": "点击插入(含参数)",
"console_close": "关闭",
"console_commands": "命令",
"console_common_rpc": "常用 RPC 命令:",
"console_completions": "补全:",
"console_connected": "已连接到守护进程",
"console_copy_all": "全部复制",
"console_copy_selected": "复制",
"console_daemon": "守护进程",
"console_daemon_error": "守护进程错误!",
"console_daemon_started": "守护进程已启动",
"console_daemon_stopped": "守护进程已停止",
"console_disconnected": "已断开与守护进程的连接",
"console_errors": "错误",
"console_filter_hint": "过滤输出...",
"console_help_clear": " clear - 清除控制台",
"console_help_getbalance": " getbalance - 显示透明余额",
"console_help_getblockcount": " getblockcount - 显示当前区块高度",
"console_help_getinfo": " getinfo - 显示节点信息",
"console_help_getmininginfo": " getmininginfo - 显示挖矿状态",
"console_help_getpeerinfo": " getpeerinfo - 显示已连接节点",
"console_help_gettotalbalance": " gettotalbalance - 显示总余额",
"console_help_help": " help - 显示此帮助信息",
"console_help_setgenerate": " setgenerate - 控制挖矿",
"console_help_stop": " stop - 停止守护进程",
"console_line_count": "%zu 行",
"console_new_lines": "%d 新行",
"console_no_daemon": "无守护进程",
"console_not_connected": "错误:未连接到守护进程",
"console_rpc_reference": "RPC 命令参考",
"console_scanline": "控制台扫描线",
"console_search_commands": "搜索命令...",
"console_select_all": "全选",
"console_show_daemon_output": "显示守护进程输出",
"console_show_errors_only": "仅显示错误",
"console_show_rpc_ref": "显示 RPC 命令参考",
"console_showing_lines": "显示 %zu / %zu 行",
"console_starting_node": "正在启动节点...",
"console_status_error": "错误",
"console_status_running": "运行中",
"console_status_starting": "启动中",
"console_status_stopped": "已停止",
"console_status_stopping": "停止中",
"console_status_unknown": "未知",
"console_tab_completion": "Tab 补全",
"console_type_help": "输入 'help' 查看可用命令",
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
"console_zoom_in": "放大",
"console_zoom_out": "缩小",
"copy": "复制",
"copy_address": "复制完整地址",
"copy_error": "复制错误",
"copy_to_clipboard": "复制到剪贴板",
"copy_txid": "复制交易ID",
"copy_uri": "复制 URI",
"current_price": "当前价格",
"custom_fees": "自定义手续费",
"dark": "深色",
"date": "日期",
"date_label": "日期:",
"delete": "删除",
"difficulty": "难度",
"disconnected": "已断开",
"dismiss": "关闭",
"display": "显示",
"dragonx_green": "DragonX绿色",
"edit": "编辑",
"error": "错误",
"est_time_to_block": "预计出块时间",
"exit": "退出",
"explorer": "浏览器",
"export": "导出",
"export_csv": "导出 CSV",
"export_keys_btn": "导出密钥",
"export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。",
"export_keys_include_t": "包含 T 地址(透明)",
"export_keys_include_z": "包含 Z 地址(屏蔽)",
"export_keys_options": "导出选项:",
"export_keys_success": "密钥导出成功",
"export_keys_title": "导出所有私钥",
"export_private_key": "导出私钥",
"export_tx_count": "导出 %zu 笔交易到 CSV 文件。",
"export_tx_file_fail": "无法创建 CSV 文件",
"export_tx_none": "没有交易可导出",
"export_tx_success": "交易导出成功",
"export_tx_title": "导出交易到 CSV",
"export_viewing_key": "导出查看密钥",
"failed_create_shielded": "无法创建屏蔽地址",
"failed_create_transparent": "无法创建透明地址",
"fee": "手续费",
"fee_high": "",
"fee_label": "手续费:",
"fee_low": "",
"fee_normal": "普通",
"fetch_prices": "获取价格",
"file": "文件",
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
"font_scale": "字体大小",
"from": "",
"from_upper": "",
"full_details": "完整详情",
"general": "常规",
"go_to_receive": "前往接收",
"height": "高度",
"help": "帮助",
"hide": "隐藏",
"history": "历史",
"immature_type": "未成熟",
"import": "导入",
"import_key_btn": "导入密钥",
"import_key_formats": "支持的密钥格式:",
"import_key_full_rescan": "0 = 完整重扫)",
"import_key_label": "私钥:",
"import_key_no_valid": "输入中未找到有效密钥",
"import_key_rescan": "导入后重新扫描区块链",
"import_key_start_height": "起始高度:",
"import_key_success": "密钥导入成功",
"import_key_t_format": "T 地址 WIF 私钥",
"import_key_title": "导入私钥",
"import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。",
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
"import_private_key": "导入私钥...",
"invalid_address": "无效的地址格式",
"ip_address": "IP 地址",
"keep": "保留",
"keep_daemon": "保持守护进程运行",
"key_export_fetching": "正在从钱包获取密钥...",
"key_export_private_key": "私钥:",
"key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。",
"key_export_reveal": "显示密钥",
"key_export_viewing_key": "查看密钥:",
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
"label": "标签:",
"language": "语言",
"light": "浅色",
"loading": "加载中...",
"loading_addresses": "正在加载地址...",
"local_hashrate": "本地算力",
"low_spec_mode": "低配模式",
"market": "市场",
"market_12h": "12小时",
"market_18h": "18小时",
"market_24h": "24小时",
"market_24h_volume": "24小时交易量",
"market_6h": "6小时",
"market_attribution": "价格数据来自 NonKYC",
"market_btc_price": "BTC 价格",
"market_cap": "市值",
"market_no_history": "无价格历史",
"market_no_price": "无价格数据",
"market_now": "现在",
"market_pct_shielded": "%.0f%% 屏蔽",
"market_portfolio": "投资组合",
"market_price_unavailable": "价格数据不可用",
"market_refresh_price": "刷新价格数据",
"market_trade_on": "%s 交易",
"mature": "已成熟",
"max": "最大",
"memo": "备注(可选,加密)",
"memo_label": "备注:",
"memo_optional": "备注(可选)",
"memo_upper": "备注",
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
"merge_funds": "合并资金",
"merge_started": "合并操作已开始",
"merge_title": "合并到地址",
"mine_when_idle": "空闲时挖矿",
"mined": "已挖得",
"mined_filter": "已挖得",
"mined_type": "已挖得",
"mined_upper": "已挖得",
"miner_fee": "矿工费",
"mining": "挖矿",
"mining_active": "活跃",
"mining_address_copied": "挖矿地址已复制",
"mining_all_time": "所有时间",
"mining_already_saved": "矿池 URL 已保存",
"mining_block_copied": "区块哈希已复制",
"mining_chart_1m_ago": "1分钟前",
"mining_chart_5m_ago": "5分钟前",
"mining_chart_now": "现在",
"mining_chart_start": "开始",
"mining_click": "点击",
"mining_click_copy_address": "点击复制地址",
"mining_click_copy_block": "点击复制区块哈希",
"mining_click_copy_difficulty": "点击复制难度",
"mining_connected": "已连接",
"mining_connecting": "连接中...",
"mining_control": "挖矿控制",
"mining_difficulty_copied": "难度已复制",
"mining_est_block": "预计区块",
"mining_est_daily": "预计日收益",
"mining_filter_all": "全部",
"mining_filter_tip_all": "显示所有收益",
"mining_filter_tip_pool": "仅显示矿池收益",
"mining_filter_tip_solo": "仅显示单人收益",
"mining_idle_off_tooltip": "启用空闲挖矿",
"mining_idle_on_tooltip": "禁用空闲挖矿",
"mining_local_hashrate": "本地算力",
"mining_mine": "挖矿",
"mining_mining_addr": "挖矿地址",
"mining_network": "网络",
"mining_no_blocks_yet": "尚未找到区块",
"mining_no_payouts_yet": "尚无矿池支付",
"mining_no_saved_addresses": "没有保存的地址",
"mining_no_saved_pools": "没有保存的矿池",
"mining_off": "挖矿已关闭",
"mining_on": "挖矿已开启",
"mining_open_in_explorer": "在浏览器中打开",
"mining_payout_address": "支付地址",
"mining_payout_tooltip": "接收挖矿奖励的地址",
"mining_pool": "矿池",
"mining_pool_hashrate": "矿池算力",
"mining_pool_url": "矿池 URL",
"mining_recent_blocks": "最近区块",
"mining_recent_payouts": "最近矿池支付",
"mining_remove": "移除",
"mining_reset_defaults": "重置默认值",
"mining_save_payout_address": "保存支付地址",
"mining_save_pool_url": "保存矿池 URL",
"mining_saved_addresses": "已保存地址:",
"mining_saved_pools": "已保存矿池:",
"mining_shares": "份额",
"mining_show_chart": "图表",
"mining_show_log": "日志",
"mining_solo": "单人",
"mining_starting": "启动中...",
"mining_starting_tooltip": "矿工正在启动...",
"mining_statistics": "挖矿统计",
"mining_stop": "停止",
"mining_stop_solo_for_pool": "启动矿池挖矿前请先停止单人挖矿",
"mining_stop_solo_for_pool_settings": "请停止单人挖矿以更改矿池设置",
"mining_stopping": "停止中...",
"mining_stopping_tooltip": "矿工正在停止...",
"mining_syncing_tooltip": "区块链同步中...",
"mining_threads": "挖矿线程",
"mining_to_save": "保存",
"mining_today": "今天",
"mining_uptime": "运行时间",
"mining_yesterday": "昨天",
"network": "网络",
"network_fee": "网络手续费",
"network_hashrate": "全网算力",
"new": "+ 新建",
"new_shielded_created": "新屏蔽地址已创建",
"new_t_address": "新 T 地址",
"new_t_transparent": "新 t 地址(透明)",
"new_transparent_created": "新透明地址已创建",
"new_z_address": "新 Z 地址",
"new_z_shielded": "新 z 地址(屏蔽)",
"no_addresses": "未找到地址。请使用上方按钮创建一个。",
"no_addresses_available": "无可用地址",
"no_addresses_match": "没有匹配过滤器的地址",
"no_addresses_with_balance": "没有有余额的地址",
"no_matching": "没有匹配的交易",
"no_recent_receives": "没有最近的接收",
"no_recent_sends": "没有最近的发送",
"no_transactions": "未找到交易",
"node": "节点",
"node_security": "节点与安全",
"noise": "噪点",
"not_connected": "未连接到守护进程...",
"not_connected_to_daemon": "未连接到守护进程",
"notes": "备注",
"notes_optional": "备注(可选):",
"output_filename": "输出文件名:",
"overview": "概览",
"paste": "粘贴",
"paste_from_clipboard": "从剪贴板粘贴",
"pay_from": "付款来源",
"payment_request": "付款请求",
"payment_request_copied": "付款请求已复制",
"payment_uri_copied": "付款 URI 已复制",
"peers": "节点",
"peers_avg_ping": "平均延迟",
"peers_ban_24h": "封禁节点 24 小时",
"peers_ban_score": "封禁评分:%d",
"peers_banned": "已封禁",
"peers_banned_count": "已封禁:%d",
"peers_best_block": "最佳区块",
"peers_blockchain": "区块链",
"peers_blocks": "区块",
"peers_blocks_left": "剩余 %d 个区块",
"peers_clear_all_bans": "解除所有封禁",
"peers_click_copy": "点击复制",
"peers_connected": "已连接",
"peers_connected_count": "已连接:%d",
"peers_copy_ip": "复制 IP",
"peers_dir_in": "",
"peers_dir_out": "",
"peers_hash_copied": "哈希已复制",
"peers_hashrate": "算力",
"peers_in_out": "入/出",
"peers_longest": "最长",
"peers_longest_chain": "最长链",
"peers_memory": "内存",
"peers_no_banned": "无已封禁节点",
"peers_no_connected": "无已连接节点",
"peers_no_tls": "无 TLS",
"peers_notarized": "已公证",
"peers_p2p_port": "P2P 端口",
"peers_protocol": "协议",
"peers_received": "已接收",
"peers_refresh": "刷新",
"peers_refresh_tooltip": "刷新节点列表",
"peers_refreshing": "刷新中...",
"peers_sent": "已发送",
"peers_tt_id": "ID%d",
"peers_tt_received": "已接收:%s",
"peers_tt_sent": "已发送:%s",
"peers_tt_services": "服务:%s",
"peers_tt_start_height": "起始高度:%d",
"peers_tt_synced": "已同步 H/B%d/%d",
"peers_tt_tls_cipher": "TLS%s",
"peers_unban": "解除封禁",
"peers_upper": "节点",
"peers_version": "版本",
"pending": "待处理",
"ping": "延迟",
"price_chart": "价格图表",
"qr_code": "二维码",
"qr_failed": "无法生成二维码",
"qr_title": "二维码",
"qr_unavailable": "二维码不可用",
"receive": "接收",
"received": "已接收",
"received_filter": "已接收",
"received_label": "已接收",
"received_upper": "已接收",
"receiving_addresses": "您的接收地址",
"recent_received": "最近接收",
"recent_sends": "最近发送",
"recipient": "收款方",
"recv_type": "接收",
"refresh": "刷新",
"refresh_now": "立即刷新",
"report_bug": "报告错误",
"request_amount": "金额(可选):",
"request_copy_uri": "复制 URI",
"request_description": "生成一个付款请求,他人可以扫描或复制。二维码包含您的地址和可选的金额/备注。",
"request_label": "标签(可选):",
"request_memo": "备注(可选):",
"request_payment": "请求付款",
"request_payment_uri": "付款 URI",
"request_receive_address": "接收地址:",
"request_select_address": "选择地址...",
"request_shielded_addrs": "-- 屏蔽地址 --",
"request_title": "请求付款",
"request_transparent_addrs": "-- 透明地址 --",
"request_uri_copied": "付款 URI 已复制到剪贴板",
"rescan": "重新扫描",
"reset_to_defaults": "重置为默认值",
"review_send": "审核发送",
"rpc_host": "RPC 主机",
"rpc_pass": "密码",
"rpc_port": "端口",
"rpc_user": "用户名",
"save": "保存",
"save_settings": "保存设置",
"save_z_transactions": "将 Z 交易保存到列表",
"search_placeholder": "搜索...",
"security": "安全",
"select_address": "选择地址...",
"select_receiving_address": "选择接收地址...",
"select_source_address": "选择来源地址...",
"send": "发送",
"send_amount": "金额",
"send_amount_details": "金额详情",
"send_amount_upper": "金额",
"send_clear_fields": "清除所有表单字段?",
"send_copy_error": "复制错误",
"send_dismiss": "关闭",
"send_error_copied": "错误已复制到剪贴板",
"send_error_prefix": "错误:%s",
"send_exceeds_available": "超过可用额 (%.8f)",
"send_fee": "手续费",
"send_fee_high": "",
"send_fee_low": "",
"send_fee_normal": "普通",
"send_form_restored": "表单已恢复",
"send_from_this_address": "从此地址发送",
"send_go_to_receive": "前往接收",
"send_keep": "保留",
"send_network_fee": "网络手续费",
"send_no_balance": "无余额",
"send_no_recent": "没有最近的发送",
"send_recent_sends": "最近发送",
"send_recipient": "收款方",
"send_select_source": "选择来源地址...",
"send_sending_from": "发送来源",
"send_submitting": "正在提交交易...",
"send_switch_to_receive": "切换到接收页面获取您的地址并开始接收资金。",
"send_to": "发送至",
"send_tooltip_enter_amount": "请输入发送金额",
"send_tooltip_exceeds_balance": "金额超过可用余额",
"send_tooltip_in_progress": "交易正在进行中",
"send_tooltip_invalid_address": "请输入有效的收款地址",
"send_tooltip_not_connected": "未连接到守护进程",
"send_tooltip_select_source": "请先选择来源地址",
"send_tooltip_syncing": "请等待区块链同步",
"send_total": "合计",
"send_transaction": "发送交易",
"send_tx_failed": "交易失败",
"send_tx_sent": "交易已发送!",
"send_tx_success": "交易发送成功!",
"send_txid_copied": "交易ID 已复制到剪贴板",
"send_txid_label": "TxID%s",
"send_valid_shielded": "有效的屏蔽地址",
"send_valid_transparent": "有效的透明地址",
"send_wallet_empty": "您的钱包是空的",
"send_yes_clear": "是,清除",
"sending": "正在发送交易",
"sending_from": "发送来源",
"sent": "已发送",
"sent_filter": "已发送",
"sent_type": "已发送",
"sent_upper": "已发送",
"settings": "设置",
"setup_wizard": "设置向导",
"share": "分享",
"shield_check_status": "检查状态",
"shield_completed": "操作成功完成!",
"shield_description": "通过将透明地址的 coinbase 输出发送到屏蔽地址来屏蔽您的挖矿奖励。这可以隐藏您的挖矿收入,提高隐私性。",
"shield_from_address": "从地址:",
"shield_funds": "屏蔽资金",
"shield_in_progress": "操作进行中...",
"shield_max_utxos": "每次操作最大 UTXO 数",
"shield_merge_done": "屏蔽/合并完成!",
"shield_select_z": "选择 z 地址...",
"shield_started": "屏蔽操作已开始",
"shield_title": "屏蔽 Coinbase 奖励",
"shield_to_address": "至地址(屏蔽):",
"shield_utxo_limit": "UTXO 限制:",
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
"shielded": "屏蔽",
"shielded_to": "屏蔽至",
"shielded_type": "屏蔽",
"show": "显示",
"show_qr_code": "显示二维码",
"showing_transactions": "显示第 %d\xe2\x80\x93%d 笔,共 %d 笔交易(总计:%zu",
"simple_background": "简单背景",
"start_mining": "开始挖矿",
"status": "状态",
"stop_external": "停止外部守护进程",
"stop_mining": "停止挖矿",
"submitting_transaction": "正在提交交易...",
"success": "成功",
"summary": "摘要",
"syncing": "同步中...",
"t_addresses": "T 地址",
"test_connection": "测试",
"theme": "主题",
"theme_effects": "主题效果",
"time_days_ago": "%d 天前",
"time_hours_ago": "%d 小时前",
"time_minutes_ago": "%d 分钟前",
"time_seconds_ago": "%d 秒前",
"to": "",
"to_upper": "",
"tools": "工具",
"total": "合计",
"transaction_id": "交易 ID",
"transaction_sent": "交易发送成功",
"transaction_sent_msg": "交易已发送!",
"transaction_url": "交易 URL",
"transactions": "交易",
"transactions_upper": "交易",
"transparent": "透明",
"tx_confirmations": "%d 次确认",
"tx_details_title": "交易详情",
"tx_from_address": "发送地址:",
"tx_id_label": "交易 ID",
"tx_immature": "未成熟",
"tx_mined": "已挖得",
"tx_received": "已接收",
"tx_sent": "已发送",
"tx_to_address": "接收地址:",
"tx_view_explorer": "在浏览器中查看",
"txs_count": "%d 笔交易",
"type": "类型",
"ui_opacity": "界面透明度",
"unban": "解除封禁",
"unconfirmed": "未确认",
"undo_clear": "撤销清除",
"unknown": "未知",
"use_embedded_daemon": "使用内置 dragonxd",
"use_tor": "使用 Tor",
"validate_btn": "验证",
"validate_description": "输入一个 DragonX 地址来检查它是否有效以及是否属于此钱包。",
"validate_invalid": "无效",
"validate_is_mine": "此钱包拥有该地址",
"validate_not_mine": "不属于此钱包",
"validate_ownership": "所有权:",
"validate_results": "结果:",
"validate_shielded_type": "屏蔽z 地址)",
"validate_status": "状态:",
"validate_title": "验证地址",
"validate_transparent_type": "透明t 地址)",
"validate_type": "类型:",
"validate_valid": "有效",
"validating": "验证中...",
"verbose_logging": "详细日志",
"version": "版本",
"view": "查看",
"view_details": "查看详情",
"view_on_explorer": "在浏览器中查看",
"waiting_for_daemon": "等待守护进程连接...",
"wallet": "钱包",
"wallet_empty": "您的钱包是空的",
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
"warning": "警告",
"warning_upper": "警告!",
"website": "网站",
"window_opacity": "窗口透明度",
"yes_clear": "是,清除",
"your_addresses": "您的地址",
"z_addresses": "Z 地址",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "zh.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Chinese translations to {os.path.abspath(out)}")

View File

@@ -7,7 +7,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
VERSION="1.0.0"
VERSION="1.2.0"
# Colors for output
RED='\033[0;31m'
@@ -137,7 +137,7 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
"${SCRIPT_DIR}/../hush-arrakis-chain"
"${SCRIPT_DIR}/hush-arrakis-chain"
"$HOME/hush3/src/hush-arrakis-chain"
"$HOME/dragonx/src/hush-arrakis-chain"
)
for lpath in "${LAUNCHER_PATHS[@]}"; do
@@ -155,7 +155,7 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hushd"
"${SCRIPT_DIR}/../hushd"
"${SCRIPT_DIR}/hushd"
"$HOME/hush3/src/hushd"
"$HOME/dragonx/src/hushd"
)
for hpath in "${HUSHD_PATHS[@]}"; do
@@ -172,7 +172,7 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/dragonxd"
"${SCRIPT_DIR}/../dragonxd"
"${SCRIPT_DIR}/dragonxd"
"$HOME/hush3/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for dpath in "${DRAGONXD_PATHS[@]}"; do
@@ -189,8 +189,8 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/asmap.dat"
"${SCRIPT_DIR}/../asmap.dat"
"${SCRIPT_DIR}/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
)
for apath in "${ASMAP_PATHS[@]}"; do

View File

@@ -130,8 +130,8 @@ done
# Look for asmap.dat
ASMAP_PATHS=(
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/../asmap.dat"
"$SCRIPT_DIR/asmap.dat"
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
@@ -411,7 +411,7 @@ if [ -f "bin/ObsidianDragon.exe" ]; then
echo ""
echo -e "${GREEN}Creating distribution package...${NC}"
cd bin
DIST_NAME="DragonX-Wallet-Windows-x64"
DIST_NAME="ObsidianDragon-Windows-x64"
rm -rf "$DIST_NAME" "$DIST_NAME.zip"
mkdir -p "$DIST_NAME"
cp ObsidianDragon.exe "$DIST_NAME/"
@@ -441,7 +441,7 @@ The wallet will look for the daemon config at:
This will be auto-created on first run if the daemon is present.
For support: https://git.hush.is/hush/ObsidianDragon
For support: https://git.dragonx.is/dragonx/ObsidianDragon
READMEEOF
if command -v zip &> /dev/null; then

View File

@@ -1,414 +0,0 @@
#!/usr/bin/env bash
# ── scripts/setup.sh ────────────────────────────────────────────────────────
# DragonX Wallet — Development Environment Setup
# Copyright 2024-2026 The Hush Developers
# Released under the GPLv3
#
# Detects your OS/distro, installs build prerequisites, fetches libraries,
# and validates the environment so you can build immediately with:
#
# ./build.sh # dev build
# ./build.sh --win-release # Windows cross-compile
# ./build.sh --linux-release # Linux release + AppImage
#
# Usage:
# ./scripts/setup.sh # Interactive — install everything needed
# ./scripts/setup.sh --check # Just report what's missing, don't install
# ./scripts/setup.sh --all # Install dev + all cross-compile targets
# ./scripts/setup.sh --win # Also install Windows cross-compile deps
# ./scripts/setup.sh --mac # Also install macOS cross-compile deps
# ./scripts/setup.sh --sapling # Also download Sapling params (~51 MB)
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
miss() { echo -e " ${RED}${NC} $1"; }
skip() { echo -e " ${YELLOW}${NC} $1"; }
info() { echo -e "${GREEN}[*]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1"; }
header(){ echo -e "\n${CYAN}── $1 ──${NC}"; }
# ── Parse args ───────────────────────────────────────────────────────────────
CHECK_ONLY=false
SETUP_WIN=false
SETUP_MAC=false
SETUP_SAPLING=false
while [[ $# -gt 0 ]]; do
case $1 in
--check) CHECK_ONLY=true; shift ;;
--win) SETUP_WIN=true; shift ;;
--mac) SETUP_MAC=true; shift ;;
--sapling) SETUP_SAPLING=true; shift ;;
--all) SETUP_WIN=true; SETUP_MAC=true; SETUP_SAPLING=true; shift ;;
-h|--help)
sed -n '2,/^# ─\{10\}/{ /^# ─\{10\}/d; s/^# \?//p; }' "$0"
exit 0
;;
*) err "Unknown option: $1"; exit 1 ;;
esac
done
# ── Detect OS / distro ──────────────────────────────────────────────────────
detect_os() {
OS="$(uname -s)"
DISTRO="unknown"
PKG=""
case "$OS" in
Linux)
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "${ID:-}" in
ubuntu|debian|linuxmint|pop|elementary|zorin|neon)
DISTRO="debian"; PKG="apt" ;;
fedora|rhel|centos|rocky|alma)
DISTRO="fedora"; PKG="dnf" ;;
arch|manjaro|endeavouros|garuda)
DISTRO="arch"; PKG="pacman" ;;
opensuse*|suse*)
DISTRO="suse"; PKG="zypper" ;;
void)
DISTRO="void"; PKG="xbps" ;;
gentoo)
DISTRO="gentoo"; PKG="emerge" ;;
*)
# Fallback: check for package managers
command -v apt &>/dev/null && { DISTRO="debian"; PKG="apt"; } ||
command -v dnf &>/dev/null && { DISTRO="fedora"; PKG="dnf"; } ||
command -v pacman &>/dev/null && { DISTRO="arch"; PKG="pacman"; }
;;
esac
fi
;;
Darwin)
DISTRO="macos"
command -v brew &>/dev/null && PKG="brew"
;;
*)
err "Unsupported OS: $OS"
exit 1
;;
esac
}
# ── Package lists per distro ────────────────────────────────────────────────
# Core: minimum to do a dev build (Linux native)
pkgs_core_debian="build-essential cmake git pkg-config
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
libsodium-dev libcurl4-openssl-dev"
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
libsodium-devel libcurl-devel"
pkgs_core_arch="base-devel cmake git pkg-config
mesa libx11 libxcursor libxrandr libxinerama libxi
libxkbcommon wayland libsodium curl"
pkgs_core_macos="cmake"
# Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip"
pkgs_win_fedora="mingw64-gcc mingw64-gcc-c++ zip"
pkgs_win_arch="mingw-w64-gcc zip"
# macOS cross-compile helpers (osxcross is separate)
pkgs_mac_debian="genisoimage icnsutils"
pkgs_mac_fedora="genisoimage"
pkgs_mac_arch="cdrtools"
# ── Helpers ──────────────────────────────────────────────────────────────────
has_cmd() { command -v "$1" &>/dev/null; }
# Install packages for the detected distro
install_pkgs() {
local pkgs="$1"
local desc="$2"
if $CHECK_ONLY; then
warn "Would install ($desc): $pkgs"
return
fi
info "Installing $desc packages..."
case "$PKG" in
apt) sudo apt-get update -qq && sudo apt-get install -y $pkgs ;;
dnf) sudo dnf install -y $pkgs ;;
pacman) sudo pacman -S --needed --noconfirm $pkgs ;;
zypper) sudo zypper install -y $pkgs ;;
brew) brew install $pkgs ;;
*) err "No supported package manager found for $DISTRO"
echo "Please install manually: $pkgs"
return 1 ;;
esac
}
# Select the right package list variable
get_pkgs() {
local category="$1" # core, win, mac
local var="pkgs_${category}_${DISTRO}"
echo "${!var:-}"
}
# ── Check individual tools ──────────────────────────────────────────────────
MISSING=0
check_tool() {
local cmd="$1"
local label="${2:-$1}"
if has_cmd "$cmd"; then
ok "$label"
else
miss "$label (not found: $cmd)"
MISSING=$((MISSING + 1))
fi
}
check_file() {
local path="$1"
local label="$2"
if [[ -f "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
check_dir() {
local path="$1"
local label="$2"
if [[ -d "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════
echo -e "${BOLD}DragonX Wallet — Development Setup${NC}"
echo "═══════════════════════════════════"
detect_os
info "Detected: $OS / $DISTRO (package manager: ${PKG:-none})"
# ── 1. Core build dependencies ──────────────────────────────────────────────
header "Core Build Dependencies"
core_pkgs="$(get_pkgs core)"
if [[ -z "$core_pkgs" ]]; then
warn "No package list for $DISTRO — check README for manual instructions"
else
# Check if key tools are already present
NEED_CORE=false
has_cmd cmake && has_cmd g++ && has_cmd pkg-config || NEED_CORE=true
if $NEED_CORE; then
install_pkgs "$core_pkgs" "core build"
else
ok "Core tools already installed (cmake, g++, pkg-config)"
fi
fi
check_tool cmake "cmake"
check_tool g++ "g++ (C++ compiler)"
check_tool git "git"
check_tool make "make"
# ── 2. libsodium ────────────────────────────────────────────────────────────
header "libsodium"
SODIUM_OK=false
# Check system libsodium
if pkg-config --exists libsodium 2>/dev/null; then
ok "libsodium (system, $(pkg-config --modversion libsodium))"
SODIUM_OK=true
elif [[ -f "$PROJECT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
ok "libsodium (local build)"
SODIUM_OK=true
else
miss "libsodium not found"
if ! $CHECK_ONLY; then
info "Building libsodium from source..."
"$SCRIPT_DIR/fetch-libsodium.sh" && SODIUM_OK=true
fi
fi
# ── 3. Windows cross-compile (optional) ─────────────────────────────────────
header "Windows Cross-Compile"
if $SETUP_WIN; then
win_pkgs="$(get_pkgs win)"
if [[ -n "$win_pkgs" ]]; then
install_pkgs "$win_pkgs" "Windows cross-compile"
fi
# Set posix thread model if available
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
if ! $CHECK_ONLY; then
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
fi
fi
# Fetch libsodium for Windows
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
info "Building libsodium for Windows target..."
"$SCRIPT_DIR/fetch-libsodium.sh" --win
else
miss "libsodium-win (not built yet)"
fi
else
ok "libsodium-win"
fi
fi
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "mingw-w64 ($(x86_64-w64-mingw32-g++-posix --version 2>/dev/null | head -1 || x86_64-w64-mingw32-g++ --version 2>/dev/null | head -1))"
else
if $SETUP_WIN; then
miss "mingw-w64"
else
skip "mingw-w64 (use --win to install)"
fi
fi
# ── 4. macOS cross-compile (optional) ───────────────────────────────────────
header "macOS Cross-Compile"
if $SETUP_MAC; then
mac_pkgs="$(get_pkgs mac)"
if [[ -n "$mac_pkgs" ]]; then
install_pkgs "$mac_pkgs" "macOS cross-compile helpers"
fi
# Fetch libsodium for macOS
if [[ ! -f "$PROJECT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
info "Building libsodium for macOS target..."
"$SCRIPT_DIR/fetch-libsodium.sh" --mac
else
miss "libsodium-mac (not built yet)"
fi
else
ok "libsodium-mac"
fi
fi
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
ok "osxcross"
else
if $SETUP_MAC; then
miss "osxcross (must be set up manually — see README)"
else
skip "osxcross (use --mac to set up macOS deps)"
fi
fi
# ── 5. Sapling parameters ───────────────────────────────────────────────────
header "Sapling Parameters"
SAPLING_DIR=""
for d in "$HOME/.zcash-params" "$HOME/.hush-params"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
# Also check project-local locations
if [[ -z "$SAPLING_DIR" ]]; then
for d in "$PROJECT_DIR/prebuilt-binaries/dragonxd-linux" "$PROJECT_DIR/prebuilt-binaries/dragonxd-win"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
fi
if [[ -n "$SAPLING_DIR" ]]; then
ok "sapling-spend.params ($(du -h "$SAPLING_DIR/sapling-spend.params" | cut -f1))"
ok "sapling-output.params ($(du -h "$SAPLING_DIR/sapling-output.params" | cut -f1))"
elif $SETUP_SAPLING; then
if ! $CHECK_ONLY; then
info "Downloading Sapling parameters (~51 MB)..."
PARAMS_DIR="$HOME/.zcash-params"
mkdir -p "$PARAMS_DIR"
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
ok "Downloaded sapling-spend.params"
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
ok "Downloaded sapling-output.params"
fi
else
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
fi
# ── 6. Binary directories ───────────────────────────────────────────────────
header "Binary Directories"
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
if [[ -d "$dir" ]]; then
# Count actual files (not .gitkeep)
count=$(find "$dir" -maxdepth 1 -type f ! -name '.gitkeep' | wc -l)
if [[ $count -gt 0 ]]; then
ok "prebuilt-binaries/$platform/ ($count files)"
else
skip "prebuilt-binaries/$platform/ (empty — place binaries here)"
fi
else
if ! $CHECK_ONLY; then
mkdir -p "$dir"
touch "$dir/.gitkeep"
ok "Created prebuilt-binaries/$platform/"
else
miss "prebuilt-binaries/$platform/"
fi
fi
done
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
if [[ $MISSING -eq 0 ]]; then
echo -e "${GREEN}${BOLD} Setup complete — ready to build!${NC}"
echo ""
echo " Quick start:"
echo " ./build.sh # Dev build"
echo " ./build.sh --linux-release # Linux release + AppImage"
echo " ./build.sh --win-release # Windows cross-compile"
else
echo -e "${YELLOW}${BOLD} $MISSING item(s) still need attention${NC}"
if $CHECK_ONLY; then
echo ""
echo " Run without --check to install automatically:"
echo " ./scripts/setup.sh"
echo " ./scripts/setup.sh --all # Include cross-compile + Sapling"
fi
fi
echo "═══════════════════════════════════════════"

67
scripts/sign-daemon-release.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
#
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset.
#
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
# (the same flow the wallet's unit tests verify for the miner updater).
#
# Usage:
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
#
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
set -euo pipefail
die() { echo "error: $*" >&2; exit 1; }
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
cmd="${1:-}"; shift || true
case "$cmd" in
keygen)
prefix="${1:-dragonx-daemon}"
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
chmod 600 "$prefix.ed25519.key"
pub="$(pubkey_b64 "$prefix.ed25519.key")"
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
echo "public key : $prefix.ed25519.pub.b64"
echo
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
echo " $pub"
;;
pubkey)
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
pubkey_b64 "$1"
;;
sign)
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
key="$1"; shift
[ -f "$key" ] || die "no such key: $key"
for f in "$@"; do
[ -f "$f" ] || die "no such file: $f"
raw="$(mktemp)"
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
openssl base64 -A -in "$raw" > "$f.sig"
printf '\n' >> "$f.sig"
rm -f "$raw"
echo "signed: $f -> $f.sig"
done
echo "Upload each .sig as a release asset next to its archive."
;;
*)
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
;;
esac

66
scripts/sign-xmrig-release.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Sign DRG-XMRig release archives for the wallet's in-app updater (opt-in ed25519 signatures).
#
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
# key pinned in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64). For each archive
# <name>.zip this produces <name>.zip.sig holding the base64 of the raw 64-byte ed25519 signature —
# upload that .sig next to the .zip as a release asset.
#
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
# (verified by the wallet's unit tests + an interop check).
#
# Usage:
# scripts/sign-xmrig-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
# scripts/sign-xmrig-release.sh pubkey <secret.key> # print the base64 public key to pin
# scripts/sign-xmrig-release.sh sign <secret.key> <file>...# -> <file>.sig per file
#
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
# kXmrigSignaturePublicKeyBase64 in src/util/xmrig_updater.h.
set -euo pipefail
die() { echo "error: $*" >&2; exit 1; }
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
cmd="${1:-}"; shift || true
case "$cmd" in
keygen)
prefix="${1:-drg-xmrig}"
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
chmod 600 "$prefix.ed25519.key"
pub="$(pubkey_b64 "$prefix.ed25519.key")"
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
echo "public key : $prefix.ed25519.pub.b64"
echo
echo "Pin this in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64):"
echo " $pub"
;;
pubkey)
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
pubkey_b64 "$1"
;;
sign)
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
key="$1"; shift
[ -f "$key" ] || die "no such key: $key"
for f in "$@"; do
[ -f "$f" ] || die "no such file: $f"
raw="$(mktemp)"
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
openssl base64 -A -in "$raw" > "$f.sig"
printf '\n' >> "$f.sig"
rm -f "$raw"
echo "signed: $f -> $f.sig"
done
echo "Upload each .sig as a release asset next to its archive."
;;
*)
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
;;
esac

840
setup.sh Executable file
View File

@@ -0,0 +1,840 @@
#!/usr/bin/env bash
# ── setup.sh ────────────────────────────────────────────────────────────────
# DragonX Wallet — Development Environment Setup
# Copyright 2024-2026 The Hush Developers
# Released under the GPLv3
#
# Detects your OS/distro, installs build prerequisites, fetches libraries,
# and validates the environment so you can build immediately with:
#
# ./build.sh # dev build
# ./build.sh --win-release # Windows cross-compile
# ./build.sh --linux-release # Linux release + AppImage
#
# Usage:
# ./setup.sh # Interactive — install everything needed
# ./setup.sh --check # Just report what's missing, don't install
# ./setup.sh --all # Install dev + all cross-compile targets
# ./setup.sh --win # Also install Windows cross-compile deps
# ./setup.sh --mac # Also install macOS cross-compile deps
# ./setup.sh --sapling # Also download Sapling params (~51 MB)
# ./setup.sh -j6 # Use 6 threads for building
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Parallel builds ─────────────────────────────────────────────────────────
# Default to 1 core; use -jN to increase (e.g. -j6)
NPROC=1
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
miss() { echo -e " ${RED}${NC} $1"; }
skip() { echo -e " ${YELLOW}${NC} $1"; }
info() { echo -e "${GREEN}[*]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1"; }
header(){ echo -e "\n${CYAN}── $1 ──${NC}"; }
# ── Parse args ───────────────────────────────────────────────────────────────
CHECK_ONLY=false
SETUP_WIN=false
SETUP_MAC=false
SETUP_SAPLING=false
while [[ $# -gt 0 ]]; do
case $1 in
--check) CHECK_ONLY=true; shift ;;
--win) SETUP_WIN=true; shift ;;
--mac) SETUP_MAC=true; shift ;;
--sapling) SETUP_SAPLING=true; shift ;;
--all) SETUP_WIN=true; SETUP_MAC=true; SETUP_SAPLING=true; shift ;;
-j*) NPROC="${1#-j}"; shift ;;
-h|--help)
sed -n '2,/^# ─\{10\}/{ /^# ─\{10\}/d; s/^# \?//p; }' "$0"
exit 0
;;
*) err "Unknown option: $1"; exit 1 ;;
esac
done
# Apply parallel build flag (after arg parsing so -jN override takes effect)
# NOTE: We do NOT export MAKEFLAGS globally — that interferes with autotools
# builds (dragonx daemon). Instead, -j is passed explicitly where needed.
# ── Detect OS / distro ──────────────────────────────────────────────────────
detect_os() {
OS="$(uname -s)"
DISTRO="unknown"
PKG=""
case "$OS" in
Linux)
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "${ID:-}" in
ubuntu|debian|linuxmint|pop|elementary|zorin|neon)
DISTRO="debian"; PKG="apt" ;;
fedora|rhel|centos|rocky|alma)
DISTRO="fedora"; PKG="dnf" ;;
arch|manjaro|endeavouros|garuda)
DISTRO="arch"; PKG="pacman" ;;
opensuse*|suse*)
DISTRO="suse"; PKG="zypper" ;;
void)
DISTRO="void"; PKG="xbps" ;;
gentoo)
DISTRO="gentoo"; PKG="emerge" ;;
*)
# Fallback: check for package managers
command -v apt &>/dev/null && { DISTRO="debian"; PKG="apt"; } ||
command -v dnf &>/dev/null && { DISTRO="fedora"; PKG="dnf"; } ||
command -v pacman &>/dev/null && { DISTRO="arch"; PKG="pacman"; } ||
true
;;
esac
fi
;;
Darwin)
DISTRO="macos"
if command -v brew &>/dev/null; then PKG="brew"; fi
;;
*)
err "Unsupported OS: $OS"
exit 1
;;
esac
}
# ── Package lists per distro ────────────────────────────────────────────────
# Core: minimum to do a dev build (Linux native)
pkgs_core_debian="build-essential cmake git pkg-config
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
libsodium-dev libcurl4-openssl-dev
autoconf automake libtool wget python3 xxd"
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
libsodium-devel libcurl-devel
autoconf automake libtool wget python3 vim-common"
pkgs_core_arch="base-devel cmake git pkg-config
mesa libx11 libxcursor libxrandr libxinerama libxi
libxkbcommon wayland libsodium curl
autoconf automake libtool wget python xxd"
pkgs_core_macos="cmake python xxd"
# Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip"
pkgs_win_fedora="mingw64-gcc mingw64-gcc-c++ zip"
pkgs_win_arch="mingw-w64-gcc zip"
# macOS cross-compile helpers (osxcross is separate)
pkgs_mac_debian="genisoimage icnsutils"
pkgs_mac_fedora="genisoimage"
pkgs_mac_arch="cdrtools"
# ── Helpers ──────────────────────────────────────────────────────────────────
has_cmd() { command -v "$1" &>/dev/null; }
# Install packages for the detected distro
install_pkgs() {
local pkgs="$1"
local desc="$2"
if $CHECK_ONLY; then
warn "Would install ($desc): $pkgs"
return
fi
info "Installing $desc packages..."
case "$PKG" in
apt) sudo apt-get update -qq && sudo apt-get install -y $pkgs ;;
dnf) sudo dnf install -y $pkgs ;;
pacman)
# Try pacman first; fall back to AUR helper for packages not in official repos
if ! sudo pacman -S --needed --noconfirm $pkgs 2>/dev/null; then
if command -v yay &>/dev/null; then
yay -S --needed --noconfirm $pkgs
elif command -v paru &>/dev/null; then
paru -S --needed --noconfirm $pkgs
else
err "pacman failed and no AUR helper (yay/paru) found"
echo "Some packages may be in the AUR. Install yay or paru, then retry."
return 1
fi
fi
;;
zypper) sudo zypper install -y $pkgs ;;
brew) brew install $pkgs ;;
*) err "No supported package manager found for $DISTRO"
echo "Please install manually: $pkgs"
return 1 ;;
esac
}
# Select the right package list variable
get_pkgs() {
local category="$1" # core, win, mac
local var="pkgs_${category}_${DISTRO}"
echo "${!var:-}"
}
# ── Check individual tools ──────────────────────────────────────────────────
MISSING=0
check_tool() {
local cmd="$1"
local label="${2:-$1}"
if has_cmd "$cmd"; then
ok "$label"
else
miss "$label (not found: $cmd)"
MISSING=$((MISSING + 1))
fi
}
check_file() {
local path="$1"
local label="$2"
if [[ -f "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
check_dir() {
local path="$1"
local label="$2"
if [[ -d "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════
echo -e "${BOLD}DragonX Wallet — Development Setup${NC}"
echo "═══════════════════════════════════"
detect_os
info "Detected: $OS / $DISTRO (package manager: ${PKG:-none})"
# ── 1. Core build dependencies ──────────────────────────────────────────────
header "Core Build Dependencies"
core_pkgs="$(get_pkgs core)"
if [[ -z "$core_pkgs" ]]; then
warn "No package list for $DISTRO — check README for manual instructions"
else
# Check if key tools are already present
NEED_CORE=false
has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
if $NEED_CORE; then
install_pkgs "$core_pkgs" "core build"
else
ok "Core tools already installed (cmake, g++, pkg-config)"
fi
fi
check_tool cmake "cmake"
check_tool g++ "g++ (C++ compiler)"
check_tool git "git"
check_tool make "make"
check_tool python3 "python3 (theme expansion)"
check_tool xxd "xxd (embedded language headers)"
# ── 2. libsodium ────────────────────────────────────────────────────────────
header "libsodium"
SODIUM_OK=false
# Check system libsodium
if pkg-config --exists libsodium 2>/dev/null; then
ok "libsodium (system, $(pkg-config --modversion libsodium))"
SODIUM_OK=true
elif [[ -f "$PROJECT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
ok "libsodium (local build)"
SODIUM_OK=true
else
miss "libsodium not found"
if ! $CHECK_ONLY; then
info "Building libsodium from source..."
"$PROJECT_DIR/scripts/fetch-libsodium.sh" && SODIUM_OK=true
fi
fi
# ── 3. Windows cross-compile (optional) ─────────────────────────────────────
header "Windows Cross-Compile"
if $SETUP_WIN; then
win_pkgs="$(get_pkgs win)"
if [[ -n "$win_pkgs" ]]; then
install_pkgs "$win_pkgs" "Windows cross-compile"
fi
# Set posix thread model if available
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
if ! $CHECK_ONLY; then
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
fi
fi
# Fetch libsodium for Windows
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
info "Building libsodium for Windows target..."
"$PROJECT_DIR/scripts/fetch-libsodium.sh" --win
else
miss "libsodium-win (not built yet)"
fi
else
ok "libsodium-win"
fi
fi
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "mingw-w64 ($(x86_64-w64-mingw32-g++-posix --version 2>/dev/null | head -1 || x86_64-w64-mingw32-g++ --version 2>/dev/null | head -1))"
else
if $SETUP_WIN; then
miss "mingw-w64"
else
skip "mingw-w64 (use --win to install)"
fi
fi
# ── 4. macOS cross-compile (optional) ───────────────────────────────────────
header "macOS Cross-Compile"
if $SETUP_MAC; then
mac_pkgs="$(get_pkgs mac)"
if [[ -n "$mac_pkgs" ]]; then
install_pkgs "$mac_pkgs" "macOS cross-compile helpers"
fi
# Fetch libsodium for macOS
if [[ ! -f "$PROJECT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
# Requires osxcross — skip gracefully if not available
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
info "Building libsodium for macOS target..."
"$PROJECT_DIR/scripts/fetch-libsodium.sh" --mac || warn "libsodium-mac build failed"
else
skip "libsodium-mac (requires osxcross — see README)"
fi
else
miss "libsodium-mac (not built yet)"
fi
else
ok "libsodium-mac"
fi
fi
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
ok "osxcross"
else
if $SETUP_MAC; then
miss "osxcross (must be set up manually — see README)"
else
skip "osxcross (use --mac to set up macOS deps)"
fi
fi
# ── 5. Sapling parameters ───────────────────────────────────────────────────
header "Sapling Parameters"
SAPLING_DIR=""
for d in "$HOME/.zcash-params" "$HOME/.hush-params"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
# Also check project-local locations
if [[ -z "$SAPLING_DIR" ]]; then
for d in "$PROJECT_DIR/prebuilt-binaries/dragonxd-linux" "$PROJECT_DIR/prebuilt-binaries/dragonxd-win"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
fi
if [[ -n "$SAPLING_DIR" ]]; then
ok "sapling-spend.params ($(du -h "$SAPLING_DIR/sapling-spend.params" | cut -f1))"
ok "sapling-output.params ($(du -h "$SAPLING_DIR/sapling-output.params" | cut -f1))"
elif $SETUP_SAPLING; then
if ! $CHECK_ONLY; then
info "Downloading Sapling parameters (~51 MB)..."
PARAMS_DIR="$HOME/.zcash-params"
mkdir -p "$PARAMS_DIR"
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
ok "Downloaded sapling-spend.params"
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
ok "Downloaded sapling-output.params"
fi
else
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
fi
# ── 6. DragonX daemon ────────────────────────────────────────────────────────
header "DragonX Daemon"
DRAGONX_SRC="$PROJECT_DIR/external/dragonx"
DRAGONXD_LINUX="$PROJECT_DIR/prebuilt-binaries/dragonxd-linux"
DRAGONXD_WIN="$PROJECT_DIR/prebuilt-binaries/dragonxd-win"
DRAGONXD_MAC="$PROJECT_DIR/prebuilt-binaries/dragonxd-mac"
DAEMON_BINS="dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx"
DAEMON_DATA="asmap.dat sapling-spend.params sapling-output.params"
# Helper: clone / update dragonx source
clone_dragonx_if_needed() {
if [[ ! -d "$DRAGONX_SRC" ]]; then
info "Cloning dragonx..."
git clone https://git.dragonx.is/DragonX/dragonx.git "$DRAGONX_SRC"
else
ok "dragonx source already present"
info "Switching to dragonx branch and pulling latest..."
(cd "$DRAGONX_SRC" && git checkout dragonx 2>/dev/null && git pull --ff-only 2>/dev/null || true)
fi
}
# Helper: copy data files (asmap, sapling params) into a destination dir
copy_daemon_data() {
local dest="$1"
for p in "$DRAGONX_SRC/asmap.dat" "$DRAGONX_SRC/contrib/asmap/asmap.dat"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/asmap.dat"
ok " Installed asmap.dat"
break
fi
done
for p in sapling-spend.params sapling-output.params; do
if [[ -f "$DRAGONX_SRC/$p" ]]; then
cp "$DRAGONX_SRC/$p" "$dest/"
ok " Installed $p"
fi
done
}
# ── Stale-daemon guard ───────────────────────────────────────────────────────
# A prebuilt daemon binary is only rebuilt on its platform's flag (--win/--mac),
# and build.sh merely BUNDLES whatever binary already exists — so a daemon left
# over from an old source revision silently ships in the wallet (e.g. the Network
# tab once reported v1.0.1 while the source was v1.0.2). These helpers compare the
# version baked into a prebuilt binary against the dragonx source and flag drift.
STALE_DAEMON=0
# MAJOR.MINOR.REVISION from the checked-out dragonx source (empty if unavailable).
dragonx_source_version() {
local hdr="$DRAGONX_SRC/src/clientversion.h"
[[ -f "$hdr" ]] || return 1
local maj min rev
maj=$(awk '/#define[ \t]+CLIENT_VERSION_MAJOR/{print $3; exit}' "$hdr")
min=$(awk '/#define[ \t]+CLIENT_VERSION_MINOR/{print $3; exit}' "$hdr")
rev=$(awk '/#define[ \t]+CLIENT_VERSION_REVISION/{print $3; exit}' "$hdr")
[[ -n "$maj" && -n "$min" && -n "$rev" ]] || return 1
printf '%s.%s.%s' "$maj" "$min" "$rev"
}
# vX.Y.Z baked into a built daemon binary (the daemon embeds "vX.Y.Z-<githash>").
# Uses grep -a so no `strings`/binutils dependency is required.
dragonx_binary_version() {
local bin="$1"
[[ -f "$bin" ]] || return 1
LC_ALL=C grep -aoE 'v[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{6,}' "$bin" 2>/dev/null \
| head -1 | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
}
# Compare a prebuilt daemon against the source; warn (and set STALE_DAEMON) on drift.
# $1 = label, $2 = binary path, $3 = rebuild flag(s) (e.g. "--win", "" for Linux)
daemon_version_guard() {
local label="$1" bin="$2" rebuild_hint="$3"
[[ -f "$bin" ]] || return 0
local src bv
src=$(dragonx_source_version) || return 0 # no source checked out → can't compare
bv=$(dragonx_binary_version "$bin")
[[ -n "$bv" ]] || return 0 # couldn't read the binary's version
if [[ "$bv" == "$src" ]]; then
ok " $label daemon is v$bv (matches dragonx source)"
else
warn " $label daemon is v$bv but dragonx source is v$src — STALE"
warn " rebuild so the wallet ships the current daemon: ./setup.sh${rebuild_hint:+ $rebuild_hint}"
STALE_DAEMON=1
fi
}
# ── Linux daemon ─────────────────────────────────────────────────────────────
# Skip Linux daemon build if only cross-compile targets were requested
# and we already have Linux binaries (avoids contaminating the build tree)
SKIP_LINUX_DAEMON=false
if ($SETUP_WIN || $SETUP_MAC) && [[ -f "$DRAGONXD_LINUX/dragonxd" || -f "$DRAGONXD_LINUX/hushd" ]]; then
SKIP_LINUX_DAEMON=true
fi
# Clean previous prebuilt daemon binaries so we always rebuild
if ! $CHECK_ONLY && ! $SKIP_LINUX_DAEMON; then
for f in $DAEMON_BINS $DAEMON_DATA; do
rm -f "$DRAGONXD_LINUX/$f" 2>/dev/null || true
done
fi
if $CHECK_ONLY; then
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
ok "dragonxd daemon (Linux) present"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
miss "dragonxd daemon (Linux) not built"
fi
elif $SKIP_LINUX_DAEMON; then
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
clone_dragonx_if_needed
info "Building dragonx daemon for Linux (this may take a while)..."
(
cd "$DRAGONX_SRC"
bash build.sh -j"$NPROC"
)
# Copy binaries to prebuilt-binaries
mkdir -p "$DRAGONXD_LINUX"
local_found=0
for f in dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx; do
if [[ -f "$DRAGONX_SRC/src/$f" ]]; then
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_LINUX/"
chmod +x "$DRAGONXD_LINUX/$f"
ok " Installed $f"
local_found=1
fi
done
copy_daemon_data "$DRAGONXD_LINUX"
if [[ $local_found -eq 0 ]]; then
err "DragonX daemon (Linux) build failed — no binaries found"
MISSING=$((MISSING + 1))
else
ok "DragonX daemon built and installed to prebuilt-binaries/dragonxd-linux/"
fi
fi
# ── Windows daemon (cross-compile, only with --win or --all) ─────────────────
if ! $SETUP_WIN; then
skip "dragonxd (Windows) — use --win to cross-compile"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
ok "dragonxd daemon (Windows) present"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
else
miss "dragonxd daemon (Windows) not built"
fi
else
clone_dragonx_if_needed
# Clean previous Windows prebuilt binaries
if ! $CHECK_ONLY; then
rm -f "$DRAGONXD_WIN"/*.exe "$DRAGONXD_WIN"/*.bat 2>/dev/null || true
for f in $DAEMON_DATA; do rm -f "$DRAGONXD_WIN/$f" 2>/dev/null || true; done
fi
info "Building dragonx daemon for Windows (cross-compile, this may take a long time)..."
(
cd "$DRAGONX_SRC"
bash build.sh --win-release -j"$NPROC"
)
# Copy binaries from release directory
mkdir -p "$DRAGONXD_WIN"
local_found=0
# Find the release subdirectory (e.g. release/dragonx-1.0.0-win64/)
WIN_RELEASE_DIR=$(find "$DRAGONX_SRC/release" -maxdepth 1 -type d -name '*win64*' 2>/dev/null | head -1)
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
if [[ -n "$WIN_RELEASE_DIR" ]] && [[ -f "$WIN_RELEASE_DIR/$f" ]]; then
cp "$WIN_RELEASE_DIR/$f" "$DRAGONXD_WIN/"
ok " Installed $f"
local_found=1
elif [[ -f "$DRAGONX_SRC/src/$f" ]]; then
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_WIN/"
ok " Installed $f (from src/)"
local_found=1
fi
done
# .bat launchers
for f in dragonxd.bat dragonx-cli.bat bootstrap-dragonx.bat; do
if [[ -n "$WIN_RELEASE_DIR" ]] && [[ -f "$WIN_RELEASE_DIR/$f" ]]; then
cp "$WIN_RELEASE_DIR/$f" "$DRAGONXD_WIN/"
ok " Installed $f"
fi
done
copy_daemon_data "$DRAGONXD_WIN"
if [[ $local_found -eq 0 ]]; then
err "DragonX daemon (Windows) build failed — no binaries found"
MISSING=$((MISSING + 1))
else
ok "DragonX daemon (Windows) built and installed to prebuilt-binaries/dragonxd-win/"
fi
fi
# ── macOS daemon (only with --mac or --all) ──────────────────────────────────
if ! $SETUP_MAC; then
skip "dragonxd (macOS) — use --mac to cross-compile"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
ok "dragonxd daemon (macOS) present"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
else
miss "dragonxd daemon (macOS) not built"
fi
else
clone_dragonx_if_needed
# Clean previous macOS prebuilt binaries
if ! $CHECK_ONLY; then
for f in $DAEMON_BINS $DAEMON_DATA; do
rm -f "$DRAGONXD_MAC/$f" 2>/dev/null || true
done
fi
# macOS build requires either native macOS or osxcross
if [[ "$OSTYPE" == "darwin"* ]]; then
info "Building dragonx daemon for macOS (native)..."
(
cd "$DRAGONX_SRC"
bash build.sh -j"$NPROC"
)
else
info "Building dragonx daemon for macOS (requires osxcross)..."
OSXCROSS=""
for d in "$PROJECT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross"; do
if [[ -d "$d/target/bin" ]]; then
OSXCROSS="$d"
break
fi
done
if [[ -z "$OSXCROSS" ]]; then
warn "osxcross not found — skipping macOS daemon build"
warn "Install osxcross to external/osxcross to enable macOS cross-compilation"
else
info "Using osxcross at $OSXCROSS"
(
cd "$DRAGONX_SRC"
export PATH="$OSXCROSS/target/bin:$PATH"
bash util/build-mac-cross.sh 2>/dev/null || bash util/build-mac.sh
)
fi
fi
# Copy binaries
mkdir -p "$DRAGONXD_MAC"
local_found=0
for f in dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx; do
if [[ -f "$DRAGONX_SRC/src/$f" ]]; then
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_MAC/"
chmod +x "$DRAGONXD_MAC/$f"
ok " Installed $f"
local_found=1
fi
done
copy_daemon_data "$DRAGONXD_MAC"
if [[ $local_found -eq 0 ]]; then
warn "DragonX daemon (macOS) — no binaries found (osxcross may be missing)"
else
ok "DragonX daemon (macOS) built and installed to prebuilt-binaries/dragonxd-mac/"
fi
fi
# Prominent reminder if any prebuilt daemon drifted from the source — these are bundled verbatim
# by build.sh, so a stale binary ships in the wallet (and shows an old version in the Network tab).
if [[ "$STALE_DAEMON" -eq 1 ]]; then
warn "One or more prebuilt daemons are OLDER than the dragonx source (see above)."
warn "build.sh bundles them as-is, so rebuild the stale platform(s) before releasing:"
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
fi
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
header "xmrig-hac Mining Binary"
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
# Clean previous prebuilt xmrig binaries so we always rebuild
# Only clean the binary for the platform(s) we are actually building,
# otherwise a plain ./setup.sh deletes xmrig.exe without rebuilding it.
if ! $CHECK_ONLY; then
rm -f "$XMRIG_PREBUILT/xmrig" 2>/dev/null || true
if $SETUP_WIN; then
rm -f "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true
fi
fi
# Helper: clone xmrig-hac if not present
clone_xmrig_if_needed() {
if [[ ! -d "$XMRIG_SRC" ]]; then
info "Cloning xmrig-hac..."
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
else
ok "xmrig-hac source already present"
info "Pulling latest xmrig-hac..."
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
fi
}
# ── Linux xmrig ─────────────────────────────────────────────────────────────
XMRIG_LINUX="$XMRIG_PREBUILT/xmrig"
if $CHECK_ONLY; then
if [[ -f "$XMRIG_LINUX" ]]; then
ok "xmrig (Linux) ($(du -h "$XMRIG_LINUX" | cut -f1))"
else
miss "xmrig (Linux) not built (run setup without --check to build)"
fi
else
clone_xmrig_if_needed
# Clean previous build
rm -rf "$XMRIG_SRC/build"
# Build dependencies (libuv, hwloc, openssl)
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
(
cd "$XMRIG_SRC/scripts"
sh build_deps.sh
)
ok "xmrig-hac dependencies built"
# Build xmrig
info "Building xmrig-hac (Linux)..."
mkdir -p "$XMRIG_SRC/build"
(
cd "$XMRIG_SRC/build"
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_OPENCL=OFF \
-DWITH_CUDA=OFF \
-DWITH_HWLOC=ON \
-DCMAKE_PREFIX_PATH="$XMRIG_SRC/scripts/deps"
make -j"$NPROC"
)
# Copy binary to prebuilt-binaries
mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
else
err "xmrig (Linux) build failed — binary not found"
MISSING=$((MISSING + 1))
fi
fi
# ── Windows xmrig (cross-compile, only with --win or --all) ─────────────────
XMRIG_WIN="$XMRIG_PREBUILT/xmrig.exe"
if ! $SETUP_WIN; then
skip "xmrig.exe (Windows) — use --win to cross-compile"
elif $CHECK_ONLY; then
if [[ -f "$XMRIG_WIN" ]]; then
ok "xmrig.exe (Windows) ($(du -h "$XMRIG_WIN" | cut -f1))"
else
miss "xmrig.exe (Windows) not built (run setup --win without --check to build)"
fi
else
clone_xmrig_if_needed
# Clean previous Windows build
rm -rf "$XMRIG_SRC/build-windows"
info "Building xmrig-hac (Windows cross-compile)..."
(
cd "$XMRIG_SRC/scripts"
bash build_windows.sh
)
# Copy binary to prebuilt-binaries
mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
else
err "xmrig.exe (Windows) build failed — binary not found"
MISSING=$((MISSING + 1))
fi
fi
# ── 8. Binary directories ───────────────────────────────────────────────────
header "Binary Directories"
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
if [[ -d "$dir" ]]; then
# Count actual files (not .gitkeep)
count=$(find "$dir" -maxdepth 1 -type f ! -name '.gitkeep' | wc -l)
if [[ $count -gt 0 ]]; then
ok "prebuilt-binaries/$platform/ ($count files)"
else
skip "prebuilt-binaries/$platform/ (empty — place binaries here)"
fi
else
if ! $CHECK_ONLY; then
mkdir -p "$dir"
touch "$dir/.gitkeep"
ok "Created prebuilt-binaries/$platform/"
else
miss "prebuilt-binaries/$platform/"
fi
fi
done
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
if [[ $MISSING -eq 0 ]]; then
echo -e "${GREEN}${BOLD} Setup complete — ready to build!${NC}"
echo ""
echo " Quick start:"
echo " ./build.sh # Dev build"
echo " ./build.sh --linux-release # Linux release + AppImage"
echo " ./build.sh --win-release # Windows cross-compile"
else
echo -e "${YELLOW}${BOLD} $MISSING item(s) still need attention${NC}"
if $CHECK_ONLY; then
echo ""
echo " Run without --check to install automatically:"
echo " ./scripts/setup.sh"
echo " ./scripts/setup.sh --all # Include cross-compile + Sapling"
fi
fi
echo "═══════════════════════════════════════════"

File diff suppressed because it is too large Load Diff

458
src/app.h
View File

@@ -10,8 +10,18 @@
#include <thread>
#include <atomic>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
#include "util/async_task_manager.h"
#include "util/pool_stats_service.h"
#include "wallet/wallet_capabilities.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"
@@ -23,8 +33,9 @@ namespace dragonx {
class RPCWorker;
}
namespace config { class Settings; }
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; }
namespace wallet { class LiteWalletController; }
}
namespace dragonx {
@@ -123,6 +134,13 @@ public:
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
@@ -138,7 +156,22 @@ public:
// Accessors for subsystems
rpc::RPCClient* rpc() { return rpc_.get(); }
rpc::RPCWorker* worker() { return worker_.get(); }
// Console backend accessors (fast-lane-preferring, defined in app.cpp where the
// subsystem types are complete) used by the shared console executor.
rpc::RPCClient* consoleRpc();
rpc::RPCWorker* consoleWorker();
daemon::EmbeddedDaemon* consoleDaemon();
daemon::XmrigManager* consoleXmrig();
config::Settings* settings() { return settings_.get(); }
// Lite wallet controller (non-null only in lite builds with a linked backend).
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
const std::string& liteOpenError() const { return lite_open_error_; }
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
// (Re)build the lite controller from current settings so a changed lite-server selection
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
void rebuildLiteWallet(bool force = false);
WalletState& state() { return state_; }
const WalletState& state() const { return state_; }
const WalletState& getWalletState() const { return state_; }
@@ -171,10 +204,34 @@ public:
// Pool mining (xmrig)
void startPoolMining(int threads);
void stopPoolMining();
int getXmrigRequestedThreads() const {
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
}
// True while the pool miner process is live — used to refuse replacing the binary under it.
bool isPoolMinerRunning() const {
return xmrig_manager_ && xmrig_manager_->isRunning();
}
// Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
// and a request to refresh it now (Refresh button / switching into Auto mode).
util::PoolStatsService::Snapshot poolStatsSnapshot() const {
return pool_stats_service_.snapshot();
}
void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }
// Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
// detection on first call) so the mining tab can show it before mining starts.
std::string poolMiningInstalledVersion();
// Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; }
// Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
bool isPeerRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
}
void banPeer(const std::string& ip, int duration_seconds = 86400);
void unbanPeer(const std::string& ip);
void clearBans();
@@ -193,29 +250,76 @@ public:
void unfavoriteAddress(const std::string& addr);
bool isAddressFavorite(const std::string& addr) const;
// Address metadata (labels, icons, custom ordering)
void setAddressLabel(const std::string& addr, const std::string& label);
void setAddressIcon(const std::string& addr, const std::string& icon);
std::string getAddressLabel(const std::string& addr) const;
std::string getAddressIcon(const std::string& addr) const;
int getAddressSortOrder(const std::string& addr) const;
void setAddressSortOrder(const std::string& addr, int order);
int getNextSortOrder() const;
void swapAddressOrder(const std::string& a, const std::string& b);
// Assign dense sort orders (0..N-1) to the given addresses in the given order and
// persist once. Used by drag-reorder so a drop always takes effect (even from the
// default un-ordered state, where a pairwise swap would be a no-op).
void reorderAddresses(const std::vector<std::string>& orderedAddrs);
bool isMiningAddress(const std::string& addr) const;
void setMiningAddress(const std::string& addr, bool mining);
void invalidateAddressValidationCache();
// Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
void exportAllKeys(std::function<void(const std::string&)> callback);
// 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 importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
// Wallet backup
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool success, const std::string& result)> callback);
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
// auto-shield) with the shared opid poller so its eventual success/failure is
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
// richer pending-send path internally; this is for operations with no optimistic
// transaction row of their own.
void trackOperation(const std::string& opid);
// Force refresh
void refreshNow();
void refreshMiningInfo();
void refreshPeerInfo();
void refreshMarketData();
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
// near-static); populates state.market.exchanges. Safe to call every frame.
void refreshExchanges();
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
void refreshMarketChart();
/// @brief Per-category refresh intervals, adjusted by active tab
using RefreshIntervals = services::NetworkRefreshService::Intervals;
/// @brief Get recommended refresh intervals for a given page
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
// UI navigation
void setCurrentPage(ui::NavPage page) { current_page_ = page; }
void setCurrentPage(ui::NavPage page);
ui::NavPage getCurrentPage() const { return current_page_; }
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
void startScreenshotSweep();
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
void onScreenshotCaptured();
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
// Dialog triggers (used by settings page to open modal dialogs)
void showImportKeyDialog() { show_import_key_ = true; }
void showExportKeyDialog() { show_export_key_ = true; }
@@ -239,8 +343,27 @@ public:
bool startEmbeddedDaemon();
void stopEmbeddedDaemon();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
// block data), reconciling the wallet's stale spent-state without a daemon restart.
void runtimeRescan(int startHeight);
// Async binary-search probe for the lowest block height the node still has on disk.
// cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
// non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
// daemon is back up (consumed in update()).
void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
bool runtimeRescanActive() const { return runtime_rescan_active_; }
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
@@ -255,6 +378,8 @@ public:
// Logo texture accessor (wallet branding icon)
ImTextureID getLogoTexture() const { return logo_tex_; }
int getLogoWidth() const { return logo_w_; }
int getLogoHeight() const { return logo_h_; }
// Coin logo texture accessor (DragonX currency icon for balance tab)
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
@@ -284,6 +409,12 @@ public:
// Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase);
// Post-encrypt daemon restart: the daemon shuts itself down after
// encryptwallet, so restart it off the main thread. Shared by the
// immediate and deferred encryption continuations. When
// announceRestartStatus is true, connection_status_ is updated first so
// the loading overlay explains the restart.
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
void unlockWallet(const std::string& passphrase, int timeout);
void lockWallet();
void changePassphrase(const std::string& oldPass, const std::string& newPass);
@@ -303,9 +434,7 @@ public:
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
void showDecryptDialog() {
show_decrypt_dialog_ = true;
decrypt_phase_ = 0; // passphrase entry
decrypt_status_.clear();
decrypt_in_progress_ = false;
wallet_security_workflow_.reset();
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
}
@@ -317,8 +446,100 @@ public:
/// @brief Check if RPC worker has queued results waiting to be processed
bool hasPendingRPCResults() const;
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
std::string transactionSendProgressText() const;
std::string transactionRefreshProgressText() const;
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
// something the user copied afterwards). Only a hash of the secret is retained, never the
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
void copySecretToClipboard(const std::string& secret);
void pumpSecretClipboardClear();
bool isTransactionRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
}
private:
friend class AppDaemonLifecycleRuntime;
friend class AppDaemonLifecycleTaskContext;
// Global keyboard-shortcut handling, dispatched once per frame from update().
void handleGlobalShortcuts();
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
void maybeFinishTransactionSendProgress();
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
void upsertPendingSendTransaction(const std::string& opid,
const std::string& from,
const std::string& to,
double amount,
const std::string& memo,
double fee = 0.0);
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
// self-output that lifts the daemon's selection target past the boundary so it grabs another
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool, const std::string&)> callback);
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
// retry of a retry is reported as a real error.
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
const std::string& memo, const nlohmann::json& recipients,
const char* traceLabel, bool markFeeGapRetry,
std::function<void(bool, const std::string&)> callback);
void markPendingSendTransactionSucceeded(const std::string& opid,
const std::string& txid);
void removePendingSendTransactions(const std::vector<std::string>& opids,
bool restoreBalances);
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
// same clamp. Shared by the three pending-send delta sites so they can't drift.
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
bool includeAggregates);
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
// reaches a terminal status. Returns true if a callback was registered (and fired).
bool invokeSendResultCallback(const std::string& opid, bool ok,
const std::string& result);
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
// curve on every failed path (a PIN RPC failure used to skip it).
void applyUnlockSuccess(const std::string& passphrase, int timeout);
void applyUnlockFailure(const std::string& errorMessage);
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
// rescan-completion sites so a new witness field can't be forgotten in one copy.
void resetWitnessRescanProgress();
void loadTransactionHistoryCacheIfAvailable();
void storeTransactionHistoryCacheIfAvailable();
void wipePendingTransactionHistoryCachePassphrase();
void resetTransactionHistoryCacheSession();
void pruneShieldedHistoryScanProgress();
void invalidateShieldedHistoryScanProgress(bool persistCache);
// Auto-balance pool selection: drive the periodic hashrate refresh and apply a
// freshly-completed snapshot (weighted-random pick + optional miner restart).
void updatePoolAutoBalance();
void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);
// Subsystems
std::unique_ptr<rpc::RPCClient> rpc_;
std::unique_ptr<rpc::RPCWorker> worker_;
@@ -333,8 +554,35 @@ private:
rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_;
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
// controller's async broadcast result arrives). Only one lite send runs at a time.
std::function<void(bool, const std::string&)> lite_send_callback_;
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
bool lite_autoopen_done_ = false;
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
std::string lite_open_error_;
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
bool lite_unlock_prompt_ = false;
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
bool lite_startup_lock_checked_ = false;
std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
// Auto-balance runtime state (pool mining, full-node only). The service fetches
// pool hashrates off-thread; the RNG drives the weighted-random pick.
util::PoolStatsService pool_stats_service_;
std::mt19937 balance_rng_;
long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
// Wallet state
@@ -343,16 +591,22 @@ private:
// Shutdown state
std::atomic<bool> shutting_down_{false};
std::atomic<bool> shutdown_complete_{false};
std::atomic<bool> refresh_in_progress_{false};
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false};
std::thread daemon_restart_thread_;
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
std::atomic<int> pending_delete_result_{-1};
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
// UI State
bool quit_requested_ = false;
@@ -365,9 +619,10 @@ private:
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = true;
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// Export/Import state
std::string export_result_;
@@ -381,6 +636,16 @@ private:
// Connection
std::string connection_status_ = "Disconnected";
bool connection_in_progress_ = false;
bool remote_rpc_plaintext_warning_shown_ = false;
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
int daemon_wait_attempts_ = 0;
bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
float loading_timer_ = 0.0f; // spinner animation for loading overlay
// Current page (sidebar navigation)
@@ -388,6 +653,21 @@ private:
ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode
// Debug screenshot sweep state.
bool screenshot_sweep_active_ = false;
bool sweep_capture_this_frame_ = false;
int sweep_skin_idx_ = 0;
int sweep_page_idx_ = 0;
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
std::vector<std::string> sweep_skins_; // skin ids to cycle
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
std::string sweep_dir_; // output folder for this sweep
std::string sweep_current_path_; // PNG path for the frame about to be captured
std::string sweep_saved_skin_; // restore on completion
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
void updateScreenshotSweep(); // called at the top of render() while active
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
@@ -408,8 +688,9 @@ private:
int coin_logo_h_ = 0;
bool coin_logo_loaded_ = false;
// Console tab
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
ui::ConsoleTab console_tab_;
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
// Pending payment from URI
bool pending_payment_valid_ = false;
@@ -418,51 +699,123 @@ private:
std::string pending_memo_;
std::string pending_label_;
// Timers (in seconds since last update)
float refresh_timer_ = 0.0f;
float price_timer_ = 0.0f;
float fast_refresh_timer_ = 0.0f; // For mining stats
// Refresh intervals (seconds)
static constexpr float REFRESH_INTERVAL = 5.0f;
static constexpr float PRICE_INTERVAL = 60.0f;
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
// Mining refresh guard (prevents worker queue pileup)
std::atomic<bool> mining_refresh_in_progress_{false};
// Per-category refresh timers, policy, and worker queue guards.
services::NetworkRefreshService network_refresh_;
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
// Mining toggle guard (prevents concurrent setgenerate calls)
std::atomic<bool> mining_toggle_in_progress_{false};
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
std::atomic<bool> pool_starting_{false};
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};
// P4: Incremental transaction cache
int last_tx_block_height_ = -1; // block height at last full tx fetch
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
std::size_t shielded_history_scan_cursor_ = 0;
bool shielded_history_scan_pending_ = false;
// False until the first full shielded-history scan finishes. Drives the History tab's
// "Loading older history…" progress so the user knows transactions are still streaming in
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
bool initial_history_scan_complete_ = false;
std::unordered_map<std::string, int> shielded_history_scan_heights_;
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
// txids we've already enriched. Keyed by txid.
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
// are accumulated here and reused across refresh cycles. Only
// recent/unconfirmed txns are re-fetched from the daemon each time.
std::vector<TransactionInfo> confirmed_tx_cache_;
std::unordered_set<std::string> confirmed_tx_ids_; // fast lookup
int confirmed_cache_block_ = -1; // block height when cache was last built
// Dirty flags for demand-driven refresh
bool addresses_dirty_ = true; // true → refreshAddresses() will run
bool address_validation_cache_dirty_ = true;
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
bool rescan_status_poll_in_progress_ = false;
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
bool rescan_confirmed_active_ = false;
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false;
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false;
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
int witness_rebuild_total_blocks_ = 0;
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
// dedups the daemon's occasional double-prints) over the reported total N.
std::unordered_set<std::string> witness_seen_txids_;
int witness_total_txs_ = 0;
bool opid_poll_in_progress_ = false;
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
int consecutive_core_failures_ = 0;
// Pending z_sendmany operation tracking
bool send_progress_active_ = false;
int send_submissions_in_flight_ = 0;
std::vector<std::string> pending_opids_; // opids to poll for completion
struct PendingSendInfo {
std::string from;
std::string to;
std::string memo;
double amount = 0.0;
double fee = 0.0;
std::int64_t timestamp = 0;
};
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
// fails again is reported to the user instead of looping.
std::unordered_set<std::string> send_feegap_retried_opids_;
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
// user isn't told "sent successfully" before the tx is actually built/broadcast.
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
pending_send_callbacks_;
// Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or
// z_listreceivedbyaddress.
std::unordered_set<std::string> send_txids_;
// First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_;
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
std::string wizard_pending_passphrase_; // held until daemon connects
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
// Deferred encryption (wizard background task)
std::string deferred_encrypt_passphrase_;
std::string deferred_encrypt_pin_;
bool deferred_encrypt_pending_ = false;
// Wallet security flow state shared by wizard/settings encryption paths.
services::WalletSecurityController wallet_security_;
services::WalletSecurityWorkflow wallet_security_workflow_;
// Wizard: stopping an external daemon before bootstrap
bool wizard_stopping_external_ = false;
std::string wizard_stop_status_;
std::thread wizard_stop_thread_;
// PIN vault
std::unique_ptr<util::SecureVault> vault_;
data::TransactionHistoryCache transaction_history_cache_;
std::string pending_transaction_history_cache_passphrase_;
bool transaction_history_cache_loaded_ = false;
// Lock screen state
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
@@ -504,10 +857,7 @@ private:
// Decrypt wallet dialog state
bool show_decrypt_dialog_ = false;
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
char decrypt_pass_buf_[256] = {};
std::string decrypt_status_;
bool decrypt_in_progress_ = false;
// Wizard PIN setup state
char wizard_pin_buf_[16] = {};
@@ -517,9 +867,14 @@ private:
// Auto-lock on idle
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
// Mine-when-idle: auto-start/stop mining based on system idle state
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
// Private methods - rendering
void renderStatusBar();
void renderAboutDialog();
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
void renderImportKeyDialog();
void renderExportKeyDialog();
void renderBackupDialog();
@@ -535,15 +890,36 @@ private:
void tryConnect();
void onConnected();
void onDisconnected(const std::string& reason);
// Set the "node is initializing" UI state (status line + overlay description) from the
// embedded/external daemon's launch state and its own console output (current phase + block
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
// false when the daemon is merely launching (not bound yet). Returns the status title.
std::string applyDaemonInitStatus(bool reachableButBusy);
// Tear down a connection that died mid-session (daemon crash / restart / dropped
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
void handleLostConnection(const std::string& reason);
void applyDefaultBanlist();
// Private methods - data refresh
void refreshData();
void refreshBalance();
void refreshAddresses();
void refreshTransactions();
void refreshData(); // Orchestrator: dispatches per-category refreshes
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
void refreshAddressData(); // Address lists + balances
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
bool refreshEncryptionState(); // Wallet encryption/lock state
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
void refreshAddresses(); // Legacy: standalone address refresh
void refreshPrice();
void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page);
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;
bool shouldRefreshRecentTransactions() const;
void checkAutoLock();
void checkIdleMining();
};
} // namespace dragonx

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "config/settings.h"
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h"
#include "ui/notifications.h"
#include "ui/material/color_theme.h"
@@ -39,13 +40,48 @@ namespace dragonx {
using json = nlohmann::json;
namespace {
struct WizardLowSpecSnapshot {
bool valid = false;
float blur = 0.0f;
float uiOp = 0.0f;
bool fx = false;
bool scanline = false;
};
struct WizardUiState {
float blur_amount = 1.5f;
bool theme_effects = true;
float ui_opacity = 1.0f;
bool low_spec = false;
bool scanline = true;
std::string balance_layout = "classic";
int language_index = 0;
bool appearance_init = false;
WizardLowSpecSnapshot low_spec_snapshot;
float card0_max_h = 0.0f;
float card1_max_h = 0.0f;
double external_last_check = -10.0;
bool daemon_prestarted = false;
};
WizardUiState s_wizardUi;
} // namespace
void App::restartWizard()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
return;
}
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt
if (embedded_daemon_) {
embedded_daemon_->resetCrashCount();
if (daemon_controller_) {
daemon_controller_->resetCrashCount();
}
// Disconnect RPC
@@ -56,10 +92,11 @@ void App::restartWizard()
// Stop the embedded daemon in a background thread to avoid
// blocking the UI for up to 32 seconds (RPC stop + process wait).
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
std::thread([this]() {
if (daemon_controller_ && isEmbeddedDaemonRunning()) {
async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) return;
stopEmbeddedDaemon();
}).detach();
});
}
// Enter wizard — the wizard completion handler already calls
@@ -73,6 +110,7 @@ void App::restartWizard()
// ===========================================================================
void App::renderFirstRunWizard() {
auto& wizardUi = s_wizardUi;
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
@@ -94,21 +132,6 @@ void App::renderFirstRunWizard() {
ImU32 bgCol = ui::material::Surface();
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
// Handle Done/None — wizard complete
if (wizard_phase_ == WizardPhase::Done || wizard_phase_ == WizardPhase::None) {
wizard_phase_ = WizardPhase::None;
if (!state_.connected) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
startEmbeddedDaemon();
}
tryConnect();
}
settings_->setWizardCompleted(true);
settings_->save();
ImGui::End();
return;
}
// --- Determine which of the 3 masonry sections is focused ---
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
int focusIdx = 0;
@@ -258,15 +281,14 @@ void App::renderFirstRunWizard() {
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 14.0f * dp;
// Statics for appearance settings
static float wiz_blur_amount = 1.5f;
static bool wiz_theme_effects = true;
static float wiz_ui_opacity = 1.0f;
static bool wiz_low_spec = false;
static bool wiz_scanline = true;
static std::string wiz_balance_layout = "classic";
static int wiz_language_index = 0;
static bool wiz_appearance_init = false;
float& wiz_blur_amount = wizardUi.blur_amount;
bool& wiz_theme_effects = wizardUi.theme_effects;
float& wiz_ui_opacity = wizardUi.ui_opacity;
bool& wiz_low_spec = wizardUi.low_spec;
bool& wiz_scanline = wizardUi.scanline;
std::string& wiz_balance_layout = wizardUi.balance_layout;
int& wiz_language_index = wizardUi.language_index;
bool& wiz_appearance_init = wizardUi.appearance_init;
if (!wiz_appearance_init) {
wiz_blur_amount = settings_->getBlurMultiplier();
wiz_theme_effects = settings_->getThemeEffectsEnabled();
@@ -413,7 +435,7 @@ void App::renderFirstRunWizard() {
// --- Low-spec mode checkbox ---
// Snapshot for restoring settings when low-spec is turned off
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
@@ -589,7 +611,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
// Save appearance choices, advance to Bootstrap
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
@@ -611,7 +633,7 @@ void App::renderFirstRunWizard() {
cy += cardPad;
// Lock card height to the tallest content ever seen
static float card0MaxH = 0.0f;
float& card0MaxH = wizardUi.card0_max_h;
card0MaxH = std::max(card0MaxH, cy - card0Top);
card0Bot = card0Top + card0MaxH;
@@ -673,8 +695,13 @@ void App::renderFirstRunWizard() {
} else {
auto prog = bootstrap_->getProgress();
const char* statusTitle = (prog.state == util::Bootstrap::State::Downloading)
? "Downloading bootstrap..." : "Extracting blockchain data...";
const char* statusTitle;
if (prog.state == util::Bootstrap::State::Downloading)
statusTitle = "Downloading bootstrap...";
else if (prog.state == util::Bootstrap::State::Verifying)
statusTitle = "Verifying checksums...";
else
statusTitle = "Extracting blockchain data...";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
cy += bodyFont->LegacySize + 12.0f * dp;
@@ -706,6 +733,28 @@ void App::renderFirstRunWizard() {
dimCol, "(wallet.dat is protected)");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Daemon status indicator
{
bool daemonUp = isEmbeddedDaemonRunning();
const std::string& dStatus = getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green
: IM_COL32(120, 120, 120, 160); // gray
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200); // orange
float dotR = 3.5f * dp;
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? "Daemon stopping..."
: "Daemon running")
: "Daemon stopped";
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
cy += captionFont->LegacySize + 6.0f * dp;
}
cy += 12.0f * dp;
// Cancel button
@@ -714,7 +763,7 @@ void App::renderFirstRunWizard() {
float cancelBX = rightX + (colW - cancelW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
bootstrap_->cancel();
}
ImGui::PopStyleVar();
@@ -725,6 +774,8 @@ void App::renderFirstRunWizard() {
auto finalProg = bootstrap_->getProgress();
if (finalProg.state == util::Bootstrap::State::Completed) {
bootstrap_.reset();
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
markPostBootstrapRescanPending();
wizard_phase_ = WizardPhase::EncryptOffer;
} else {
wizard_phase_ = WizardPhase::BootstrapFailed;
@@ -760,18 +811,22 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -787,17 +842,23 @@ void App::renderFirstRunWizard() {
if (isFocused) {
static std::atomic<bool> s_extCached{false};
static std::atomic<bool> s_checkInFlight{false};
static double s_extLastCheck = -10.0;
double& s_extLastCheck = wizardUi.external_last_check;
double now = ImGui::GetTime();
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
s_extLastCheck = now;
bool embeddedRunning = isEmbeddedDaemonRunning();
s_checkInFlight.store(true);
std::thread([embeddedRunning]() {
async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) {
s_checkInFlight.store(false);
return;
}
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
s_extCached.store(inUse && !embeddedRunning);
if (!token.cancelled()) {
s_extCached.store(inUse && !embeddedRunning);
}
s_checkInFlight.store(false);
}).detach();
});
}
externalRunning = s_extCached.load();
}
@@ -835,22 +896,22 @@ void App::renderFirstRunWizard() {
IM_COL32(220, 60, 60, 255)));
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true;
wizard_stop_status_ = "Sending stop command...";
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
wizard_stop_thread_ = std::thread([this]() {
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
auto config = rpc::Connection::autoDetectConfig();
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
if (tmp_rpc->connect(config.host, config.port,
config.rpcuser, config.rpcpassword)) {
try { tmp_rpc->call("stop"); } catch (...) {}
config.rpcuser, config.rpcpassword,
config.use_tls)) {
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
tmp_rpc->disconnect();
}
}
wizard_stop_status_ = "Waiting for daemon to shut down...";
for (int i = 0; i < 60; i++) {
for (int i = 0; i < 60 && !token.cancelled(); i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
wizard_stop_status_ = "Daemon stopped.";
@@ -858,6 +919,7 @@ void App::renderFirstRunWizard() {
return;
}
}
if (token.cancelled()) return;
wizard_stop_status_ = "Daemon did not stop — try manually.";
wizard_stopping_external_ = false;
});
@@ -867,7 +929,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -889,38 +951,88 @@ void App::renderFirstRunWizard() {
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
const char* twText = "Only use bootstrap.dragonx.is. Using files from untrusted sources could compromise your node.";
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
float twWrap = contentW - iw - 4.0f * dp;
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
cy += twSize.y + 12.0f * dp;
}
// Daemon status indicator (subtle, before buttons)
{
bool daemonUp = isEmbeddedDaemonRunning();
const std::string& dStatus = getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
: IM_COL32(120, 120, 120, 160);
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200);
float dotR = 3.5f * dp;
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? "Daemon stopping..."
: "Daemon running")
: "Daemon stopped";
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
cy += captionFont->LegacySize + 10.0f * dp;
}
// Buttons (only when focused)
if (isFocused) {
float dlBtnW = 180.0f * dp;
float dlBtnW = 150.0f * dp;
float mirrorW = 150.0f * dp;
float skipW2 = 80.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = dlBtnW + 12.0f * dp + skipW2;
float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f;
// --- Download button (main / Cloudflare) ---
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 12.0f * dp, cy));
// --- Mirror Download button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -931,7 +1043,7 @@ void App::renderFirstRunWizard() {
cy += cardPad;
// Lock card height to the tallest content ever seen (but not when collapsed)
static float card1MaxH = 0.0f;
float& card1MaxH = wizardUi.card1_max_h;
if (isCollapsed) {
card1Bot = card1Top + (cy - card1Top);
} else {
@@ -956,7 +1068,7 @@ void App::renderFirstRunWizard() {
// Pre-start daemon when encrypt card becomes focused so it's ready
// by the time the user finishes typing their passphrase
if (isFocused) {
static bool wiz_daemon_prestarted = false;
bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
if (!wiz_daemon_prestarted) {
wiz_daemon_prestarted = true;
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
@@ -1015,7 +1127,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
@@ -1173,10 +1285,24 @@ void App::renderFirstRunWizard() {
cy += captionFont->LegacySize + 6.0f * dp;
}
// Warn + block if the passphrase has leading/trailing whitespace. Silently trimming it would
// change the passphrase the user believes they set and lock them out on the next unlock.
bool passEdgeSpace = false;
if (size_t pl = strlen(encrypt_pass_buf_)) {
char a = encrypt_pass_buf_[0], b = encrypt_pass_buf_[pl - 1];
passEdgeSpace = (a == ' ' || a == '\t' || b == ' ' || b == '\t');
}
if (passEdgeSpace) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "Passphrase has leading/trailing spaces — remove them");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Buttons
{
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
!passEdgeSpace;
// PIN is optional: if entered, must be valid + confirmed
std::string pinStr(wizard_pin_buf_);
bool pinEntered = !pinStr.empty();
@@ -1198,12 +1324,11 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canEncrypt);
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
if (pinEntered && pinOk)
deferred_encrypt_pin_ = pinStr;
deferred_encrypt_pending_ = true;
wallet_security_.beginDeferredEncryption(
std::string(encrypt_pass_buf_),
(pinEntered && pinOk) ? pinStr : std::string());
// Clear sensitive buffers
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
@@ -1213,7 +1338,10 @@ void App::renderFirstRunWizard() {
// Start daemon + finish wizard immediately
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
}
tryConnect();
wizard_phase_ = WizardPhase::Done;
@@ -1227,14 +1355,25 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
static bool s_skipEncConfirm = false;
if (!s_skipEncConfirm) {
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
s_skipEncConfirm = true;
encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm.";
} else {
s_skipEncConfirm = false;
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
}
tryConnect();
}
tryConnect();
}
ImGui::PopStyleVar();
cy += btnH2;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,505 @@
#pragma once
#include "chat_protocol.h"
#include <cstddef>
#include <string>
#include <vector>
// HushChat compatibility-fixture / capture-manifest / seed-projection tooling.
//
// Everything in this header is DEAD AT RUNTIME in the shipping app and test
// binaries — its only caller is the standalone dev CLI
// tools/hushchat_fixture_check.cpp. It is deliberately compiled ONLY into the
// HushChatFixtureCheck target so this validation scaffolding (and its libsodium
// seed-projection path) never reaches the wallet binary. Keep it that way: do
// not add chat_fixture_tooling.cpp to APP_SOURCES or the test target.
//
// It builds on the runtime types declared in chat_protocol.h.
namespace dragonx::chat {
enum class HushChatDecryptPreflightError {
None,
FeatureDisabled,
NonMessageHeader,
InvalidHeaderNumber,
UnsupportedVersion,
MissingReplyAddress,
MissingConversationId,
InvalidSecretstreamHeader,
InvalidPublicKey,
EmptyCiphertext,
OversizedCiphertext,
OddLengthCiphertext,
InvalidCiphertextHex,
TruncatedCiphertext
};
struct HushChatDecryptPreflightInput {
HushChatHeader header;
std::string ciphertext_hex;
};
struct HushChatDecryptPreflightResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
const char* error_name = "None";
std::size_t ciphertext_size = 0;
};
enum class HushChatHexDecodeError {
None,
Empty,
OddLength,
InvalidHex,
UnexpectedByteLength
};
struct HushChatHexDecodeResult {
bool ok = false;
HushChatHexDecodeError error = HushChatHexDecodeError::None;
const char* error_name = "None";
std::vector<unsigned char> bytes;
};
enum class HushChatDecryptDirection {
Incoming,
Outgoing
};
enum class HushChatSessionKeySelection {
ClientRx,
ServerTx
};
enum class HushChatDecryptInputError {
None,
FeatureDisabled,
InvalidStoredChatKey,
DecryptPreflightFailed,
InvalidPeerPublicKey,
InvalidStreamHeader,
InvalidCiphertext
};
struct HushChatDecryptInputMaterial {
std::string stored_chat_key_hex;
HushChatHeader header;
std::string ciphertext_hex;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
std::string peer_public_key_hex;
};
struct HushChatPreparedDecryptInput {
std::vector<unsigned char> stored_chat_key_bytes;
std::vector<unsigned char> seed_bytes;
std::vector<unsigned char> peer_public_key_bytes;
std::vector<unsigned char> stream_header_bytes;
std::vector<unsigned char> ciphertext_bytes;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t plaintext_capacity = 0;
};
struct HushChatDecryptInputPreparationResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptInputError error = HushChatDecryptInputError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatPreparedDecryptInput prepared;
};
struct HushChatDecryptFixtureReadinessResult {
bool ready = false;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t stream_header_size = 0;
std::size_t ciphertext_size = 0;
std::size_t plaintext_capacity = 0;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
};
enum class HushChatCompatibilityFixtureError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidLocalPublicKey,
InvalidPeerPublicKey,
InvalidHeaderMemo,
InvalidMemoPair,
NonMemoHeader,
HeaderPublicKeyMismatch,
DecryptInputFailed,
NotFixtureReady,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
ExpectedPeerPublicKeyLengthMismatch,
ExpectedStreamHeaderLengthMismatch,
ExpectedCiphertextLengthMismatch,
ExpectedPlaintextLengthMismatch,
ExpectedRoleMismatch,
InvalidPlaintextHash
};
struct HushChatCompatibilityFixture {
std::string fixture_id;
std::string stored_chat_key_hex;
std::string local_public_key_hex;
std::string peer_public_key_hex;
std::string header_memo;
std::string ciphertext_memo;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t expected_stored_chat_key_size = 32;
std::size_t expected_seed_size = 32;
std::size_t expected_local_public_key_size = 32;
std::size_t expected_peer_public_key_size = 32;
std::size_t expected_stream_header_size = 24;
std::size_t expected_ciphertext_size = 0;
std::size_t expected_plaintext_size = 0;
std::string expected_plaintext_hash_hex;
};
struct HushChatCompatibilityFixtureVerificationResult {
bool ok = false;
bool feature_enabled = false;
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatHeader header;
HushChatDecryptInputPreparationResult preparation;
HushChatDecryptFixtureReadinessResult readiness;
std::size_t local_public_key_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t plaintext_hash_size = 0;
};
enum class HushChatCompatibilityFixtureKind {
IncomingMemo,
OutgoingMemo,
SeedPublicKeyProjection,
CorruptedAuthFailure,
ContactExclusion
};
enum class HushChatCompatibilityFixtureFileStatus {
Pending,
Ready
};
enum class HushChatCompatibilityFixtureFileError {
None,
FeatureDisabled,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingKind,
UnknownKind,
MissingStatus,
UnknownStatus,
MissingFixtureId,
MissingPendingReason,
MissingFixtureObject,
InvalidFixtureField,
FixtureVerificationFailed,
ContactFixtureNotExcluded,
FileReadFailed
};
struct HushChatCompatibilityFixtureFile {
std::string schema;
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
std::string fixture_id;
std::string pending_reason;
HushChatCompatibilityFixture fixture;
};
struct HushChatCompatibilityFixtureFileParseResult {
bool ok = false;
bool feature_enabled = false;
bool pending = false;
bool verified = false;
bool excluded_from_decrypt = false;
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFile file;
HushChatCompatibilityFixtureVerificationResult verification;
};
enum class HushChatSeedPublicKeyProjectionError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidStoredChatKey,
InvalidLocalPublicKey,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
SodiumInitializationFailed,
KeypairProjectionFailed,
ProjectedPublicKeyMismatch
};
struct HushChatSeedPublicKeyProjectionResult {
bool ok = false;
bool feature_enabled = false;
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t local_public_key_size = 0;
std::size_t projected_public_key_size = 0;
};
enum class HushChatCorruptedAuthFailureReadinessError {
None,
FeatureDisabled,
FixturePending,
WrongFixtureKind,
FixtureNotVerified,
SeedProjectionNotVerified
};
struct HushChatCorruptedAuthFailureReadinessResult {
bool ok = false;
bool feature_enabled = false;
bool structurally_ready_for_future_auth_check = false;
bool requires_future_secretstream_auth_failure = false;
bool decrypted = false;
bool authenticated = false;
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
const char* error_name = "None";
};
enum class HushChatCompatibilityFixtureImportError {
None,
FeatureDisabled,
MissingRequiredKind,
DuplicateKind,
FixtureLoadFailed,
FixtureKindMismatch,
FixturePending,
FixtureInvalid,
FixtureNotVerified,
SeedProjectionFailed,
AuthFailureScaffoldFailed,
ContactFixtureNotExcluded
};
struct HushChatCompatibilityFixtureImportCandidate {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
};
struct HushChatCompatibilityFixtureImportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFileParseResult parsed;
HushChatSeedPublicKeyProjectionResult seed_projection;
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
};
struct HushChatCompatibilityFixtureImportChecklistResult {
bool ok = false;
bool feature_enabled = false;
bool replacement_ready = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureImportItem> items;
};
struct HushChatCompatibilityFixtureReplacementReportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool refused = true;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
bool cont_excluded = false;
bool decrypted = false;
bool authenticated = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
};
struct HushChatCompatibilityFixtureReplacementDryRunResult {
bool ok = false;
bool feature_enabled = false;
bool dry_run_only = true;
bool redacted_report = true;
bool would_replace = false;
bool replacement_refused = true;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
};
enum class HushChatCaptureManifestError {
None,
FeatureDisabled,
FileReadFailed,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingManifestId,
MissingStatus,
UnknownStatus,
MissingFixtureDirectory,
MissingDryRunCommand,
InvalidDryRunCommand,
MissingProvenance,
MissingSourceClient,
InvalidSourceClient,
MissingSourceClientVersion,
MissingCaptureDate,
MissingNetwork,
MissingCaptureMethod,
MissingHandling,
MissingHandlingFlag,
HandlingFlagNotTrue,
MissingCategories,
InvalidCategoryEntry,
UnknownCategory,
DuplicateCategory,
MissingRequiredCategory,
ProhibitedFieldPresent
};
enum class HushChatCaptureManifestStatus {
Staged
};
struct HushChatCaptureManifestCategoryReport {
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string staged_filename;
bool declared = false;
};
struct HushChatCaptureManifestValidationResult {
bool ok = false;
bool feature_enabled = false;
bool redacted_report = true;
bool validates_provenance_only = true;
bool no_sensitive_material_declared = false;
bool has_dry_run_command = false;
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
const char* error_name = "None";
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
std::string manifest_path;
std::string fixture_directory;
std::size_t required_count = 0;
std::size_t declared_count = 0;
std::size_t missing_count = 0;
std::size_t duplicate_count = 0;
std::size_t prohibited_field_count = 0;
std::size_t handling_flag_count = 0;
std::vector<HushChatCaptureManifestCategoryReport> categories;
};
constexpr std::size_t kHushChatSecretstreamABytes = 17;
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
constexpr std::size_t kHushChatSeedByteLength = 32;
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
const HushChatDecryptPreflightInput& input,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
std::size_t expectedByteLength);
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
const HushChatDecryptInputMaterial& material,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
const HushChatPreparedDecryptInput& prepared);
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
const HushChatCompatibilityFixtureFileParseResult& parsed,
const HushChatSeedPublicKeyProjectionResult& seedProjection,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
} // namespace dragonx::chat

284
src/chat/chat_protocol.cpp Normal file
View File

@@ -0,0 +1,284 @@
#include "chat_protocol.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <optional>
#include <utility>
namespace dragonx::chat {
namespace {
bool isHexString(const std::string& value)
{
for (unsigned char ch : value) {
if (!std::isxdigit(ch)) return false;
}
return true;
}
bool isCiphertextPayloadCandidate(const std::string& value)
{
return !value.empty() && value.size() <= kHushChatMemoByteLimit &&
value.size() % 2 == 0 && isHexString(value);
}
bool isContactPayloadCandidate(const std::string& value)
{
return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.front() != '{';
}
bool readRequiredString(const nlohmann::json& object,
const char* key,
std::string& value,
std::string& error)
{
auto it = object.find(key);
if (it == object.end()) {
error = std::string("missing field: ") + key;
return false;
}
if (!it->is_string()) {
error = std::string("field is not a string: ") + key;
return false;
}
value = it->get<std::string>();
return true;
}
bool readRequiredInt(const nlohmann::json& object,
const char* key,
int& value,
std::string& error)
{
auto it = object.find(key);
if (it == object.end()) {
error = std::string("missing field: ") + key;
return false;
}
if (!it->is_number_integer()) {
error = std::string("field is not an integer: ") + key;
return false;
}
value = it->get<int>();
return true;
}
HushChatHeaderParseResult fail(std::string error)
{
HushChatHeaderParseResult result;
result.error = std::move(error);
return result;
}
} // namespace
const char* hushChatHeaderTypeName(HushChatHeaderType type)
{
switch (type) {
case HushChatHeaderType::Message:
return "Memo";
case HushChatHeaderType::ContactRequest:
return "Cont";
}
return "Unknown";
}
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue)
{
switch (issue) {
case HushChatMemoGroupingIssue::InvalidHeader:
return "InvalidHeader";
case HushChatMemoGroupingIssue::MissingPayload:
return "MissingPayload";
case HushChatMemoGroupingIssue::DuplicateHeader:
return "DuplicateHeader";
case HushChatMemoGroupingIssue::OversizedMemo:
return "OversizedMemo";
}
return "Unknown";
}
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
{
if (memo.empty()) return fail("empty memo");
if (memo.size() > kHushChatMemoByteLimit) return fail("memo exceeds HushChat memo byte limit");
if (memo.front() != '{') return fail("memo is not a HushChat header JSON object");
nlohmann::json object;
try {
object = nlohmann::json::parse(memo);
} catch (const nlohmann::json::parse_error& e) {
return fail(std::string("invalid JSON: ") + e.what());
}
if (!object.is_object()) return fail("header memo JSON is not an object");
HushChatHeader header;
std::string type;
std::string error;
if (!readRequiredInt(object, "h", header.header_number, error)) return fail(error);
if (!readRequiredInt(object, "v", header.version, error)) return fail(error);
if (!readRequiredString(object, "z", header.reply_zaddr, error)) return fail(error);
if (!readRequiredString(object, "cid", header.conversation_id, error)) return fail(error);
if (!readRequiredString(object, "t", type, error)) return fail(error);
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
if (header.header_number < 1) return fail("header number must be positive");
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
if (header.reply_zaddr.empty()) return fail("reply z-address is empty");
if (header.conversation_id.empty()) return fail("conversation id is empty");
if (type == "Memo") {
header.type = HushChatHeaderType::Message;
} else if (type == "Cont") {
header.type = HushChatHeaderType::ContactRequest;
} else {
return fail("unknown HushChat header type");
}
if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) {
return fail("public key must be 32 bytes encoded as hex");
}
if (header.type == HushChatHeaderType::Message) {
if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength ||
!isHexString(header.secretstream_header_hex)) {
return fail("message header must include a 24 byte secretstream header encoded as hex");
}
} else if (!header.secretstream_header_hex.empty()) {
return fail("contact request header must not include a secretstream header");
}
HushChatHeaderParseResult result;
result.ok = true;
result.header = std::move(header);
return result;
}
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs)
{
struct OrderedOutput {
std::size_t input_index = 0;
HushChatMemoOutput output;
};
std::vector<OrderedOutput> ordered;
ordered.reserve(outputs.size());
for (std::size_t index = 0; index < outputs.size(); ++index) {
ordered.push_back(OrderedOutput{index, outputs[index]});
}
std::stable_sort(ordered.begin(), ordered.end(), [](const OrderedOutput& left, const OrderedOutput& right) {
if (left.output.position == right.output.position) return left.input_index < right.input_index;
return left.output.position < right.output.position;
});
HushChatMemoGroupingResult result;
std::optional<HushChatMemoOutput> pending_header_output;
std::optional<HushChatHeader> pending_header;
auto addIssue = [&](HushChatMemoGroupingIssue issue, std::size_t position, std::string detail) {
result.issues.push_back(HushChatMemoGroupingIssueInfo{issue, position, std::move(detail)});
};
auto clearPendingAsMissing = [&]() {
if (!pending_header_output) return;
addIssue(HushChatMemoGroupingIssue::MissingPayload,
pending_header_output->position,
"header did not have a matching payload memo");
pending_header_output.reset();
pending_header.reset();
};
for (const auto& entry : ordered) {
const auto& output = entry.output;
if (output.memo.size() > kHushChatMemoByteLimit) {
addIssue(HushChatMemoGroupingIssue::OversizedMemo,
output.position,
"memo exceeds HushChat memo byte limit");
continue;
}
if (!output.memo.empty() && output.memo.front() == '{') {
auto parsed = parseHushChatHeaderMemo(output.memo);
if (!parsed.ok) {
addIssue(HushChatMemoGroupingIssue::InvalidHeader, output.position, parsed.error);
continue;
}
if (pending_header_output) {
addIssue(HushChatMemoGroupingIssue::DuplicateHeader,
output.position,
"encountered another HushChat header before a payload");
clearPendingAsMissing();
}
pending_header_output = output;
pending_header = std::move(parsed.header);
continue;
}
if (!pending_header_output || !pending_header) {
++result.ignored_memo_count;
continue;
}
const bool payload_matches = pending_header->type == HushChatHeaderType::Message
? isCiphertextPayloadCandidate(output.memo)
: isContactPayloadCandidate(output.memo);
if (!payload_matches) {
++result.ignored_memo_count;
continue;
}
HushChatMemoPair pair;
pair.header = std::move(*pending_header);
pair.header_position = pending_header_output->position;
pair.payload_position = output.position;
pair.payload_memo = output.memo;
result.pairs.push_back(std::move(pair));
pending_header_output.reset();
pending_header.reset();
}
clearPendingAsMissing();
return result;
}
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled)
{
HushChatTransactionExtractionResult result;
result.feature_enabled = featureEnabled;
if (!featureEnabled || transaction.txid.empty()) return result;
auto grouped = groupHushChatMemoOutputs(transaction.outputs);
result.ignored_memo_count = grouped.ignored_memo_count;
result.issues.reserve(grouped.issues.size());
for (const auto& issue : grouped.issues) {
result.issues.push_back(HushChatMemoGroupingIssueInfo{
issue.issue,
issue.position,
hushChatMemoGroupingIssueName(issue.issue)
});
}
result.metadata.reserve(grouped.pairs.size());
for (const auto& pair : grouped.pairs) {
HushChatTransactionMetadata metadata;
metadata.txid = transaction.txid;
metadata.type = pair.header.type;
metadata.conversation_id = pair.header.conversation_id;
metadata.reply_zaddr = pair.header.reply_zaddr;
metadata.header_position = pair.header_position;
metadata.payload_position = pair.payload_position;
metadata.payload_size = pair.payload_memo.size();
result.metadata.push_back(std::move(metadata));
}
return result;
}
} // namespace dragonx::chat

105
src/chat/chat_protocol.h Normal file
View File

@@ -0,0 +1,105 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#ifndef DRAGONX_ENABLE_CHAT
#define DRAGONX_ENABLE_CHAT 0
#endif
namespace dragonx::chat {
enum class HushChatHeaderType {
Message,
ContactRequest
};
struct HushChatHeader {
int header_number = 0;
int version = 0;
std::string reply_zaddr;
std::string conversation_id;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string secretstream_header_hex;
std::string public_key_hex;
};
struct HushChatHeaderParseResult {
bool ok = false;
HushChatHeader header;
std::string error;
};
struct HushChatMemoOutput {
std::size_t position = 0;
std::string memo;
};
struct HushChatMemoPair {
HushChatHeader header;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::string payload_memo;
};
enum class HushChatMemoGroupingIssue {
InvalidHeader,
MissingPayload,
DuplicateHeader,
OversizedMemo
};
struct HushChatMemoGroupingIssueInfo {
HushChatMemoGroupingIssue issue = HushChatMemoGroupingIssue::InvalidHeader;
std::size_t position = 0;
std::string detail;
};
struct HushChatMemoGroupingResult {
std::vector<HushChatMemoPair> pairs;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
struct HushChatTransactionInput {
std::string txid;
std::vector<HushChatMemoOutput> outputs;
};
struct HushChatTransactionMetadata {
std::string txid;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string conversation_id;
std::string reply_zaddr;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::size_t payload_size = 0;
};
struct HushChatTransactionExtractionResult {
bool feature_enabled = false;
std::vector<HushChatTransactionMetadata> metadata;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
constexpr int kHushChatSupportedVersion = 0;
constexpr std::size_t kHushChatMemoByteLimit = 512;
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
constexpr bool hushChatFeatureEnabledAtBuild()
{
return DRAGONX_ENABLE_CHAT != 0;
}
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo);
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs);
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatHeaderTypeName(HushChatHeaderType type);
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
} // namespace dragonx::chat

View File

@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// settings.cpp — JSON settings persistence. Loads/saves user preferences
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
#include "settings.h"
#include "version.h"
@@ -8,8 +11,12 @@
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include <ctime>
#include <algorithm>
#include <type_traits>
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
@@ -27,35 +34,87 @@ namespace config {
Settings::Settings() = default;
Settings::~Settings() = default;
namespace {
Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
const json& value)
{
if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
const std::string mode = value.get<std::string>();
if (mode == "random" || mode == "Random") {
return Settings::LiteServerSelectionPreferenceMode::Random;
}
return Settings::LiteServerSelectionPreferenceMode::Sticky;
}
const char* liteServerSelectionPreferenceModeName(
Settings::LiteServerSelectionPreferenceMode mode)
{
switch (mode) {
case Settings::LiteServerSelectionPreferenceMode::Sticky:
return "sticky";
case Settings::LiteServerSelectionPreferenceMode::Random:
return "random";
}
return "sticky";
}
Settings::PoolSelectMode parsePoolSelectMode(const json& value)
{
if (!value.is_string()) return Settings::PoolSelectMode::Manual;
const std::string mode = value.get<std::string>();
if (mode == "auto_balance" || mode == "auto") {
return Settings::PoolSelectMode::AutoBalance;
}
return Settings::PoolSelectMode::Manual;
}
const char* poolSelectModeName(Settings::PoolSelectMode mode)
{
switch (mode) {
case Settings::PoolSelectMode::Manual: return "manual";
case Settings::PoolSelectMode::AutoBalance: return "auto_balance";
}
return "manual";
}
// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
// read so a malformed value (wrong type / missing key) leaves the field's default
// instead of throwing out of the whole load().
template <typename T>
bool jsonHasType(const json& v)
{
if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
else if constexpr (std::is_floating_point_v<T>) return v.is_number();
else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
else return false;
}
// Reads j[key] into field only when present AND of the matching type.
template <typename T>
void loadScalar(const json& j, const char* key, T& field)
{
if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
}
// Same as loadScalar but clamps the read value into [lo, hi].
template <typename T>
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
{
if (j.contains(key) && jsonHasType<T>(j[key]))
field = std::max(lo, std::min(hi, j[key].get<T>()));
}
} // namespace
std::string Settings::getDefaultPath()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return dir + "\\settings.json";
}
return "settings.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
const std::string dir = util::Platform::getConfigDir();
fs::create_directories(dir);
return dir + "/settings.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/settings.json";
#endif
return (fs::path(dir) / "settings.json").string();
}
bool Settings::load()
@@ -76,29 +135,29 @@ bool Settings::load(const std::string& path)
json j;
file >> j;
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
if (j.contains("language")) language_ = j["language"].get<std::string>();
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
loadScalar(j, "theme", theme_);
loadScalar(j, "save_ztxs", save_ztxs_);
loadScalar(j, "auto_shield", auto_shield_);
loadScalar(j, "use_tor", use_tor_);
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
loadScalar(j, "default_fee", default_fee_);
loadScalar(j, "fetch_prices", fetch_prices_);
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
loadScalar(j, "address_explorer_url", address_explorer_url_);
loadScalar(j, "language", language_);
loadScalar(j, "skin_id", skin_id_);
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
loadScalar(j, "acrylic_quality", acrylic_quality_);
loadScalar(j, "blur_multiplier", blur_multiplier_);
loadScalar(j, "noise_opacity", noise_opacity_);
loadScalar(j, "gradient_background", gradient_background_);
// Migrate legacy reduced_transparency bool -> ui_opacity float
if (j.contains("ui_opacity")) {
ui_opacity_ = j["ui_opacity"].get<float>();
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
}
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
loadScalar(j, "window_opacity", window_opacity_);
if (j.contains("balance_layout")) {
if (j["balance_layout"].is_string())
balance_layout_ = j["balance_layout"].get<std::string>();
@@ -112,7 +171,7 @@ bool Settings::load(const std::string& path)
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
}
}
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
loadScalar(j, "scanline_enabled", scanline_enabled_);
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
hidden_addresses_.clear();
for (const auto& a : j["hidden_addresses"])
@@ -123,38 +182,198 @@ bool Settings::load(const std::string& path)
for (const auto& a : j["favorite_addresses"])
if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
}
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
if (j.contains("address_meta") && j["address_meta"].is_object()) {
address_meta_.clear();
for (auto& [addr, meta] : j["address_meta"].items()) {
AddressMeta m;
if (meta.contains("label") && meta["label"].is_string())
m.label = meta["label"].get<std::string>();
if (meta.contains("icon") && meta["icon"].is_string())
m.icon = meta["icon"].get<std::string>();
if (meta.contains("order") && meta["order"].is_number_integer())
m.sortOrder = meta["order"].get<int>();
if (meta.contains("mining") && meta["mining"].is_boolean())
m.mining = meta["mining"].get<bool>();
address_meta_[addr] = m;
}
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
loadScalar(j, "unlock_duration", unlock_duration_);
loadScalar(j, "pin_enabled", pin_enabled_);
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
loadScalar(j, "max_connections", max_connections_);
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) {
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
lite["server_selection_mode"]);
}
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
}
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
lite_chain_name_ = lite["chain_name"].get<std::string>();
}
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
// value to "main" and flag a re-save so the corrected setting persists.
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
lite_chain_name_ != "regtest") {
lite_chain_name_ = "main";
needs_upgrade_save_ = true;
}
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
const auto seed = lite["random_selection_seed"].get<long long>();
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
}
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
}
if (lite.contains("servers") && lite["servers"].is_array()) {
lite_servers_.clear();
for (const auto& server : lite["servers"]) {
if (!server.is_object()) continue;
LiteServerPreference preference;
if (server.contains("url") && server["url"].is_string()) {
preference.url = server["url"].get<std::string>();
}
if (server.contains("label") && server["label"].is_string()) {
preference.label = server["label"].get<std::string>();
}
if (server.contains("enabled") && server["enabled"].is_boolean()) {
preference.enabled = server["enabled"].get<bool>();
}
lite_servers_.push_back(preference);
}
}
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
const auto v = lite["rollout_override"].get<std::string>();
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
}
if (lite.contains("install_id") && lite["install_id"].is_string()) {
lite_install_id_ = lite["install_id"].get<std::string>();
}
if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
lite_hidden_servers_.clear();
for (const auto& u : lite["hidden_servers"])
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
}
}
loadScalar(j, "verbose_logging", verbose_logging_);
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear();
for (const auto& c : j["debug_categories"])
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
}
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("font_scale") && j["font_scale"].is_number())
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
if (j.contains("window_width") && j["window_width"].is_number_integer())
window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>();
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
loadScalar(j, "low_spec_mode", low_spec_mode_);
loadScalar(j, "reduce_motion", reduce_motion_);
loadScalar(j, "selected_exchange", selected_exchange_);
loadScalar(j, "selected_pair", selected_pair_);
loadScalar(j, "pool_url", pool_url_);
// Migrate old default pool URL that was missing the stratum port
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
loadScalar(j, "pool_algo", pool_algo_);
loadScalar(j, "pool_worker", pool_worker_);
loadScalar(j, "pool_threads", pool_threads_);
loadScalar(j, "pool_tls", pool_tls_);
loadScalar(j, "pool_hugepages", pool_hugepages_);
loadScalar(j, "pool_mode", pool_mode_);
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
loadScalar(j, "mine_when_idle", mine_when_idle_);
loadScalar(j, "xmrig_version", xmrig_version_);
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
loadScalar(j, "idle_threads_active", idle_threads_active_);
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"])
if (u.is_string()) saved_pool_urls_.push_back(u.get<std::string>());
}
if (j.contains("saved_pool_workers") && j["saved_pool_workers"].is_array()) {
saved_pool_workers_.clear();
for (const auto& w : j["saved_pool_workers"])
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
}
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
portfolio_entries_.clear();
for (const auto& e : j["portfolio_entries"]) {
if (!e.is_object()) continue;
PortfolioEntry entry;
if (e.contains("label") && e["label"].is_string())
entry.label = e["label"].get<std::string>();
if (e.contains("addresses") && e["addresses"].is_array())
for (const auto& a : e["addresses"])
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
if (e.contains("icon") && e["icon"].is_string())
entry.icon = e["icon"].get<std::string>();
if (e.contains("color") && e["color"].is_number())
entry.color = e["color"].get<unsigned int>();
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
entry.outlineOpacity = e["outline_opacity"].get<int>();
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
entry.priceBasis = e["price_basis"].get<int>();
if (e.contains("manual_price") && e["manual_price"].is_number())
entry.manualPrice = e["manual_price"].get<double>();
if (e.contains("manual_currency") && e["manual_currency"].is_string())
entry.manualCurrency = e["manual_currency"].get<std::string>();
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
entry.showDrgx = e["show_drgx"].get<bool>();
if (e.contains("show_value") && e["show_value"].is_boolean())
entry.showValue = e["show_value"].get<bool>();
if (e.contains("show_24h") && e["show_24h"].is_boolean())
entry.show24h = e["show_24h"].get<bool>();
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
entry.showSparkline = e["show_sparkline"].get<bool>();
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
entry.sparklineInterval = e["sparkline_interval"].get<int>();
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
entry.gridCol = e["grid_col"].get<int>();
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
entry.gridRow = e["grid_row"].get<int>();
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
entry.gridW = e["grid_w"].get<int>();
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
entry.gridH = e["grid_h"].get<int>();
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
}
}
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
loadScalar(j, "window_width", window_width_);
loadScalar(j, "window_height", window_height_);
// Version tracking — detect upgrades so we can re-save with new defaults
loadScalar(j, "settings_version", settings_version_);
if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
DRAGONX_VERSION);
needs_upgrade_save_ = true;
}
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
// next save() doesn't silently overwrite it with defaults — the user's data stays
// recoverable. Proceed with in-memory defaults.
file.close();
std::error_code ec;
const std::string quarantine =
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
fs::rename(path, quarantine, ec);
if (!ec) {
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
}
return false;
}
}
@@ -197,17 +416,54 @@ bool Settings::save(const std::string& path)
j["favorite_addresses"] = json::array();
for (const auto& addr : favorite_addresses_)
j["favorite_addresses"].push_back(addr);
{
json meta_obj = json::object();
for (const auto& [addr, m] : address_meta_) {
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0 && !m.mining) continue;
json entry = json::object();
if (!m.label.empty()) entry["label"] = m.label;
if (!m.icon.empty()) entry["icon"] = m.icon;
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
if (m.mining) entry["mining"] = true;
meta_obj[addr] = entry;
}
j["address_meta"] = meta_obj;
}
j["wizard_completed"] = wizard_completed_;
j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_;
j["pin_enabled"] = pin_enabled_;
j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
{
json lite = json::object();
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
lite["sticky_server_url"] = lite_sticky_server_url_;
lite["chain_name"] = lite_chain_name_;
lite["random_selection_seed"] = lite_random_selection_seed_;
lite["persist_selected_server"] = lite_persist_selected_server_;
lite["servers"] = json::array();
for (const auto& server : lite_servers_) {
json entry = json::object();
entry["url"] = server.url;
entry["label"] = server.label;
entry["enabled"] = server.enabled;
lite["servers"].push_back(entry);
}
lite["rollout_override"] = lite_rollout_override_;
lite["install_id"] = lite_install_id_;
lite["hidden_servers"] = json::array();
for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
j["lite_wallet"] = lite;
}
j["verbose_logging"] = verbose_logging_;
j["debug_categories"] = json::array();
for (const auto& cat : debug_categories_)
j["debug_categories"].push_back(cat);
j["theme_effects_enabled"] = theme_effects_enabled_;
j["low_spec_mode"] = low_spec_mode_;
j["reduce_motion"] = reduce_motion_;
j["selected_exchange"] = selected_exchange_;
j["selected_pair"] = selected_pair_;
j["pool_url"] = pool_url_;
@@ -217,24 +473,56 @@ bool Settings::save(const std::string& path)
j["pool_tls"] = pool_tls_;
j["pool_hugepages"] = pool_hugepages_;
j["pool_mode"] = pool_mode_;
j["pool_select_mode"] = poolSelectModeName(pool_select_mode_);
j["mine_when_idle"] = mine_when_idle_;
j["xmrig_version"] = xmrig_version_;
j["mine_idle_delay"]= mine_idle_delay_;
j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_;
j["idle_threads_idle"] = idle_threads_idle_;
j["idle_gpu_aware"] = idle_gpu_aware_;
j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u);
j["saved_pool_workers"] = json::array();
for (const auto& w : saved_pool_workers_)
j["saved_pool_workers"].push_back(w);
j["portfolio_entries"] = json::array();
for (const auto& e : portfolio_entries_) {
json entry;
entry["label"] = e.label;
entry["addresses"] = json::array();
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
entry["icon"] = e.icon;
entry["color"] = e.color;
entry["outline_opacity"] = e.outlineOpacity;
entry["price_basis"] = e.priceBasis;
entry["manual_price"] = e.manualPrice;
entry["manual_currency"] = e.manualCurrency;
entry["show_drgx"] = e.showDrgx;
entry["show_value"] = e.showValue;
entry["show_24h"] = e.show24h;
entry["show_sparkline"] = e.showSparkline;
entry["sparkline_interval"] = e.sparklineInterval;
entry["grid_col"] = e.gridCol;
entry["grid_row"] = e.gridRow;
entry["grid_w"] = e.gridW;
entry["grid_h"] = e.gridH;
j["portfolio_entries"].push_back(std::move(entry));
}
j["font_scale"] = font_scale_;
j["settings_version"] = std::string(DRAGONX_VERSION);
if (window_width_ > 0 && window_height_ > 0) {
j["window_width"] = window_width_;
j["window_height"] = window_height_;
}
try {
// Ensure directory exists
fs::path p(path);
fs::create_directories(p.parent_path());
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << j.dump(4);
return true;
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
// A crash mid-write can no longer truncate settings.json (which would silently
// reset every preference on the next launch). Owner-only (0600) — it carries the
// lite-server list and address metadata.
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
return false;

View File

@@ -4,8 +4,12 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <set>
#include <vector>
namespace dragonx {
namespace config {
@@ -51,6 +55,51 @@ public:
*/
static std::string getDefaultPath();
enum class LiteServerSelectionPreferenceMode {
Sticky,
Random
};
// Pool selection mode for the mining tab: Manual (user picks the pool) or
// AutoBalance (the wallet spreads miners across the official pools by hashrate).
enum class PoolSelectMode {
Manual,
AutoBalance
};
struct LiteServerPreference {
std::string url;
std::string label;
bool enabled = true;
};
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
// The Market tab's portfolio card sums these addresses' balances under the label.
struct PortfolioEntry {
std::string label;
std::vector<std::string> addresses;
std::string icon; // project_icons wallet-icon name; empty = no icon
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
// (show DRGX + USD value).
int priceBasis = 0;
double manualPrice = 0.0; // price per DRGX for the Manual basis
std::string manualCurrency = "USD";
bool showDrgx = true; // show the DRGX amount on the card
bool showValue = true; // show the converted/fiat value on the card
bool show24h = false; // show the 24h % change (live-market bases only)
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
int gridCol = -1;
int gridRow = -1;
int gridW = 8; // default group width (cells); minimum is 8
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
};
// Theme
std::string getTheme() const { return theme_; }
void setTheme(const std::string& theme) { theme_ = theme; }
@@ -139,6 +188,54 @@ public:
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
// Address metadata (labels, icons, custom ordering)
struct AddressMeta {
std::string label;
std::string icon; // material icon name, e.g. "savings"
int sortOrder = -1; // -1 = auto (use default sort)
bool mining = false;
};
const AddressMeta& getAddressMeta(const std::string& addr) const {
static const AddressMeta empty{};
auto it = address_meta_.find(addr);
return it != address_meta_.end() ? it->second : empty;
}
void setAddressLabel(const std::string& addr, const std::string& label) {
address_meta_[addr].label = label;
}
void setAddressIcon(const std::string& addr, const std::string& icon) {
address_meta_[addr].icon = icon;
}
void setAddressSortOrder(const std::string& addr, int order) {
address_meta_[addr].sortOrder = order;
}
bool isMiningAddress(const std::string& addr) const {
auto it = address_meta_.find(addr);
return it != address_meta_.end() && it->second.mining;
}
void setMiningAddress(const std::string& addr, bool mining) {
address_meta_[addr].mining = mining;
}
std::set<std::string> getMiningAddresses() const {
std::set<std::string> addresses;
for (const auto& [addr, meta] : address_meta_) {
if (meta.mining) addresses.insert(addr);
}
return addresses;
}
int getNextSortOrder() const {
int mx = -1;
for (const auto& [k, v] : address_meta_)
if (v.sortOrder > mx) mx = v.sortOrder;
return mx + 1;
}
void swapAddressOrder(const std::string& a, const std::string& b) {
int oa = address_meta_[a].sortOrder;
int ob = address_meta_[b].sortOrder;
address_meta_[a].sortOrder = ob;
address_meta_[b].sortOrder = oa;
}
// First-run wizard
bool getWizardCompleted() const { return wizard_completed_; }
void setWizardCompleted(bool v) { wizard_completed_ = v; }
@@ -163,6 +260,47 @@ public:
bool getStopExternalDaemon() const { return stop_external_daemon_; }
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
// Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
std::string getLiteChainName() const { return lite_chain_name_; }
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
void setPortfolioEntries(const std::vector<PortfolioEntry>& entries) { portfolio_entries_ = entries; }
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
void hideLiteServer(const std::string& url) { lite_hidden_servers_.insert(url); }
void unhideLiteServer(const std::string& url) { lite_hidden_servers_.erase(url); }
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
// Never transmitted; carries no PII. Generated on first use if empty.
std::string getLiteInstallId() const { return lite_install_id_; }
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
// Daemon — debug logging categories
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
@@ -180,6 +318,10 @@ public:
bool getLowSpecMode() const { return low_spec_mode_; }
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
// Reduce motion — disables animated transitions for accessibility
bool getReduceMotion() const { return reduce_motion_; }
void setReduceMotion(bool v) { reduce_motion_ = v; }
// Market — last selected exchange + pair
std::string getSelectedExchange() const { return selected_exchange_; }
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
@@ -201,6 +343,53 @@ public:
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
bool getPoolMode() const { return pool_mode_; }
void setPoolMode(bool v) { pool_mode_ = v; }
PoolSelectMode getPoolSelectMode() const { return pool_select_mode_; }
void setPoolSelectMode(PoolSelectMode v) { pool_select_mode_ = v; }
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
std::string getXmrigVersion() const { return xmrig_version_; }
void setXmrigVersion(const std::string& v) { xmrig_version_ = v; }
// Mine when idle (auto-start mining when system is idle)
bool getMineWhenIdle() const { return mine_when_idle_; }
void setMineWhenIdle(bool v) { mine_when_idle_ = v; }
int getMineIdleDelay() const { return mine_idle_delay_; }
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
// Idle thread scaling — scale thread count instead of start/stop
bool getIdleThreadScaling() const { return idle_thread_scaling_; }
void setIdleThreadScaling(bool v) { idle_thread_scaling_ = v; }
int getIdleThreadsActive() const { return idle_threads_active_; }
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
int getIdleThreadsIdle() const { return idle_threads_idle_; }
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
bool getIdleGpuAware() const { return idle_gpu_aware_; }
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
// Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
void addSavedPoolUrl(const std::string& url) {
// Don't add duplicates
for (const auto& u : saved_pool_urls_) if (u == url) return;
saved_pool_urls_.push_back(url);
}
void removeSavedPoolUrl(const std::string& url) {
saved_pool_urls_.erase(
std::remove(saved_pool_urls_.begin(), saved_pool_urls_.end(), url),
saved_pool_urls_.end());
}
// Saved pool worker addresses (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolWorkers() const { return saved_pool_workers_; }
void addSavedPoolWorker(const std::string& addr) {
for (const auto& a : saved_pool_workers_) if (a == addr) return;
saved_pool_workers_.push_back(addr);
}
void removeSavedPoolWorker(const std::string& addr) {
saved_pool_workers_.erase(
std::remove(saved_pool_workers_.begin(), saved_pool_workers_.end(), addr),
saved_pool_workers_.end());
}
// Font scale (user accessibility setting, 1.01.5)
float getFontScale() const { return font_scale_; }
@@ -211,6 +400,10 @@ public:
int getWindowHeight() const { return window_height_; }
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
// Returns true once after an upgrade (version mismatch detected on load)
bool needsUpgradeSave() const { return needs_upgrade_save_; }
void clearUpgradeSave() { needs_upgrade_save_ = false; }
private:
std::string settings_path_;
@@ -231,32 +424,73 @@ private:
float blur_multiplier_ = 0.10f;
float noise_opacity_ = 0.5f;
bool gradient_background_ = false;
#ifdef _WIN32
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.31.0, 1.0 = opaque)
float window_opacity_ = 0.75f; // Background alpha (0.31.0, <1 = desktop visible)
#else
float ui_opacity_ = 1.0f; // Mac/Linux: default fully opaque
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
#endif
std::string balance_layout_ = "classic";
bool scanline_enabled_ = true;
std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false;
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
// Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here.
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
std::size_t lite_random_selection_seed_ = 0;
bool lite_persist_selected_server_ = true;
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
std::string lite_install_id_; // random local-only id; rollout-bucket source
std::vector<LiteServerPreference> lite_servers_ = {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
bool verbose_logging_ = false;
std::set<std::string> debug_categories_;
bool theme_effects_enabled_ = true;
bool low_spec_mode_ = false;
std::string selected_exchange_ = "TradeOgre";
std::string selected_pair_ = "DRGX/BTC";
bool reduce_motion_ = false;
std::string selected_exchange_ = "Nonkyc.io";
std::string selected_pair_ = "DRGX/USDT";
// Pool mining
std::string pool_url_ = "pool.dragonx.is";
std::string pool_url_ = "pool.dragonx.is:3433";
std::string pool_algo_ = "rx/hush";
std::string pool_worker_ = "x";
std::string pool_worker_ = "";
int pool_threads_ = 0;
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
PoolSelectMode pool_select_mode_ = PoolSelectMode::Manual; // manual vs auto-balance pool choice
std::string xmrig_version_; // installed DRG-XMRig release tag (update detection)
bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
int idle_threads_active_ = 0; // threads when user active (0 = auto)
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
// Font scale (user accessibility, 1.03.0; 1.0 = default)
float font_scale_ = 1.0f;
@@ -264,6 +498,10 @@ private:
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
int window_width_ = 0;
int window_height_ = 0;
// Wallet version that last wrote this settings file (for upgrade detection)
std::string settings_version_;
bool needs_upgrade_save_ = false; // true when version changed
};
} // namespace config

View File

@@ -1,28 +1,3 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#define DRAGONX_VERSION "1.0.0"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 0
#define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"
#include "dragonx_generated_version.h"

32
src/config/version.h.in Normal file
View File

@@ -0,0 +1,32 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// !! DO NOT EDIT generated version output — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) for the full-node app,
// !! or DRAGONX_LITE_VERSION for ObsidianDragonLite. DRAGONX_APP_VERSION is the active variant.
#define DRAGONX_VERSION "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@"
#define DRAGONX_VERSION_MAJOR @DRAGONX_APP_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @DRAGONX_APP_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @DRAGONX_APP_VERSION_PATCH@
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"

Some files were not shown because too many files have changed in this diff Show More