7 Commits
Author SHA1 Message Date
dan_s f12470f7f7 UX improvements 2026-02-28 00:58:43 -06:00
DanS f8d828c97a Update README.md 2026-02-28 00:53:41 -06:00
dan_s 6eeadcb595 console and mining tab visual improvements 2026-02-27 13:30:06 -06:00
dan_s b4e09cdf75 Reduce redundant RPC calls in periodic refresh cycle
- Remove getinfo from refreshMiningInfo slow path; daemon_version,
  protocol_version, and p2p_port are static per connection (set in
  onConnected). Move longestchain/notarized into refreshBalance's
  existing getblockchaininfo callback.

- Remove refreshMiningInfo from refreshData(); it already runs on
  the 1-second fast_refresh_timer_ independently.

- Make refreshAddresses demand-driven via addresses_dirty_ flag;
  only re-fetch when a new address is created or a send completes,
  not unconditionally every 5 seconds.

- Gate refreshPeerInfo to only run when the Peers tab is active.

- Skip duplicate getwalletinfo on connect; onConnected() already
  prefetches it for immediate lock-screen display, so suppress the
  redundant call in the first refreshData() cycle.

Steady-state savings: ~8 fewer RPC calls per 5-second cycle
(from ~12+ down to ~4-5 in the common case).
2026-02-27 02:28:29 -06:00
dan_s b1d87a65f8 added visual feedback to mining button to show miner is in process of startup/shutdown 2026-02-27 00:48:13 -06:00
dan_s d90e271097 update README link 2026-02-27 00:29:35 -06:00
dan_s c809666624 ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
2026-02-27 00:26:01 -06:00
538 changed files with 34203 additions and 177211 deletions
+173
View File
@@ -0,0 +1,173 @@
# 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` |
+3 -32
View File
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
!prebuilt-binaries/dragonxd-win/.gitkeep
prebuilt-binaries/dragonxd-mac/*
!prebuilt-binaries/dragonxd-mac/.gitkeep
prebuilt-binaries/drg-xmrig/*
!prebuilt-binaries/drg-xmrig/.gitkeep
prebuilt-binaries/xmrig-hac/*
!prebuilt-binaries/xmrig-hac/.gitkeep
# External sources / toolchains (created by scripts/setup.sh)
@@ -32,33 +32,4 @@ imgui.ini
*.bak
*.bak*
*.params
asmap.dat
# Wallet files hold PRIVATE KEYS — never commit them
wallet.dat
wallet-*.dat
wallet.dat.*
/external/drg-xmrig
/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
# Cross-built mingw FreeType (color emoji) — regenerated by scripts/build-freetype-mingw.sh
third_party/freetype-mingw/
third_party/.freetype-mingw-build/
asmap.dat
-58
View File
@@ -1,58 +0,0 @@
# Changelog
All notable user-facing changes to ObsidianDragon are documented here. The format loosely
follows [Keep a Changelog](https://keepachangelog.com/); the project uses Conventional Commits.
## [Unreleased]
### ⚠️ Breaking changes
- **Remote RPC over plain HTTP is now refused by default.** If your wallet is configured to
reach a **remote** `rpchost`/`rpcconnect` **without TLS**, it will no longer connect — it
previously sent your `rpcuser`/`rpcpassword` in cleartext (capturable by anyone on the
network path) after only a dismissible warning. To reconnect, either:
- add **`rpctls=1`** to `DRAGONX.conf` (preferred, if your daemon supports TLS), or
- add **`rpcallowplaintext=1`** to `DRAGONX.conf` to explicitly accept the plaintext link.
Local and embedded daemons (`127.0.0.0/8`, `localhost`, `::1`) are unaffected.
### Security
- Refuse remote plaintext RPC credential transmission by default (see Breaking changes above).
- Tightened localhost detection: a hostname that merely *starts* with `127.` (e.g.
`127.evil.com`) is no longer mistaken for a loopback address, so it can no longer bypass the
plaintext-RPC protection.
- Sapling parameters are now integrity-checked (SHA-256) against pinned canonical digests
before use, instead of only checking that the files exist. A truncated or corrupt parameter
file is caught up front rather than surfacing later as a confusing shielded-operation failure.
(Cached via a `size:mtime` marker so it doesn't re-hash ~48 MB on every launch.)
### Fixed
- Daemon crashes are no longer occasionally missed: a race between the UI thread and the
process monitor could consume the daemon's exit status, hiding a crash and defeating the
automatic-restart cap. The monitor is now the sole reaper.
- A daemon that fails to launch (missing execute permission, wrong architecture, corrupt
binary) now reports a precise error immediately instead of briefly showing "running" and
then a generic "exited unexpectedly (exit code 127)".
- A quick stop→start no longer triggers a restart storm: the wallet now waits briefly for a
previous daemon to release the data-directory lock and shows a clear, non-crash message
instead of exhausting the crash-restart budget.
- Failures while writing the daemon binaries or Sapling parameters (disk full, permission
denied) are now surfaced clearly up front instead of failing opaquely when the daemon later
can't start.
- Directory-creation failures on startup (read-only home, permission denied) now produce a
clear "Cannot create <dir>" message instead of a confusing downstream "config missing" /
"binary not found" error (or, in one path, an uncaught exception).
### Added
- A "Taking longer than expected" notice now appears if the daemon is reachable but hasn't
finished initializing after ~45 s (configurable via `ui.toml`), with guidance to restart the
daemon or open the Console — instead of an indefinite silent spinner. It clears itself
automatically once the daemon connects.
---
Engineering detail and the finding-by-finding rationale for this batch live in
`docs/daemon-startup-hardening.md`.
-111
View File
@@ -1,111 +0,0 @@
# 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/*`): the HushChat protocol port (Contacts/Chat tabs, seed-derived identity, secretstream crypto, seed-encrypted sqlite store, two-variant send/receive transport). Runtime behavior is gated by `DRAGONX_ENABLE_CHAT`, now **default ON** (the sources always compile; the flag folds the feature away at runtime via `hushChatFeatureEnabledAtBuild()`). Full-node chat derives its identity from the wallet's mnemonic (`z_exportmnemonic`, portable/SDXLite-compatible) or, for legacy/non-mnemonic wallets, a stable z-address spending key (`z_exportkey`).
## 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. The dialog is a two-pane version picker (every `/releases` entry on the left, newest first, pre-releases included) so users can pin an older or pre-release build — same verify/install path via `startInstallRelease()`. It shares only the `ReleaseRow` row model with the daemon updater (`ui/windows/release_list_view.h`); each dialog renders its own tactile Material list.
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
## 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()`). The dialog is a two-pane version picker (every `/releases` entry on the left) so users can pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. It shares only the `ReleaseRow` row model (`ui/windows/release_list_view.h`) with the miner updater; each renders its own tactile Material list.
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
## Seed phrase & migrate-to-seed (full node)
Full-node wallets are **BIP39-mnemonic-backed** and can **migrate a legacy (non-mnemonic) wallet into a seed wallet**. All of this **requires the `hd-transparent-keys`/`dev` daemon** (`z_exportmnemonic` + `-usemnemonic`); the older bundled binary lacks those RPCs, so these features degrade gracefully (chat falls back to a `z_exportkey` identity; the backup screen shows a "legacy wallet" note). The daemon source is vendored at `external/dragonx/` (build with its own `./build.sh`); deploy the built `dragonxd`/`dragonx-cli`/`dragonx-tx` into the wallet's daemon dir (`build/*/bin`) or via the daemon updater.
- **New wallets get a phrase.** `EmbeddedDaemon::getChainParams()` always passes `-usemnemonic=1`. The daemon reads it **only inside `GenerateNewSeed()` when a wallet has no seed yet**, so it is inert on existing wallets (safe to pass unconditionally) and makes every fresh wallet mnemonic-backed.
- **Back up seed phrase.** Settings → Backup & Data → "Seed phrase" opens `renderSeedBackupDialog` (`App::exportSeedPhrase` → `z_exportmnemonic`, wiped via `sodium_memzero`). A one-time nudge (`maybeRemindSeedBackup`, settings flag `seed_backup_reminded`) reminds mnemonic-wallet users to back up.
- **Migrate-to-seed** (`showSeedMigrationDialog` / `renderSeedMigrationDialog`, state machine `SeedMigrationStep`). **Phase 1 — create:** `daemon::SeedWalletCreator` runs a **second, isolated `dragonxd`** on its own port + throwaway datadir (`<config>/seed-migrate/DRAGONX`, basename MUST be the acname; `-usemnemonic=1 -connect=0`; RPC is plaintext http, not TLS), mints a mnemonic wallet, exports its seed + a sweep-target z-address, then stops. Uses the one-shot `EmbeddedDaemon::setNextStartOverride` + `setSkipPortCheck`. **Phase 2 — sweep + adopt:** `z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]` sweeps all funds to the new address; a **Confirming gate** (`pollSweepStatus`) only allows adopt once the sweep tx is **mined (≥1 conf) AND the legacy balance is ~0** (offering a remainder re-sweep); adopt (`beginAdoptSeedWallet`, background) stops the daemon, moves `wallet.dat` aside to a **timestamped `.bak` (never deleted, restored on failure)**, installs the new wallet, and restarts with `-rescan`. Fund-moving code — **two rounds of adversarial review + a live mainnet run** gate it; the pending stage persists (`seed_migration_*` settings) so a restart resumes at sweep/confirm.
## 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.
- **DPI / display scaling:** `schema::UI()` returns **logical (raw) px**; `Layout::dpiScale()` (= OS DPI × the in-app font-scale) is the single scale factor. The `Layout::k*()` helpers and `BeginOverlayDialog` already fold it in, and ImGui auto-layout scales via the DPI-rebuilt font atlas + `ScaleAllSizes` — so tabs/standard widgets scale for free. But **hand-drawn absolute geometry** (`dl->AddText` at manual `cy += 24.0f` offsets, `SetNextWindowSize`, explicit `ImVec2(width,0)` button sizes, `SameLine(x)` column strides) is immune to all of that and must be multiplied by `ui::Layout::dpiScale()` yourself, or it renders native-size (tiny) on a HiDPI display. Font metrics (`font->LegacySize`, `CalcTextSize`) are already scaled — don't double-scale those. Verify at scale with a full sweep run at `font_scale: 1.5` (same `dpiScale()==1.5` code path as OS 150%).
- **i18n:** user-facing strings are translated via `src/util/i18n`; the English source of truth is the `strings_[...]` map in `src/util/i18n.cpp`, and the per-language translations live as the **source of truth** in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). **Edit those JSONs directly and additively** — write with `json.dump(..., indent=4, sort_keys=True, ensure_ascii=False)`; never bulk-regenerate/overwrite a whole file (that path silently dropped ~285 keys/language before). `scripts/add_missing_translations.py` back-fills only the keys missing from a JSON (non-destructive), and `scripts/build_cjk_subset.py` rebuilds the CJK subset font (`res/fonts/NotoSansCJK-Subset.ttf`) after new CJK glyphs are added.
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.
+40 -843
View File
File diff suppressed because it is too large Load Diff
+2 -2
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.dragonx.is/<your-username>/ObsidianDragon.git
git clone https://git.hush.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.dragonx.is/dragonx/ObsidianDragon/issues)
- Use the [issue tracker](https://git.hush.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.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+29 -40
View File
@@ -1,8 +1,6 @@
# ObsidianDragon - DragonX Wallet
# DragonX Wallet - ImGui Edition
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
Current pre-release: **1.2.0-rc1**.
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
![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)
@@ -11,13 +9,10 @@ Current pre-release: **1.2.0-rc1**.
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
- **Shielded Transactions**: Full z-address support with encrypted memos
- **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
- **Integrated Mining**: CPU mining controls with hashrate monitoring
- **Market Data**: Real-time price charts from CoinGecko
- **QR Codes**: Generate and display QR codes for receiving addresses
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
- **Multi-language**: i18n support (English, Spanish, more coming)
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
- **Fast Builds**: Compiles in seconds, not minutes
@@ -38,10 +33,10 @@ Current pre-release: **1.2.0-rc1**.
The setup script detects your OS, installs all build dependencies, and validates your environment:
```bash
./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
./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
```
### Manual Prerequisites
@@ -76,20 +71,20 @@ brew install cmake
### Binaries
Download linux and windows binaries of latest releases and place in binary directories:
**DragonX daemon** (https://git.dragonx.is/DragonX/dragonx):
**DragonX daemon** (https://git.hush.is/dragonx/hush3):
- prebuilt-binaries/dragonxd-linux/
- prebuilt-binaries/dragonxd-win/
- prebuilt-binaries/dragonxd-mac/
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
- prebuilt-binaries/drg-xmrig/
**xmrig HAC fork** (https://git.hush.is/dragonx/xmrig-hac):
- prebuilt-binaries/xmrig-hac/
## Build Steps
```
### Clone repository (if not already)
git clone https://git.dragonx.is/dragonx/ObsidianDragon.git
git clone https://git.hush.is/dragonx/ObsidianDragon.git
cd ObsidianDragon/
```
@@ -121,8 +116,7 @@ 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:
@@ -134,13 +128,12 @@ 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`, `~/dragonx/src`, etc.)
3. System-wide locations (`/usr/local/bin`, `~/hush3/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
@@ -155,46 +148,44 @@ ObsidianDragon/
├── src/
│ ├── main.cpp # Entry point, SDL/ImGui setup
│ ├── app.cpp/h # Main application class
│ ├── data/ # WalletState, address book, exchange info
│ ├── config/ # Settings persistence and committed/generated version.h
│ ├── wallet_state.h # Wallet data structures
│ ├── version.h # Version definitions
│ ├── ui/
│ │ ├── schema/ # TOML UI schema and skin manager
│ │ ├── material/ # Material components, typography, layout
│ │ ├── windows/ # Tabs and dialogs
│ │ └── pages/ # Multi-page screens such as Settings
│ │ ├── theme.cpp/h # DragonX theme
│ │ └── windows/ # UI tabs and dialogs
│ ├── rpc/
│ │ ├── rpc_client.cpp # JSON-RPC client
│ │ └── connection.cpp # Daemon connection
│ ├── resources/ # Embedded resource extraction
│ ├── platform/ # Windows DX11/backdrop helpers
│ ├── config/
│ │ └── settings.cpp # Settings persistence
│ ├── util/
│ │ ├── i18n.cpp # Internationalization
│ │ └── ...
│ └── daemon/
│ └── embedded_daemon.cpp
├── res/
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
│ ├── fonts/ # Ubuntu font
│ └── lang/ # Translation files
├── libs/
│ └── qrcode/ # QR code generation
├── CMakeLists.txt
├── build.sh # Release/cross-platform build script
└── scripts/create-appimage.sh # AppImage packaging
├── build-release.sh # Build script
└── create-appimage.sh # AppImage packaging
```
## Dependencies
Fetched or discovered by CMake:
Fetched automatically by CMake (no manual install needed):
- **[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/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
Bundled in `libs/`:
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
- **[libsodium](https://libsodium.org)** — Cryptographic operations (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)
@@ -211,11 +202,9 @@ 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
@@ -234,4 +223,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.dragonx.is/dragonx/ObsidianDragon
- Source: https://git.hush.is/dragonx/ObsidianDragon
-432
View File
@@ -1,432 +0,0 @@
# Sent-history sweep: GUI watermark fix, then daemon follow-up
**Audience:** ObsidianDragon (GUI) team and the DragonX daemon maintainer.
**Date:** 2026-10-16. **Status:** proposal.
**Code cited**
- GUI: `ObsidianDragon` @ `064c9d1` (unprefixed paths).
- Daemon: `dragonx` @ `e9247f5c6` (`perf/history-rpc-copies`), paths prefixed `dx:`. `test/v1.3.1-candidate` @ `ff28e34c8` is the same plus a crash fix; on that branch, `dx:wallet.cpp` lines past ~1263 shift by +10.
Anything not marked *(inference)* or *(estimate)* was verified in the code or measured.
---
## 1. Problem
**The wallet measured:** an isolated copy of the owner's mainnet wallet, run in a network namespace with no peers.
- 16,206 Sapling notes, all unspent.
- 5 z-addresses and 11,889 wallet txs.
- Unencrypted; `wallet.dat` is about 2.1 GB.
| Call | v1.3.0 | After daemon PR-A+B+C |
|---|---|---|
| Balance RPCs | ~14 s | ~50 ms |
| Windowed `z_listsentbyaddress` (filter type 2) | 19.5 s | 128 ms |
| **Full `z_listsentbyaddress` (no watermark)** | **424 s**; RSS 8 → 19.4 GB, never released | **113 s** |
| Full-sweep reply | 26.2 MB (all 11,889 txs) | same |
### Why every launch pays for the full sweep
- **The watermark lives only in memory.** `history_sent_scan_height_` starts at `-1` (`src/app.h:1764`).
- **Every disconnect resets it to `-1`.** That covers daemon restarts, reconnects and wallet switches. The path is `onDisconnected` → `resetTransactionHistoryCacheSession` → `invalidateShieldedHistoryScanProgress(false)` (`src/app_network.cpp:936`, `:1847`, `:1889-1895`).
- **Other resets:**
- a locked-wallet connect;
- imports (`:4013`);
- rescans (`:7279`, `:7302`; `src/app.cpp:6298`) and zap (`app.cpp:6352`);
- a new, never-scanned z-address (`app_network.cpp:2593-2597`);
- the end of a warmup during which a rescan was observed (`:2186-2195`).
- **No watermark means a full sweep.** `sentSweepIsFull` is then true (`src/services/history_scan_policy.cpp:35-38`). The GUI sends `[z,0]` via `callLong(..., kFullSentSweepTimeoutSec = 120)` (`src/services/network_refresh_service.cpp:1324-1325`, `history_scan_policy.h:31`).
- **The daemon ignores the address argument.** It walks every wallet tx under `LOCK2(cs_main, cs_wallet)` and calls `zsWalletTxJSON(wtx, ret, "*", false, 0, …)` for each one (`dx:src/wallet/rpcwallet.cpp:3719-3746`; the call is at `:3743`).
- **The full sweep barely fits, or never fits, the timeout.**
- On the fixed daemon the sweep finishes only **7 s inside** the 120 s timeout.
- On v1.3.0 it never finishes. Failures back off 10 min → 1 h → 6 h (`fullSentSweepRetryDelaySec`). That backoff is also kept only in memory (`app.h:1765-1766`).
### What the GUI uses from the reply
- **Sweep consumer** (`network_refresh_service.cpp:1328-1332`, `appendSentTransparentOutputs` ~`:1076-1123`):
- It reads only `txid`, `confirmations`, `blocktime`/`time` and `sends.transparentSends`.
- It never reads `received` (20.8 MB of the 26.2 MB) or `spends`.
- **Shielded sends come from enrichment.** Every txid goes into `knownTxids` for `z_viewtransaction` enrichment, capped at 25 per cycle (`app.h:1753`). The rest wait in the in-memory `history_enrich_backlog_`.
- **One exception:** `locateSweepTxid` (seed-migration resume) does read `sends.saplingSends` (`app_network.cpp:6394`).
---
## 2. Part 1: GUI fix (persist the watermark)
**Goal:** a relaunch, reconnect or daemon restart runs only the windowed sweep, `[z,0,2,(tip-W)+margin]`. A full sweep then happens only in three cases:
- there is no persisted state;
- the persisted state fails verification;
- the periodic safety-net re-sweep is due.
### 2.1 Where to store it
**Use the existing encrypted history snapshot.** Store an optional `sent_sweep` object in its JSON payload, not in `settings.json` or `wallets.json`. Why:
- The watermark is only valid together with the rows it produced. Both are written in one `INSERT OR REPLACE` in WAL mode (`src/data/transaction_history_cache.cpp:438-457`), so a crash leaves the previous pair intact.
- The snapshot already provides what the watermark needs:
- per-wallet keying (BLAKE2b of the sorted z+t address set, `:157-186`);
- encryption (Argon2id from the passphrase, or salted BLAKE2b when the wallet is unencrypted);
- a tip check on load (`:338-344`);
- one-wallet pruning (`pruneOtherWallets`, `:655-672`).
- **Keep `kSchemaVersion = 1`.** `load()` rejects any other version (`:351`), so a bump would throw away every user's cache.
- Unknown keys are ignored, so older builds read new payloads safely.
- A missing `sent_sweep` means `W = -1`, which is today's behaviour.
| Field | Type | Meaning |
|---|---|---|
| `height` | int | `W`: the tip used for the sweep's `maxDepth`. Read in the same worker job, right before the call |
| `anchor_height` | int | `A = W - margin`: the bottom edge of the window the sweep covered |
| `anchor_hash` | hex | `H_A = getblockhash(A)`, fetched by the worker after the sweep succeeds |
| `backlog` | [txid] | `history_enrich_backlog_`. **Required**, see 2.4 |
| `full_failures`, `full_retry_delay_s` | int | Full-sweep backoff. The delay is relative to save time, not absolute |
| `daemon_id` | string | Daemon version + start time, from `getinfo` / uptime. If it changes, the restored backoff resets (2.5) |
| `last_full_height`, `last_full_time` | int | Last successful full sweep; drives the safety net (2.6) |
| `txcount`, `k` | int | Optional tripwire (2.3). Both captured **in the sweep's worker job** |
Do **not** reuse the snapshot's `tip_height`/`tip_hash` columns. They record the tip at store time, not at sweep time (`app_network.cpp:2069-2070`).
### 2.2 Code changes
| File | Change |
|---|---|
| `src/data/transaction_history_cache.h:20-28`, `:33-48`, `:79-84` | Add a `SentSweepState` struct to `LoadResult`, `SnapshotJob` and `replace()` |
| `src/data/transaction_history_cache.cpp:412-426` (`writeSnapshot`) | Emit `sent_sweep` only if `0 < A <= W <= job.tipHeight` |
| `src/data/transaction_history_cache.cpp:351-370` (`load`) | Parse with defaults (`W=-1`, empty backlog). Drop the object if it has the wrong type or `W` is above the stored `tip_height` |
| `src/data/transaction_history_store_policy.h:35-41` / `.cpp:62-113` | Add `W`/`A`/`H_A` to the `slow` signature. They move every block, so this means a write every 600 s and always on disconnect, lock and exit. Add the backlog to `content` (written within about 20 s). Otherwise an invalidation that only changes the watermark is skipped as "unchanged". Update both callers (`app_network.cpp:2012-2014`, `:2041-2045`). The epoch counter is never persisted, so it doesn't belong in a signature |
| `src/app_network.cpp:2066-2079` (`storeTransactionHistoryCacheIfAvailable`) | Fill `job->sentSweep` from the `history_sent_*` and backlog members |
| `src/app_network.cpp:1998-2017` (`loadTransactionHistoryCacheIfAvailable`) | Restore the members. `++history_sent_epoch_`. Set `history_sent_verify_pending_ = true`. Apply any **pending rescan action** (2.3) |
| `src/app_network.cpp:2028` | The store returns early when there are no rows. On a `persistCache=true` invalidation with no rows, call `clearWallet()` so a stale W can't survive on disk |
| `src/services/network_refresh_service.h:256-266`, `:299` | New snapshot/result fields: `anchorHeight`/`anchorHash` in, the verify result and `H_A` out, and the band cursor |
| `src/services/network_refresh_service.cpp:1314-1345` | Read the tip immediately before the call. After success, fetch `getblockhash(A)`. Add the verify step (2.3) and the large-window path (2.5) |
| `src/app_network.cpp:2784-2792` | Gate both the end-of-pass sweep and the **mid-pass** windowed sweep (`allowMidPassSentSweep`) on the pending verify. Extend the retry gate at `:2792`, which today applies only when `W < 0`, to large windows |
| `src/app_network.cpp:2843-2854` | Count large-window failures toward the backoff; today only `sentSweepFull` failures count (`:2849`). Store the band cursor in `App` and reset it with `history_sent_epoch_`, like the watermark |
| `src/app.cpp:restoreOriginalWallet` | Call `invalidateShieldedHistoryScanProgress(true)` before `onDisconnected`, as `rescanBlockchain` and zap already do (`app.cpp:6298`, `:6352`) |
| `src/services/history_scan_policy.{h,cpp}` | Pure helpers: `sentSweepAnchor(W)`, `sentSweepNeedsLongCall(tip, W)`, `sentSweepMargin(firstAfterLoad)`, `nextSentSweepBand(...)` |
### 2.3 Invalidation and safety rules
**Verify at the bottom edge of the window, not at its tip.**
- **The check:** before the first windowed sweep after a load, call `getblockhash(A)` and compare it with `H_A`.
- **Match:** any fork point lies above `A`, so it is inside the window. Run the windowed sweep.
- **Mismatch:** a reorg went deeper than the margin, or this is a different chain or datadir. Drop W and run the banded full sweep (2.5).
- **Why not the tip:** checking `getblockhash(W)` would trigger a full sweep on an ordinary one-block reorg of the tip, even though the window already covers that reorg.
| Event | Action | Mechanism |
|---|---|---|
| First windowed sweep after a load | Anchor verify (above) | `getblockhash`: O(1), under `cs_main` |
| Stored `tip_height` > current tip at load (replay after an unclean stop, older datadir) | **Today:** `load()` deletes the whole snapshot (`cache.cpp:338-344`, `clearWalletByHash` at `:341`). Rows and W are lost, and a full sweep runs | Keep as the default. **Optional improvement:** have `load()` keep the rows and `sent_sweep`, flag "ahead", and defer the sent sweep until `tip >= A`, then verify `H_A`. That requires changes in `sentSweepIsFull` (`history_scan_policy.cpp:37`: `W > tip` means full today), the gate at `app_network.cpp:2789-2792`, and the test at `tests/test_phase4.cpp:15831` |
| Wallet switch, or an address added or imported while the GUI was closed | Nothing new | Different address set → different snapshot key → full sweep |
| GUI-initiated rescan, zap, import, runtime rescan | Already `invalidate(true)`; with 2.2 it now reaches disk | `app.cpp:6298`, `:6352`; `app_network.cpp:4013`, `:7279`, `:7302` |
| New never-scanned z-address during a session | Already resets W (`:2593-2597`); the reset must be persisted | Signature change in 2.2 |
| Disconnect, daemon restart, same-wallet reconnect | Keep today's in-memory reset | The disk copy is written first (`:857`) and reloaded after connect |
| **Rescan observed in the daemon log** (the daemon's catch-up "Rescanning last N blocks (from block H)", or an external `-rescan`) | **Sticky pending action** (see below) | The warmup-end invalidation at `:2186-2195` runs *before* the snapshot loads, so today it can't reach a persisted W |
| Wallet changed outside the GUI with the same address set (Console `z_importviewingkey`, external `-rescan`, same-seed restore) | Optional tripwire: after the first windowed sweep returns R rows, run the banded full sweep if `txcount − txcount_saved > \|R\| − k + slack`. `k` is the rows of the saved sweep within the same `100 + E` depth band the first window uses | `getwalletinfo.txcount` = `mapWallet.size()` (`dx:rpcwallet.cpp:2791`). The GUI already fetches it on every connect (`network_refresh_service.cpp:251-252`, `:295-298`; `app_network.cpp:2140-2143`). Measured cost: ~8-20 ms warm, ~180 ms on the first call after a start |
| Daemon version change | Keep W. Reset the persisted backoff | Same filter semantics since 2020; v1.3.0 and the candidate both report 1.3.0.50 |
**The sticky rescan signal.** Why it's needed:
- During warmup no address list is fetched (`scan_policy.h:42-44`, `:64-68`; `app_network.cpp:2492`), and `state_.clear()` has emptied the rows (`wallet_state.h:406-409`).
- So when `invalidate(true)` runs at `app_network.cpp:2195`, it finds an empty identity and returns (`:2028`, `:2032`).
- The cache then loads afterwards (`:2608`) and would restore the old W unchanged.
The fix:
1. Whenever `rescan_confirmed_active_` is set (`app.cpp:1193`, `:1324`, `:1397`), record a pending action that survives `resetTransactionHistoryCacheSession`:
- **clamp** to `H`, parsed from "Rescanning last N blocks (from block H)" or "Rescanning from height H";
- **full reset** if `H` is unknown or near genesis.
2. `loadTransactionHistoryCacheIfAvailable` applies it to the loaded W (`W = min(W, H)`, and `A` follows) and to the receive-scan heights, then clears it.
With daemon item D4 (wallet locator on clean shutdown, 3.2) shipped, the daemon's catch-up rescan line disappears from normal starts.
**Reorg overlap.**
- **Window extent.** Filter type 2 drops only txs deeper than N (`dx:rpcwallet.cpp:3740`), and `maxDepth = tip − W + margin`. So the window covers heights ≥ `W − margin + 1`, plus the mempool (depth 0 always passes).
- **Margin.**
- `MAX_REORG_LENGTH` defaults to 99 (`dx:src/main.h:79`), and a longer reorg shuts the node down (`dx:main.cpp:4314-4333`).
- A margin of 100 therefore leaves 1 block of slack, which tip drift between reading the tip and making the call can use up.
- Use **margin ≈ 120** in steady state *(recommendation)*, and read the tip in the same worker job as the call.
- **First sweep after a load.** Use margin `120 + E` with E ≈ 1000 blocks (~10 h).
- This absorbs wallet-notifier lag at save time: `ThreadNotifyWallets` applies blocks asynchronously (`dx:src/validationinterface.cpp:89-262`), and no RPC exposes the wallet's processed tip.
- E is an inferred bound, not a measurement.
- **Large `-maxreorg`.** A `-maxreorg` set above the margin is caught by the anchor check.
- **Conflicted txs.** Conflicted txs (depth −1) are dropped by both the full and the windowed sweep (`dx:rpcwallet.cpp:3725`), so nothing changes there.
**Locked-wallet rule.**
- **The rule:** never advance or persist W from a sweep dispatched or completed while the wallet is locked, and never load or store the snapshot while it is locked.
- **What already protects this today:**
- History scans don't run while locked (`src/services/scan_policy.h:48-51`).
- An encrypted wallet's snapshot opens only after unlock (`app_network.cpp:797-798`, `:1944-1962`).
- Locking keeps W and stores immediately (`:1851-1859`).
- **What breaks if it's ever violated:** a locked daemon wallet returns empty `saplingReceived` and `saplingSends`, because `GetSaplingHistoryKeys` needs spending keys (`dx:rpchushwallet.cpp:19-39`).
- The fields the main consumer reads (txid, `transparentSends`) don't depend on the lock.
- `locateSweepTxid` reads `saplingSends`, so it must never run on a locked wallet.
### 2.4 Backlog rule (required)
- **The risk.** Enrichment is capped at 25 per cycle, and the txids that miss the cap go to `history_enrich_backlog_` (`app_network.cpp:2843`).
- A pure z→z send leaves only a change note, which produces no receive row (`network_refresh_service.cpp:972`).
- So its txid exists **only** in a sweep result.
- If W is persisted while those txids sit in the backlog, the sends are lost for good once the window moves past them.
- **The rule:** persist the backlog together with W.
- **Rejected fallback:** "persist `W = -1` while the backlog is non-empty" isn't enough on its own. Enrichment retries are unbounded (`network_refresh_service.cpp:1521`), so one txid that keeps failing would stop W from ever being persisted. If that fallback is wanted as well, cap the retries per txid.
### 2.5 Timeouts, large windows, banded full sweep
**Large windows.**
- The windowed sweep uses `rpc.call`, which has curl's 30 s timeout (`src/rpc/rpc_client.cpp:223`). Only full-sweep failures count toward the backoff (`app_network.cpp:2846-2853`).
- So a watermark saved days ago produces a huge "windowed" call that times out on every block, while the daemon keeps working on each abandoned call.
- Rule: if `tip − W > 2400` (about 1 day of 36 s blocks), use `callLong` and count failures the same way as for a full sweep.
**Banded full sweep.**
- **The call:** replace the single 113 s (fixed daemon) or 424 s (v1.3.0) call with depth bands, `z_listsentbyaddress(z, minconf=a, 2, maxDepth=b)`. `minconf` (`dx:rpcwallet.cpp:3732`) and filter type 2 (`:3740`) are independent filters, so each call returns exactly the depth band `[a, b]`.
- **Order:**
- Go newest band first; the first band uses `minconf=0` so it includes the mempool.
- Overlap bands by at least 10 blocks, plus the blocks that arrive between calls, and dedupe by txid.
- Size bands adaptively, aiming for about 5 s each.
- **Scheduling:** run **one band per history cycle**, using the epoch-guarded band cursor from 2.2. All bands in one job would tie up the shared RPC worker for about 113 s, exactly as today.
- **Watermark:** take `W` from the tip at the first band, and set it only after every band has succeeded.
- **Don't page with `nCount`.** The iteration order is `nOrderPos`, not height (`:3709`, `:3746`).
- **Limits:**
- Banding shortens each `LOCK2` hold but not the total CPU.
- On v1.3.0 every call has a ~19.5 s floor, because every `CWalletTx` is deep-copied *(inference from the windowed measurement)*. There the bands need `callLong`.
**Backoff persistence.**
- Store the failure count and the relative delay (2.1), so a relaunch doesn't immediately re-run a sweep that keeps failing.
- Reset both when `daemon_id` changes. Otherwise a user who installs the fixed daemon could wait up to 6 h for a full sweep that would now succeed.
### 2.6 Safety net
- **When:** about every 7 days, run one banded full re-sweep.
- **Conditions:** only when synced, unlocked, idle (not on the Mining tab) and admitted by the governor.
- **On success**, update `last_full_*`. On failure, back off.
- **Why:** it catches what the guards can't see, such as a Console `z_importviewingkey`. That case is also missed within a session today.
### 2.7 Edge cases
| Case | Handling |
|---|---|
| Stored tip < current tip: `load()` doesn't check the hash (`cache.cpp:338-344`) | The anchor verify covers the watermark. Optionally extend `load()` too |
| Transparent send rows from `appendSentTransparentOutputs` aren't reproduced by windowed sweeps | Probably dropped on the next clean cycle (`network_refresh_service.cpp:1356-1368`, `:1528-1539`) *(inference)*. Negligible on DragonX, where consensus bans non-notary/burn t-outputs (`dx:main.cpp:1606`, `:1633`, `:1657`). Optional fix: carry over `send` rows that `listtransactions` didn't produce |
| `locateSweepTxid` runs a full sweep under the 30 s timeout (`app_network.cpp:6388`) | That times out on this wallet. Switch it to a windowed or banded call |
| Watch-only addresses aren't part of the snapshot key | Covered by the tripwire and the safety net |
| Switching between two wallets | `pruneOtherWallets` keeps one snapshot, so a switch still costs a full sweep (open question 4) |
| GUI crash mid-sweep | W advances only on `sentSweepOk`, and the write is atomic |
| Regtest or testnet | Different z-address prefixes, so a different snapshot key |
### 2.8 Test plan
**Unit tests** (`tests/test_phase4.cpp`):
- In `testTransactionHistoryCache` (`:9057`):
- every `sent_sweep` field round-trips;
- a missing key reads as −1;
- a wrong type, `W > tip_height` or `A > W` reads as −1;
- unknown keys are ignored.
- Signatures (~`:9244`): a change to W, A or `H_A` changes `slow`; a change to the backlog changes `content`.
- Policy (~`:15829`, `:16098-16213`):
- the anchor is `W − margin`;
- the first window after a load uses `120 + E`;
- `tip − W > 2400` uses `callLong` and counts failures;
- band planning: boundaries, overlap, dedupe, and cursor reset on an epoch change;
- an anchor mismatch means a full sweep;
- the pending rescan action clamps W;
- `daemon_id` changes reset the backoff.
**Integration tests** (regtest or `-testnode`):
1. Full sweep, quit, relaunch: the log shows only `[z,0,2,N]` calls.
2. `invalidateblock` one block above A while the GUI is closed: the windowed sweep runs and returns the right rows.
3. `invalidateblock` below A: the verify fails and a banded full sweep runs.
4. More than 25 z→z sends, then quit before enrichment finishes: after relaunch every send appears.
5. A daemon started with `-rescan` while the GUI was closed: the sticky action clamps or resets W.
6. `restoreOriginalWallet`: W is invalidated.
7. Locked encrypted wallet: W doesn't advance and the snapshot isn't loaded.
**Mainnet copy:** relaunch against the isolated owner copy. Expect no `[z,0]` call and a sent sweep well under 1 s.
### 2.9 Expected effect
| Scenario | Today | With Part 1 |
|---|---|---|
| Relaunch or reconnect, fixed daemon | 113 s full sweep under `LOCK2`, 7 s from the timeout | `getblockhash` plus one windowed sweep over `120 + E` + the closure gap. Steady-state windows measure ~128 ms; the first window after a load is larger *(unmeasured)* |
| Relaunch, v1.3.0 daemon | 424 s; never fits in 120 s, so it loops in backoff with RSS at 19.4 GB | ~19.5 s+ windowed once a W exists. Seeding the first W needs the banded sweep |
| Safety net | — | One banded full sweep a week, each `LOCK2` hold about 5 s |
---
## 3. Part 2: daemon follow-up
### Where the 113 s goes (owner wallet, full sweep)
| Component | Cost | Basis |
|---|---|---|
| Trial decryption of outputs that aren't wallet notes: ~172,800 outputs × 5 ivks ≈ 864k failed tries × ~0.13 ms | **~112 s (~99%)** | Output count derived from tx sizes; per-try cost fitted to the full (112.7 s) and windowed (126 ms / 740 tries) measurements *(estimate)*. Code: `OutputDecryptions::Get` (`dx:rpchushwallet.cpp:87-100`), loop at `:303-346` |
| `DecryptSent` esk-path decrypts | **0 on this wallet** | No tx spends a wallet note, so the sends helper is gated off (`:586`) |
| UniValue build + copy under `LOCK2` | ~0.28 s | Synthetic benchmark on the repo's UniValue |
| Reply serialization (after the lock is released) | ~0.31-0.34 s | Same benchmark |
### Where the foreign outputs come from
**Sietch padding.** `z_sendmany` pads its z-recipients up to `-sietch-min-zouts` (default 7, clamped to 3-50; `dx:rpcwallet.cpp:5424-5434`), with change on top.
- Recipient and dummy outputs take the sender's ovk: the from-address's `fvk.ovk`, or the seed ovk for t-address sources (`dx:asyncrpcoperation_sendmany.cpp:352-368`, applied at `:437`).
- Consolidation (`saplingconsolidation.cpp:244`), sweep (`sweep.cpp:299`), shieldcoinbase (`:239-254`) and autoshield (`:435`) also add dummies with the sender's ovk.
- So a **sending** wallet's `DecryptSent` opens every one of its own dummies. That is the ~0.36 ms esk-path decrypt each, which is D2.
- Every other wallet sees them as foreign outputs and tries them under every ivk, which is D1.
**On this wallet** most foreign outputs are **not** sietch dummies. About 158.6k of the ~174k sit in the 9,597 txs with 10 or more outputs, which are mostly multi-recipient payouts *(inference from tx sizes)*. That doesn't change D1.
### Items
| # | Item | Output | Gain (owner wallet) | Risk | GUI change |
|---|---|---|---|---|---|
| D1 | Per-output negative ivk cache, plus parallel pre-fill of the txs a call selects | Byte-identical | Warm 113 s → 0.4-1 s. First full sweep after a restart ~20-25 s (pre-fill at the measured 4.75× warm-up scaling). A windowed call stays ~100 ms *(estimate)* | Low-med | None |
| D2 | `DecryptSent` cache per (output, ovk) | Byte-identical | 0 here; ~0.36 ms per send output on sending wallets | Low | None |
| D3 | `getalldata` balance loop | Byte-identical | ~20-35 s → 10-30 ms *(estimate)* | Low | None (the GUI never calls it) |
| D4 | Write the wallet locator on clean shutdown | n/a | Start: −49-62 s (post-rescan locator write, *inferred from the log gap*) and −5-10 s of block rescan; the ~25 s witness-root pass moves to the first live block. Shutdown: +3-60 s *(inferred)* | Medium | Check the stop policy |
| D5 | JSON trims | Byte-identical | 0.2-0.4 s | Low | None |
| D6 | Sends-only `returnType` (6th parameter) | **Changes output; opt-in** | 113 s → 0.1-0.3 s; 26.2 → ~4.1 MB *(estimate)* | Low | Probe + opt-in |
| D7 | Honour the address argument | **Changes output** | None (slower) | Breaks the GUI | Not recommended |
- **D6 is the single largest win**, but it needs a GUI release.
- **D1-D5 are invisible to the GUI.**
- **With Part 1 shipped, full sweeps become rare**, so D1 mostly helps older GUIs, other clients and the safety net.
### 3.1 D1: per-output decryption cache (byte-identical)
**Shape**
- A `mutable std::vector<uint64_t>` on `CWalletTx`, with one mask per `vShieldedOutput` index.
- It is sized once, never serialized, and cleared in `Init()`.
- Precedents: `cachedPlaintext` (`dx:src/wallet/wallet.h:340-348`) and the mutable caches at `:472-490`.
- Bit k means "history key k does not decrypt this output".
- An append-only ivk → index registry on `CWallet`, under `cs_wallet`. It is resolved once per RPC into `SaplingHistoryKeys::SpendableAddress` (`dx:rpchushwallet.cpp:19-39`).
**Logic in `OutputDecryptions::Get`**
- If the bit is set, return null without decrypting.
- After a failed `Decrypt`, set the bit.
- Never cache a success: wallet notes already carry `cachedPlaintext`.
- Keys with index ≥ 64 aren't cached.
**Invariant: the result is a pure function of (txid, n, ivk).** A v4 txid commits to every output byte, so:
- nothing ever goes stale;
- a newly added key has a clear bit and is still tried;
- locking, or losing keys, only means fewer keys are consulted;
- reorgs, deletions and reloads are harmless;
- output stays byte-identical.
**Thread safety**
- The mask is mutable behind a `const CWalletTx&`, so the compiler won't enforce locking. Every mask read and write therefore happens on the RPC thread under `cs_wallet`, with an `AssertLockHeld` in the accessor.
- Pre-fill workers follow `FillSaplingNoteCache`'s rule that workers must not lock, or even assert a lock (`dx:wallet.cpp:5745-5746`). They call only `decrypt_with_note` on pre-resolved (output, ivk) pairs and write private masks, which are merged serially after the join.
- Workers must not call `GetSaplingHistoryKeys` or `IsNullifierOfAddress`, because both assert locks.
**Fill**
- Lazily, as the helpers run.
- In addition, pre-fill in parallel, but **only the txs the call has already selected**:
1. In `z_listsentbyaddress`, first collect the `wtx` pointers that pass the filters at `dx:rpcwallet.cpp:3722-3741`, honouring `nCount`.
2. Pre-fill their outputs, using the 128-item threshold and `-witnessbuildthreads` from `dx:wallet.cpp:5739-5797`.
3. Then emit.
- Pre-fill time is spent under `LOCK2`. For a full sweep that's ~20-25 s once; a windowed call stays small.
- Optionally, also record the misses `FindMySaplingNotes` already computes (`:2282-2322`).
- A startup precompute is opt-in only: ~112 CPU-s, about 24 s of wall time per start *(estimate)*.
**Also benefits:** `getalldata`'s tx list (`dx:rpcwallet.cpp:3254`) and `z_listreceivedaddress` (`:3555`).
**Tests**
- Extend the verbatim oracle `dx:src/gtest/test_wallet_history.cpp`: cold, warm and pre-filled runs must match byte for byte.
- A key added after the mask was filled, which opens a foreign output, must appear in the output.
- More than 64 keys; locked and encrypted wallets; `-witnessbuildthreads=1` vs 24; a `DEBUG_LOCKORDER` build.
- The mainnet A/B harness: `cmp` cold and warm replies against the captured 26,233,144-byte reply (`/home/d/dragonx-ab/mainnet-ab/out/B/cap/023_z_listsentbyaddress_addr0_full.json`).
**D2.** Cache `DecryptSent` results per (n, ovk index): the plaintext and address on success, or "no". That's about 650 B per entry. Key the seed ovk by value, so lock state still decides whether the cache is consulted. Use the same oracle tests as D1.
### 3.2 Smaller items
**D3: the `getalldata` balance loop** (`dx:rpcwallet.cpp:3108-3151`, under `LOCK2` at `:2998`).
- Today it copies each `SaplingNoteData` by value, witnesses included (`:3110`). For every unspent note and every address it re-derives the spending key and trial-decrypts (`:3117-3121`).
- Fix:
- build `SaplingHistoryKeys` once, and reuse it at `:3233`;
- take note data by `const&`;
- use `OutputDecryptions(wtx, op.n).Get(a.ivk)` over `keys.spendable`, moving `OutputDecryptions` out of its anonymous namespace (`rpchushwallet.cpp:56-120`) into `rpchushwallet.h`.
- Keep two quirks for byte-identity, and fix them later as a separate change with a release note:
- The no-op `txType == 3;` at `:3124` means locked notes are never counted as locked. When fixed, use a per-note local, `noteType = (txType == 0 && IsLockedNote(op)) ? 3 : txType`. `txType` is per tx (`:3043`), so assigning to it would mark every later note of the tx as locked.
- Every address whose ivk opens the note is credited, because the `continue` doesn't exit the address loop.
- Don't switch to `GetFilteredNotes`: that changes the output.
- Test: an oracle gtest with a verbatim copy of the old loop (shared-ivk, locked-note, watch-only and encrypted cases).
**D4: write the wallet locator on clean shutdown.**
- **Evidence:** the copy, taken from a clean Windows shutdown, was 12 blocks behind its chainstate. So every start rescans those blocks.
- **Cause:**
- `FlushStateToDisk` writes the locator only if `DATABASE_WRITE_INTERVAL` (3600 s, `dx:main.h:145`) has passed, even for `FLUSH_STATE_ALWAYS` (`dx:main.cpp:3838-3841`).
- `Shutdown()` calls it at `dx:init.cpp:259`.
- `pwalletMain->Flush(true)` at `:280` only flushes BDB.
- **Hook:** in `Shutdown()`, between `:259` and `:280`:
```cpp
LOCK2(cs_main, pwalletMain->cs_wallet);
const CBlockIndex* tip = pwalletMain->GetWalletTip(); // new accessor: pindexWalletTip is private (wallet.h:988-994)
if (tip && tip != lastStamped)
pwalletMain->SetBestChainINTERNAL(walletdb, chainActive.GetLocator(tip));
```
- **Don't route it through `CWallet::SetBestChain`.** That swaps in `pindexWalletTip` when set but falls back to the passed locator when it's null (`dx:wallet.cpp:829-834`). The null case is exactly "stamp the tip before the init rescan ran", which would skip the unscanned blocks for good.
- **The failure path needs the lock too.** AppInit2's failure path calls `Shutdown()` without `join_all` (`dx:bitcoind.cpp:232-243`), so the notifier can still be running. Reading the tip under `LOCK2` keeps that safe.
- **Don't drop the interval check globally.** `gettxoutsetinfo` and init also flush with ALWAYS, and each would then rewrite ~2 GB under `cs_main`.
- **Trade-off:** one locator write rewrites every note-bearing `CWalletTx`, ~2 GB here (`SetBestChainINTERNAL`, `dx:wallet.h:939-986`, WriteTx loop `:947-962`). The cost moves to shutdown rather than disappearing; see the D4 row in the table above.
- **Ship it together with a parallel witness-root check at init.**
- **Risk:** the GUI's stop policy (`src/daemon/daemon_controller.h:360`, `evaluateStopWait`) must allow about 16 s plus 3-60 s. Context: the 2026-09-30 Windows kill loop.
- **Tests:**
- Stop within the hour and restart: no "Rescanning last" line, and identical balances.
- Stop during the startup rescan: the locator does not advance.
**D5: JSON trims.**
- Use `static_cast<const CTransaction&>` at `dx:rpchushwallet.cpp:553`. Today it copies 190.6 MB of tx data per full sweep.
- Drop the `getValues` / reverse / `push_backV` copy (`dx:rpcwallet.cpp:3749-3755`). Collect the selected `wtx` pointers in the existing reverse loop, which keeps `nCount` picking the newest txs, then build `ret` oldest to newest. The same pattern is at `getalldata` `:3259-3265`.
**Also found; each needs its own design.**
- The hourly `SetBestChain` rewrites ~2 GB while `cs_main` is held (3-47 s gaps in the live log, *inferred from log gaps*).
- `FindMySaplingNotes` tries foreign outputs once per viewing key and again per address (`dx:wallet.cpp:2282-2322`). De-duplicating ivks would probably halve the per-block tries *(inference)*.
### 3.3 Options that change RPC output (need a GUI opt-in)
**D6: sends-only mode.**
- **The parameter:** add an optional 6th parameter `returnType` to `z_listsentbyaddress`, default 0 (unchanged output).
- The code's meanings are 1 = spends, 2 = received, 3 = sends. The comment at `dx:rpchushwallet.cpp:561-566` swaps 1 and 3; fix it in the same change.
- **With `3` and no address filter:**
- the spends helper still runs (`:569`), but its output isn't emitted;
- `received`, where all the trial decryption happens, is skipped (`:576-581`);
- `sends` is emitted (`:584-592`), and every tx is still pushed (`:594-598`).
- The GUI gets the same txids and the same `sends.transparentSends`.
- **Daemon changes:**
- Raise the check at `dx:rpcwallet.cpp:3606` from `> 5` to `> 6`, keeping the `== 3` rejection.
- Change `:3693` to `params.size() >= 5`. Today it reads `nCount` only when `size() == 5`, so every 6-parameter call would silently drop it.
- Read `params[5]`, reject anything outside 0-3, and document it in the help text. `dx:src/rpc/client.cpp:165` already converts index 5.
- **GUI side:** send 6 parameters. An old daemon throws its help text for more than 5, so fall back to 5 parameters and remember that per connection.
- Alternative: a new RPC name, probed through "Method not found" the same way as `z_autoshieldstatus` (`app.h:1744-1747`).
- **Owner decision:** a variant that returns only txs with wallet spends would return 0 rows here, but it changes the `knownTxids` set.
**D7: honour the address argument.**
- The GUI passes `shieldedAddresses.front()` (`network_refresh_service.cpp:1319`) and relies on the daemon using `"*"` (`dx:rpcwallet.cpp:3743`). Honouring the argument by default would silently shrink GUI sweeps to one address.
- It isn't faster either: with a filter set, `fBool=true` runs `zsTxSendsToJSON` for every tx (`:586`).
- Recommendation: document `"*"` as a compatibility contract. If honouring the argument is ever wanted, put it behind a flag.
**Additive and safe for the GUI: a wallet-tip / history fingerprint.**
- Expose `pindexWalletTip`, for example in `getwalletinfo` or a new `z_getwallethistorystate(maxheight)` returning `{count, txid-hash, wallet_tip}`.
- The GUI ignores unknown JSON fields.
- This would make Part 1's invalidation exact, replacing the E-margin guess and the `txcount` tripwire.
---
## 4. Rollout and compatibility
| Step | Ships in | Works with | Notes |
|---|---|---|---|
| Part 1: watermark + anchor verify + backlog + sticky rescan + large-window path + banding + safety net | Next GUI release | Every daemon version; it uses only existing `z_listsentbyaddress` parameters, `getblockhash`, `getwalletinfo` and `getinfo` | Old GUI with a new snapshot: the key is ignored. New GUI with an old snapshot: one full sweep, which is today's behaviour. On v1.3.0, seeding the first W needs the banded sweep |
| D1, D2, D3, D5 | Daemon v1.3.1 / v1.3.2 | Invisible to the GUI | Gate each on the byte-identical oracle and the mainnet `cmp` |
| D6 + GUI probe | Daemon first, then GUI | The GUI falls back on old daemons | Makes the safety net and any full sweep cheap |
| D4 | Together with a parallel witness-root check | Invisible apart from shutdown time | Confirm the GUI stop policy first |
---
## 5. Open questions
1. How big is the backlog after the first full sweep on the owner's wallet? Unmeasured; it sizes the persisted `backlog` field.
2. What is the full-sweep time on the owner's 24-core Windows node? The 113 s was measured on Linux/WSL, 7 s inside the 120 s timeout.
3. How long does the wallet notifier lag after a catch-up on this wallet? This sets E.
4. Should the snapshot store keep more than one wallet, so that switching wallets also benefits?
5. D6 shape: a 6th parameter or a new RPC? Should a "wallet-spends-only" variant exist?
6. Does any client besides ObsidianDragon depend on `returnType 0` output? This sets D1's priority once D6 exists.
7. Are the 5 ivks distinct? Assumed. The per-address note counts sum to exactly 16,206, which fits but doesn't prove it.
8. Does the owner want the additive wallet-tip / fingerprint RPC, which makes Part 1's invalidation exact?
+2 -17
View File
@@ -361,22 +361,7 @@ https://www.apache.org/licenses/LICENSE-2.0
---
## 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
## 13. IconFontCppHeaders
- **Location:** `src/embedded/IconsMaterialDesign.h`
- **Source:** https://github.com/juliettef/IconFontCppHeaders
@@ -405,7 +390,7 @@ freely, subject to the following restrictions:
---
## 15. Ubuntu Font Family
## 14. Ubuntu Font Family
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
- **Source:** https://design.ubuntu.com/font
+206 -663
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
# CMake script to embed binary files as C arrays
function(embed_resource RESOURCE_FILE OUTPUT_FILE VAR_NAME)
file(READ "${RESOURCE_FILE}" HEX_CONTENT HEX)
# Convert hex to C array format
string(LENGTH "${HEX_CONTENT}" HEX_LENGTH)
math(EXPR BYTE_COUNT "${HEX_LENGTH} / 2")
set(ARRAY_CONTENT "")
set(INDEX 0)
while(INDEX LESS HEX_LENGTH)
string(SUBSTRING "${HEX_CONTENT}" ${INDEX} 2 BYTE)
string(APPEND ARRAY_CONTENT "0x${BYTE},")
math(EXPR INDEX "${INDEX} + 2")
math(EXPR BYTE_NUM "${INDEX} / 2")
math(EXPR MOD "${BYTE_NUM} % 16")
if(MOD EQUAL 0)
string(APPEND ARRAY_CONTENT "\n ")
endif()
endwhile()
# Generate header file
set(HEADER_CONTENT "// Auto-generated embedded resource - DO NOT EDIT
#pragma once
#include <cstddef>
#include <cstdint>
namespace embedded {
inline constexpr unsigned char ${VAR_NAME}_data[] = {
${ARRAY_CONTENT}
};
inline constexpr size_t ${VAR_NAME}_size = ${BYTE_COUNT};
} // namespace embedded
")
file(WRITE "${OUTPUT_FILE}" "${HEADER_CONTENT}")
message(STATUS "Embedded ${RESOURCE_FILE} -> ${OUTPUT_FILE} (${BYTE_COUNT} bytes)")
endfunction()
-31
View File
@@ -1,31 +0,0 @@
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
#
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
# functions unconditionally — so any TU including <math.h> fails to compile with
# "_Float16 is not supported on this target".
#
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
# is unnecessary on every platform we target. Blanking the SSE entries (indices
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
endif()
file(READ "${CPU_CMAKE}" _contents)
string(REPLACE
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
_patched "${_contents}")
if(_patched STREQUAL _contents)
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
else()
file(WRITE "${CPU_CMAKE}" "${_patched}")
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
endif()
-43
View File
@@ -1,43 +0,0 @@
**DragonX (DRGX) Lite Desktop Wallet — v1.1.1**
ObsidianDragonLite is the lightweight, node-free companion to ObsidianDragon — it connects to a DragonX lite-wallet server for near-instant startup, with the same native ImGui interface and shielded-first workflow. **v1.1.1 is a maintenance release** centered on an important wallet-encryption fix.
---
## ⚠️ Security fix — please read
**Creating or restoring a Lite wallet with a passphrase now actually encrypts it.**
In prior versions, the create / restore / open flow used by the Settings UI **silently discarded the passphrase you entered**:
- A **newly created or restored** wallet was saved to disk **unencrypted** — the seed and spending keys were stored in the clear even though you set a passphrase.
- **Opening** an already-encrypted wallet did not unlock it with the supplied passphrase (it opened locked and re-prompted).
v1.1.1 fixes this: create/restore now **encrypt** the wallet with your passphrase (the backend encrypts and locks it), and open now **unlocks** it.
**What you should do:** if you created or restored a Lite wallet **with a passphrase** on an earlier version, treat that wallet's on-disk data as unencrypted. Open it in v1.1.1 and re-apply encryption from **Settings → encrypt** (and, if you're concerned the plaintext wallet file may have been exposed, move the funds to a freshly created encrypted wallet). Wallets you never set a passphrase on are unaffected.
---
## Also fixed
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
---
## Downloads
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
| File | SHA-256 |
|------|---------|
| ObsidianDragonLite-1.1.1.AppImage | `323ff5f8658a5550aa12678ce4be37e32a2bfd3f3b0543074cc86a4267895dcf` |
| ObsidianDragonLite-1.1.1-Linux-x64.zip | `e8604e479aed7e020946df45bf0d9373401354907e10565011694efa9978e631` |
| ObsidianDragonLite-1.1.1.exe | `4cdcf291fe43c4fed04258aa55bb4e9b591f52780f9db5fb8f8bef522c43a351` |
| ObsidianDragonLite-1.1.1-Windows-x64.zip | `c387bfcda508f32195c41fabec9f23d8b094d693a5dba41fbf8ec4abab6f2f6e` |
| ObsidianDragonLite-1.1.1-macOS-x86_64.dmg | `bb0c491cb8f90ae55d162ec7c04c8f30988fa2f79c7911471220cddab44f99a5` |
| ObsidianDragonLite-1.1.1-macOS-x86_64.app.zip | `a2a75fe40bf1f4db3abed8206dcc40d9fc74d8db2df7704afc9c834106b1c247` |
---
For the full feature set (node-free operation, wallet lifecycle, shielded + transparent send/shield/receive, HushChat, server browser with failover, 8-language UI), see the **lite-v1.0.0** release notes — v1.1.1 changes only the items listed above.
-41
View File
@@ -1,41 +0,0 @@
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.2**
ObsidianDragon is a native ImGui-based full-node wallet for the DragonX network, with an embedded `dragonxd` daemon and the integrated DRG-XMRig CPU miner. **v2.0.2 is a maintenance release** — a focused set of transaction-history, sync-safety, and correctness fixes on top of v2.0.x. No configuration changes are required; just replace the binary.
---
## What's Fixed in v2.0.2
### Transaction history
- **Outgoing shielded sends now appear in History.** Sends *from* a shielded (z-)address — both **z→z** (shielded-to-shielded) and **z→t** (deshield to a transparent address) — were missing from the History tab, because the daemon's `listtransactions` doesn't report them and `z_viewtransaction` doesn't expose the transparent leg. The wallet now enumerates them via `z_listsentbyaddress`, so **your History matches your balance.** (Your funds were never affected — only the display.)
- **More accurate send rows** — a single transaction with two equal-value outputs to different recipients is no longer collapsed into one row, and a transparent self-send / change output is no longer double-counted as both sent and received.
### Sync & fund safety
- **A node that loses all its peers is no longer reported as "synced."** Previously, a wallet whose node dropped to **0 peers** could keep showing a stale balance as if it were current — and allow a Send against it. The wallet now treats a node with no peer-derived chain tip as *not synced*, and a new **"Connected, but no network peers — the node can't sync"** banner explains the stall instead of silently looking healthy.
- **Migrate-to-seed hardening.** If a migrate-to-seed was interrupted (app or daemon restart) *after* the sweep transaction had broadcast but *before* it confirmed, the wallet could mistake the temporarily-zero balance for an empty wallet and adopt the new seed wallet — swapping `wallet.dat` — before the sweep was mined. It now records that a sweep is in flight and **will not swap your wallet until the sweep confirms**, so funds can't be stranded in the timestamped backup.
- **Fixed a rare crash** caused by a data race on the daemon-status text (the daemon-monitor thread writing it while the UI thread read it).
### Display / internationalization
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
---
## Downloads
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
| File | SHA-256 |
|------|---------|
| ObsidianDragon-2.0.2.AppImage | `9bf7328965c6348113ba7a30b0284f15787c635af17efae3dfa8a87e56e10c9b` |
| ObsidianDragon-2.0.2-Linux-x64.zip | `1371880c4187a94d4cb56d535471142c5f1a8053ef73fffd99d59e7b78c75c42` |
| ObsidianDragon-2.0.2.exe | `673347d2696fb6f5bd09dc25e415d5ae8e532abde75b8d48c3cd26545461de50` |
| ObsidianDragon-2.0.2-Windows-x64.zip | `f5ea7c45e66c6c1b90a8127f96dee04235691acb913ddc86327f2060798cd28c` |
| ObsidianDragon-2.0.2-macOS-x86_64.dmg | `7038580807a8e93e5443bdc04f2aede3ae1c30e5bc9f8776a207edb2e87771d1` |
| ObsidianDragon-2.0.2-macOS-x86_64.app.zip | `78159512ca684b95528231fd46f2d07b5bcc6f2da4d815fc1ecb2ff52127f41c` |
---
For the full feature set (HushChat messaging, seed-phrase wallets & migrate-to-seed, in-app node/miner updaters, built-in mining, 8-language UI), see the **v2.0.0** release notes — v2.0.2 changes only the fixes listed above.
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: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 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: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1 -2
View File
@@ -67,8 +67,7 @@
//#define IMGUI_USE_LEGACY_CRC32_ADLER
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
//---- Enabled so chat can render emoji (U+1F300+, above the BMP) — see Typography::loadFont emoji merge (Q12).
#define IMGUI_USE_WCHAR32
//#define IMGUI_USE_WCHAR32
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
-744
View File
@@ -1,744 +0,0 @@
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
// (code)
// Get the latest version at https://github.com/ocornut/imgui/tree/master/misc/freetype
// Original code by @vuhdo (Aleksei Skriabin) in 2017, with improvements by @mikesart.
// Maintained since 2019 by @ocornut.
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2025/06/11: refactored for the new ImFontLoader architecture, and ImGuiBackendFlags_RendererHasTextures support.
// 2024/10/17: added plutosvg support for SVG Fonts (seems faster/better than lunasvg). Enable by using '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG'. (#7927)
// 2023/11/13: added support for ImFontConfig::RasterizationDensity field for scaling render density without scaling metrics.
// 2023/08/01: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG'. (#6591)
// 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly.
// 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns nullptr.
// 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs.
// 2021/03/02: set 'atlas->TexPixelsUseColors = true' to help some backends with deciding of a preferred texture format.
// 2021/01/28: added support for color-layered glyphs via ImGuiFreeTypeBuilderFlags_LoadColor (require Freetype 2.10+).
// 2021/01/26: simplified integration by using '#define IMGUI_ENABLE_FREETYPE'. renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. removed ImGuiFreeType::BuildFontAtlas().
// 2020/06/04: fix for rare case where FT_Get_Char_Index() succeed but FT_Load_Glyph() fails.
// 2019/02/09: added RasterizerFlags::Monochrome flag to disable font anti-aliasing (combine with ::MonoHinting for best results!)
// 2019/01/15: added support for imgui allocators + added FreeType only override function SetAllocatorFunctions().
// 2019/01/10: re-factored to match big update in STB builder. fixed texture height waste. fixed redundant glyphs when merging. support for glyph padding.
// 2018/06/08: added support for ImFontConfig::GlyphMinAdvanceX, GlyphMaxAdvanceX.
// 2018/02/04: moved to main imgui repository (away from http://www.github.com/ocornut/imgui_club)
// 2018/01/22: fix for addition of ImFontAtlas::TexUvscale member.
// 2017/10/22: minor inconsequential change to match change in master (removed an unnecessary statement).
// 2017/09/26: fixes for imgui internal changes.
// 2017/08/26: cleanup, optimizations, support for ImFontConfig::RasterizerFlags, ImFontConfig::RasterizerMultiply.
// 2017/08/16: imported from https://github.com/Vuhdo/imgui_freetype into http://www.github.com/ocornut/imgui_club, updated for latest changes in ImFontAtlas, minor tweaks.
// About Gamma Correct Blending:
// - FreeType assumes blending in linear space rather than gamma space.
// - See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Render_Glyph
// - For correct results you need to be using sRGB and convert to linear space in the pixel shader output.
// - The default dear imgui styles will be impacted by this change (alpha values will need tweaking).
// FIXME: cfg.OversampleH, OversampleV are not supported, but generally not necessary with this rasterizer because Hinting makes everything look better.
#include "imgui.h"
#ifndef IMGUI_DISABLE
#include "imgui_freetype.h"
#include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*,
#include <stdint.h>
#include <ft2build.h>
#include FT_FREETYPE_H // <freetype/freetype.h>
#include FT_MODULE_H // <freetype/ftmodapi.h>
#include FT_GLYPH_H // <freetype/ftglyph.h>
#include FT_SIZES_H // <freetype/ftsizes.h>
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
// Handle LunaSVG and PlutoSVG
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
#error "Cannot enable both IMGUI_ENABLE_FREETYPE_LUNASVG and IMGUI_ENABLE_FREETYPE_PLUTOSVG"
#endif
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
#include FT_OTSVG_H // <freetype/otsvg.h>
#include FT_BBOX_H // <freetype/ftbbox.h>
#include <lunasvg.h>
#endif
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
#include <plutosvg.h>
#endif
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG)
#if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
#error IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12
#endif
#endif
#ifdef _MSC_VER
#pragma warning (push)
#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff)
#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
#endif
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used
#ifndef __clang__
#pragma GCC diagnostic ignored "-Wsubobject-linkage" // warning: 'xxxx' has a field 'xxxx' whose type uses the anonymous namespace
#endif
#endif
//-------------------------------------------------------------------------
// Data
//-------------------------------------------------------------------------
// Default memory allocators
static void* ImGuiFreeTypeDefaultAllocFunc(size_t size, void* user_data) { IM_UNUSED(user_data); return IM_ALLOC(size); }
static void ImGuiFreeTypeDefaultFreeFunc(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_FREE(ptr); }
// Current memory allocators
static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFreeTypeDefaultAllocFunc;
static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc;
static void* GImGuiFreeTypeAllocatorUserData = nullptr;
// Lunasvg support
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* state);
static void ImGuiLunasvgPortFree(FT_Pointer* state);
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state);
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state);
#endif
//-------------------------------------------------------------------------
// Code
//-------------------------------------------------------------------------
#define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point
#define FT_SCALEFACTOR 64.0f
// Glyph metrics:
// --------------
//
// xmin xmax
// | |
// |<-------- width -------->|
// | |
// | +-------------------------+----------------- ymax
// | | ggggggggg ggggg | ^ ^
// | | g:::::::::ggg::::g | | |
// | | g:::::::::::::::::g | | |
// | | g::::::ggggg::::::gg | | |
// | | g:::::g g:::::g | | |
// offsetX -|-------->| g:::::g g:::::g | offsetY |
// | | g:::::g g:::::g | | |
// | | g::::::g g:::::g | | |
// | | g:::::::ggggg:::::g | | |
// | | g::::::::::::::::g | | height
// | | gg::::::::::::::g | | |
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
// / | | g:::::g | |
// origin | | gggggg g:::::g | |
// | | g:::::gg gg:::::g | |
// | | g::::::ggg:::::::g | |
// | | gg:::::::::::::g | |
// | | ggg::::::ggg | |
// | | gggggg | v
// | +-------------------------+----------------- ymin
// | |
// |------------- advanceX ----------->|
// Stored in ImFontAtlas::FontLoaderData. ALLOCATED BY US.
struct ImGui_ImplFreeType_Data
{
FT_Library Library;
FT_MemoryRec_ MemoryManager;
ImGui_ImplFreeType_Data() { memset((void*)this, 0, sizeof(*this)); }
};
// Stored in ImFontConfig::FontLoaderData. ALLOCATED BY US.
struct ImGui_ImplFreeType_FontSrcData
{
// Initialize from an external data buffer. Doesn't copy data, and you must ensure it stays valid up to this object lifetime.
bool InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_user_flags);
void CloseFont();
ImGui_ImplFreeType_FontSrcData() { memset((void*)this, 0, sizeof(*this)); }
~ImGui_ImplFreeType_FontSrcData() { CloseFont(); }
// Members
FT_Face FtFace;
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
FT_Int32 LoadFlags;
ImFontBaked* BakedLastActivated;
};
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
struct ImGui_ImplFreeType_FontSrcBakedData
{
FT_Size FtSize; // This represent a FT_Face with a given size.
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
};
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
{
FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace);
if (error != 0)
return false;
error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE);
if (error != 0)
return false;
// Convert to FreeType flags (NB: Bold and Oblique are processed separately)
UserFlags = (ImGuiFreeTypeLoaderFlags)(src->FontLoaderFlags | extra_font_loader_flags);
LoadFlags = 0;
if ((UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) == 0)
LoadFlags |= FT_LOAD_NO_BITMAP;
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoHinting)
LoadFlags |= FT_LOAD_NO_HINTING;
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoAutoHint)
LoadFlags |= FT_LOAD_NO_AUTOHINT;
if (UserFlags & ImGuiFreeTypeLoaderFlags_ForceAutoHint)
LoadFlags |= FT_LOAD_FORCE_AUTOHINT;
if (UserFlags & ImGuiFreeTypeLoaderFlags_LightHinting)
LoadFlags |= FT_LOAD_TARGET_LIGHT;
else if (UserFlags & ImGuiFreeTypeLoaderFlags_MonoHinting)
LoadFlags |= FT_LOAD_TARGET_MONO;
else
LoadFlags |= FT_LOAD_TARGET_NORMAL;
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
LoadFlags |= FT_LOAD_COLOR;
return true;
}
void ImGui_ImplFreeType_FontSrcData::CloseFont()
{
if (FtFace)
{
FT_Done_Face(FtFace);
FtFace = nullptr;
}
}
static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint)
{
uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint);
if (glyph_index == 0)
return nullptr;
// If this crash for you: FreeType 2.11.0 has a crash bug on some bitmap/colored fonts.
// - https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076
// - https://github.com/ocornut/imgui/issues/4567
// - https://github.com/ocornut/imgui/issues/4566
// You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version.
FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags);
if (error)
return nullptr;
// Need an outline for this to work
FT_GlyphSlot slot = src_data->FtFace->glyph;
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG);
#else
#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG in imconfig.h and install required libraries in order to use this font");
#endif
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP);
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
// Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting)
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold)
FT_GlyphSlot_Embolden(slot);
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique)
{
FT_GlyphSlot_Oblique(slot);
//FT_BBox bbox;
//FT_Outline_Get_BBox(&slot->outline, &bbox);
//slot->metrics.width = bbox.xMax - bbox.xMin;
//slot->metrics.height = bbox.yMax - bbox.yMin;
}
return &slot->metrics;
}
static void ImGui_ImplFreeType_BlitGlyph(const FT_Bitmap* ft_bitmap, uint32_t* dst, uint32_t dst_pitch)
{
IM_ASSERT(ft_bitmap != nullptr);
const uint32_t w = ft_bitmap->width;
const uint32_t h = ft_bitmap->rows;
const uint8_t* src = ft_bitmap->buffer;
const uint32_t src_pitch = ft_bitmap->pitch;
switch (ft_bitmap->pixel_mode)
{
case FT_PIXEL_MODE_GRAY: // Grayscale image, 1 byte per pixel.
{
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
for (uint32_t x = 0; x < w; x++)
dst[x] = IM_COL32(255, 255, 255, src[x]);
break;
}
case FT_PIXEL_MODE_MONO: // Monochrome image, 1 bit per pixel. The bits in each byte are ordered from MSB to LSB.
{
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
{
uint8_t bits = 0;
const uint8_t* bits_ptr = src;
for (uint32_t x = 0; x < w; x++, bits <<= 1)
{
if ((x & 7) == 0)
bits = *bits_ptr++;
dst[x] = IM_COL32(255, 255, 255, (bits & 0x80) ? 255 : 0);
}
}
break;
}
case FT_PIXEL_MODE_BGRA:
{
// FIXME: Converting pre-multiplied alpha to straight. Doesn't smell good.
#define DE_MULTIPLY(color, alpha) ImMin((ImU32)(255.0f * (float)color / (float)(alpha + FLT_MIN) + 0.5f), 255u)
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
for (uint32_t x = 0; x < w; x++)
{
uint8_t r = src[x * 4 + 2], g = src[x * 4 + 1], b = src[x * 4], a = src[x * 4 + 3];
dst[x] = IM_COL32(DE_MULTIPLY(r, a), DE_MULTIPLY(g, a), DE_MULTIPLY(b, a), a);
}
#undef DE_MULTIPLY
break;
}
default:
IM_ASSERT(0 && "FreeTypeFont::BlitGlyph(): Unknown bitmap pixel mode!");
}
}
// FreeType memory allocation callbacks
static void* FreeType_Alloc(FT_Memory /*memory*/, long size)
{
return GImGuiFreeTypeAllocFunc((size_t)size, GImGuiFreeTypeAllocatorUserData);
}
static void FreeType_Free(FT_Memory /*memory*/, void* block)
{
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
}
static void* FreeType_Realloc(FT_Memory /*memory*/, long cur_size, long new_size, void* block)
{
// Implement realloc() as we don't ask user to provide it.
if (block == nullptr)
return GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
if (new_size == 0)
{
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
return nullptr;
}
if (new_size > cur_size)
{
void* new_block = GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
memcpy(new_block, block, (size_t)cur_size);
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
return new_block;
}
return block;
}
static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas)
{
IM_ASSERT(atlas->FontLoaderData == nullptr);
ImGui_ImplFreeType_Data* bd = IM_NEW(ImGui_ImplFreeType_Data)();
// FreeType memory management: https://www.freetype.org/freetype2/docs/design/design-4.html
bd->MemoryManager.user = nullptr;
bd->MemoryManager.alloc = &FreeType_Alloc;
bd->MemoryManager.free = &FreeType_Free;
bd->MemoryManager.realloc = &FreeType_Realloc;
// https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library
FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library);
if (error != 0)
{
IM_DELETE(bd);
return false;
}
// If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator.
FT_Add_Default_Modules(bd->Library);
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
// Install svg hooks for FreeType
// https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks
// https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts
SVG_RendererHooks hooks = { ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot };
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", &hooks);
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
// With plutosvg, use provided hooks
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks());
#endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG
// Store our data
atlas->FontLoaderData = (void*)bd;
return true;
}
static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas)
{
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
IM_ASSERT(bd != nullptr);
FT_Done_Library(bd->Library);
IM_DELETE(bd);
atlas->FontLoaderData = nullptr;
}
static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
{
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData);
IM_ASSERT(src->FontLoaderData == nullptr);
src->FontLoaderData = bd_font_data;
if (!bd_font_data->InitFont(bd->Library, src, (ImGuiFreeTypeLoaderFlags)atlas->FontLoaderFlags))
{
IM_DELETE(bd_font_data);
src->FontLoaderData = nullptr;
return false;
}
return true;
}
static void ImGui_ImplFreeType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
{
IM_UNUSED(atlas);
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
IM_DELETE(bd_font_data);
src->FontLoaderData = nullptr;
}
static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
{
IM_UNUSED(atlas);
float size = baked->Size;
if (src->MergeMode && src->SizePixels != 0.0f)
size *= (src->SizePixels / baked->OwnerFont->Sources[0]->SizePixels);
size *= src->ExtraSizeScale;
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
bd_font_data->BakedLastActivated = baked;
// We use one FT_Size per (source + baked) combination.
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
IM_ASSERT(bd_baked_data != nullptr);
IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData();
FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize);
FT_Activate_Size(bd_baked_data->FtSize);
// Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height'
// is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me.
// FT_Set_Pixel_Sizes() doesn't seem to get us the same result."
// (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL)
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
FT_Size_RequestRec req;
req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM;
req.width = 0;
req.height = (uint32_t)(size * 64 * rasterizer_density);
req.horiResolution = 0;
req.vertResolution = 0;
FT_Request_Size(bd_font_data->FtFace, &req);
// Output
if (src->MergeMode == false)
{
// Read metrics
FT_Size_Metrics metrics = bd_baked_data->FtSize->metrics;
const float scale = 1.0f / (rasterizer_density * src->ExtraSizeScale);
baked->Ascent = (float)FT_CEIL(metrics.ascender) * scale; // The pixel extents above the baseline in pixels (typically positive).
baked->Descent = (float)FT_CEIL(metrics.descender) * scale; // The extents below the baseline in pixels (typically negative).
//LineSpacing = (float)FT_CEIL(metrics.height) * scale; // The baseline-to-baseline distance. Note that it usually is larger than the sum of the ascender and descender taken as absolute values. There is also no guarantee that no glyphs extend above or below subsequent baselines when using this distance. Think of it as a value the designer of the font finds appropriate.
//LineGap = (float)FT_CEIL(metrics.height - metrics.ascender + metrics.descender) * scale; // The spacing in pixels between one row's descent and the next row's ascent.
//MaxAdvanceWidth = (float)FT_CEIL(metrics.max_advance) * scale; // This field gives the maximum horizontal cursor advance for all glyphs in the font.
}
return true;
}
static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
{
IM_UNUSED(atlas);
IM_UNUSED(baked);
IM_UNUSED(src);
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
IM_ASSERT(bd_baked_data != nullptr);
FT_Done_Size(bd_baked_data->FtSize);
bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE()
}
static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
{
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
if (glyph_index == 0)
return false;
if (bd_font_data->BakedLastActivated != baked) // <-- could use id
{
// Activate current size
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
FT_Activate_Size(bd_baked_data->FtSize);
bd_font_data->BakedLastActivated = baked;
}
const FT_Glyph_Metrics* metrics = ImGui_ImplFreeType_LoadGlyph(bd_font_data, codepoint);
if (metrics == nullptr)
return false;
FT_Face face = bd_font_data->FtFace;
FT_GlyphSlot slot = face->glyph;
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
// Load metrics only mode
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
if (out_advance_x != NULL)
{
IM_ASSERT(out_glyph == NULL);
*out_advance_x = advance_x;
return true;
}
// Render glyph into a bitmap (currently held by FreeType)
FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL;
FT_Error error = FT_Render_Glyph(slot, render_mode);
const FT_Bitmap* ft_bitmap = &slot->bitmap;
if (error != 0 || ft_bitmap == nullptr)
return false;
const int w = (int)ft_bitmap->width;
const int h = (int)ft_bitmap->rows;
const bool is_visible = (w != 0 && h != 0);
// Prepare glyph
out_glyph->Codepoint = codepoint;
out_glyph->AdvanceX = advance_x;
// Pack and retrieve position inside texture atlas
if (is_visible)
{
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
if (pack_id == ImFontAtlasRectId_Invalid)
{
// Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
return false;
}
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
// Render pixels to our temporary buffer
atlas->Builder->TempBuffer.resize(w * h * 4);
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); // Snap scaled offset.
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
float recip_h = 1.0f / rasterizer_density;
float recip_v = 1.0f / rasterizer_density;
// Register glyph
float glyph_off_x = (float)face->glyph->bitmap_left;
float glyph_off_y = (float)-face->glyph->bitmap_top;
out_glyph->X0 = glyph_off_x * recip_h + font_off_x;
out_glyph->Y0 = glyph_off_y * recip_v + font_off_y;
out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x;
out_glyph->Y1 = (glyph_off_y + h) * recip_v + font_off_y;
out_glyph->Visible = true;
out_glyph->Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
out_glyph->PackId = pack_id;
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
}
return true;
}
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
{
IM_UNUSED(atlas);
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
return glyph_index != 0;
}
const ImFontLoader* ImGuiFreeType::GetFontLoader()
{
static ImFontLoader loader;
loader.Name = "FreeType";
loader.LoaderInit = ImGui_ImplFreeType_LoaderInit;
loader.LoaderShutdown = ImGui_ImplFreeType_LoaderShutdown;
loader.FontSrcInit = ImGui_ImplFreeType_FontSrcInit;
loader.FontSrcDestroy = ImGui_ImplFreeType_FontSrcDestroy;
loader.FontSrcContainsGlyph = ImGui_ImplFreetype_FontSrcContainsGlyph;
loader.FontBakedInit = ImGui_ImplFreeType_FontBakedInit;
loader.FontBakedDestroy = ImGui_ImplFreeType_FontBakedDestroy;
loader.FontBakedLoadGlyph = ImGui_ImplFreeType_FontBakedLoadGlyph;
loader.FontBakedSrcLoaderDataSize = sizeof(ImGui_ImplFreeType_FontSrcBakedData);
return &loader;
}
void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data)
{
GImGuiFreeTypeAllocFunc = alloc_func;
GImGuiFreeTypeFreeFunc = free_func;
GImGuiFreeTypeAllocatorUserData = user_data;
}
bool ImGuiFreeType::DebugEditFontLoaderFlags(unsigned int* p_font_loader_flags)
{
bool edited = false;
edited |= ImGui::CheckboxFlags("NoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoHinting);
edited |= ImGui::CheckboxFlags("NoAutoHint", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoAutoHint);
edited |= ImGui::CheckboxFlags("ForceAutoHint",p_font_loader_flags, ImGuiFreeTypeLoaderFlags_ForceAutoHint);
edited |= ImGui::CheckboxFlags("LightHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LightHinting);
edited |= ImGui::CheckboxFlags("MonoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_MonoHinting);
edited |= ImGui::CheckboxFlags("Bold", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bold);
edited |= ImGui::CheckboxFlags("Oblique", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Oblique);
edited |= ImGui::CheckboxFlags("Monochrome", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Monochrome);
edited |= ImGui::CheckboxFlags("LoadColor", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LoadColor);
edited |= ImGui::CheckboxFlags("Bitmap", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bitmap);
return edited;
}
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c
// The original code from the demo is licensed under CeCILL-C Free Software License Agreement (https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT)
struct LunasvgPortState
{
FT_Error err = FT_Err_Ok;
lunasvg::Matrix matrix;
std::unique_ptr<lunasvg::Document> svg = nullptr;
};
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state)
{
*_state = IM_NEW(LunasvgPortState)();
return FT_Err_Ok;
}
static void ImGuiLunasvgPortFree(FT_Pointer* _state)
{
IM_DELETE(*(LunasvgPortState**)_state);
}
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state)
{
LunasvgPortState* state = *(LunasvgPortState**)_state;
// If there was an error while loading the svg in ImGuiLunasvgPortPresetSlot(), the renderer hook still get called, so just returns the error.
if (state->err != FT_Err_Ok)
return state->err;
// rows is height, pitch (or stride) equals to width * sizeof(int32)
lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch);
#if LUNASVG_VERSION_MAJOR >= 3
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
#else
state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
#endif
state->err = FT_Err_Ok;
return state->err;
}
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state)
{
FT_SVG_Document document = (FT_SVG_Document)slot->other;
LunasvgPortState* state = *(LunasvgPortState**)_state;
FT_Size_Metrics& metrics = document->metrics;
// This function is called twice, once in the FT_Load_Glyph() and another right before ImGuiLunasvgPortRender().
// If it's the latter, don't do anything because it's // already done in the former.
if (cache)
return state->err;
state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length);
if (state->svg == nullptr)
{
state->err = FT_Err_Invalid_SVG_Document;
return state->err;
}
#if LUNASVG_VERSION_MAJOR >= 3
lunasvg::Box box = state->svg->boundingBox();
#else
lunasvg::Box box = state->svg->box();
#endif
double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h);
double xx = (double)document->transform.xx / (1 << 16);
double xy = -(double)document->transform.xy / (1 << 16);
double yx = -(double)document->transform.yx / (1 << 16);
double yy = (double)document->transform.yy / (1 << 16);
double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem;
double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem;
#if LUNASVG_VERSION_MAJOR >= 3
// Scale, transform and pre-translate the matrix for the rendering step
state->matrix = lunasvg::Matrix::translated(-box.x, -box.y);
state->matrix.multiply(lunasvg::Matrix(xx, xy, yx, yy, x0, y0));
state->matrix.scale(scale, scale);
// Apply updated transformation to the bounding box
box.transform(state->matrix);
#else
// Scale and transform, we don't translate the svg yet
state->matrix.identity();
state->matrix.scale(scale, scale);
state->matrix.transform(xx, xy, yx, yy, x0, y0);
state->svg->setMatrix(state->matrix);
// Pre-translate the matrix for the rendering step
state->matrix.translate(-box.x, -box.y);
// Get the box again after the transformation
box = state->svg->box();
#endif
// Calculate the bitmap size
slot->bitmap_left = FT_Int(box.x);
slot->bitmap_top = FT_Int(-box.y);
slot->bitmap.rows = (unsigned int)(ImCeil((float)box.h));
slot->bitmap.width = (unsigned int)(ImCeil((float)box.w));
slot->bitmap.pitch = slot->bitmap.width * 4;
slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA;
// Compute all the bearings and set them correctly. The outline is scaled already, we just need to use the bounding box.
double metrics_width = box.w;
double metrics_height = box.h;
double horiBearingX = box.x;
double horiBearingY = -box.y;
double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0;
double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0;
slot->metrics.width = FT_Pos(IM_ROUND(metrics_width * 64.0)); // Using IM_ROUND() assume width and height are positive
slot->metrics.height = FT_Pos(IM_ROUND(metrics_height * 64.0));
slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64);
slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64);
slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64);
slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64);
if (slot->metrics.vertAdvance == 0)
slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0);
state->err = FT_Err_Ok;
return state->err;
}
#endif // #ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
//-----------------------------------------------------------------------------
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#ifdef _MSC_VER
#pragma warning (pop)
#endif
#endif // #ifndef IMGUI_DISABLE
-83
View File
@@ -1,83 +0,0 @@
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
// (headers)
#pragma once
#include "imgui.h" // IMGUI_API
#ifndef IMGUI_DISABLE
// Usage:
// - Add '#define IMGUI_ENABLE_FREETYPE' in your imconfig to automatically enable support
// for imgui_freetype in imgui. It is equivalent to selecting the default loader with:
// io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())
// Optional support for OpenType SVG fonts:
// - Add '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG' to use plutosvg (not provided). See #7927.
// - Add '#define IMGUI_ENABLE_FREETYPE_LUNASVG' to use lunasvg (not provided). See #6591.
// Forward declarations
struct ImFontAtlas;
struct ImFontLoader;
// Hinting greatly impacts visuals (and glyph sizes).
// - By default, hinting is enabled and the font's native hinter is preferred over the auto-hinter.
// - When disabled, FreeType generates blurrier glyphs, more or less matches the stb_truetype.h
// - The Default hinting mode usually looks good, but may distort glyphs in an unusual way.
// - The Light hinting mode generates fuzzier glyphs but better matches Microsoft's rasterizer.
// You can set those flags globally in ImFontAtlas::FontLoaderFlags
// You can set those flags on a per font basis in ImFontConfig::FontLoaderFlags
typedef unsigned int ImGuiFreeTypeLoaderFlags;
enum ImGuiFreeTypeLoaderFlags_
{
ImGuiFreeTypeLoaderFlags_NoHinting = 1 << 0, // Disable hinting. This generally generates 'blurrier' bitmap glyphs when the glyph are rendered in any of the anti-aliased modes.
ImGuiFreeTypeLoaderFlags_NoAutoHint = 1 << 1, // Disable auto-hinter.
ImGuiFreeTypeLoaderFlags_ForceAutoHint = 1 << 2, // Indicates that the auto-hinter is preferred over the font's native hinter.
ImGuiFreeTypeLoaderFlags_LightHinting = 1 << 3, // A lighter hinting algorithm for gray-level modes. Many generated glyphs are fuzzier but better resemble their original shape. This is achieved by snapping glyphs to the pixel grid only vertically (Y-axis), as is done by Microsoft's ClearType and Adobe's proprietary font renderer. This preserves inter-glyph spacing in horizontal text.
ImGuiFreeTypeLoaderFlags_MonoHinting = 1 << 4, // Strong hinting algorithm that should only be used for monochrome output.
ImGuiFreeTypeLoaderFlags_Bold = 1 << 5, // Styling: Should we artificially embolden the font?
ImGuiFreeTypeLoaderFlags_Oblique = 1 << 6, // Styling: Should we slant the font, emulating italic style?
ImGuiFreeTypeLoaderFlags_Monochrome = 1 << 7, // Disable anti-aliasing. Combine this with MonoHinting for best results!
ImGuiFreeTypeLoaderFlags_LoadColor = 1 << 8, // Enable FreeType color-layered glyphs
ImGuiFreeTypeLoaderFlags_Bitmap = 1 << 9, // Enable FreeType bitmap glyphs
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
ImGuiFreeTypeBuilderFlags_NoHinting = ImGuiFreeTypeLoaderFlags_NoHinting,
ImGuiFreeTypeBuilderFlags_NoAutoHint = ImGuiFreeTypeLoaderFlags_NoAutoHint,
ImGuiFreeTypeBuilderFlags_ForceAutoHint = ImGuiFreeTypeLoaderFlags_ForceAutoHint,
ImGuiFreeTypeBuilderFlags_LightHinting = ImGuiFreeTypeLoaderFlags_LightHinting,
ImGuiFreeTypeBuilderFlags_MonoHinting = ImGuiFreeTypeLoaderFlags_MonoHinting,
ImGuiFreeTypeBuilderFlags_Bold = ImGuiFreeTypeLoaderFlags_Bold,
ImGuiFreeTypeBuilderFlags_Oblique = ImGuiFreeTypeLoaderFlags_Oblique,
ImGuiFreeTypeBuilderFlags_Monochrome = ImGuiFreeTypeLoaderFlags_Monochrome,
ImGuiFreeTypeBuilderFlags_LoadColor = ImGuiFreeTypeLoaderFlags_LoadColor,
ImGuiFreeTypeBuilderFlags_Bitmap = ImGuiFreeTypeLoaderFlags_Bitmap,
#endif
};
// Obsolete names (will be removed)
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
typedef ImGuiFreeTypeLoaderFlags_ ImGuiFreeTypeBuilderFlags_;
#endif
namespace ImGuiFreeType
{
// This is automatically assigned when using '#define IMGUI_ENABLE_FREETYPE'.
// If you need to dynamically select between multiple builders:
// - you can manually assign this builder with 'atlas->SetFontLoader(ImGuiFreeType::GetFontLoader())'
// - prefer deep-copying this into your own ImFontLoader instance if you use hot-reloading that messes up static data.
IMGUI_API const ImFontLoader* GetFontLoader();
// Override allocators. By default ImGuiFreeType will use IM_ALLOC()/IM_FREE()
// However, as FreeType does lots of allocations we provide a way for the user to redirect it to a separate memory heap if desired.
IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = nullptr);
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
// Obsolete names (will be removed)
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
//static inline bool BuildFontAtlas(ImFontAtlas* atlas, unsigned int flags = 0) { atlas->FontBuilderIO = GetBuilderForFreeType(); atlas->FontLoaderFlags = flags; return atlas->Build(); } // Prefer using '#define IMGUI_ENABLE_FREETYPE'
#endif
}
#endif // #ifndef IMGUI_DISABLE
+1 -1
View File
@@ -40,7 +40,7 @@ extern "C" {
* read-only segment just like a normal const array would.
*/
#if defined(__APPLE__)
# define INCBIN_SECTION "__DATA,__const"
# define INCBIN_SECTION ".const_data"
# define INCBIN_MANGLE "_"
#else
# define INCBIN_SECTION ".rodata"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-51
View File
@@ -1,51 +0,0 @@
<?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>
+10 -17
View File
@@ -4,13 +4,6 @@
// 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
@@ -19,8 +12,8 @@
#include <winver.h>
VS_VERSION_INFO VERSIONINFO
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
FILEVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
PRODUCTVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
@@ -31,14 +24,14 @@ BEGIN
BEGIN
BLOCK "040904B0" // US-English, Unicode
BEGIN
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"
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"
END
END
BLOCK "VarFileInfo"
-10
View File
@@ -1,10 +0,0 @@
# 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

-23
View File
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #d82652;
}
</style>
</defs>
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
<g>
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
<g>
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+124 -2510
View File
File diff suppressed because it is too large Load Diff
+131 -2517
View File
File diff suppressed because it is too large Load Diff
+128 -2514
View File
File diff suppressed because it is too large Load Diff
+129 -2515
View File
File diff suppressed because it is too large Load Diff
+129 -2515
View File
File diff suppressed because it is too large Load Diff
+131 -2517
View File
File diff suppressed because it is too large Load Diff
+126 -2512
View File
File diff suppressed because it is too large Load Diff
+126 -2512
View File
File diff suppressed because it is too large Load Diff
+8 -8
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 = "#282836", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#26262E", --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 = "#282836"
--surface-variant = "#20202C"
--on-primary = "#FFFFFF"
--on-secondary = "#FFFFFF"
--on-background = "#E8E6F0"
--on-surface = "#E8E6F0"
--on-surface-medium = "rgba(232,230,240,0.85)"
--on-surface-disabled = "rgba(232,230,240,0.58)"
--on-surface-medium = "rgba(232,230,240,0.72)"
--on-surface-disabled = "rgba(232,230,240,0.40)"
--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.24)"
--card-border = "rgba(200,190,240,0.10)"
--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(124,108,255,0.28)"
--glass-border = "rgba(200,190,240,0.10)"
--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.16)"
--rim-light = "rgba(124,108,255,0.10)"
--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.85)"
--window-control = "rgba(232,230,240,0.72)"
--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)"
+7 -7
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.86)"
--on-surface-disabled = "rgba(30,30,42,0.62)"
--on-surface-medium = "rgba(30,30,42,0.72)"
--on-surface-disabled = "rgba(30,30,42,0.38)"
--error = "#E0304A"
--on-error = "#FFFFFF"
--success = "#18A860"
--on-success = "#FFFFFF"
--warning = "#E09020"
--on-warning = "#000000"
--divider = "rgba(30,30,60,0.20)"
--outline = "rgba(30,30,60,0.24)"
--divider = "rgba(30,30,60,0.12)"
--outline = "rgba(30,30,60,0.15)"
--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.20)"
--glass-fill = "rgba(255,255,255,0.55)"
--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.10)"
--hover-overlay = "rgba(96,64,224,0.04)"
--active-overlay = "rgba(96,64,224,0.08)"
--rim-light = "rgba(30,30,42,0.22)"
--rim-light = "rgba(96,64,224,0.06)"
--status-divider = "rgba(30,30,60,0.08)"
--sidebar-hover = "rgba(96,64,224,0.07)"
--sidebar-icon = "rgba(30,30,42,0.50)"
+8 -8
View File
@@ -2,7 +2,7 @@
name = "Dark"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2E2E32", --elevation-3 = "#363638", --elevation-4 = "#404044" }
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2C2C2E", --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 = "#2E2E32"
--surface-variant = "#262628"
--on-primary = "#000000"
--on-secondary = "#000000"
--on-background = "#D0D0D4"
--on-surface = "#D0D0D4"
--on-surface-medium = "rgba(208,208,212,0.85)"
--on-surface-disabled = "rgba(208,208,212,0.58)"
--on-surface-medium = "rgba(208,208,212,0.75)"
--on-surface-disabled = "rgba(208,208,212,0.45)"
--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.24)"
--card-border = "rgba(220,220,225,0.12)"
--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(154,175,200,0.28)"
--glass-border = "rgba(220,220,225,0.12)"
--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.14)"
--rim-light = "rgba(220,220,225,0.08)"
--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.85)"
--window-control = "rgba(208,208,212,0.75)"
--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)"
+8 -8
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.86)"
--on-surface-disabled = "rgba(58,46,34,0.62)"
--on-surface-medium = "rgba(58,46,34,0.68)"
--on-surface-disabled = "rgba(58,46,34,0.38)"
--error = "#A0524A"
--on-error = "#FFFFFF"
--success = "#4E8A42"
--success = "#6A8A5C"
--on-success = "#FFFFFF"
--warning = "#C08840"
--on-warning = "#000000"
--divider = "rgba(140,110,70,0.20)"
--outline = "rgba(140,110,70,0.24)"
--divider = "rgba(140,110,70,0.14)"
--outline = "rgba(140,110,70,0.16)"
--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.20)"
--glass-fill = "rgba(255,252,245,0.58)"
--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.10)"
--hover-overlay = "rgba(176,120,64,0.05)"
--active-overlay = "rgba(176,120,64,0.10)"
--rim-light = "rgba(58,46,34,0.22)"
--rim-light = "rgba(212,160,108,0.10)"
--status-divider = "rgba(176,120,64,0.10)"
--sidebar-hover = "rgba(176,120,64,0.08)"
--sidebar-icon = "rgba(58,46,34,0.50)"
+7 -7
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.86)"
--on-surface-disabled = "rgba(28,21,37,0.62)"
--on-surface-medium = "rgba(28,21,37,0.72)"
--on-surface-disabled = "rgba(28,21,37,0.40)"
--error = "#C62828"
--on-error = "#FFFFFF"
--success = "#2E7D32"
--on-success = "#FFFFFF"
--warning = "#E65100"
--on-warning = "#000000"
--divider = "rgba(120,80,160,0.20)"
--outline = "rgba(120,80,160,0.24)"
--divider = "rgba(120,80,160,0.12)"
--outline = "rgba(120,80,160,0.14)"
--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.20)"
--glass-fill = "rgba(255,255,255,0.55)"
--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.10)"
--hover-overlay = "rgba(140,107,175,0.05)"
--active-overlay = "rgba(140,107,175,0.10)"
--rim-light = "rgba(28,21,37,0.22)"
--rim-light = "rgba(180,140,255,0.10)"
--status-divider = "rgba(140,107,175,0.10)"
--sidebar-hover = "rgba(140,107,175,0.08)"
--sidebar-icon = "rgba(28,21,37,0.50)"
-190
View File
@@ -1,190 +0,0 @@
[theme]
name = "Jade"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#071210", --elevation-1 = "#0C1A16", --elevation-2 = "#16261F", --elevation-3 = "#1D3128", --elevation-4 = "#243B30" }
images = { background_image = "backgrounds/texture/jade_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
--primary = "#2FA07A"
--primary-variant = "#1E7357"
--primary-light = "#7FD1B5"
--secondary = "#C9A24E"
--secondary-variant = "#A8842F"
--secondary-light = "#E0C583"
--background = "#071210"
--surface = "#0C1A16"
--surface-variant = "#16261F"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#DCEDE4"
--on-surface = "#DCEDE4"
--on-surface-medium = "rgba(220,237,228,0.85)"
--on-surface-disabled = "rgba(220,237,228,0.58)"
--error = "#CF6679"
--on-error = "#000000"
--success = "#81C784"
--on-success = "#000000"
--warning = "#FFB74D"
--on-warning = "#000000"
--divider = "rgba(130,205,170,0.14)"
--outline = "rgba(130,205,170,0.16)"
--scrim = "rgba(0,0,0,0.6)"
--surface-hover = "rgba(130,205,170,0.07)"
--surface-alt = "rgba(130,205,170,0.05)"
--surface-active = "rgba(130,205,170,0.10)"
--glass-button = "rgba(130,205,170,0.06)"
--glass-button-hover = "rgba(130,205,170,0.12)"
--card-border = "rgba(130,205,170,0.26)"
--text-shadow = "rgba(0,0,0,0.50)"
--input-overlay-text = "rgba(220,237,228,0.30)"
--slider-text = "rgba(220,237,228,0.85)"
--thumb-fill = "rgba(130,205,170,0.15)"
--thumb-border = "rgba(130,205,170,0.50)"
--disabled-label = "rgba(130,205,170,0.18)"
--chart-grid = "rgba(130,205,170,0.05)"
--chart-crosshair = "rgba(130,205,170,0.15)"
--chart-hover-ring = "rgba(130,205,170,0.30)"
--tooltip-bg = "rgba(9,20,16,0.92)"
--tooltip-border = "rgba(130,205,170,0.12)"
--glass-fill = "rgba(130,205,170,0.08)"
--glass-border = "rgba(47,160,122,0.30)"
--glass-noise-tint = "rgba(130,205,170,0.03)"
--tactile-top = "rgba(130,205,170,0.06)"
--tactile-bottom = "rgba(130,205,170,0.0)"
--hover-overlay = "rgba(130,205,170,0.05)"
--active-overlay = "rgba(130,205,170,0.10)"
--rim-light = "rgba(130,205,170,0.14)"
--status-divider = "rgba(130,205,170,0.08)"
--sidebar-hover = "rgba(130,205,170,0.10)"
--sidebar-icon = "rgba(130,205,170,0.42)"
--sidebar-badge = "rgba(220,237,228,1.0)"
--sidebar-divider = "rgba(130,205,170,0.06)"
--chart-line = "rgba(130,205,170,0.10)"
--window-control = "rgba(220,237,228,0.78)"
--window-control-hover = "rgba(130,205,170,0.12)"
--window-close-hover = "rgba(232,17,35,0.78)"
--spinner-track = "rgba(130,205,170,0.10)"
--spinner-active = "rgba(79,184,154,0.85)"
--shutdown-panel-bg = "rgba(7,18,14,0.90)"
--shutdown-panel-border = "rgba(130,205,170,0.07)"
--ram-bar-app = "#2FA07A"
--ram-bar-system = "rgba(255,255,255,0.18)"
--accent-total = "#7FD1B5"
--accent-shielded = "#4FB89A"
--accent-transparent = "#C9A24E"
--accent-action = "#2FA07A"
--accent-market = "#4FB89A"
--accent-portfolio = "#7FD1B5"
--toast-info-accent = "#2FA07A"
--toast-info-text = "#7FD1B5"
--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)"
--toast-warning-text = "rgba(255,230,130,1.0)"
--toast-error-accent = "rgba(204,64,64,1.0)"
--toast-error-text = "rgba(255,153,153,1.0)"
--snackbar-bg = "rgba(24,40,34,0.95)"
--snackbar-text = "rgba(220,237,228,0.87)"
--snackbar-action = "rgba(79,184,154,1.0)"
--snackbar-action-hover = "rgba(127,209,181,1.0)"
--switch-track-off = "rgba(130,205,170,0.12)"
--switch-track-on = "rgba(47,160,122,0.50)"
--switch-thumb-off = "#A0C0B4"
--switch-thumb-on = "#DCEDE4"
--control-shadow = "rgba(0,0,0,0.24)"
--checkbox-check = "#000000"
--app-bar-shadow = "rgba(0,0,0,0.25)"
[backdrop]
base-color-top = "rgba(14,32,26,210)"
base-color-bottom = "rgba(6,18,14,210)"
texture-tint-alpha = 120
gradient-top-r = 10
gradient-top-g = 30
gradient-top-b = 22
gradient-top-a = 90
gradient-bottom-r = 5
gradient-bottom-g = 16
gradient-bottom-b = 12
gradient-bottom-a = 70
background-alpha = 0.42
surface-alpha = 0.56
frame-alpha = 0.78
surface-inline-alpha = 0.58
background-inline-alpha = 0.40
# ---------------------------------------------------------------------------
# Theme Visual Effects — Jade (veins of gold shifting through the stone)
# Jade's signature is a slow jade→gold color-shifting border on every glass
# panel + the active nav button — a vein of gold surfacing through nephrite.
# It's drawn via AddRect so it hugs the real rounded corners (no polygonal
# edge-trace). Sparse jade motes drift up the viewport. No other theme turns
# gradient-border-panels on, so the panel-wide vein is Jade's own —
# deliberately NOT Obsidian's specular glare.
# ---------------------------------------------------------------------------
[effects]
hue-cycle-enabled = { size = 0.0 }
rainbow-border-enabled = { size = 0.0 }
# No shimmer sweep — replaced by specular glare
shimmer-enabled = { size = 0.0 }
positional-hue-enabled = { size = 0.0 }
glow-pulse-enabled = { size = 0.0 }
# Edge-trace OFF — its hand-walked perimeter chamfers rounded corners.
# Jade's vein is the gradient-border below (corner-clean via AddRect).
edge-trace-enabled = { size = 0.0 }
edge-trace-speed = { size = 0.16 }
edge-trace-length = { size = 0.34 }
edge-trace-thickness = { size = 1.6 }
edge-trace-alpha = { size = 0.55 }
edge-trace-color = { color = "#C9A24E" }
# Specular glare OFF — that's Obsidian's signature; Jade shouldn't echo it.
specular-glare-enabled = { size = 0.0 }
specular-glare-speed = { size = 0.018 }
specular-glare-intensity = { size = 0.008 }
specular-glare-radius = { size = 0.65 }
specular-glare-count = { size = 1.0 }
specular-glare-color = { color = "rgba(150,220,180,1.0)" }
# HERO — vein of gold: a slow jade→gold color-shifting border on the active
# nav button AND (via gradient-border-panels) every glass panel. Drawn with
# AddRect so it follows the rounded corners exactly. Panels drift at a softer
# alpha and position-phased offset, so a screenful reads like veins at
# different depths rather than one synchronized pulse.
gradient-border-enabled = { size = 1.0 }
gradient-border-panels = { size = 1.0 }
gradient-border-speed = { size = 0.10 }
gradient-border-thickness = { size = 1.5 }
gradient-border-alpha = { size = 0.55 }
gradient-border-color-a = { color = "#7FD1B5" }
gradient-border-color-b = { color = "#C9A24E" }
# Ambient jade motes — sparse, slow, cool green particles drifting up the
# viewport (recolored ember-rise; a different mood from dragonx's fire embers).
ember-rise-enabled = { size = 1.0 }
ember-rise-count = { size = 5.0 }
ember-rise-speed = { size = 0.18 }
ember-rise-particle-size = { size = 1.4 }
ember-rise-alpha = { size = 0.26 }
ember-rise-color = { color = "#7FD1B5" }
# Shader-like viewport overlay — deep green stone atmosphere
viewport-wash-enabled = { size = 1.0 }
viewport-wash-alpha = { size = 0.05 }
viewport-wash-tl = { color = "#12402E" }
viewport-wash-tr = { color = "#0E3828" }
viewport-wash-bl = { color = "#16442E" }
viewport-wash-br = { color = "#1A4A34" }
viewport-wash-rotate = { size = 0.015 }
viewport-wash-pulse = { size = 0.0 }
viewport-wash-pulse-depth = { size = 0.0 }
viewport-vignette-enabled = { size = 1.0 }
viewport-vignette-color = { color = "#04140D" }
viewport-vignette-radius = { size = 0.22 }
viewport-vignette-alpha = { size = 0.15 }
+9 -9
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.86)"
--on-surface-disabled = "rgba(42,44,48,0.62)"
--on-surface-medium = "rgba(42,44,48,0.68)"
--on-surface-disabled = "rgba(42,44,48,0.38)"
--error = "#8C5A62"
--on-error = "#FFFFFF"
--success = "#3D7A42"
--success = "#5A7E5C"
--on-success = "#FFFFFF"
--warning = "#9A7A2E"
--warning = "#8A7A52"
--on-warning = "#000000"
--divider = "rgba(42,44,48,0.20)"
--outline = "rgba(42,44,48,0.24)"
--divider = "rgba(42,44,48,0.12)"
--outline = "rgba(42,44,48,0.14)"
--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.20)"
--glass-fill = "rgba(255,255,255,0.55)"
--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.10)"
--hover-overlay = "rgba(42,44,48,0.04)"
--active-overlay = "rgba(42,44,48,0.08)"
--rim-light = "rgba(42,44,48,0.22)"
--rim-light = "rgba(42,44,48,0.06)"
--status-divider = "rgba(42,44,48,0.08)"
--sidebar-hover = "rgba(42,44,48,0.05)"
--sidebar-icon = "rgba(42,44,48,0.45)"
+9 -9
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.86)"
--on-surface-disabled = "rgba(44,42,40,0.62)"
--on-surface-medium = "rgba(44,42,40,0.68)"
--on-surface-disabled = "rgba(44,42,40,0.38)"
--error = "#8C5250"
--on-error = "#FFFFFF"
--success = "#3F7A48"
--success = "#5C7A62"
--on-success = "#FFFFFF"
--warning = "#9A7A2E"
--warning = "#8A7A4C"
--on-warning = "#000000"
--divider = "rgba(80,75,68,0.20)"
--outline = "rgba(80,75,68,0.24)"
--divider = "rgba(80,75,68,0.12)"
--outline = "rgba(80,75,68,0.14)"
--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.20)"
--glass-fill = "rgba(255,255,254,0.62)"
--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.10)"
--hover-overlay = "rgba(110,117,128,0.04)"
--active-overlay = "rgba(110,117,128,0.08)"
--rim-light = "rgba(44,42,40,0.22)"
--rim-light = "rgba(180,175,168,0.10)"
--status-divider = "rgba(110,117,128,0.08)"
--sidebar-hover = "rgba(110,117,128,0.06)"
--sidebar-icon = "rgba(44,42,40,0.48)"
+32 -32
View File
@@ -2,25 +2,25 @@
name = "Obsidian"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#252030", --elevation-3 = "#2D2838", --elevation-4 = "#353040" }
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#1C1625", --elevation-3 = "#211A2C", --elevation-4 = "#261E33" }
images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
--primary = "#9A6BA8"
--primary-variant = "#7A4888"
--primary-light = "#C5A8CC"
--secondary = "#A898D8"
--secondary-variant = "#8074C8"
--secondary-light = "#CCC4D9"
--primary = "#AB47BC"
--primary-variant = "#8E24AA"
--primary-light = "#CE93D8"
--secondary = "#B388FF"
--secondary-variant = "#7C4DFF"
--secondary-light = "#D1C4E9"
--background = "#0A0810"
--surface = "#110E18"
--surface-variant = "#252030"
--surface-variant = "#1C1625"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#E8E0F0"
--on-surface = "#E8E0F0"
--on-surface-medium = "rgba(232,224,240,0.85)"
--on-surface-disabled = "rgba(232,224,240,0.58)"
--on-surface-medium = "rgba(232,224,240,0.75)"
--on-surface-disabled = "rgba(232,224,240,0.45)"
--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.26)"
--card-border = "rgba(200,180,255,0.14)"
--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(154,107,168,0.30)"
--glass-border = "rgba(200,180,255,0.14)"
--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.14)"
--rim-light = "rgba(200,180,255,0.08)"
--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(168,152,216,0.85)"
--spinner-active = "rgba(179,136,255,0.85)"
--shutdown-panel-bg = "rgba(10,8,16,0.90)"
--shutdown-panel-border = "rgba(200,180,255,0.07)"
--ram-bar-app = "#9A6BA8"
--ram-bar-app = "#AB47BC"
--ram-bar-system = "rgba(255,255,255,0.18)"
--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"
--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"
--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(168,152,216,1.0)"
--snackbar-action-hover = "rgba(197,168,204,1.0)"
--snackbar-action = "rgba(179,136,255,1.0)"
--snackbar-action-hover = "rgba(206,147,216,1.0)"
--switch-track-off = "rgba(200,180,255,0.12)"
--switch-track-on = "rgba(154,107,168,0.50)"
--switch-track-on = "rgba(171,71,188,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 = "#C5A8CC" }
gradient-border-color-b = { color = "#6878A8" }
gradient-border-color-a = { color = "#CE93D8" }
gradient-border-color-b = { color = "#3F51B5" }
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 = "#3A2860" }
viewport-wash-tr = { color = "#2A3058" }
viewport-wash-bl = { color = "#302860" }
viewport-wash-br = { color = "#4A3068" }
viewport-wash-tl = { color = "#4A148C" }
viewport-wash-tr = { color = "#1A237E" }
viewport-wash-bl = { color = "#311B92" }
viewport-wash-br = { color = "#6A1B9A" }
viewport-wash-rotate = { size = 0.015 }
viewport-wash-pulse = { size = 0.0 }
viewport-wash-pulse-depth = { size = 0.0 }
+30 -98
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 = "#281C16", --elevation-3 = "#30241C", --elevation-4 = "#382C22" }
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#201410", --elevation-3 = "#261914", --elevation-4 = "#2C1E18" }
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 = "#281C16"
--surface-variant = "#201410"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#F0E0D8"
--on-surface = "#F0E0D8"
--on-surface-medium = "rgba(240,224,216,0.85)"
--on-surface-disabled = "rgba(240,224,216,0.58)"
--on-surface-medium = "rgba(240,224,216,0.7)"
--on-surface-disabled = "rgba(240,224,216,0.44)"
--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(211,47,47,0.26)"
--glass-border = "rgba(255,180,140,0.13)"
--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.14)"
--rim-light = "rgba(255,180,140,0.08)"
--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 = 560.0 }
confirm-popup-max-width = { size = 420.0 }
confirm-addr-card-height = { size = 28.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 }
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 }
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 = 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 }
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 }
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.92 }
confirm-popup-width-ratio = { size = 0.85 }
confirm-glass-rounding-ratio = { size = 0.75 }
confirm-addr-trunc-len = { size = 48 }
confirm-divider-thickness = { size = 1.0 }
@@ -700,13 +700,6 @@ status-pill-bg-alpha = { size = 30 }
status-pill-y-offset = { size = 1 }
confirmed-threshold = { size = 10 }
# NOTE: [banners.*] and [layout] are parsed as flat sections (kFlatSections in ui_schema.cpp).
# Persistent node/RPC error strip at the top of the content column (see App::renderNodeStatusBanner).
# Slightly taller than the per-tab sync banner so it comfortably holds the Reconnect/Restart action.
[banners.node-status]
min-height = { size = 26.0 }
height = { size = 30.0 }
[tabs.transactions]
search-max-width = 300.0
search-width-ratio = 0.3
@@ -773,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.72 }
button-label-y-ratio = { size = 0.78 }
chart-line-thickness = { size = 1.5 }
details-card-min-height = { size = 50.0 }
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
@@ -786,11 +779,8 @@ 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
@@ -817,7 +807,6 @@ 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 }
@@ -881,7 +870,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 = 54.0 }
chart-y-axis-min-padding = { size = 40.0 }
chart-y-axis-padding = { size = 70.0 }
chart-dot-min-radius = { size = 1.5 }
chart-dot-radius = { size = 2.0 }
@@ -901,21 +890,8 @@ 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 }
@@ -938,7 +914,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 = 2.0 }
scanline-line-alpha = { size = 4.0 }
scanline-glow-spread = { size = 4.0 }
scanline-glow-intensity = { size = 0.6 }
scanline-glow-color = { size = 255.0 }
@@ -966,11 +942,6 @@ edition-label = { position = 120 }
link-button = { width = 100, font = "button-sm" }
close-button = { width = 120, font = "button", align = "center" }
[dialogs.faq]
width = 860.0
height = 660.0
window = { width = 860, height = 660 }
[dialogs.settings]
width = 600.0
height = 550.0
@@ -1098,7 +1069,6 @@ fee-input = { width = 150 }
utxo-limit-input = { width = 150 }
shield-button = { width = 120, font = "button" }
cancel-button = { width = 100, font = "button" }
list-max-rows = { size = 7 } # address picker / run rows shown before the list scrolls
[dialogs.request-payment]
width = 500.0
@@ -1182,8 +1152,6 @@ 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]
@@ -1204,16 +1172,6 @@ connection-dot = { opacity = 1.0 }
height = 30
padding = [8, 0]
[components.startup-files-chip]
width = { size = 340.0 }
height = { size = 46.0 }
bottom-margin = { size = 38.0 }
rounding = { size = 10.0 }
padding-x = { size = 14.0 }
bar-height = { size = 3.0 }
bar-bottom = { size = 10.0 }
show-delay-seconds = { size = 0.4 }
[components.notifications]
width = 350.0
height = 60.0
@@ -1225,14 +1183,12 @@ pill-margin-y = { size = 3.0 }
pill-rounding = { size = 12.0 }
padding-x = { size = 12.0 }
glass-fill-alpha = { size = 18.0 }
accent-fill-alpha = { size = 0.22 } # severity-tint fill opacity (fraction of the accent alpha)
badge-fill-alpha = { size = 0.35 } # "+N more" badge fill opacity (fraction of the accent alpha)
glass-border-alpha = { size = 35.0 }
progress-bar-height = { size = 2.0 }
duration-info = { size = 1.0 }
duration-success = { size = 2.5 }
duration-warning = { size = 7.0 }
duration-error = { size = 9.0 }
duration-warning = { size = 3.5 }
duration-error = { size = 4.0 }
window = { width = 350, height = 60, border-radius = 8, border-width = 1 }
viewport-padding = { size = 20.0 }
stack-gap = { size = 10.0 }
@@ -1244,9 +1200,6 @@ 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 }
@@ -1259,10 +1212,9 @@ width = { size = 140.0 }
collapsed-width = { size = 64.0 }
collapse-anim-speed = { size = 10.0 }
auto-collapse-threshold = { size = 800.0 }
section-gap = { size = 8.0 }
section-gap = { size = 4.0 }
section-label-pad-left = { size = 16.0 }
section-label-pad-bottom = { size = 4.0 }
item-height = { size = 36.0 }
item-height = { size = 42.0 }
item-pad-x = { size = 8.0 }
min-height = { size = 360.0 }
margin-top = { size = -12 }
@@ -1278,8 +1230,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 = 6.0 }
bottom-padding = { size = 4.0 }
button-spacing = { size = 4.0 }
bottom-padding = { size = 0.0 }
exit-icon-gap = { size = 4.0 }
cutout-shadow-alpha = { size = 55 }
cutout-highlight-alpha = { size = 8 }
@@ -1295,8 +1247,8 @@ inset-shadow-fade-ratio = { size = 5.0 }
[components.content-area]
padding-x = 0.0
padding-y = 0.0
margin-top = { size = 6.0 }
margin-bottom = { size = -39.0 }
margin-top = { size = 4.0 }
margin-bottom = { size = -40.0 }
edge-fade-zone = { size = 0.0 }
[components.content-area.window]
@@ -1308,10 +1260,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 = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
header-title = { font = "subtitle1", size = 14.0, pad-x = 16.0, pad-y = 12.0, logo-gap = 8.0, opacity = 0.7 }
[components.main-window.window]
padding = [12, 36]
padding = [12, 38]
[components.shutdown]
content-height = { height = 120.0 }
@@ -1353,17 +1305,8 @@ 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 }
# Wrap width of the Security band's "PIN out of date" tooltip (a long sentence in every language).
pin-tooltip-wrap-width = { size = 420.0 }
# Wrap width of the disabled "Use Tor" checkbox's tooltip (Settings page and the legacy Settings window).
tor-tooltip-wrap-width = { size = 420.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 }
@@ -1414,17 +1357,6 @@ 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
@@ -1532,7 +1464,6 @@ progress-bar = { height = 6.0, radius = 3.0 }
progress-width = { size = 260.0 }
backdrop-alpha = { opacity = 0.80 }
vertical-gap = { size = 8.0 }
stall-timeout-sec = { size = 45.0 }
# ---------------------------------------------------------------------------
# First-Run Wizard Screens
@@ -1558,6 +1489,7 @@ 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
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
# Cross-build a MINIMAL static FreeType for the mingw-w64 (Windows) target.
#
# Why: the wallet's optional color-emoji rendering needs FreeType (to rasterize the COLR/CPAL Twemoji
# font). Native Linux/macOS pick up the system FreeType via find_package; the Debian/Ubuntu mingw-w64
# cross toolchain ships no FreeType, so we build one here. The Twemoji font is COLRv0 (layered vector),
# which FreeType renders WITHOUT libpng / harfbuzz / brotli / zlib — so this is a dependency-free static
# build (no external libs to also cross-compile), producing a self-contained libfreetype.a.
#
# Output: <prefix>/include/freetype2/... + <prefix>/lib/libfreetype.a (default prefix: third_party/freetype-mingw)
# build.sh --win-release runs this automatically and passes -DDRAGONX_MINGW_FREETYPE_PREFIX to CMake.
set -euo pipefail
FT_VERSION="2.13.3"
FT_SHA256="5c3a8e78f7b24c20b25b54ee575d6daa40007a5f4eea2845861c3409b3021747" # freetype-2.13.3.tar.gz
FT_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FT_VERSION}.tar.gz"
FT_URL_MIRROR="https://downloads.sourceforge.net/project/freetype/freetype2/${FT_VERSION}/freetype-${FT_VERSION}.tar.gz"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PREFIX="${1:-$SCRIPT_DIR/third_party/freetype-mingw}"
WORK="$SCRIPT_DIR/third_party/.freetype-mingw-build"
# Already built? (libfreetype.a present) → nothing to do.
if [[ -f "$PREFIX/lib/libfreetype.a" && -d "$PREFIX/include/freetype2" ]]; then
echo "FreeType (mingw) already built at: $PREFIX"
exit 0
fi
# Pick the mingw compilers (posix threads variant preferred, matching build.sh).
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
MINGW_GCC=x86_64-w64-mingw32-gcc-posix; MINGW_GXX=x86_64-w64-mingw32-g++-posix
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
MINGW_GCC=x86_64-w64-mingw32-gcc; MINGW_GXX=x86_64-w64-mingw32-g++
else
echo "ERROR: x86_64-w64-mingw32-gcc not found (install mingw-w64)." >&2
exit 1
fi
mkdir -p "$WORK"
cd "$WORK"
TARBALL="freetype-${FT_VERSION}.tar.gz"
if [[ ! -f "$TARBALL" ]]; then
echo "Downloading FreeType ${FT_VERSION} ..."
curl -fsSL -o "$TARBALL" "$FT_URL" || curl -fsSL -o "$TARBALL" "$FT_URL_MIRROR"
fi
echo "Verifying SHA-256 ..."
echo "${FT_SHA256} ${TARBALL}" | sha256sum -c - || {
echo "ERROR: FreeType tarball checksum mismatch (expected ${FT_SHA256})." >&2
echo " got: $(sha256sum "$TARBALL" | cut -d' ' -f1)" >&2
exit 1
}
rm -rf "freetype-${FT_VERSION}"
tar xf "$TARBALL"
SRC="$WORK/freetype-${FT_VERSION}"
# Minimal mingw toolchain for FreeType's own CMake.
cat > "$WORK/ft-mingw-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER ${MINGW_GCC})
set(CMAKE_CXX_COMPILER ${MINGW_GXX})
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
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)
TOOLCHAIN
echo "Configuring FreeType (static, no external deps) ..."
rm -rf "$WORK/build"
cmake -S "$SRC" -B "$WORK/build" \
-DCMAKE_TOOLCHAIN_FILE="$WORK/ft-mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
-DBUILD_SHARED_LIBS=OFF \
-DFT_DISABLE_ZLIB=ON \
-DFT_DISABLE_BZIP2=ON \
-DFT_DISABLE_PNG=ON \
-DFT_DISABLE_HARFBUZZ=ON \
-DFT_DISABLE_BROTLI=ON
echo "Building + installing FreeType ..."
cmake --build "$WORK/build" -j "$(nproc)"
cmake --install "$WORK/build"
if [[ -f "$PREFIX/lib/libfreetype.a" ]]; then
echo "OK: mingw FreeType -> $PREFIX/lib/libfreetype.a"
else
echo "ERROR: build did not produce libfreetype.a" >&2
exit 1
fi
-755
View File
@@ -1,755 +0,0 @@
#!/usr/bin/env bash
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
# present (Homebrew), and fail with a clear message otherwise.
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
done
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
echo " On macOS: brew install bash" >&2
exit 1
fi
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.
--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.
-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 lite backend is always built from the vendored in-tree source
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
and self-attested signature metadata are NOT accepted (F15-1) — the previous
scheme only recorded an unverified "verified" claim. The script captures the
freshly-built artifact's symbols and checksum, and records build provenance.
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|--no-build)
die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
;;
--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-file|--signature-path|--signature-format|\
--signature-verification-tool|--signature-tool|--signature-verification-command|\
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
--signature-transparency-log-url|--signature-verified-sha256)
die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
;;
-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"
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
[[ -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 ' "built_from_source": 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
View File
@@ -1,131 +0,0 @@
#!/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!")
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
Build a monochrome Noto Emoji subset for the chat/message UI.
Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome
(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's
*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed)
and merge it into the text fonts (see Typography::loadFont, the block after the
CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ
sequences / regional-indicator flags won't compose — single-codepoint emoji
(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use.
Source is the variable font pinned to wght=400 → static, then subset to the
emoji planes plus the higher symbol/star ranges the base UI font doesn't cover.
The base Ubuntu font already owns U+2600–26FF etc.; ImGui's MergeMode gives the
first-loaded glyph precedence, so those stay text-styled and only the codepoints
the base lacks fall through to this font.
Get the source once (OFL, redistributable):
curl -fsSL -o /tmp/NotoEmoji-VF.ttf \
'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf'
Then: python3 scripts/build_emoji_subset.py
Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN)
"""
import os
from fontTools import ttLib, subset
from fontTools.varLib.instancer import instantiateVariableFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SOURCE_VF = '/tmp/NotoEmoji-VF.ttf'
STATIC = '/tmp/NotoEmoji-Static.ttf'
OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf')
# Emoji codepoints to keep. Ranges are inclusive.
RANGES = [
(0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended)
(0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats
(0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows
(0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style)
(0x2194, 0x21AA), # arrows used as emoji
(0x231A, 0x231B), # ⌚ ⌛
(0x23E9, 0x23FA), # media-control emoji
(0x25AA, 0x25FE), # small squares
]
SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299,
0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2]
def main():
if not os.path.exists(SOURCE_VF):
raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command")
# 1. Pin the weight axis so stb_truetype rasterizes a clean static instance.
f = ttLib.TTFont(SOURCE_VF)
if 'fvar' in f:
instantiateVariableFont(f, {'wght': 400}, inplace=True)
f.save(STATIC)
unicodes = list(SINGLES)
for lo, hi in RANGES:
unicodes.extend(range(lo, hi + 1))
opts = subset.Options()
opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS
opts.name_IDs = []
opts.notdef_outline = True
opts.glyph_names = False
opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern']
font = subset.load_font(STATIC, opts)
ss = subset.Subsetter(options=opts)
ss.populate(unicodes=unicodes)
ss.subset(font)
subset.save_font(font, OUTPUT, opts)
out = ttLib.TTFont(OUTPUT)
cmap = out.getBestCmap()
color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR'))
print(f"Output: {OUTPUT}")
print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}")
if color:
raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it")
if __name__ == '__main__':
main()
-86
View File
@@ -1,86 +0,0 @@
#!/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
# ── Content guards (UX-hygiene threads) ──────────────────────────────────────
# Low-false-positive regression checks for debt the UX audit cleared, so it can't
# silently regrow. Each is deliberately narrow; widen only with an allowlist.
for f in "${files[@]}"; do
[[ "$f" == *.cpp ]] || continue
[[ -f "$f" ]] || continue
# app_sweep.cpp is the dev screenshot-sweep harness: its fixture strings are never shipped UI.
[[ "$(basename "$f")" == "app_sweep.cpp" ]] && continue
# (1) Tactile button sized with a raw multi-digit pixel literal that ignores HiDPI.
# Real sizes multiply by ui::Layout::dpiScale() / dp / hScale, or come from schema.
if hits=$(grep -nE 'Tactile(Small)?Button\([^;]*ImVec2\([1-9][0-9]+(\.[0-9]+f?)?,' "$f" \
| grep -vE 'dpiScale|\* *dp\b|\* *hdpi|hScale|schema|Scale\('); then
while IFS= read -r line; do
echo "✗ unscaled button size (multiply the px by ui::Layout::dpiScale()): $f:$line" >&2
fail=1
done <<< "$hits"
fi
# (2) User-visible migrate/sweep status assigned a bare English literal instead of TR(...).
# "__SENTINEL__" comparisons and empty strings are fine.
if hits=$(grep -nE '(sweep_status_|seed_migration_status_)[[:space:]]*=[[:space:]]*"[A-Za-z]' "$f" \
| grep -vE 'TR\(|"__'); then
while IFS= read -r line; do
echo "✗ untranslated status string (route through TR(...)): $f:$line" >&2
fail=1
done <<< "$hits"
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)"
-64
View File
@@ -1,64 +0,0 @@
#!/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) ===")
-47
View File
@@ -1,47 +0,0 @@
#!/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)")
-214
View File
@@ -1,214 +0,0 @@
#!/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)
+10 -19
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.2.0"
VERSION="1.0.0"
# Colors
GREEN='\033[0;32m'
@@ -24,27 +24,18 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
exit 1
fi
# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
# The old "continuous" tag is a moving, unverified network download that runs on the release
# builder; verify it or refuse to package.
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
# Check for appimagetool
APPIMAGETOOL=""
if command -v appimagetool &> /dev/null; then
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
APPIMAGETOOL="appimagetool"
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
else
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
print_status "Downloading appimagetool 1.9.0 (pinned)..."
wget -q -O "$AT" "$APPIMAGETOOL_URL"
if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
print_error "appimagetool SHA-256 verification failed — refusing to use it"
rm -f "$AT"
exit 1
fi
chmod +x "$AT"
fi
APPIMAGETOOL="$AT"
print_status "Downloading appimagetool..."
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
fi
print_status "Creating AppDir structure..."
-122
View File
@@ -1,122 +0,0 @@
#!/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()
+7 -78
View File
@@ -50,20 +50,12 @@ if [[ ! -f "$TARBALL" ]]; then
curl -fSL -o "$TARBALL" "$SODIUM_URL"
fi
# 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
# Verify checksum
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
# ── Extract ─────────────────────────────────────────────────────────────────
if [[ ! -d "$SRC_DIR" ]]; then
@@ -85,7 +77,7 @@ case "$TARGET" in
mac)
# Cross-compile for macOS via osxcross
if [[ -z "${OSXCROSS:-}" ]]; then
for try in "$PROJECT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross" "$PROJECT_DIR/osxcross"; do
for try in "$HOME/osxcross" "/opt/osxcross" "$PROJECT_DIR/osxcross"; do
[[ -d "$try/target" ]] && OSXCROSS="$try" && break
done
fi
@@ -123,69 +115,6 @@ 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
View File
@@ -1,36 +0,0 @@
#!/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
View File
@@ -1,35 +0,0 @@
#!/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
+6 -6
View File
@@ -7,7 +7,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
VERSION="1.2.0"
VERSION="1.0.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/dragonx/src/hush-arrakis-chain"
"$HOME/hush3/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/dragonx/src/hushd"
"$HOME/hush3/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/dragonx/src/dragonxd"
"$HOME/hush3/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/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
)
for apath in "${ASMAP_PATHS[@]}"; do
+6 -6
View File
@@ -130,8 +130,8 @@ done
# Look for asmap.dat
ASMAP_PATHS=(
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/../asmap.dat"
"$SCRIPT_DIR/asmap.dat"
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
@@ -256,8 +256,8 @@ HEADER_START
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
fi
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
@@ -411,7 +411,7 @@ if [ -f "bin/ObsidianDragon.exe" ]; then
echo ""
echo -e "${GREEN}Creating distribution package...${NC}"
cd bin
DIST_NAME="ObsidianDragon-Windows-x64"
DIST_NAME="DragonX-Wallet-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.dragonx.is/dragonx/ObsidianDragon
For support: https://git.hush.is/hush/ObsidianDragon
READMEEOF
if command -v zip &> /dev/null; then
+414
View File
@@ -0,0 +1,414 @@
#!/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 "═══════════════════════════════════════════"
-157
View File
@@ -1,157 +0,0 @@
#!/usr/bin/env bash
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
#
# The wallet verifies a detached ed25519 signature over the 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 "<archive>.sig" is
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
# checksum table in the release body, so `release` prints that table for you to paste in.
#
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
#
# 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>... # sign existing files -> <file>.sig
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
# # binary staged are skipped.
#
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
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; }
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
else shasum -a 256 "$1" | awk '{print $1}'; fi
}
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
sign_file() {
local key="$1" f="$2" raw
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"
}
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
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"
sign_file "$key" "$f"
echo "signed: $f -> $f.sig"
done
echo "Upload each .sig as a release asset next to its archive."
;;
release)
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
key="$1"; version="$2"; shift 2
src="$PROJECT_ROOT/prebuilt-binaries"
out="$PROJECT_ROOT/release/daemon"
while [ $# -gt 0 ]; do
case "$1" in
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
*) die "unknown option: $1" ;;
esac
done
[ -f "$key" ] || die "no such key: $key"
[ -d "$src" ] || die "no such source dir: $src"
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
mkdir -p "$out"
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
# then reject every signature made with it — only expected when deliberately rotating the key).
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
mine="$(pubkey_b64 "$key")"
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
echo " signing key -> $mine" >&2
echo " pinned key -> $pinned" >&2
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
echo >&2
fi
made=0
table=""
for plat in linux mac win; do
d="$src/$(plat_dir "$plat")"
daemon="$d/$(plat_daemon "$plat")"
if [ ! -f "$daemon" ]; then
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
continue
fi
archive="dragonx-$version-$(plat_token "$plat").zip"
apath="$out/$archive"
rm -f "$apath"
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
files=()
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
sign_file "$key" "$apath"
sum="$(sha256_of "$apath")"
table+="| $archive | \`$sum\` |"$'\n'
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
made=$((made + 1))
done
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
echo
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
echo "| Archive | SHA-256 |"
echo "|---|---|"
printf '%s' "$table"
echo
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
;;
*)
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
;;
esac
-66
View File
@@ -1,66 +0,0 @@
#!/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
-79
View File
@@ -1,79 +0,0 @@
#!/usr/bin/env bash
# Cross-compile the test suite (ObsidianDragonTests.exe) and the fake daemon (tools/fake_dragonxd)
# for Windows with mingw-w64, then run the tests: directly under WSL (Windows interop), else with
# wine if installed, else just build.
#
# scripts/test-windows.sh [--no-run] [--jobs N]
#
# Uses its own build/windows-tests tree (full-node config), so build/windows is left alone. Needs
# the mingw toolchain file that ./build.sh --win-release writes to build/windows/. Dependency sources
# already fetched into build/windows/_deps are reused, so nothing is downloaded twice.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bd="$repo/build/windows-tests"
toolchain="$repo/build/windows/mingw-toolchain.cmake"
deps="$repo/build/windows/_deps"
run=true
jobs=8 # each test TU is large; several parallel mingw builds can exhaust RAM
while [[ $# -gt 0 ]]; do
case "$1" in
--no-run) run=false; shift ;;
--jobs) jobs="$2"; shift 2 ;;
-h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done
if [[ ! -f "$toolchain" ]]; then
echo "missing $toolchain; run ./build.sh --win-release once to create the mingw toolchain" >&2
exit 1
fi
src_args=()
for dep in sdl3 json tomlplusplus sqlite3 curl libwebp; do
if [[ -d "$deps/$dep-src" ]]; then
src_args+=("-DFETCHCONTENT_SOURCE_DIR_${dep^^}=$deps/$dep-src")
fi
done
cmake -S "$repo" -B "$bd" \
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_BUILD_LITE=OFF \
-DDRAGONX_ENABLE_LITE_BACKEND=OFF \
-DBUILD_TESTING=ON \
-DDRAGONX_BUILD_FAKE_DAEMON=ON \
"${src_args[@]}"
cmake --build "$bd" --target ObsidianDragonTests fake_dragonxd -j "$jobs"
exe="$bd/bin/ObsidianDragonTests.exe"
echo "built: $exe"
echo "built: $bd/fake_dragonxd/dragonxd.exe"
$run || exit 0
# The fixture path compiled into the binary is a Linux path; hand the Windows process its own.
# APPDATA/LOCALAPPDATA point at a sandbox so anything resolving them from the environment stays out
# of the real profile. (SHGetFolderPath-based paths ignore the environment; the tests that would
# write through those are compiled out on Windows.)
export DRAGONX_TEST_FIXTURE_DIR="$repo/tests/fixtures"
sandbox="$bd/test-appdata"
mkdir -p "$sandbox"
export APPDATA="$sandbox" LOCALAPPDATA="$sandbox"
# The fake dragonxd.exe plays a "foreign" node (isolated datadir/port) for the never-kill-by-name test.
export DRAGONX_TEST_FAKE_DAEMON="$bd/fake_dragonxd/dragonxd.exe"
if grep -qi microsoft /proc/version 2>/dev/null && [[ -e /proc/sys/fs/binfmt_misc/WSLInterop ]]; then
# /p: translate each to a Windows path for the Windows process
export WSLENV="${WSLENV:+$WSLENV:}DRAGONX_TEST_FIXTURE_DIR/p:APPDATA/p:LOCALAPPDATA/p:DRAGONX_TEST_FAKE_DAEMON/p"
cd "$bd/bin"
exec "$exe"
elif command -v wine >/dev/null 2>&1; then
export DRAGONX_TEST_FIXTURE_DIR="$(winepath -w "$DRAGONX_TEST_FIXTURE_DIR")"
export DRAGONX_TEST_FAKE_DAEMON="$(winepath -w "$DRAGONX_TEST_FAKE_DAEMON")"
cd "$bd/bin"
exec wine "$exe"
else
echo "not on WSL and no wine: copy $exe (and tests/fixtures) to Windows to run it" >&2
fi
-866
View File
@@ -1,866 +0,0 @@
#!/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="bash 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
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "Windows cross-compile toolchain already present — skipping apt install"
else
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
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"
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
ok "Downloaded + verified sapling-spend.params"
else
rm -f "$PARAMS_DIR/sapling-spend.params"
err "sapling-spend.params download or SHA-256 verification failed — not installed"
fi
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
ok "Downloaded + verified sapling-output.params"
else
rm -f "$PARAMS_DIR/sapling-output.params"
err "sapling-output.params download or SHA-256 verification failed — not installed"
fi
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. drg-xmrig (mining binary) ────────────────────────────────────────────
header "drg-xmrig Mining Binary"
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
# 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 drg-xmrig if not present
clone_xmrig_if_needed() {
if [[ ! -d "$XMRIG_SRC" ]]; then
info "Cloning drg-xmrig..."
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
else
ok "drg-xmrig source already present"
info "Pulling latest drg-xmrig..."
(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 drg-xmrig dependencies (libuv, hwloc, openssl)..."
(
cd "$XMRIG_SRC/scripts"
sh build_deps.sh
)
ok "drg-xmrig dependencies built"
# Build xmrig
info "Building drg-xmrig (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/drg-xmrig/"
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 drg-xmrig (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/drg-xmrig/"
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 drg-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 "═══════════════════════════════════════════"
+804 -6807
View File
File diff suppressed because it is too large Load Diff
+63 -1732
View File
File diff suppressed because it is too large Load Diff

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