Compare commits
21 Commits
f58d009703
...
24e8fb4942
| Author | SHA1 | Date | |
|---|---|---|---|
| 24e8fb4942 | |||
| 1393d9ae1a | |||
| 3ce62326f9 | |||
| b20e7efb16 | |||
| 4473e7e00a | |||
| 2e8e214689 | |||
| 69a6fb3e64 | |||
| ed6120eef9 | |||
| a5bd2dadd7 | |||
| 25ee1496b4 | |||
| e2bc3623b6 | |||
| b32fe07cb1 | |||
| a0532275dd | |||
| 7df00b0909 | |||
| 090e288c44 | |||
| 6544c10ac1 | |||
| b2e104358d | |||
| de70e68472 | |||
| 0fe12d65df | |||
| 37c8287a12 | |||
| 6ff80354df |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -47,3 +47,10 @@ docs/_archive/
|
|||||||
# ed25519 release-signing keys — the secret key must NEVER be committed
|
# ed25519 release-signing keys — the secret key must NEVER be committed
|
||||||
*.ed25519.key
|
*.ed25519.key
|
||||||
*.ed25519.pub.b64
|
*.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
|
||||||
|
|||||||
10
CLAUDE.md
10
CLAUDE.md
@@ -61,7 +61,7 @@ There is no per-test filtering — it is one binary that runs every assertion. T
|
|||||||
|
|
||||||
Variants are selected with CMake options (set by `build.sh` flags), surfaced to C++ as compile definitions:
|
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_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.
|
- `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.
|
- `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`.
|
Guard full-node-only code paths with `#if DRAGONX_LITE_BUILD` / chat code with `DRAGONX_ENABLE_CHAT`.
|
||||||
@@ -79,10 +79,16 @@ The detailed milestone plan and design history (the v2 plan, backend artifact/AB
|
|||||||
|
|
||||||
## Miner updater (xmrig)
|
## Miner updater (xmrig)
|
||||||
|
|
||||||
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live.
|
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. A **"Browse all releases…"** button (the `/releases` list, newest first, pre-releases included) lets users pin an older or pre-release build — same verify/install path via `startInstallRelease()`; the picker UI is shared with the daemon updater (`ui/windows/release_list_view.h`).
|
||||||
|
|
||||||
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
|
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
|
||||||
|
|
||||||
|
## Daemon updater (dragonxd)
|
||||||
|
|
||||||
|
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). A **"Browse all releases…"** button (shared `release_list_view.h` picker) lets users pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data.
|
||||||
|
|
||||||
|
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
|
||||||
|
|
||||||
## Versioning
|
## 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`.
|
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`.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.3.0
|
VERSION 2.0.0
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
@@ -502,6 +502,8 @@ set(APP_SOURCES
|
|||||||
src/util/lite_server_probe.cpp
|
src/util/lite_server_probe.cpp
|
||||||
src/util/xmrig_updater.cpp
|
src/util/xmrig_updater.cpp
|
||||||
src/util/xmrig_updater_core.cpp
|
src/util/xmrig_updater_core.cpp
|
||||||
|
src/util/daemon_updater.cpp
|
||||||
|
src/util/daemon_updater_core.cpp
|
||||||
src/util/secure_vault.cpp
|
src/util/secure_vault.cpp
|
||||||
src/ui/effects/framebuffer.cpp
|
src/ui/effects/framebuffer.cpp
|
||||||
src/ui/effects/blur_shader.cpp
|
src/ui/effects/blur_shader.cpp
|
||||||
@@ -744,6 +746,30 @@ if(DRAGONX_LITE_BACKEND_READY)
|
|||||||
if(UNIX)
|
if(UNIX)
|
||||||
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
|
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Real-backend SEND smoke tool — drives the exact GUI send path (bridge.execute("send", ...)).
|
||||||
|
add_executable(lite_send_smoke
|
||||||
|
tools/lite_send_smoke.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(lite_send_smoke PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
target_compile_definitions(lite_send_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
|
||||||
|
target_link_libraries(lite_send_smoke PRIVATE
|
||||||
|
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(lite_send_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Platform-specific settings
|
# Platform-specific settings
|
||||||
@@ -1011,6 +1037,8 @@ if(BUILD_TESTING)
|
|||||||
src/util/lite_server_probe.cpp
|
src/util/lite_server_probe.cpp
|
||||||
src/util/xmrig_updater.cpp
|
src/util/xmrig_updater.cpp
|
||||||
src/util/xmrig_updater_core.cpp
|
src/util/xmrig_updater_core.cpp
|
||||||
|
src/util/daemon_updater.cpp
|
||||||
|
src/util/daemon_updater_core.cpp
|
||||||
${MINIZ_SOURCES}
|
${MINIZ_SOURCES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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="1.2.0.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>
|
|
||||||
@@ -7,7 +7,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||||||
|
|
||||||
ABI_VERSION="sdxl-c-v1"
|
ABI_VERSION="sdxl-c-v1"
|
||||||
LINK_MODE="imported"
|
LINK_MODE="imported"
|
||||||
BACKEND_DIR="$PROJECT_ROOT/external/SilentDragonXLite/lib"
|
BACKEND_DIR="$PROJECT_ROOT/third_party/silentdragonxlite/lib"
|
||||||
BACKEND_SOURCE_DIR=""
|
BACKEND_SOURCE_DIR=""
|
||||||
BUILD_BACKEND_DIR=""
|
BUILD_BACKEND_DIR=""
|
||||||
BACKEND_DEPENDENCY_DIR=""
|
BACKEND_DEPENDENCY_DIR=""
|
||||||
@@ -314,6 +314,33 @@ validate_backend_dependency_source() {
|
|||||||
fi
|
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() {
|
prepare_backend_source() {
|
||||||
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
|
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
|
||||||
|
|
||||||
@@ -348,6 +375,9 @@ prepare_backend_source() {
|
|||||||
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||||
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||||
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
[[ -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"
|
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
|
||||||
|
|
||||||
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
|
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
|
||||||
@@ -489,6 +519,8 @@ build_with_cargo() {
|
|||||||
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
|
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] && ensure_sapling_params "$BACKEND_DEPENDENCY_DIR/zcash-params"
|
||||||
|
|
||||||
local cargo_cmd=(cargo build --locked --lib --release)
|
local cargo_cmd=(cargo build --locked --lib --release)
|
||||||
if [[ -n "$RUST_TARGET" ]]; then
|
if [[ -n "$RUST_TARGET" ]]; then
|
||||||
cargo_cmd+=(--target "$RUST_TARGET")
|
cargo_cmd+=(--target "$RUST_TARGET")
|
||||||
|
|||||||
35
scripts/gen-lite-checkpoints.sh
Executable file
35
scripts/gen-lite-checkpoints.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate SDXL lite-wallet mainnet checkpoint entries from a fully-synced dragonxd.
|
||||||
|
# Each entry is (height,"blockhash","serialized_sapling_tree") in checkpoints.rs format.
|
||||||
|
# Fills the 1,770,000 -> tip gap so wallets reseed close to their birthday on rescan,
|
||||||
|
# bounding the (divergence-prone) compact-block replay span. Usage:
|
||||||
|
# scripts/gen-lite-checkpoints.sh [start] [step] > /tmp/new_checkpoints.txt
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CLI=${DRAGONX_CLI:-/home/d/dragonx/src/dragonx-cli}
|
||||||
|
START=${1:-1770000}
|
||||||
|
STEP=${2:-10000}
|
||||||
|
|
||||||
|
tip=$("$CLI" getblockcount)
|
||||||
|
end=$(( (tip / STEP) * STEP ))
|
||||||
|
|
||||||
|
# Sanity: confirm the method reproduces a KNOWN checkpoint tree before trusting it.
|
||||||
|
ref_hash=$("$CLI" getblockhash 1760000 | tr -d '"[:space:]')
|
||||||
|
ref_tree=$("$CLI" getblockmerkletree 1760000 | tr -d '"[:space:]')
|
||||||
|
expect_hash="0000545a45b8d4ee4e4b423cb1ea74d67e3a04c320c6ea2f59ee06c08f91a117"
|
||||||
|
if [ "$ref_hash" != "$expect_hash" ]; then
|
||||||
|
echo "ABORT: getblockhash 1760000 = $ref_hash != known $expect_hash" >&2; exit 1
|
||||||
|
fi
|
||||||
|
echo "# self-check: 1760000 hash matches; tree len=${#ref_tree}" >&2
|
||||||
|
|
||||||
|
n=0
|
||||||
|
h=$START
|
||||||
|
while [ "$h" -le "$end" ]; do
|
||||||
|
hash=$("$CLI" getblockhash "$h" | tr -d '"[:space:]')
|
||||||
|
tree=$("$CLI" getblockmerkletree "$h" | tr -d '"[:space:]')
|
||||||
|
if [ -z "$hash" ] || [ -z "$tree" ]; then echo "ABORT: empty hash/tree at $h" >&2; exit 1; fi
|
||||||
|
printf '\t(%s,"%s",\n\t\t"%s"\n\t),\n' "$h" "$hash" "$tree"
|
||||||
|
n=$((n+1))
|
||||||
|
h=$((h+STEP))
|
||||||
|
done
|
||||||
|
echo "# generated $n checkpoints from $START to $end (tip=$tip)" >&2
|
||||||
67
scripts/sign-daemon-release.sh
Executable file
67
scripts/sign-daemon-release.sh
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
|
||||||
|
#
|
||||||
|
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||||
|
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
|
||||||
|
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
|
||||||
|
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the
|
||||||
|
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset.
|
||||||
|
#
|
||||||
|
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
|
||||||
|
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
||||||
|
# (the same flow the wallet's unit tests verify for the miner updater).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||||
|
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||||
|
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||||
|
#
|
||||||
|
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||||
|
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
|
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||||
|
|
||||||
|
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
|
||||||
|
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||||
|
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||||
|
|
||||||
|
cmd="${1:-}"; shift || true
|
||||||
|
case "$cmd" in
|
||||||
|
keygen)
|
||||||
|
prefix="${1:-dragonx-daemon}"
|
||||||
|
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
|
||||||
|
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
|
||||||
|
chmod 600 "$prefix.ed25519.key"
|
||||||
|
pub="$(pubkey_b64 "$prefix.ed25519.key")"
|
||||||
|
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
|
||||||
|
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
|
||||||
|
echo "public key : $prefix.ed25519.pub.b64"
|
||||||
|
echo
|
||||||
|
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||||
|
echo " $pub"
|
||||||
|
;;
|
||||||
|
pubkey)
|
||||||
|
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||||
|
pubkey_b64 "$1"
|
||||||
|
;;
|
||||||
|
sign)
|
||||||
|
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||||
|
key="$1"; shift
|
||||||
|
[ -f "$key" ] || die "no such key: $key"
|
||||||
|
for f in "$@"; do
|
||||||
|
[ -f "$f" ] || die "no such file: $f"
|
||||||
|
raw="$(mktemp)"
|
||||||
|
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||||
|
openssl base64 -A -in "$raw" > "$f.sig"
|
||||||
|
printf '\n' >> "$f.sig"
|
||||||
|
rm -f "$raw"
|
||||||
|
echo "signed: $f -> $f.sig"
|
||||||
|
done
|
||||||
|
echo "Upload each .sig as a release asset next to its archive."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
441
src/app.cpp
441
src/app.cpp
@@ -50,6 +50,7 @@
|
|||||||
#include "ui/windows/address_transfer_dialog.h"
|
#include "ui/windows/address_transfer_dialog.h"
|
||||||
#include "ui/windows/bootstrap_download_dialog.h"
|
#include "ui/windows/bootstrap_download_dialog.h"
|
||||||
#include "ui/windows/xmrig_download_dialog.h"
|
#include "ui/windows/xmrig_download_dialog.h"
|
||||||
|
#include "ui/windows/daemon_download_dialog.h"
|
||||||
#include "ui/windows/console_tab.h"
|
#include "ui/windows/console_tab.h"
|
||||||
#include "ui/pages/settings_page.h"
|
#include "ui/pages/settings_page.h"
|
||||||
#include "ui/theme.h"
|
#include "ui/theme.h"
|
||||||
@@ -431,6 +432,21 @@ wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& setting
|
|||||||
}
|
}
|
||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True when a z_sendmany failure means the wallet's shielded note data (witnesses/anchors) is
|
||||||
|
// out of sync with the chain — the symptom after a bootstrap/reindex without a full rescan. These
|
||||||
|
// are consensus/proof-build rejections, not user mistakes, and the only fix is a full rescan.
|
||||||
|
bool sendErrorNeedsRescan(const std::string& raw)
|
||||||
|
{
|
||||||
|
auto has = [&](const char* s) { return raw.find(s) != std::string::npos; };
|
||||||
|
return has("shielded-requirements-not-met") ||
|
||||||
|
has("shielded requirements not met") ||
|
||||||
|
has("missing sapling anchor") ||
|
||||||
|
has("Invalid sapling spend proof") ||
|
||||||
|
has("Invalid output proof") ||
|
||||||
|
has("bad-txns-sapling-") ||
|
||||||
|
has("anchor");
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void App::rebuildLiteWallet(bool force)
|
void App::rebuildLiteWallet(bool force)
|
||||||
@@ -588,19 +604,30 @@ void App::update()
|
|||||||
// Fast refresh (mining stats + daemon memory) every second
|
// Fast refresh (mining stats + daemon memory) every second
|
||||||
// Skip when wallet is locked — no need to poll, and queued tasks
|
// Skip when wallet is locked — no need to poll, and queued tasks
|
||||||
// would delay the PIN unlock worker task.
|
// would delay the PIN unlock worker task.
|
||||||
|
//
|
||||||
|
// Also enter this block when the embedded daemon is running even if RPC currently reads
|
||||||
|
// disconnected: during a long rescan the daemon holds cs_main, so getinfo times out and the
|
||||||
|
// socket is marked down — but the rescan/witness PROGRESS we parse comes from the daemon's
|
||||||
|
// stdout pipe (no RPC needed), and getrescaninfo still answers (it takes no lock). Without this,
|
||||||
|
// the daemon-output parser stopped exactly when a rescan started, so witness progress never
|
||||||
|
// surfaced. The RPC-dependent pollers inside stay individually gated (mining skips during
|
||||||
|
// warmup/rescan; the getrescaninfo poll only runs once rescanning is flagged).
|
||||||
if (network_refresh_.consumeDue(RefreshTimer::Fast)) {
|
if (network_refresh_.consumeDue(RefreshTimer::Fast)) {
|
||||||
if (rpcConnected && !state_.isLocked()) {
|
const bool daemonAlive = daemon_controller_ && daemon_controller_->isRunning();
|
||||||
|
if ((rpcConnected || daemonAlive) && !state_.isLocked()) {
|
||||||
// Skip the mining poll while the daemon is in warmup (e.g. during -rescan). Otherwise
|
// Skip the mining poll while the daemon is in warmup (e.g. during -rescan). Otherwise
|
||||||
// getmininginfo is rejected with -28 ("Rescanning...") every second and floods the log.
|
// getmininginfo is rejected with -28 ("Rescanning...") every second and floods the log.
|
||||||
// The rescan progress poll below still runs — that's how we track the warmup/rescan.
|
// The rescan progress poll below still runs — that's how we track the warmup/rescan.
|
||||||
if (!state_.warming_up) {
|
if (rpcConnected && !state_.warming_up && !runtime_rescan_active_) {
|
||||||
refreshMiningInfo();
|
refreshMiningInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll getrescaninfo for rescan progress (if rescan flag is set)
|
// Poll getrescaninfo for rescan progress (if rescan flag is set)
|
||||||
// Use fast_rpc_ when available to avoid blocking on rpc_'s
|
// Use fast_rpc_ when available to avoid blocking on rpc_'s
|
||||||
// curl_mutex (which may be held by a long-running import).
|
// curl_mutex (which may be held by a long-running import).
|
||||||
if (state_.sync.rescanning && fast_worker_ && !rescan_status_poll_in_progress_) {
|
// Suppressed during a runtime rescanblockchain: the daemon holds cs_main for the whole
|
||||||
|
// scan, so every poll would block, and completion is signalled by the rescan RPC callback.
|
||||||
|
if (state_.sync.rescanning && !runtime_rescan_active_ && fast_worker_ && !rescan_status_poll_in_progress_) {
|
||||||
auto* rescanRpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
auto* rescanRpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
rescan_status_poll_in_progress_ = true;
|
rescan_status_poll_in_progress_ = true;
|
||||||
fast_worker_->post([this, rescanRpc]() -> rpc::RPCWorker::MainCb {
|
fast_worker_->post([this, rescanRpc]() -> rpc::RPCWorker::MainCb {
|
||||||
@@ -632,6 +659,13 @@ void App::update()
|
|||||||
rescan_confirmed_active_ = false;
|
rescan_confirmed_active_ = false;
|
||||||
state_.sync.rescan_progress = 1.0f;
|
state_.sync.rescan_progress = 1.0f;
|
||||||
state_.sync.rescan_status.clear();
|
state_.sync.rescan_status.clear();
|
||||||
|
state_.sync.building_witnesses = false;
|
||||||
|
state_.sync.witness_phase = 0;
|
||||||
|
state_.sync.witness_progress = 0.0f;
|
||||||
|
state_.sync.witness_remaining = 0;
|
||||||
|
witness_rebuild_total_blocks_ = 0;
|
||||||
|
witness_seen_txids_.clear();
|
||||||
|
witness_total_txs_ = 0;
|
||||||
}
|
}
|
||||||
// else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting.
|
// else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting.
|
||||||
};
|
};
|
||||||
@@ -697,14 +731,30 @@ void App::update()
|
|||||||
// Check daemon output for rescan progress (offloaded to worker)
|
// Check daemon output for rescan progress (offloaded to worker)
|
||||||
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
||||||
std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_);
|
std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_);
|
||||||
if (!newOutput.empty() && fast_worker_) {
|
if (!newOutput.empty()) {
|
||||||
fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb {
|
// Parse inline on the main thread. It's cheap string scanning of a small incremental
|
||||||
// Parse on worker thread — pure string work, no shared state access
|
// buffer, and keeping it OFF the worker is essential: during a rescan the worker can
|
||||||
|
// be blocked for minutes on a getrescaninfo call (it waits on cs_main while the daemon
|
||||||
|
// holds it for a witness rebuild). On the worker, the parse queued behind that and the
|
||||||
|
// rescan/witness progress froze. Inline, progress updates every frame regardless.
|
||||||
|
const std::string output = std::move(newOutput);
|
||||||
bool foundRescan = false;
|
bool foundRescan = false;
|
||||||
bool finished = false;
|
bool finished = false;
|
||||||
float rescanPct = 0.0f;
|
float rescanPct = 0.0f;
|
||||||
std::string lastStatus;
|
std::string lastStatus;
|
||||||
|
|
||||||
|
// Sapling witness rebuild progress. Two daemon signals:
|
||||||
|
// "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" (the common one —
|
||||||
|
// one per wallet tx; <hash> is the stable per-tx key, <N> the total tx count)
|
||||||
|
// "Building Witnesses for block <h> <frac> complete, <n> remaining" (rarer)
|
||||||
|
bool foundWitness = false;
|
||||||
|
float witnessPct = -1.0f; // -1 = no fraction parsed this batch
|
||||||
|
int witnessRemaining = -1;
|
||||||
|
int witnessReadTotal = -1; // new daemon: the <total> in "<done> / <total>"
|
||||||
|
bool witnessRebuilt = false; // new daemon's completion line seen this batch
|
||||||
|
std::vector<std::string> witnessTxids; // distinct-tx keys seen this batch
|
||||||
|
int witnessTotalTxs = -1; // max N seen this batch
|
||||||
|
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
while (pos < output.size()) {
|
while (pos < output.size()) {
|
||||||
size_t eol = output.find('\n', pos);
|
size_t eol = output.find('\n', pos);
|
||||||
@@ -771,10 +821,103 @@ void App::update()
|
|||||||
finished = true;
|
finished = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sapling note witness rebuild: "Building Witnesses for block <h> <frac>
|
||||||
|
// complete, <n> remaining". The float before "complete" is a 0..1 fraction.
|
||||||
|
auto witIdx = line.find("Building Witnesses for block");
|
||||||
|
if (witIdx != std::string::npos) {
|
||||||
|
foundWitness = true;
|
||||||
|
auto compIdx = line.find(" complete", witIdx);
|
||||||
|
if (compIdx != std::string::npos) {
|
||||||
|
size_t numEnd = compIdx, numStart = numEnd;
|
||||||
|
while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) ||
|
||||||
|
line[numStart - 1] == '.')) numStart--;
|
||||||
|
if (numStart < numEnd) {
|
||||||
|
try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto remIdx = line.find(" remaining", witIdx);
|
||||||
|
if (remIdx != std::string::npos) {
|
||||||
|
size_t numEnd = remIdx, numStart = numEnd;
|
||||||
|
while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--;
|
||||||
|
if (numStart < numEnd) {
|
||||||
|
try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastStatus = line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newer multi-threaded daemon: "Reading blocks for witness rebuild: <done> / <total>".
|
||||||
|
// This replaced the per-block "Building Witnesses" line and is an exact done/total
|
||||||
|
// count, so capture BOTH (total drives the denominator directly — no peak anchoring).
|
||||||
|
auto rdIdx = line.find("Reading blocks for witness rebuild:");
|
||||||
|
if (rdIdx != std::string::npos) {
|
||||||
|
foundWitness = true;
|
||||||
|
auto slash = line.find('/', rdIdx);
|
||||||
|
if (slash != std::string::npos) {
|
||||||
|
size_t dEnd = slash;
|
||||||
|
while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--;
|
||||||
|
size_t dStart = dEnd;
|
||||||
|
while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--;
|
||||||
|
size_t tStart = slash + 1;
|
||||||
|
while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++;
|
||||||
|
size_t tEnd = tStart;
|
||||||
|
while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++;
|
||||||
|
if (dStart < dEnd && tStart < tEnd) {
|
||||||
|
try {
|
||||||
|
long done = std::stol(line.substr(dStart, dEnd - dStart));
|
||||||
|
long total = std::stol(line.substr(tStart, tEnd - tStart));
|
||||||
|
if (total > 0 && done >= 0 && done <= total) {
|
||||||
|
witnessRemaining = static_cast<int>(total - done);
|
||||||
|
witnessReadTotal = static_cast<int>(total);
|
||||||
|
}
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastStatus = line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New daemon's witness-rebuild completion line:
|
||||||
|
// "... rebuilt <n> note witness cache(s) to height <h> in <ms>ms using <t> thread(s)".
|
||||||
|
// The parallel rebuild logs no progress, so on completion snap the bar to 100%.
|
||||||
|
if (line.find("note witness cache") != std::string::npos &&
|
||||||
|
line.find("thread(s)") != std::string::npos) {
|
||||||
|
foundWitness = true;
|
||||||
|
witnessRebuilt = true;
|
||||||
|
lastStatus = line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary signal: "Setting Initial Sapling Witness for tx <hash>, <i> of <N>".
|
||||||
|
// <i> is the tx's slot in an unordered map (bounces — useless as progress), so
|
||||||
|
// we key on <hash> (one per tx) and count distinct txs against <N>. Extract the
|
||||||
|
// hash between "for tx " and the following comma, and N after " of ".
|
||||||
|
{
|
||||||
|
auto setIdx = line.find("Setting Initial Sapling Witness for tx ");
|
||||||
|
if (setIdx != std::string::npos) {
|
||||||
|
foundWitness = true;
|
||||||
|
size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size();
|
||||||
|
size_t comma = line.find(',', hStart);
|
||||||
|
if (comma != std::string::npos && comma > hStart) {
|
||||||
|
witnessTxids.push_back(line.substr(hStart, comma - hStart));
|
||||||
|
auto ofIdx = line.find(" of ", comma);
|
||||||
|
if (ofIdx != std::string::npos) {
|
||||||
|
size_t nStart = ofIdx + 4, nEnd = nStart;
|
||||||
|
while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++;
|
||||||
|
if (nEnd > nStart) {
|
||||||
|
try {
|
||||||
|
int n = std::stoi(line.substr(nStart, nEnd - nStart));
|
||||||
|
if (n > witnessTotalTxs) witnessTotalTxs = n;
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastStatus = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return callback to apply results on main thread
|
// Apply results directly — we are already on the main thread.
|
||||||
return [this, foundRescan, finished, rescanPct, status = std::move(lastStatus)]() {
|
const std::string& status = lastStatus;
|
||||||
if (finished) {
|
if (finished) {
|
||||||
if (state_.sync.rescanning) {
|
if (state_.sync.rescanning) {
|
||||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||||
@@ -783,6 +926,86 @@ void App::update()
|
|||||||
rescan_confirmed_active_ = false;
|
rescan_confirmed_active_ = false;
|
||||||
state_.sync.rescan_progress = 1.0f;
|
state_.sync.rescan_progress = 1.0f;
|
||||||
state_.sync.rescan_status.clear();
|
state_.sync.rescan_status.clear();
|
||||||
|
// Witness rebuild finishes with the rescan it's part of.
|
||||||
|
state_.sync.building_witnesses = false;
|
||||||
|
state_.sync.witness_phase = 0;
|
||||||
|
state_.sync.witness_progress = 0.0f;
|
||||||
|
state_.sync.witness_remaining = 0;
|
||||||
|
witness_rebuild_total_blocks_ = 0;
|
||||||
|
witness_seen_txids_.clear();
|
||||||
|
witness_total_txs_ = 0;
|
||||||
|
} else if (foundWitness) {
|
||||||
|
// Witness rebuild is the tail phase of a rescan — keep rescanning set so
|
||||||
|
// the broader gating holds, but surface the witness-specific progress.
|
||||||
|
state_.sync.rescanning = true;
|
||||||
|
rescan_confirmed_active_ = true;
|
||||||
|
state_.sync.building_witnesses = true;
|
||||||
|
|
||||||
|
// Which sub-phase is this batch? A "<n> remaining" token means the cache
|
||||||
|
// walk (phase 2); otherwise per-tx "Setting Initial" lines are the initial
|
||||||
|
// pass (phase 1). The two INTERLEAVE during a rescan (the daemon does both
|
||||||
|
// per block), so the phase is UPGRADE-ONLY: once the cache walk is seen it
|
||||||
|
// never drops back to the initial pass. Otherwise an interleaved initial
|
||||||
|
// line would flip the phase back and reset the bar every batch (thrash).
|
||||||
|
int phase = state_.sync.witness_phase;
|
||||||
|
if (witnessRemaining >= 0 || witnessRebuilt) phase = 2;
|
||||||
|
else if (!witnessTxids.empty() && phase < 2) phase = 1;
|
||||||
|
|
||||||
|
// Entering a higher sub-phase → reset its progress + accumulators so the
|
||||||
|
// bar restarts from 0 (the two phases have different scales). Upgrade-only,
|
||||||
|
// so this happens at most twice (→1, →2), never repeatedly.
|
||||||
|
if (phase != state_.sync.witness_phase) {
|
||||||
|
state_.sync.witness_phase = phase;
|
||||||
|
state_.sync.witness_progress = 0.0f;
|
||||||
|
state_.sync.witness_remaining = 0;
|
||||||
|
witness_rebuild_total_blocks_ = 0;
|
||||||
|
witness_seen_txids_.clear();
|
||||||
|
witness_total_txs_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase == 2) {
|
||||||
|
state_.sync.witness_remaining = witnessRemaining;
|
||||||
|
float p = -1.0f;
|
||||||
|
if (witnessRebuilt) {
|
||||||
|
// Parallel rebuild finished (it logs no progress) — snap to 100%.
|
||||||
|
p = 1.0f;
|
||||||
|
state_.sync.witness_remaining = 0;
|
||||||
|
} else if (witnessReadTotal > 0) {
|
||||||
|
// New daemon: exact done/total. Use the reported total as the
|
||||||
|
// denominator directly (no peak anchoring) for a smooth 0..1 sweep.
|
||||||
|
int done = witnessReadTotal - (witnessRemaining >= 0 ? witnessRemaining : 0);
|
||||||
|
p = static_cast<float>(done) / static_cast<float>(witnessReadTotal);
|
||||||
|
} else if (witnessRemaining >= 0) {
|
||||||
|
// Older daemon: bare "<n> remaining" with no total — anchor the % to
|
||||||
|
// the largest remaining seen (its first/largest value ≈ the full span).
|
||||||
|
if (witnessRemaining > witness_rebuild_total_blocks_)
|
||||||
|
witness_rebuild_total_blocks_ = witnessRemaining;
|
||||||
|
if (witness_rebuild_total_blocks_ > 0)
|
||||||
|
p = 1.0f - static_cast<float>(witnessRemaining) /
|
||||||
|
static_cast<float>(witness_rebuild_total_blocks_);
|
||||||
|
}
|
||||||
|
if (p >= 0.0f) {
|
||||||
|
if (p > 1.0f) p = 1.0f;
|
||||||
|
if (p > state_.sync.witness_progress) // monotonic within the phase
|
||||||
|
state_.sync.witness_progress = p;
|
||||||
|
}
|
||||||
|
} else if (phase == 1) {
|
||||||
|
// Initial pass: count DISTINCT witnessed txs / total. The set only
|
||||||
|
// grows (dedups the daemon's double-prints), so it's monotonic; the
|
||||||
|
// raw "<i>" can't be used — it's an unordered-map slot that bounces.
|
||||||
|
if (witnessTotalTxs > witness_total_txs_) witness_total_txs_ = witnessTotalTxs;
|
||||||
|
for (const auto& txid : witnessTxids) witness_seen_txids_.insert(txid);
|
||||||
|
if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) {
|
||||||
|
float p = static_cast<float>(witness_seen_txids_.size()) /
|
||||||
|
static_cast<float>(witness_total_txs_);
|
||||||
|
if (p > 1.0f) p = 1.0f;
|
||||||
|
if (p > state_.sync.witness_progress) // monotonic within the phase
|
||||||
|
state_.sync.witness_progress = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!status.empty()) {
|
||||||
|
state_.sync.rescan_status = status;
|
||||||
|
}
|
||||||
} else if (foundRescan) {
|
} else if (foundRescan) {
|
||||||
state_.sync.rescanning = true;
|
state_.sync.rescanning = true;
|
||||||
// Reading "Still rescanning" straight from the daemon log is hard proof the
|
// Reading "Still rescanning" straight from the daemon log is hard proof the
|
||||||
@@ -796,8 +1019,6 @@ void App::update()
|
|||||||
state_.sync.rescan_status = status;
|
state_.sync.rescan_status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (!daemon_controller_ || !daemon_controller_->isRunning()) {
|
} else if (!daemon_controller_ || !daemon_controller_->isRunning()) {
|
||||||
// Clear rescan state if daemon is not running (but preserve during restart)
|
// Clear rescan state if daemon is not running (but preserve during restart)
|
||||||
@@ -849,7 +1070,15 @@ void App::update()
|
|||||||
// Failures: route to the originating send UI when there is one (it shows
|
// Failures: route to the originating send UI when there is one (it shows
|
||||||
// its own error toast); otherwise surface a generic notification (this is
|
// its own error toast); otherwise surface a generic notification (this is
|
||||||
// how shield/merge/auto-shield failures become visible).
|
// how shield/merge/auto-shield failures become visible).
|
||||||
for (const auto& [opid, msg] : parsed.failureByOpid) {
|
for (const auto& [opid, rawMsg] : parsed.failureByOpid) {
|
||||||
|
// Auto-work-around the daemon's note-selection fee-gap: re-issues the send with a
|
||||||
|
// self-output so it covers the fee. If a retry was issued, defer the outcome to it.
|
||||||
|
if (maybeRetrySendForFeeGap(opid, rawMsg)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string msg = sendErrorNeedsRescan(rawMsg)
|
||||||
|
? rawMsg + "\n\n" + TR("send_err_needs_rescan")
|
||||||
|
: rawMsg;
|
||||||
if (!invokeSendResultCallback(opid, false, msg)) {
|
if (!invokeSendResultCallback(opid, false, msg)) {
|
||||||
ui::Notifications::instance().error(msg);
|
ui::Notifications::instance().error(msg);
|
||||||
}
|
}
|
||||||
@@ -892,15 +1121,36 @@ void App::update()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After a bootstrap the preserved wallet.dat has stale spent-state and must be reconciled
|
||||||
|
// against the freshly-imported chain, or the first send tries to spend already-spent notes and
|
||||||
|
// is rejected. The -rescan restart can't do it (the snapshot lacks pre-snapshot blocks), so we
|
||||||
|
// detect the lowest available block height and run a runtime rescanblockchain from there. Wait
|
||||||
|
// until the daemon is connected and out of warmup so the probe and rescan can actually run.
|
||||||
|
if (post_bootstrap_rescan_pending_ && rpcConnected && state_.connected &&
|
||||||
|
!state_.warming_up && !state_.isLocked() && !state_.sync.rescanning &&
|
||||||
|
!runtime_rescan_active_ && !bootstrap_downloading_ &&
|
||||||
|
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
|
||||||
|
post_bootstrap_rescan_pending_ = false;
|
||||||
|
ui::Notifications::instance().info("Bootstrap complete — reconciling your wallet with the new chain data.");
|
||||||
|
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
|
||||||
|
if (ok && !fullHistory) {
|
||||||
|
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
|
||||||
|
} else {
|
||||||
|
rescanBlockchain(); // full-history node (or probe failed): -rescan restart works
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Per-category refresh with tab-aware intervals
|
// Per-category refresh with tab-aware intervals
|
||||||
// Skip when wallet is locked — same reason as above.
|
// Skip when wallet is locked — same reason as above.
|
||||||
if (rpcConnected && !state_.isLocked()) {
|
if (rpcConnected && !state_.isLocked()) {
|
||||||
const bool walletDataPage = currentPageNeedsWalletDataRefresh();
|
const bool walletDataPage = currentPageNeedsWalletDataRefresh();
|
||||||
if (network_refresh_.consumeDue(RefreshTimer::Core)) {
|
if (!runtime_rescan_active_ && network_refresh_.consumeDue(RefreshTimer::Core)) {
|
||||||
refreshCoreData();
|
refreshCoreData();
|
||||||
}
|
}
|
||||||
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28
|
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28 — and during a
|
||||||
if (!state_.warming_up) {
|
// runtime rescan, when the daemon holds cs_main and these calls would block.
|
||||||
|
if (!state_.warming_up && !runtime_rescan_active_) {
|
||||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
||||||
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
||||||
refreshTransactionData();
|
refreshTransactionData();
|
||||||
@@ -1674,6 +1924,7 @@ void App::render()
|
|||||||
// Bootstrap download from settings
|
// Bootstrap download from settings
|
||||||
ui::BootstrapDownloadDialog::render();
|
ui::BootstrapDownloadDialog::render();
|
||||||
ui::XmrigDownloadDialog::render();
|
ui::XmrigDownloadDialog::render();
|
||||||
|
ui::DaemonUpdateDialog::render();
|
||||||
|
|
||||||
// Windows Defender antivirus help dialog
|
// Windows Defender antivirus help dialog
|
||||||
renderAntivirusHelpDialog();
|
renderAntivirusHelpDialog();
|
||||||
@@ -1793,7 +2044,21 @@ void App::renderStatusBar()
|
|||||||
ImGui::SameLine(0, sbSectionGap);
|
ImGui::SameLine(0, sbSectionGap);
|
||||||
ImGui::TextDisabled("|");
|
ImGui::TextDisabled("|");
|
||||||
ImGui::SameLine(0, sbSeparatorGap);
|
ImGui::SameLine(0, sbSeparatorGap);
|
||||||
if (state_.sync.rescanning) {
|
if (state_.sync.building_witnesses) {
|
||||||
|
// Witness rebuild is the tail phase of a rescan — show its own progress with priority.
|
||||||
|
// Phase 1 (initial pass) and phase 2 (cache walk) have distinct labels so the progress
|
||||||
|
// restart at the phase boundary reads as a new step rather than a glitch.
|
||||||
|
const ImVec4 witCol(0.6f, 0.8f, 1.0f, 1.0f);
|
||||||
|
const char* witLabel = (state_.sync.witness_phase == 2)
|
||||||
|
? TR("sb_witness_cache") : TR("sb_building_witnesses");
|
||||||
|
if (state_.sync.witness_progress > 0.01f) {
|
||||||
|
ImGui::TextColored(witCol, "%s %.0f%%", witLabel, state_.sync.witness_progress * 100.0f);
|
||||||
|
} else {
|
||||||
|
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
|
||||||
|
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
|
||||||
|
ImGui::TextColored(witCol, "%s%s", witLabel, dotStr);
|
||||||
|
}
|
||||||
|
} else if (state_.sync.rescanning) {
|
||||||
// Show rescan progress (takes priority over sync)
|
// Show rescan progress (takes priority over sync)
|
||||||
// Use animated dots if progress is unknown (0%)
|
// Use animated dots if progress is unknown (0%)
|
||||||
if (state_.sync.rescan_progress > 0.01f) {
|
if (state_.sync.rescan_progress > 0.01f) {
|
||||||
@@ -2971,6 +3236,86 @@ void App::rescanBlockchain()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::repairWallet()
|
||||||
|
{
|
||||||
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
|
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
|
||||||
|
daemon::DaemonController::LifecycleOperation::RepairWallet,
|
||||||
|
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
|
||||||
|
if (!decision.allowed) {
|
||||||
|
ui::Notifications::instance().warning(decision.warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
||||||
|
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
|
||||||
|
|
||||||
|
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
|
||||||
|
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
|
||||||
|
// confirmed-active gating as rescan: the first poll may still reach the pre-restart daemon.
|
||||||
|
state_.sync.rescanning = true;
|
||||||
|
rescan_confirmed_active_ = false;
|
||||||
|
state_.sync.rescan_progress = 0.0f;
|
||||||
|
state_.sync.rescan_status = decision.status;
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
invalidateShieldedHistoryScanProgress(true);
|
||||||
|
|
||||||
|
// Set the zap flag BEFORE stopping so it's ready when we restart
|
||||||
|
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||||
|
DEBUG_LOGF("[App] Wallet-repair flag set, zapOnNextStart=%d\n", daemon_controller_->zapOnNextStart() ? 1 : 0);
|
||||||
|
|
||||||
|
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||||
|
DEBUG_LOGF("[App] Stopping daemon for wallet repair...\n");
|
||||||
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
|
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::reinstallBundledDaemon()
|
||||||
|
{
|
||||||
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
|
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!resources::getBundledDaemonInfo().available) {
|
||||||
|
ui::Notifications::instance().warning("This build has no bundled daemon to install");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!daemon_controller_ || !isUsingEmbeddedDaemon()) {
|
||||||
|
ui::Notifications::instance().warning("Reinstalling the daemon requires the embedded daemon");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
||||||
|
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
|
||||||
|
|
||||||
|
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
|
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
|
||||||
|
// Stop the daemon so its binary is no longer locked (Windows) / in use (ETXTBSY on Linux).
|
||||||
|
runtime.stopDaemonWithPolicy();
|
||||||
|
// Wait (bounded, ~12s) for the process to exit and release the binary before overwriting.
|
||||||
|
for (int i = 0; i < 120 && daemon::EmbeddedDaemon::isRpcPortInUse()
|
||||||
|
&& !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
|
||||||
|
ctx.sleepForMs(100);
|
||||||
|
}
|
||||||
|
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
|
||||||
|
const bool ok = resources::reextractBundledDaemon();
|
||||||
|
DEBUG_LOGF("[App] Reinstall bundled daemon: %s\n", ok ? "ok" : "FAILED");
|
||||||
|
// Restart on the freshly-installed binary.
|
||||||
|
runtime.resetOutputOffset();
|
||||||
|
if (!ctx.cancelled() && !ctx.shuttingDown()) {
|
||||||
|
runtime.startDaemon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void App::deleteBlockchainData()
|
void App::deleteBlockchainData()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
@@ -3266,7 +3611,7 @@ void App::renderShutdownScreen()
|
|||||||
force_quit_confirm_ = true;
|
force_quit_confirm_ = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("force_quit_warning"));
|
ui::material::Tooltip("%s", TR("force_quit_warning"));
|
||||||
}
|
}
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
}
|
}
|
||||||
@@ -3484,38 +3829,42 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// 2c. Live daemon console tail (init/warmup only) — show the last few lines the node
|
// 2c. Sapling note witness rebuild progress (post-rescan phase, runs inside warmup)
|
||||||
// printed so the user can watch real progress (UpdateTip height=…, Verifying blocks…)
|
|
||||||
// without leaving the blocked overlay. Full-node only (lite has no daemon_controller_).
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
if ((state_.daemon_initializing || state_.warming_up) && daemon_controller_) {
|
if (state_.sync.building_witnesses) {
|
||||||
const auto lines = daemon_controller_->recentLines(4);
|
float progress = state_.sync.witness_progress;
|
||||||
if (!lines.empty()) {
|
if (progress < 0.0f) progress = 0.0f;
|
||||||
ImFont* logFont = Type().caption();
|
if (progress > 1.0f) progress = 1.0f;
|
||||||
if (!logFont) logFont = ImGui::GetFont();
|
float barRadius = loadElem("progress-bar", 3.0f);
|
||||||
const float blockW = std::min(ws.x * 0.8f, 560.0f);
|
float barX = wp.x + cx - barW * 0.5f;
|
||||||
const ImU32 logCol = IM_COL32(140, 150, 165, 150);
|
ImVec2 barMin(barX, curY);
|
||||||
curY += gap * 0.5f;
|
ImVec2 barMax(barX + barW, curY + barH);
|
||||||
for (const auto& raw : lines) {
|
dl->AddRectFilled(barMin, barMax,
|
||||||
// Trim trailing CR/whitespace; skip blanks.
|
ui::schema::UI().resolveColor("var(--progress-track)", IM_COL32(255, 255, 255, 30)), barRadius);
|
||||||
std::string line = raw;
|
ImVec2 fillMax(barMin.x + barW * progress, barMax.y);
|
||||||
while (!line.empty() && (line.back() == '\r' || line.back() == '\n' ||
|
if (fillMax.x > barMin.x + 1.0f) {
|
||||||
line.back() == ' ' || line.back() == '\t'))
|
dl->AddRectFilled(barMin, fillMax,
|
||||||
line.pop_back();
|
ui::schema::UI().resolveColor("var(--primary)", IM_COL32(255, 218, 0, 200)), barRadius);
|
||||||
if (line.empty()) continue;
|
|
||||||
// Truncate (with an ellipsis) to keep each line on one row within blockW.
|
|
||||||
if (logFont->CalcTextSizeA(logFont->LegacySize, FLT_MAX, 0.0f, line.c_str()).x > blockW) {
|
|
||||||
while (line.size() > 1 &&
|
|
||||||
logFont->CalcTextSizeA(logFont->LegacySize, FLT_MAX, 0.0f, (line + "…").c_str()).x > blockW)
|
|
||||||
line.pop_back();
|
|
||||||
line += "…";
|
|
||||||
}
|
|
||||||
dl->AddText(logFont, logFont->LegacySize,
|
|
||||||
ImVec2(wp.x + cx - blockW * 0.5f, curY), logCol, line.c_str());
|
|
||||||
curY += logFont->LegacySize + 2.0f;
|
|
||||||
}
|
|
||||||
curY += gap;
|
|
||||||
}
|
}
|
||||||
|
curY += barH + gap;
|
||||||
|
|
||||||
|
char wbuf[128];
|
||||||
|
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
|
||||||
|
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
|
||||||
|
progress * 100.0f, state_.sync.witness_remaining);
|
||||||
|
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
|
||||||
|
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f);
|
||||||
|
else if (progress > 0.01f)
|
||||||
|
snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f);
|
||||||
|
else
|
||||||
|
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
|
||||||
|
ImFont* capFont = Type().caption();
|
||||||
|
if (!capFont) capFont = ImGui::GetFont();
|
||||||
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
||||||
|
IM_COL32(150, 150, 150, 255), wbuf);
|
||||||
|
curY += ts.y + gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
52
src/app.h
52
src/app.h
@@ -305,7 +305,21 @@ public:
|
|||||||
bool isEmbeddedDaemonRunning() const;
|
bool isEmbeddedDaemonRunning() const;
|
||||||
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||||||
void rescanBlockchain(); // restart daemon with -rescan flag
|
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
|
||||||
|
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
|
||||||
|
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
|
||||||
|
// block data), reconciling the wallet's stale spent-state without a daemon restart.
|
||||||
|
void runtimeRescan(int startHeight);
|
||||||
|
// Async binary-search probe for the lowest block height the node still has on disk.
|
||||||
|
// cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
|
||||||
|
// non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
|
||||||
|
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
|
||||||
|
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
|
||||||
|
// daemon is back up (consumed in update()).
|
||||||
|
void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
|
||||||
|
bool runtimeRescanActive() const { return runtime_rescan_active_; }
|
||||||
|
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
|
||||||
|
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
|
||||||
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
||||||
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
||||||
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
|
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
|
||||||
@@ -410,7 +424,18 @@ private:
|
|||||||
const std::string& from,
|
const std::string& from,
|
||||||
const std::string& to,
|
const std::string& to,
|
||||||
double amount,
|
double amount,
|
||||||
const std::string& memo);
|
const std::string& memo,
|
||||||
|
double fee = 0.0);
|
||||||
|
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
|
||||||
|
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
|
||||||
|
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
|
||||||
|
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
|
||||||
|
// self-output that lifts the daemon's selection target past the boundary so it grabs another
|
||||||
|
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
|
||||||
|
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
|
||||||
|
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
||||||
|
double amount, double fee, const std::string& memo,
|
||||||
|
std::function<void(bool, const std::string&)> callback);
|
||||||
void markPendingSendTransactionSucceeded(const std::string& opid,
|
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||||
const std::string& txid);
|
const std::string& txid);
|
||||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||||
@@ -603,6 +628,25 @@ private:
|
|||||||
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
|
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
|
||||||
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
|
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
|
||||||
bool rescan_confirmed_active_ = false;
|
bool rescan_confirmed_active_ = false;
|
||||||
|
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
|
||||||
|
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
|
||||||
|
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
|
||||||
|
bool runtime_rescan_active_ = false;
|
||||||
|
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
|
||||||
|
// that reconciles the preserved wallet.dat against the freshly-imported chain.
|
||||||
|
bool post_bootstrap_rescan_pending_ = false;
|
||||||
|
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
|
||||||
|
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
|
||||||
|
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
|
||||||
|
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
|
||||||
|
int witness_rebuild_total_blocks_ = 0;
|
||||||
|
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
|
||||||
|
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
|
||||||
|
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
|
||||||
|
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
|
||||||
|
// dedups the daemon's occasional double-prints) over the reported total N.
|
||||||
|
std::unordered_set<std::string> witness_seen_txids_;
|
||||||
|
int witness_total_txs_ = 0;
|
||||||
bool opid_poll_in_progress_ = false;
|
bool opid_poll_in_progress_ = false;
|
||||||
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
||||||
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
||||||
@@ -617,9 +661,13 @@ private:
|
|||||||
std::string to;
|
std::string to;
|
||||||
std::string memo;
|
std::string memo;
|
||||||
double amount = 0.0;
|
double amount = 0.0;
|
||||||
|
double fee = 0.0;
|
||||||
std::int64_t timestamp = 0;
|
std::int64_t timestamp = 0;
|
||||||
};
|
};
|
||||||
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
||||||
|
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
|
||||||
|
// fails again is reported to the user instead of looping.
|
||||||
|
std::unordered_set<std::string> send_feegap_retried_opids_;
|
||||||
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
|
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
|
||||||
// user isn't told "sent successfully" before the tx is actually built/broadcast.
|
// user isn't told "sent successfully" before the tx is actually built/broadcast.
|
||||||
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
|
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
|
||||||
|
|||||||
@@ -767,7 +767,8 @@ void App::upsertPendingSendTransaction(const std::string& opid,
|
|||||||
const std::string& from,
|
const std::string& from,
|
||||||
const std::string& to,
|
const std::string& to,
|
||||||
double amount,
|
double amount,
|
||||||
const std::string& memo)
|
const std::string& memo,
|
||||||
|
double fee)
|
||||||
{
|
{
|
||||||
if (opid.empty()) return;
|
if (opid.empty()) return;
|
||||||
|
|
||||||
@@ -778,6 +779,7 @@ void App::upsertPendingSendTransaction(const std::string& opid,
|
|||||||
pendingInfo.to = to;
|
pendingInfo.to = to;
|
||||||
pendingInfo.amount = std::abs(amount);
|
pendingInfo.amount = std::abs(amount);
|
||||||
pendingInfo.memo = memo;
|
pendingInfo.memo = memo;
|
||||||
|
pendingInfo.fee = fee;
|
||||||
|
|
||||||
TransactionInfo pending;
|
TransactionInfo pending;
|
||||||
pending.txid = opid;
|
pending.txid = opid;
|
||||||
@@ -1144,6 +1146,33 @@ void App::refreshCoreData()
|
|||||||
state_.warmup_description.clear();
|
state_.warmup_description.clear();
|
||||||
connection_status_ = TR("connected");
|
connection_status_ = TR("connected");
|
||||||
VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n");
|
VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n");
|
||||||
|
|
||||||
|
// A -rescan runs entirely INSIDE daemon warmup (every RPC returns -28 until the
|
||||||
|
// scan finishes), so warmup completing IS the rescan completing. This is the
|
||||||
|
// reliable completion signal: some daemons lack getrescaninfo (it returns
|
||||||
|
// "Method not found") or never print a "Done rescanning"/bench line, which left
|
||||||
|
// the older detectors stuck at 99% — the user would then kill it prematurely.
|
||||||
|
// rescan_confirmed_active_ ensures we actually observed this rescan running (set
|
||||||
|
// by the getrescaninfo / daemon-log pollers) before declaring it done.
|
||||||
|
if (state_.sync.rescanning && rescan_confirmed_active_) {
|
||||||
|
state_.sync.rescanning = false;
|
||||||
|
rescan_confirmed_active_ = false;
|
||||||
|
state_.sync.rescan_progress = 1.0f;
|
||||||
|
state_.sync.rescan_status.clear();
|
||||||
|
state_.sync.building_witnesses = false;
|
||||||
|
state_.sync.witness_phase = 0;
|
||||||
|
state_.sync.witness_progress = 0.0f;
|
||||||
|
state_.sync.witness_remaining = 0;
|
||||||
|
witness_rebuild_total_blocks_ = 0;
|
||||||
|
witness_seen_txids_.clear();
|
||||||
|
witness_total_txs_ = 0;
|
||||||
|
// Notes/witnesses were rebuilt — force a fresh history + balance pull.
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
invalidateShieldedHistoryScanProgress(true);
|
||||||
|
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||||
|
}
|
||||||
|
|
||||||
NetworkRefreshService::applyConnectionInfoResult(state_, result.info);
|
NetworkRefreshService::applyConnectionInfoResult(state_, result.info);
|
||||||
// Trigger full data refresh now that daemon is ready
|
// Trigger full data refresh now that daemon is ready
|
||||||
refreshData();
|
refreshData();
|
||||||
@@ -2337,7 +2366,7 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
result_str = e.what();
|
result_str = e.what();
|
||||||
}
|
}
|
||||||
return [this, callback, ok, result_str, from, to, amount, memo]() {
|
return [this, callback, ok, result_str, from, to, amount, fee, memo]() {
|
||||||
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
// A send changes address balances — refresh on next cycle
|
// A send changes address balances — refresh on next cycle
|
||||||
@@ -2352,7 +2381,7 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
// "sent successfully" for an operation that may still fail.
|
// "sent successfully" for an operation that may still fail.
|
||||||
if (!result_str.empty()) {
|
if (!result_str.empty()) {
|
||||||
pending_opids_.push_back(result_str);
|
pending_opids_.push_back(result_str);
|
||||||
upsertPendingSendTransaction(result_str, from, to, amount, memo);
|
upsertPendingSendTransaction(result_str, from, to, amount, memo, fee);
|
||||||
if (callback) pending_send_callbacks_[result_str] = callback;
|
if (callback) pending_send_callbacks_[result_str] = callback;
|
||||||
} else if (callback) {
|
} else if (callback) {
|
||||||
callback(true, result_str); // no opid to track — report as-is
|
callback(true, result_str); // no opid to track — report as-is
|
||||||
@@ -2365,4 +2394,229 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse the daemon's "Insufficient shielded funds, have <H>, need <N>" message. std::stod stops at
|
||||||
|
// the trailing comma/text, so it extracts each amount cleanly. Returns false if it isn't that error.
|
||||||
|
static bool parseInsufficientShielded(const std::string& msg, double& have, double& need)
|
||||||
|
{
|
||||||
|
if (msg.find("Insufficient shielded funds") == std::string::npos) return false;
|
||||||
|
auto hp = msg.find("have ");
|
||||||
|
auto np = msg.find("need ");
|
||||||
|
if (hp == std::string::npos || np == std::string::npos) return false;
|
||||||
|
try {
|
||||||
|
have = std::stod(msg.substr(hp + 5));
|
||||||
|
need = std::stod(msg.substr(np + 5));
|
||||||
|
} catch (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg)
|
||||||
|
{
|
||||||
|
double have = 0.0, need = 0.0;
|
||||||
|
if (!parseInsufficientShielded(rawMsg, have, need)) return false;
|
||||||
|
|
||||||
|
auto it = pending_send_info_.find(opid);
|
||||||
|
if (it == pending_send_info_.end()) return false;
|
||||||
|
const PendingSendInfo info = it->second; // copy before any cleanup
|
||||||
|
|
||||||
|
// Only shielded sends hit the bug; a transparent "from" uses a different (correct) selector.
|
||||||
|
if (info.from.empty() || info.from[0] != 'z') return false;
|
||||||
|
// Discriminator: the daemon stopped once it covered the amount but short of the fee, so the
|
||||||
|
// selected total (have) is >= the amount. A genuine shortfall reports have < amount (it grabbed
|
||||||
|
// every note and still couldn't reach the amount) — don't "retry" those.
|
||||||
|
if (have + 1e-9 < info.amount) return false;
|
||||||
|
// Never retry a retry (the self-output already widened the target; a second failure is real).
|
||||||
|
if (send_feegap_retried_opids_.count(opid)) return false;
|
||||||
|
|
||||||
|
const double fee = info.fee > 0.0 ? info.fee : 0.0001;
|
||||||
|
|
||||||
|
// Hand the waiting UI callback to the retry so the user sees the final outcome, not the
|
||||||
|
// intermediate "insufficient" we're working around.
|
||||||
|
std::function<void(bool, const std::string&)> cb;
|
||||||
|
auto cbIt = pending_send_callbacks_.find(opid);
|
||||||
|
if (cbIt != pending_send_callbacks_.end()) {
|
||||||
|
cb = cbIt->second;
|
||||||
|
pending_send_callbacks_.erase(cbIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOGF("[send] fee-gap workaround: retrying %s with self-output (have=%.8f need=%.8f amount=%.8f fee=%.8f)\n",
|
||||||
|
opid.c_str(), have, need, info.amount, fee);
|
||||||
|
resendWithFeeGapWorkaround(info.from, info.to, info.amount, fee, info.memo, std::move(cb));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
||||||
|
double amount, double fee, const std::string& memo,
|
||||||
|
std::function<void(bool, const std::string&)> callback)
|
||||||
|
{
|
||||||
|
if (!state_.connected || !rpc_ || !worker_) {
|
||||||
|
if (callback) callback(false, "Not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipients = the real recipient + a tiny self-output (= fee) back to `from`. The extra output
|
||||||
|
// raises the daemon's note-selection target (nTotalOut) above the single largest note, so its
|
||||||
|
// greedy picker grabs another note and can therefore also cover the miner fee. The recipient
|
||||||
|
// still receives EXACTLY `amount`; the self-output and any change return to `from`.
|
||||||
|
nlohmann::json recipients = nlohmann::json::array();
|
||||||
|
nlohmann::json primary;
|
||||||
|
primary["address"] = to;
|
||||||
|
primary["amount"] = util::formatAmountFixed(amount);
|
||||||
|
if (!memo.empty()) primary["memo"] = memo;
|
||||||
|
recipients.push_back(primary);
|
||||||
|
nlohmann::json selfOut;
|
||||||
|
selfOut["address"] = from;
|
||||||
|
selfOut["amount"] = util::formatAmountFixed(fee);
|
||||||
|
recipients.push_back(selfOut);
|
||||||
|
|
||||||
|
send_progress_active_ = true;
|
||||||
|
++send_submissions_in_flight_;
|
||||||
|
worker_->post([this, from, to, amount, fee, memo, recipients, callback]() -> rpc::RPCWorker::MainCb {
|
||||||
|
bool ok = false;
|
||||||
|
std::string result_str;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Send tab / Fee-gap retry");
|
||||||
|
auto result = rpc_->call("z_sendmany", {from, recipients, 1, fee});
|
||||||
|
result_str = result.get<std::string>();
|
||||||
|
ok = true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
result_str = e.what();
|
||||||
|
}
|
||||||
|
return [this, callback, ok, result_str, from, to, amount, fee, memo]() {
|
||||||
|
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
||||||
|
if (ok) {
|
||||||
|
addresses_dirty_ = true;
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
network_refresh_.markWalletMutationRefresh();
|
||||||
|
if (!result_str.empty()) {
|
||||||
|
pending_opids_.push_back(result_str);
|
||||||
|
send_feegap_retried_opids_.insert(result_str); // a retry of a retry is a real error
|
||||||
|
upsertPendingSendTransaction(result_str, from, to, amount, memo, fee);
|
||||||
|
if (callback) pending_send_callbacks_[result_str] = callback;
|
||||||
|
} else if (callback) {
|
||||||
|
callback(true, result_str);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
send_progress_active_ = false;
|
||||||
|
if (callback) callback(false, result_str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------------
|
||||||
|
// Bootstrapped/pruned-node rescan support
|
||||||
|
//
|
||||||
|
// A node restored from a bootstrap snapshot only has block data from the snapshot base upward;
|
||||||
|
// blocks below it are absent on disk. The startup -rescan flag rescans from genesis and fails on
|
||||||
|
// such a node ("error in HDD data"), and rescanblockchain(0) hits the same missing blocks. The
|
||||||
|
// fix is to rescan from a height the snapshot includes — found here by binary-searching for the
|
||||||
|
// lowest height whose block data the node can actually read.
|
||||||
|
// --------------------------------------------------------------------------------------------
|
||||||
|
void App::detectLowestAvailableBlockHeight(std::function<void(bool, int, bool)> cb)
|
||||||
|
{
|
||||||
|
if (!rpc_ || !rpc_->isConnected()) {
|
||||||
|
if (cb) cb(false, 0, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int tip = state_.sync.blocks;
|
||||||
|
if (tip <= 1) {
|
||||||
|
// No usable tip yet (or a brand-new chain) — treat as full history, nothing to probe.
|
||||||
|
if (cb) cb(false, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared search window; each probe halves it. Invariant maintained: block at `hi` is readable.
|
||||||
|
struct SearchState { int lo; int hi; std::function<void(bool, int, bool)> cb; };
|
||||||
|
auto st = std::make_shared<SearchState>(SearchState{0, tip, std::move(cb)});
|
||||||
|
auto step = std::make_shared<std::function<void()>>();
|
||||||
|
*step = [this, st, step]() {
|
||||||
|
if (st->lo >= st->hi) {
|
||||||
|
const bool fullHistory = (st->lo <= 1); // genesis/early block present → not bootstrapped
|
||||||
|
if (st->cb) st->cb(true, st->lo, fullHistory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int mid = st->lo + (st->hi - st->lo) / 2;
|
||||||
|
rpc_->getBlock(mid, [st, step, mid](const nlohmann::json& result, const std::string& error) {
|
||||||
|
if (error.empty() && !result.is_null()) {
|
||||||
|
st->hi = mid; // block data present → lowest available is <= mid
|
||||||
|
} else {
|
||||||
|
st->lo = mid + 1; // block data missing → lowest available is > mid
|
||||||
|
}
|
||||||
|
(*step)();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*step)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::runtimeRescan(int startHeight)
|
||||||
|
{
|
||||||
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
|
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state_.connected || !rpc_ || !rpc_->isConnected() || !worker_) {
|
||||||
|
ui::Notifications::instance().warning("Not connected to daemon");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (runtime_rescan_active_) return;
|
||||||
|
if (startHeight < 0) startHeight = 0;
|
||||||
|
|
||||||
|
DEBUG_LOGF("[App] Starting runtime rescanblockchain from height %d\n", startHeight);
|
||||||
|
|
||||||
|
// The rescan runs inside the daemon (holds cs_main/cs_wallet) — it does not restart. We own
|
||||||
|
// completion via this RPC's callback; rescan_confirmed_active_ keeps the -rescan-style pollers
|
||||||
|
// from misreading state, and runtime_rescan_active_ suppresses the per-second pollers that
|
||||||
|
// would otherwise pile up behind the held lock.
|
||||||
|
runtime_rescan_active_ = true;
|
||||||
|
state_.sync.rescanning = true;
|
||||||
|
rescan_confirmed_active_ = true;
|
||||||
|
state_.sync.rescan_progress = 0.0f;
|
||||||
|
state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "...";
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
invalidateShieldedHistoryScanProgress(true);
|
||||||
|
|
||||||
|
ui::Notifications::instance().info("Rescanning blockchain from block " + std::to_string(startHeight) +
|
||||||
|
" — this can take a while.");
|
||||||
|
|
||||||
|
worker_->post([this, startHeight]() -> rpc::RPCWorker::MainCb {
|
||||||
|
bool ok = false;
|
||||||
|
std::string err;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Settings / Runtime rescan");
|
||||||
|
rpc_->call("rescan", {startHeight}); // hush "rescan <height>"; blocks until the scan finishes
|
||||||
|
ok = true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
err = e.what();
|
||||||
|
}
|
||||||
|
return [this, ok, err]() {
|
||||||
|
runtime_rescan_active_ = false;
|
||||||
|
rescan_confirmed_active_ = false;
|
||||||
|
state_.sync.rescanning = false;
|
||||||
|
state_.sync.rescan_status.clear();
|
||||||
|
state_.sync.building_witnesses = false;
|
||||||
|
state_.sync.witness_phase = 0;
|
||||||
|
state_.sync.witness_progress = 0.0f;
|
||||||
|
state_.sync.witness_remaining = 0;
|
||||||
|
witness_rebuild_total_blocks_ = 0;
|
||||||
|
witness_seen_txids_.clear();
|
||||||
|
witness_total_txs_ = 0;
|
||||||
|
if (ok) {
|
||||||
|
state_.sync.rescan_progress = 1.0f;
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
invalidateShieldedHistoryScanProgress(true);
|
||||||
|
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||||
|
refreshData();
|
||||||
|
} else {
|
||||||
|
state_.sync.rescan_progress = 0.0f;
|
||||||
|
ui::Notifications::instance().error("Rescan failed: " + err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -774,6 +774,8 @@ void App::renderFirstRunWizard() {
|
|||||||
auto finalProg = bootstrap_->getProgress();
|
auto finalProg = bootstrap_->getProgress();
|
||||||
if (finalProg.state == util::Bootstrap::State::Completed) {
|
if (finalProg.state == util::Bootstrap::State::Completed) {
|
||||||
bootstrap_.reset();
|
bootstrap_.reset();
|
||||||
|
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
|
||||||
|
markPostBootstrapRescanPending();
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
} else {
|
} else {
|
||||||
wizard_phase_ = WizardPhase::BootstrapFailed;
|
wizard_phase_ = WizardPhase::BootstrapFailed;
|
||||||
@@ -1022,7 +1024,7 @@ void App::renderFirstRunWizard() {
|
|||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|||||||
@@ -96,12 +96,23 @@ bool DaemonController::rescanOnNextStart() const
|
|||||||
return daemon_->rescanOnNextStart();
|
return daemon_->rescanOnNextStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DaemonController::setZapOnNextStart(bool enabled)
|
||||||
|
{
|
||||||
|
daemon_->setZapOnNextStart(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::zapOnNextStart() const
|
||||||
|
{
|
||||||
|
return daemon_->zapOnNextStart();
|
||||||
|
}
|
||||||
|
|
||||||
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
|
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
|
||||||
const config::Settings* settings)
|
const config::Settings* settings)
|
||||||
{
|
{
|
||||||
if (settings) syncSettings(settings);
|
if (settings) syncSettings(settings);
|
||||||
if (decision.resetCrashCount) resetCrashCount();
|
if (decision.resetCrashCount) resetCrashCount();
|
||||||
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
|
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
|
||||||
|
if (decision.setZapOnNextStart) setZapOnNextStart(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
DaemonController::ShutdownDecision DaemonController::shutdownDecision(
|
DaemonController::ShutdownDecision DaemonController::shutdownDecision(
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public:
|
|||||||
enum class LifecycleOperation {
|
enum class LifecycleOperation {
|
||||||
ManualRestart,
|
ManualRestart,
|
||||||
Rescan,
|
Rescan,
|
||||||
|
RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records)
|
||||||
DeleteBlockchainData,
|
DeleteBlockchainData,
|
||||||
BootstrapStop
|
BootstrapStop
|
||||||
};
|
};
|
||||||
@@ -46,6 +47,7 @@ public:
|
|||||||
bool setRescanOnNextStart = false;
|
bool setRescanOnNextStart = false;
|
||||||
bool disconnectRpc = false;
|
bool disconnectRpc = false;
|
||||||
int restartDelayMs = 0;
|
int restartDelayMs = 0;
|
||||||
|
bool setZapOnNextStart = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LifecycleTaskContext {
|
class LifecycleTaskContext {
|
||||||
@@ -102,6 +104,8 @@ public:
|
|||||||
void resetCrashCount();
|
void resetCrashCount();
|
||||||
void setRescanOnNextStart(bool enabled);
|
void setRescanOnNextStart(bool enabled);
|
||||||
bool rescanOnNextStart() const;
|
bool rescanOnNextStart() const;
|
||||||
|
void setZapOnNextStart(bool enabled);
|
||||||
|
bool zapOnNextStart() const;
|
||||||
|
|
||||||
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
||||||
bool externalDaemonDetected,
|
bool externalDaemonDetected,
|
||||||
@@ -141,6 +145,13 @@ public:
|
|||||||
}
|
}
|
||||||
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
|
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
|
||||||
false, true, false, 3000};
|
false, true, false, 3000};
|
||||||
|
case LifecycleOperation::RepairWallet:
|
||||||
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
|
return {operation, false, daemonRunning, "", "",
|
||||||
|
"Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."};
|
||||||
|
}
|
||||||
|
return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "",
|
||||||
|
false, false, false, 3000, true};
|
||||||
case LifecycleOperation::DeleteBlockchainData:
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
if (!usingEmbeddedDaemon || !hasDaemon) {
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
return {operation, false, daemonRunning, "", "",
|
return {operation, false, daemonRunning, "", "",
|
||||||
@@ -190,6 +201,7 @@ public:
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case LifecycleOperation::Rescan:
|
case LifecycleOperation::Rescan:
|
||||||
|
case LifecycleOperation::RepairWallet:
|
||||||
case LifecycleOperation::DeleteBlockchainData:
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
runtime.stopDaemonWithPolicy();
|
runtime.stopDaemonWithPolicy();
|
||||||
result.stopped = true;
|
result.stopped = true;
|
||||||
@@ -206,6 +218,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (decision.operation == LifecycleOperation::Rescan ||
|
if (decision.operation == LifecycleOperation::Rescan ||
|
||||||
|
decision.operation == LifecycleOperation::RepairWallet ||
|
||||||
decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
||||||
runtime.resetOutputOffset();
|
runtime.resetOutputOffset();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,8 +482,14 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add -rescan flag if requested (one-shot)
|
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
|
||||||
if (rescan_on_next_start_.exchange(false)) {
|
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
|
||||||
|
if (zap_on_next_start_.exchange(false)) {
|
||||||
|
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
|
||||||
|
args.push_back("-zapwallettxes=2");
|
||||||
|
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
|
||||||
|
} else if (rescan_on_next_start_.exchange(false)) {
|
||||||
|
// Add -rescan flag if requested (one-shot)
|
||||||
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
||||||
args.push_back("-rescan");
|
args.push_back("-rescan");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ public:
|
|||||||
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
||||||
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
|
||||||
|
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
|
||||||
|
* daemon implicitly rescans afterwards. One-shot, like the rescan flag.
|
||||||
|
*/
|
||||||
|
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
||||||
|
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
||||||
|
|
||||||
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
||||||
int getCrashCount() const { return crash_count_.load(); }
|
int getCrashCount() const { return crash_count_.load(); }
|
||||||
/** Reset crash counter (call on successful connection or manual restart) */
|
/** Reset crash counter (call on successful connection or manual restart) */
|
||||||
@@ -222,6 +230,7 @@ private:
|
|||||||
int max_connections_ = 0; // 0 = daemon default
|
int max_connections_ = 0; // 0 = daemon default
|
||||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||||
|
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace daemon
|
} // namespace daemon
|
||||||
|
|||||||
@@ -127,6 +127,19 @@ struct SyncInfo {
|
|||||||
float rescan_progress = 0.0f; // 0.0 - 1.0
|
float rescan_progress = 0.0f; // 0.0 - 1.0
|
||||||
std::string rescan_status; // e.g. "Rescanning... 25%"
|
std::string rescan_status; // e.g. "Rescanning... 25%"
|
||||||
|
|
||||||
|
// Sapling note witness rebuild — a distinct, often-long phase after a rescan/zap. The daemon
|
||||||
|
// reports it in TWO sub-phases with different signals, so we track which is active:
|
||||||
|
// 1 = initial pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") — progress
|
||||||
|
// is distinct-txs-witnessed / N (the <i> bounces, so it can't be used directly).
|
||||||
|
// 2 = witness-cache walk ("Building Witnesses for block <h> <frac> complete, <n> remaining")
|
||||||
|
// — progress derived from how far "remaining" has fallen from its per-phase peak.
|
||||||
|
// The two are sequential with different scales, so progress is NOT carried across the boundary
|
||||||
|
// (that would pin the bar at the initial pass's ~100% through the whole cache walk).
|
||||||
|
bool building_witnesses = false;
|
||||||
|
int witness_phase = 0; // 0 none, 1 initial-witness pass, 2 witness-cache walk
|
||||||
|
float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase
|
||||||
|
int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1)
|
||||||
|
|
||||||
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
|
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -225,11 +227,13 @@ bool needsParamsExtraction()
|
|||||||
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
|
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
|
||||||
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
|
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
|
||||||
|
|
||||||
// Also check if daemon binaries need updating
|
// Daemon binaries are only auto-placed when MISSING (never auto-overwritten on a size
|
||||||
|
// mismatch) — the user may be running a specific dragonxd. Replacing the bundled daemon is
|
||||||
|
// an explicit action via Settings → daemon binary. So only trigger extraction if it's absent.
|
||||||
#ifdef HAS_EMBEDDED_DAEMON
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||||
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||||
if (daemonRes && resourceNeedsUpdate(daemonRes, daemonPath)) return true;
|
if (daemonRes && !std::filesystem::exists(daemonPath)) return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_EMBEDDED_XMRIG
|
#ifdef HAS_EMBEDDED_XMRIG
|
||||||
@@ -369,12 +373,12 @@ bool extractEmbeddedResources()
|
|||||||
#ifdef HAS_EMBEDDED_DAEMON
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
DEBUG_LOGF("[INFO] Daemon extraction directory: %s\n", daemonDir.c_str());
|
DEBUG_LOGF("[INFO] Daemon extraction directory: %s\n", daemonDir.c_str());
|
||||||
|
|
||||||
|
// Daemon binaries are placed ONLY when missing — never auto-overwritten on a size mismatch
|
||||||
|
// (the user may run a specific dragonxd; replacing it is an explicit Settings action).
|
||||||
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||||
if (daemonRes) {
|
if (daemonRes) {
|
||||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||||
if (resourceNeedsUpdate(daemonRes, dest)) {
|
if (!std::filesystem::exists(dest)) {
|
||||||
if (std::filesystem::exists(dest))
|
|
||||||
DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n");
|
|
||||||
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
|
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
|
||||||
if (!extractResource(daemonRes, dest)) {
|
if (!extractResource(daemonRes, dest)) {
|
||||||
success = false;
|
success = false;
|
||||||
@@ -388,9 +392,7 @@ bool extractEmbeddedResources()
|
|||||||
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
|
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
|
||||||
if (cliRes) {
|
if (cliRes) {
|
||||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
|
||||||
if (resourceNeedsUpdate(cliRes, dest)) {
|
if (!std::filesystem::exists(dest)) {
|
||||||
if (std::filesystem::exists(dest))
|
|
||||||
DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n");
|
|
||||||
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
|
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
|
||||||
if (!extractResource(cliRes, dest)) {
|
if (!extractResource(cliRes, dest)) {
|
||||||
success = false;
|
success = false;
|
||||||
@@ -404,9 +406,7 @@ bool extractEmbeddedResources()
|
|||||||
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
|
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
|
||||||
if (txRes) {
|
if (txRes) {
|
||||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
|
||||||
if (resourceNeedsUpdate(txRes, dest)) {
|
if (!std::filesystem::exists(dest)) {
|
||||||
if (std::filesystem::exists(dest))
|
|
||||||
DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n");
|
|
||||||
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
|
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
|
||||||
if (!extractResource(txRes, dest)) {
|
if (!extractResource(txRes, dest)) {
|
||||||
success = false;
|
success = false;
|
||||||
@@ -590,6 +590,120 @@ bool forceExtractXmrig()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scan a binary blob for the daemon's version stamp: 'v' <maj>.<min>.<rev> optionally followed by
|
||||||
|
// '-' <commit hash (>=6 hex)>, e.g. "v1.0.2-ddd851dc1". Returns the first match, or "" if none.
|
||||||
|
static std::string scanBinaryVersion(const uint8_t* data, std::size_t size)
|
||||||
|
{
|
||||||
|
if (!data || size < 6) return "";
|
||||||
|
auto isdig = [](uint8_t c) { return std::isdigit(static_cast<unsigned char>(c)) != 0; };
|
||||||
|
auto isxd = [](uint8_t c) { return std::isxdigit(static_cast<unsigned char>(c)) != 0; };
|
||||||
|
for (std::size_t i = 0; i + 5 < size; ++i) {
|
||||||
|
if (data[i] != 'v') continue;
|
||||||
|
std::size_t k = i + 1, s;
|
||||||
|
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // major
|
||||||
|
if (k >= size || data[k] != '.') continue; ++k;
|
||||||
|
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // minor
|
||||||
|
if (k >= size || data[k] != '.') continue; ++k;
|
||||||
|
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // revision
|
||||||
|
std::size_t end = k;
|
||||||
|
if (k < size && data[k] == '-') { // optional -<commit>
|
||||||
|
std::size_t h = k + 1, hs = h;
|
||||||
|
while (h < size && isxd(data[h])) ++h;
|
||||||
|
if (h - hs >= 6) end = h;
|
||||||
|
}
|
||||||
|
return std::string(reinterpret_cast<const char*>(data) + i, end - i);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonBinaryInfo getInstalledDaemonInfo()
|
||||||
|
{
|
||||||
|
DaemonBinaryInfo info;
|
||||||
|
std::string daemonDir = getDaemonDirectory();
|
||||||
|
#ifdef _WIN32
|
||||||
|
info.path = daemonDir + "\\" + RESOURCE_DRAGONXD;
|
||||||
|
#else
|
||||||
|
info.path = daemonDir + "/" + RESOURCE_DRAGONXD;
|
||||||
|
#endif
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false
|
||||||
|
info.exists = true;
|
||||||
|
info.size = std::filesystem::file_size(info.path, ec);
|
||||||
|
if (ec) info.size = 0;
|
||||||
|
|
||||||
|
auto ftime = std::filesystem::last_write_time(info.path, ec);
|
||||||
|
if (!ec) {
|
||||||
|
// Convert filesystem clock → system_clock epoch (pre-C++20 portable approximation).
|
||||||
|
auto sysTime = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
||||||
|
ftime - decltype(ftime)::clock::now() + std::chrono::system_clock::now());
|
||||||
|
info.modifiedEpoch =
|
||||||
|
static_cast<std::int64_t>(std::chrono::system_clock::to_time_t(sysTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the binary and scan for its version stamp (one-off; caller caches the result).
|
||||||
|
std::ifstream f(info.path, std::ios::binary);
|
||||||
|
if (f) {
|
||||||
|
f.seekg(0, std::ios::end);
|
||||||
|
std::streamoff len = f.tellg();
|
||||||
|
f.seekg(0, std::ios::beg);
|
||||||
|
if (len > 0) {
|
||||||
|
std::vector<uint8_t> buf(static_cast<std::size_t>(len));
|
||||||
|
f.read(reinterpret_cast<char*>(buf.data()), len);
|
||||||
|
info.version = scanBinaryVersion(buf.data(), static_cast<std::size_t>(f.gcount()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
BundledDaemonInfo getBundledDaemonInfo()
|
||||||
|
{
|
||||||
|
BundledDaemonInfo info;
|
||||||
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
|
const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||||
|
if (res && res->data && res->size > 0) {
|
||||||
|
info.available = true;
|
||||||
|
info.size = res->size;
|
||||||
|
// The embedded bytes are constant for this build — scan once.
|
||||||
|
static const std::string cachedVersion = scanBinaryVersion(res->data, res->size);
|
||||||
|
info.version = cachedVersion;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reextractBundledDaemon()
|
||||||
|
{
|
||||||
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
|
std::string daemonDir = getDaemonDirectory();
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char pathSep = '\\';
|
||||||
|
#else
|
||||||
|
const char pathSep = '/';
|
||||||
|
#endif
|
||||||
|
bool ok = true;
|
||||||
|
bool wroteAny = false;
|
||||||
|
const char* names[] = { RESOURCE_DRAGONXD, RESOURCE_DRAGONX_CLI, RESOURCE_DRAGONX_TX };
|
||||||
|
for (const char* name : names) {
|
||||||
|
const EmbeddedResource* res = getEmbeddedResource(name);
|
||||||
|
if (!res) continue;
|
||||||
|
std::string dest = daemonDir + pathSep + name;
|
||||||
|
DEBUG_LOGF("[INFO] reextractBundledDaemon: writing %s (%zu MB)\n", name, res->size / (1024*1024));
|
||||||
|
if (!extractResource(res, dest)) {
|
||||||
|
DEBUG_LOGF("[ERROR] reextractBundledDaemon: failed to write %s\n", name);
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wroteAny = true;
|
||||||
|
#ifndef _WIN32
|
||||||
|
chmod(dest.c_str(), 0755);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return ok && wroteAny;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string getXmrigPath()
|
std::string getXmrigPath()
|
||||||
{
|
{
|
||||||
std::string daemonDir = getDaemonDirectory();
|
std::string daemonDir = getDaemonDirectory();
|
||||||
|
|||||||
@@ -30,6 +30,31 @@ bool needsParamsExtraction();
|
|||||||
// Get the params directory path
|
// Get the params directory path
|
||||||
std::string getParamsDirectory();
|
std::string getParamsDirectory();
|
||||||
|
|
||||||
|
// --- Daemon binary management (Settings → daemon binary panel) ------------------------------
|
||||||
|
// Info about the dragonxd binary currently installed in the dragonx/ extraction directory.
|
||||||
|
struct DaemonBinaryInfo {
|
||||||
|
bool exists = false;
|
||||||
|
std::string path;
|
||||||
|
std::uintmax_t size = 0;
|
||||||
|
std::string version; // scanned from the binary ("vX.Y.Z-<commit>"), empty if not found
|
||||||
|
std::int64_t modifiedEpoch = 0; // last-write time as unix epoch seconds, 0 if unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
// Info about the dragonxd binary bundled inside this wallet build.
|
||||||
|
struct BundledDaemonInfo {
|
||||||
|
bool available = false; // a daemon resource is embedded in this build
|
||||||
|
std::uintmax_t size = 0;
|
||||||
|
std::string version;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read + scan the installed dragonxd (reads the file; call off the UI thread or cache the result).
|
||||||
|
DaemonBinaryInfo getInstalledDaemonInfo();
|
||||||
|
// Info about the bundled daemon (scans the embedded bytes once, cached).
|
||||||
|
BundledDaemonInfo getBundledDaemonInfo();
|
||||||
|
// Force-overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones. The
|
||||||
|
// caller should stop the daemon first. Returns true if all present resources were written.
|
||||||
|
bool reextractBundledDaemon();
|
||||||
|
|
||||||
// Resource names
|
// Resource names
|
||||||
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
||||||
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
||||||
|
|||||||
@@ -668,7 +668,8 @@ void RPCClient::stop(Callback cb, ErrorCallback err)
|
|||||||
|
|
||||||
void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err)
|
void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err)
|
||||||
{
|
{
|
||||||
doRPC("rescanblockchain", {startHeight}, cb, err);
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
||||||
|
doRPC("rescan", {startHeight}, cb, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err)
|
void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err)
|
||||||
@@ -776,7 +777,8 @@ void RPCClient::getInfo(UnifiedCallback cb)
|
|||||||
|
|
||||||
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
|
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
|
||||||
{
|
{
|
||||||
doRPC("rescanblockchain", {startHeight},
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
||||||
|
doRPC("rescan", {startHeight},
|
||||||
[cb](const json& result) {
|
[cb](const json& result) {
|
||||||
if (cb) cb(result, "");
|
if (cb) cb(result, "");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "../colors.h"
|
#include "../colors.h"
|
||||||
#include "../typography.h"
|
#include "../typography.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
|
#include "../tooltip_style.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "imgui_internal.h"
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
@@ -271,7 +272,7 @@ inline bool IconButton(const char* icon, const char* tooltip, bool enabled) {
|
|||||||
|
|
||||||
// Tooltip
|
// Tooltip
|
||||||
if (tooltip && hovered) {
|
if (tooltip && hovered) {
|
||||||
ImGui::SetTooltip("%s", tooltip);
|
material::Tooltip("%s", tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pressed && enabled;
|
return pressed && enabled;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "colors.h"
|
#include "colors.h"
|
||||||
#include "type.h"
|
#include "type.h"
|
||||||
|
#include "tooltip_style.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../schema/element_styles.h"
|
#include "../schema/element_styles.h"
|
||||||
#include "../schema/color_var_resolver.h"
|
#include "../schema/color_var_resolver.h"
|
||||||
|
|||||||
66
src/ui/material/tooltip_style.h
Normal file
66
src/ui/material/tooltip_style.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// App-wide tooltip styling: a compact (small padding) and slightly-translucent tooltip. ImGui has
|
||||||
|
// no tooltip-specific padding (tooltips inherit WindowPadding, shared with every window) and PopupBg
|
||||||
|
// is shared with menus/combos, so we can't restyle tooltips via the global style without side
|
||||||
|
// effects. Instead, every tooltip goes through these wrappers, which scope the style to the tooltip:
|
||||||
|
// dragonx::ui::material::Tooltip("fmt", ...); // replaces ImGui::SetTooltip
|
||||||
|
// if (material::BeginTooltip()) { ...; material::EndTooltip(); } // replaces ImGui::Begin/EndTooltip
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
namespace material {
|
||||||
|
|
||||||
|
// Scope tooltip-only styling: small padding + a slightly-transparent background.
|
||||||
|
inline void PushTooltipStyle()
|
||||||
|
{
|
||||||
|
const float s = Layout::dpiScale();
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f * s, 4.0f * s));
|
||||||
|
ImVec4 bg = ImGui::GetStyle().Colors[ImGuiCol_PopupBg];
|
||||||
|
bg.w = 0.85f; // slightly transparent (the global PopupBg is ~0.98 opaque)
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void PopTooltipStyle()
|
||||||
|
{
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop-in replacement for ImGui::SetTooltip (same printf-style signature). (No IM_FMTARGS: GCC
|
||||||
|
// rejects the format attribute on an inline definition; call sites already passed SetTooltip's check.)
|
||||||
|
inline void Tooltip(const char* fmt, ...)
|
||||||
|
{
|
||||||
|
PushTooltipStyle();
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
ImGui::SetTooltipV(fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
PopTooltipStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop-in replacements for ImGui::BeginTooltip / ImGui::EndTooltip (must be paired, as before).
|
||||||
|
inline bool BeginTooltip()
|
||||||
|
{
|
||||||
|
PushTooltipStyle();
|
||||||
|
return ImGui::BeginTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void EndTooltip()
|
||||||
|
{
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
PopTooltipStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace material
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
#include "../windows/console_tab.h"
|
#include "../windows/console_tab.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
|
#include "../../resources/embedded_resources.h"
|
||||||
|
#include <ctime>
|
||||||
#include "../../rpc/rpc_client.h"
|
#include "../../rpc/rpc_client.h"
|
||||||
#include "../../rpc/connection.h"
|
#include "../../rpc/connection.h"
|
||||||
#include "../../rpc/rpc_worker.h"
|
#include "../../rpc/rpc_worker.h"
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
#include "../windows/export_all_keys_dialog.h"
|
#include "../windows/export_all_keys_dialog.h"
|
||||||
#include "../windows/export_transactions_dialog.h"
|
#include "../windows/export_transactions_dialog.h"
|
||||||
#include "../windows/bootstrap_download_dialog.h"
|
#include "../windows/bootstrap_download_dialog.h"
|
||||||
|
#include "../windows/daemon_download_dialog.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
@@ -146,6 +149,19 @@ struct SettingsPageState {
|
|||||||
bool confirm_clear_ztx = false;
|
bool confirm_clear_ztx = false;
|
||||||
bool confirm_delete_blockchain = false;
|
bool confirm_delete_blockchain = false;
|
||||||
bool confirm_rescan = false;
|
bool confirm_rescan = false;
|
||||||
|
// Rescan dialog: probe the node's available block range so a bootstrapped/pruned node gets a
|
||||||
|
// runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis.
|
||||||
|
bool rescan_height_detecting = false;
|
||||||
|
bool rescan_height_detected = false;
|
||||||
|
bool rescan_full_history = true; // genesis present → traditional -rescan restart
|
||||||
|
int rescan_start_height = 0; // editable pre-fill for the runtime rescan
|
||||||
|
bool confirm_repair_wallet = false;
|
||||||
|
bool confirm_reinstall_daemon = false;
|
||||||
|
// Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh,
|
||||||
|
// since reading the installed binary to scan its version is a one-off disk read).
|
||||||
|
bool daemon_info_loaded = false;
|
||||||
|
dragonx::resources::DaemonBinaryInfo installed_daemon;
|
||||||
|
dragonx::resources::BundledDaemonInfo bundled_daemon;
|
||||||
bool confirm_restart_daemon = false;
|
bool confirm_restart_daemon = false;
|
||||||
bool confirm_lite_redownload = false;
|
bool confirm_lite_redownload = false;
|
||||||
effects::ScrollFadeShader fade_shader;
|
effects::ScrollFadeShader fade_shader;
|
||||||
@@ -627,7 +643,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
ImGui::SetTooltip("%s", skin.validationError.c_str());
|
material::Tooltip("%s", skin.validationError.c_str());
|
||||||
} else {
|
} else {
|
||||||
std::string lbl = skin.name;
|
std::string lbl = skin.name;
|
||||||
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
|
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
|
||||||
@@ -669,7 +685,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
|
material::Tooltip("%s", TR("tt_theme_hotkey"));
|
||||||
|
|
||||||
ImGui::SameLine(0, comboGap);
|
ImGui::SameLine(0, comboGap);
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
@@ -692,7 +708,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
|
material::Tooltip("%s", TR("tt_layout_hotkey"));
|
||||||
|
|
||||||
ImGui::SameLine(0, comboGap);
|
ImGui::SameLine(0, comboGap);
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
@@ -709,7 +725,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_language"));
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||||||
@@ -717,7 +733,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Notifications::instance().info("Theme list refreshed");
|
Notifications::instance().info("Theme list refreshed");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip(TR("tt_scan_themes"),
|
material::Tooltip(TR("tt_scan_themes"),
|
||||||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||||||
}
|
}
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
@@ -747,7 +763,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Layout::setUserFontScale(s_settingsState.font_scale);
|
Layout::setUserFontScale(s_settingsState.font_scale);
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,20 +831,20 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec"));
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_settingsState.gradient_background)) {
|
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_settingsState.gradient_background)) {
|
||||||
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
|
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg"));
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
|
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion"));
|
||||||
|
|
||||||
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
||||||
|
|
||||||
@@ -838,14 +854,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
|
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
|
||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline"));
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &s_settingsState.theme_effects_enabled)) {
|
if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &s_settingsState.theme_effects_enabled)) {
|
||||||
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
|
||||||
|
|
||||||
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
||||||
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
||||||
@@ -871,7 +887,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur"));
|
||||||
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
||||||
|
|
||||||
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
|
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
|
||||||
@@ -891,7 +907,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise"));
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
|
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
|
||||||
|
|
||||||
@@ -907,7 +923,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity"));
|
||||||
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
|
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
|
||||||
@@ -922,7 +938,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity"));
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
|
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
|
||||||
|
|
||||||
@@ -949,11 +965,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
|
material::Tooltip("%s", TR("tt_theme_hotkey"));
|
||||||
if (active_is_custom) {
|
if (active_is_custom) {
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_theme"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_theme"));
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||||||
@@ -961,7 +977,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Notifications::instance().info("Theme list refreshed");
|
Notifications::instance().info("Theme list refreshed");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip(TR("tt_scan_themes"),
|
material::Tooltip(TR("tt_scan_themes"),
|
||||||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||||||
}
|
}
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
@@ -992,7 +1008,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
|
material::Tooltip("%s", TR("tt_layout_hotkey"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,7 +1031,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_language"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1044,7 +1060,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Layout::setUserFontScale(s_settingsState.font_scale);
|
Layout::setUserFontScale(s_settingsState.font_scale);
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1112,18 +1128,18 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec"));
|
||||||
|
|
||||||
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &s_settingsState.gradient_background)) {
|
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &s_settingsState.gradient_background)) {
|
||||||
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
|
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg_alt"));
|
||||||
|
|
||||||
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
|
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion"));
|
||||||
|
|
||||||
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
||||||
|
|
||||||
@@ -1132,14 +1148,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
|
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
|
||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline"));
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &s_settingsState.theme_effects_enabled)) {
|
if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &s_settingsState.theme_effects_enabled)) {
|
||||||
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
|
||||||
|
|
||||||
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||||||
availWidth - pad * 2.0f);
|
availWidth - pad * 2.0f);
|
||||||
@@ -1159,7 +1175,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur"));
|
||||||
|
|
||||||
ImGui::TextUnformatted(TR("noise"));
|
ImGui::TextUnformatted(TR("noise"));
|
||||||
ImGui::SetNextItemWidth(ctrlW);
|
ImGui::SetNextItemWidth(ctrlW);
|
||||||
@@ -1175,7 +1191,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise"));
|
||||||
|
|
||||||
ImGui::TextUnformatted(TR("ui_opacity"));
|
ImGui::TextUnformatted(TR("ui_opacity"));
|
||||||
ImGui::SetNextItemWidth(ctrlW);
|
ImGui::SetNextItemWidth(ctrlW);
|
||||||
@@ -1188,7 +1204,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity"));
|
||||||
|
|
||||||
ImGui::TextUnformatted(TR("window_opacity"));
|
ImGui::TextUnformatted(TR("window_opacity"));
|
||||||
ImGui::SetNextItemWidth(ctrlW);
|
ImGui::SetNextItemWidth(ctrlW);
|
||||||
@@ -1200,7 +1216,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity"));
|
||||||
|
|
||||||
ImGui::EndDisabled(); // low-spec
|
ImGui::EndDisabled(); // low-spec
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
@@ -1259,26 +1275,26 @@ void RenderSettingsPage(App* app) {
|
|||||||
float sp = cbSpacing * scale;
|
float sp = cbSpacing * scale;
|
||||||
|
|
||||||
ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &s_settingsState.save_ztxs);
|
ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &s_settingsState.save_ztxs);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_ztx"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx"));
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &s_settingsState.auto_shield);
|
ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &s_settingsState.auto_shield);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_shield"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield"));
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
|
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
||||||
if (showDaemonOptions) {
|
if (showDaemonOptions) {
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
|
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_keep_daemon"));
|
material::Tooltip("%s", TR("tt_keep_daemon"));
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
|
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_stop_external"));
|
material::Tooltip("%s", TR("tt_stop_external"));
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
|
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
|
||||||
@@ -1286,7 +1302,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_verbose"));
|
material::Tooltip("%s", TR("tt_verbose"));
|
||||||
|
|
||||||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||||||
}
|
}
|
||||||
@@ -1325,28 +1341,28 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
AddressBookDialog::show();
|
AddressBookDialog::show();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book"));
|
||||||
ImGui::SameLine(0, btnSpacing);
|
ImGui::SameLine(0, btnSpacing);
|
||||||
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
ValidateAddressDialog::show();
|
ValidateAddressDialog::show();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_validate"));
|
||||||
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
||||||
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
RequestPaymentDialog::show();
|
RequestPaymentDialog::show();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_request_payment"));
|
||||||
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
|
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
|
||||||
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining"));
|
||||||
ImGui::SameLine(0, btnSpacing);
|
ImGui::SameLine(0, btnSpacing);
|
||||||
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
|
||||||
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
||||||
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
|
||||||
s_settingsState.confirm_clear_ztx = true;
|
s_settingsState.confirm_clear_ztx = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clear_ztx"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, bottomPad));
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
||||||
@@ -1410,23 +1426,23 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
|
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
|
||||||
app->showImportKeyDialog();
|
app->showImportKeyDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[0]);
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]);
|
||||||
ImGui::SameLine(0, scaledSp);
|
ImGui::SameLine(0, scaledSp);
|
||||||
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
|
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
|
||||||
app->showExportKeyDialog();
|
app->showExportKeyDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[1]);
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[1]);
|
||||||
ImGui::SameLine(0, scaledSp);
|
ImGui::SameLine(0, scaledSp);
|
||||||
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
|
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
|
||||||
ExportAllKeysDialog::show();
|
ExportAllKeysDialog::show();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[2]);
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[2]);
|
||||||
ImGui::SameLine(0, scaledSp);
|
ImGui::SameLine(0, scaledSp);
|
||||||
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
|
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
|
||||||
app->showBackupDialog();
|
app->showBackupDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[3]);
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[3]);
|
||||||
ImGui::SameLine(0, scaledSp);
|
ImGui::SameLine(0, scaledSp);
|
||||||
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
|
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
|
||||||
ExportTransactionsDialog::show();
|
ExportTransactionsDialog::show();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[4]);
|
||||||
|
|
||||||
if (showFullNodeLifecycleActions) {
|
if (showFullNodeLifecycleActions) {
|
||||||
// Right-align Setup Wizard + Download Bootstrap
|
// Right-align Setup Wizard + Download Bootstrap
|
||||||
@@ -1445,11 +1461,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
|
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
|
||||||
BootstrapDownloadDialog::show(app);
|
BootstrapDownloadDialog::show(app);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_download_bootstrap"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_download_bootstrap"));
|
||||||
ImGui::SameLine(0, scaledSp);
|
ImGui::SameLine(0, scaledSp);
|
||||||
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
||||||
app->restartWizard();
|
app->restartWizard();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wizard"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||||||
@@ -1660,7 +1676,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openFolder(util::Platform::getLiteWalletDataDir());
|
util::Platform::openFolder(util::Platform::getLiteWalletDataDir());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_open_data_dir"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
|
||||||
|
|
||||||
// ---- Backup & keys (open wallet only) ----------------------------------
|
// ---- Backup & keys (open wallet only) ----------------------------------
|
||||||
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
||||||
@@ -1844,7 +1860,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
ImGui::SetTooltip("%s", TR("tt_lite_redownload"));
|
material::Tooltip("%s", TR("tt_lite_redownload"));
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc"));
|
scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc"));
|
||||||
@@ -1911,7 +1927,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
ImGui::SetTooltip("%s", TR("tt_open_dir"));
|
material::Tooltip("%s", TR("tt_open_dir"));
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked())
|
if (ImGui::IsItemClicked())
|
||||||
@@ -1932,7 +1948,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_open_data_dir"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
@@ -1950,7 +1966,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
||||||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||||||
ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host));
|
ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host));
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_host"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
|
||||||
|
|
||||||
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
||||||
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
|
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
|
||||||
@@ -1959,7 +1975,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
|
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
|
||||||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||||||
ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user));
|
ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user));
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_user"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
|
||||||
|
|
||||||
@@ -1971,7 +1987,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
||||||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||||||
ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port));
|
ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port));
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_port"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
|
||||||
|
|
||||||
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
||||||
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
|
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
|
||||||
@@ -1981,7 +1997,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||||||
ImGui::InputText("##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password),
|
ImGui::InputText("##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password),
|
||||||
ImGuiInputTextFlags_Password);
|
ImGuiInputTextFlags_Password);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_pass"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
|
||||||
|
|
||||||
@@ -1996,56 +2012,6 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
// Node maintenance buttons (full-node build only)
|
|
||||||
if (app->supportsFullNodeLifecycleActions()) {
|
|
||||||
ImFont* btnFont = S.resolveFont("button");
|
|
||||||
float nodeBtnW;
|
|
||||||
{
|
|
||||||
if (btnFont) ImGui::PushFont(btnFont);
|
|
||||||
nodeBtnW = rowBtnW({TR("test_connection"), TR("rescan")});
|
|
||||||
if (btnFont) ImGui::PopFont(/* btnFont */);
|
|
||||||
}
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
||||||
ImGui::BeginDisabled(!app->isConnected());
|
|
||||||
if (TactileButton(TR("test_connection"), ImVec2(nodeBtnW, 0), btnFont)) {
|
|
||||||
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
|
|
||||||
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
|
||||||
try {
|
|
||||||
rpc::RPCClient::TraceScope trace("Settings / Test connection");
|
|
||||||
rpc->call("getinfo");
|
|
||||||
return []() {
|
|
||||||
Notifications::instance().success("RPC connection OK");
|
|
||||||
};
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::string err = e.what();
|
|
||||||
return [err]() {
|
|
||||||
Notifications::instance().error("RPC error: " + err);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Notifications::instance().warning("Not connected to daemon");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_test_conn"));
|
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
|
||||||
if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) {
|
|
||||||
s_settingsState.confirm_rescan = true;
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
// Delete blockchain button (always available when using embedded daemon)
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
|
|
||||||
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
|
|
||||||
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
|
|
||||||
s_settingsState.confirm_delete_blockchain = true;
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
@@ -2073,13 +2039,13 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (!isEncrypted) {
|
if (!isEncrypted) {
|
||||||
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||||
app->showEncryptDialog();
|
app->showEncryptDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_encrypt"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_encrypt"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted"));
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted"));
|
||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||||
app->showChangePassphraseDialog();
|
app->showChangePassphraseDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pass"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pass"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
ImGui::PushFont(Type().iconSmall());
|
ImGui::PushFont(Type().iconSmall());
|
||||||
@@ -2090,7 +2056,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||||
app->lockWallet();
|
app->lockWallet();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_lock"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock"));
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
ImGui::PushFont(Type().iconSmall());
|
ImGui::PushFont(Type().iconSmall());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
|
||||||
@@ -2102,7 +2068,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
|
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
|
||||||
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||||
app->showDecryptDialog();
|
app->showDecryptDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_encrypt"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
@@ -2126,7 +2092,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
|
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
|
||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_lock"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock"));
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2142,17 +2108,17 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (!hasPIN) {
|
if (!hasPIN) {
|
||||||
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||||||
app->showPinSetupDialog();
|
app->showPinSetupDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_set_pin"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_set_pin"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin"));
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin"));
|
||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||||||
app->showPinChangeDialog();
|
app->showPinChangeDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pin"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pin"));
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||||||
app->showPinRemoveDialog();
|
app->showPinRemoveDialog();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_pin"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_pin"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
ImGui::PushFont(Type().iconSmall());
|
ImGui::PushFont(Type().iconSmall());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
|
||||||
@@ -2172,6 +2138,146 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Advance cursor past both columns
|
// Advance cursor past both columns
|
||||||
float maxBottom = std::max(leftBottom, rightBottom);
|
float maxBottom = std::max(leftBottom, rightBottom);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom));
|
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom));
|
||||||
|
|
||||||
|
// ---- DAEMON BINARY — full-width row beneath the NODE + SECURITY columns ----
|
||||||
|
if (app->supportsFullNodeLifecycleActions()) {
|
||||||
|
ImFont* dbBtnFont = S.resolveFont("button");
|
||||||
|
if (!s_settingsState.daemon_info_loaded) {
|
||||||
|
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo();
|
||||||
|
s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo();
|
||||||
|
s_settingsState.daemon_info_loaded = true;
|
||||||
|
}
|
||||||
|
const auto& inst = s_settingsState.installed_daemon;
|
||||||
|
const auto& bun = s_settingsState.bundled_daemon;
|
||||||
|
|
||||||
|
auto fmtDate = [](std::int64_t epoch) -> std::string {
|
||||||
|
if (epoch <= 0) return "—";
|
||||||
|
std::time_t t = static_cast<std::time_t>(epoch);
|
||||||
|
std::tm tmv{};
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(&tmv, &t);
|
||||||
|
#else
|
||||||
|
localtime_r(&t, &tmv);
|
||||||
|
#endif
|
||||||
|
char buf[32];
|
||||||
|
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv);
|
||||||
|
return std::string(buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
const float dbLeftX = sectionOrigin.x;
|
||||||
|
const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
|
// Installed | Bundled side by side across the full container width.
|
||||||
|
const float dbStartY = ImGui::GetCursorScreenPos().y;
|
||||||
|
const float dbLineH = ImGui::GetTextLineHeightWithSpacing();
|
||||||
|
const float dbCol2X = dbLeftX + std::min(contentW * 0.4f, 340.0f);
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY));
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_installed"));
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH));
|
||||||
|
if (inst.exists) {
|
||||||
|
ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str());
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH * 2));
|
||||||
|
ImGui::TextColored(dbDim, "%s · %s",
|
||||||
|
util::Platform::formatFileSize(inst.size).c_str(),
|
||||||
|
fmtDate(inst.modifiedEpoch).c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY));
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_bundled"));
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY + dbLineH));
|
||||||
|
if (bun.available) {
|
||||||
|
ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str());
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY + dbLineH * 2));
|
||||||
|
ImGui::TextColored(dbDim, "%s", util::Platform::formatFileSize(bun.size).c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH * 3 + Layout::spacingXs()));
|
||||||
|
if (bun.available) {
|
||||||
|
const bool sameSize = inst.exists && inst.size == bun.size;
|
||||||
|
if (!inst.exists)
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing"));
|
||||||
|
else if (sameSize)
|
||||||
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match"));
|
||||||
|
else
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-app node update: download + verify the latest dragonxd from the project Gitea.
|
||||||
|
// Refresh the cached daemon info once an install has completed.
|
||||||
|
if (ui::DaemonUpdateDialog::consumeInstalled())
|
||||||
|
s_settingsState.daemon_info_loaded = false;
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string());
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
// All node actions on one full-width toolbar row: daemon-binary actions first
|
||||||
|
// (Install bundled | Refresh), then maintenance (Test connection | Rescan |
|
||||||
|
// Delete blockchain | Repair wallet).
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
|
||||||
|
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
s_settingsState.confirm_reinstall_daemon = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
s_settingsState.daemon_info_loaded = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
ImGui::BeginDisabled(!app->isConnected());
|
||||||
|
if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
|
||||||
|
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Settings / Test connection");
|
||||||
|
rpc->call("getinfo");
|
||||||
|
return []() { Notifications::instance().success("RPC connection OK"); };
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::string err = e.what();
|
||||||
|
return [err]() { Notifications::instance().error("RPC error: " + err); };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Notifications::instance().warning("Not connected to daemon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn"));
|
||||||
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
s_settingsState.confirm_rescan = true;
|
||||||
|
// Re-probe the available block range each time the dialog is opened.
|
||||||
|
s_settingsState.rescan_height_detecting = false;
|
||||||
|
s_settingsState.rescan_height_detected = false;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
|
||||||
|
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
s_settingsState.confirm_delete_blockchain = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
|
||||||
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) {
|
||||||
|
s_settingsState.confirm_repair_wallet = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, bottomPad));
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
||||||
@@ -2216,29 +2322,29 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::SetNextItemWidth(inputTxW);
|
ImGui::SetNextItemWidth(inputTxW);
|
||||||
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
|
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tx_url"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url"));
|
||||||
ImGui::SameLine(pad + halfW + Layout::spacingLg());
|
ImGui::SameLine(pad + halfW + Layout::spacingLg());
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted(TR("address_url"));
|
ImGui::TextUnformatted(TR("address_url"));
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::SetNextItemWidth(inputAddrW);
|
ImGui::SetNextItemWidth(inputAddrW);
|
||||||
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
|
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_addr_url"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url"));
|
||||||
|
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
// Row 2: Checkboxes + Block Explorer button (on one line)
|
// Row 2: Checkboxes + Block Explorer button (on one line)
|
||||||
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_fees"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_fetch_prices"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_block_explorer"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer"));
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, bottomPad));
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
||||||
ImGui::Unindent(pad);
|
ImGui::Unindent(pad);
|
||||||
@@ -2339,18 +2445,18 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openUrl("https://dragonx.is");
|
util::Platform::openUrl("https://dragonx.is");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_website"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_website"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_report_bug"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
Notifications::instance().success("Settings saved");
|
Notifications::instance().success("Settings saved");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_settings"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||||
if (app->settings()) {
|
if (app->settings()) {
|
||||||
@@ -2358,7 +2464,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Notifications::instance().info("Settings reloaded from disk");
|
Notifications::instance().info("Settings reloaded from disk");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reset_settings"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, bottomPad));
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
||||||
@@ -2388,7 +2494,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) {
|
if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) {
|
||||||
s_settingsState.debug_expanded = !s_settingsState.debug_expanded;
|
s_settingsState.debug_expanded = !s_settingsState.debug_expanded;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
// Draw overline label + arrow on top of the invisible button
|
// Draw overline label + arrow on top of the invisible button
|
||||||
@@ -2468,7 +2574,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
s_settingsState.debug_cats_dirty = true;
|
s_settingsState.debug_cats_dirty = true;
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", debugTips[i]);
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", debugTips[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
@@ -2491,7 +2597,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
s_settingsState.confirm_restart_daemon = true;
|
s_settingsState.confirm_restart_daemon = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, bottomPad));
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
||||||
@@ -2617,8 +2723,22 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm: rescan blockchain (restarts the daemon, re-scans the whole chain — long but safe)
|
// Confirm: rescan blockchain. On a normal (full-history) node this restarts the daemon with
|
||||||
|
// -rescan; on a bootstrapped/pruned node that would fail (pre-snapshot blocks are absent), so we
|
||||||
|
// probe the lowest available block height and run a runtime rescanblockchain from a confirmed,
|
||||||
|
// editable height instead.
|
||||||
if (s_settingsState.confirm_rescan) {
|
if (s_settingsState.confirm_rescan) {
|
||||||
|
// Kick off the one-shot block-range probe the first frame the dialog is open.
|
||||||
|
if (!s_settingsState.rescan_height_detecting && !s_settingsState.rescan_height_detected) {
|
||||||
|
s_settingsState.rescan_height_detecting = true;
|
||||||
|
app->detectLowestAvailableBlockHeight([](bool ok, int lowest, bool fullHistory) {
|
||||||
|
s_settingsState.rescan_height_detecting = false;
|
||||||
|
s_settingsState.rescan_height_detected = true;
|
||||||
|
s_settingsState.rescan_full_history = (!ok) || fullHistory;
|
||||||
|
s_settingsState.rescan_start_height = (ok && !fullHistory) ? lowest : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
ImGui::PushFont(Type().iconLarge());
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
||||||
@@ -2627,9 +2747,25 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
|
||||||
ImGui::Spacing();
|
const bool detecting = s_settingsState.rescan_height_detecting;
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
const bool bootstrapped = s_settingsState.rescan_height_detected && !s_settingsState.rescan_full_history;
|
||||||
|
|
||||||
|
if (detecting) {
|
||||||
|
ImGui::TextWrapped("%s", TR("rescan_detecting"));
|
||||||
|
} else if (bootstrapped) {
|
||||||
|
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Text("%s", TR("rescan_from_height"));
|
||||||
|
ImGui::SetNextItemWidth(160.0f);
|
||||||
|
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
|
||||||
|
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
|
||||||
|
} else {
|
||||||
|
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -2639,10 +2775,77 @@ void RenderSettingsPage(App* app) {
|
|||||||
s_settingsState.confirm_rescan = false;
|
s_settingsState.confirm_rescan = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginDisabled(detecting);
|
||||||
if (ImGui::Button(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
if (ImGui::Button(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||||
app->rescanBlockchain();
|
if (bootstrapped) {
|
||||||
|
app->runtimeRescan(s_settingsState.rescan_start_height);
|
||||||
|
} else {
|
||||||
|
app->rescanBlockchain();
|
||||||
|
}
|
||||||
s_settingsState.confirm_rescan = false;
|
s_settingsState.confirm_rescan = false;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
|
||||||
|
if (s_settingsState.confirm_repair_wallet) {
|
||||||
|
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
|
||||||
|
ImGui::PushFont(Type().iconLarge());
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_repair_wallet_safe"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
if (ImGui::Button(TrId("cancel", "repair_wallet_cancel").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
s_settingsState.confirm_repair_wallet = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TrId("repair_wallet", "repair_wallet_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
app->repairWallet();
|
||||||
|
s_settingsState.confirm_repair_wallet = false;
|
||||||
|
}
|
||||||
|
EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
|
||||||
|
if (s_settingsState.confirm_reinstall_daemon) {
|
||||||
|
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
|
||||||
|
ImGui::PushFont(Type().iconLarge());
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_reinstall_daemon_safe"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
s_settingsState.confirm_reinstall_daemon = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
app->reinstallBundledDaemon();
|
||||||
|
s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap
|
||||||
|
s_settingsState.confirm_reinstall_daemon = false;
|
||||||
|
}
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
} else {
|
} else {
|
||||||
float iconCX = (indMin.x + indMax.x) * 0.5f;
|
float iconCX = (indMin.x + indMax.x) * 0.5f;
|
||||||
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
|
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
|
||||||
if (hovered) ImGui::SetTooltip("%s", NavLabel(item));
|
if (hovered) material::Tooltip("%s", NavLabel(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Badge indicator
|
// Badge indicator
|
||||||
@@ -826,7 +826,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon);
|
ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon);
|
||||||
dl->AddText(iconFont, eIconFsz,
|
dl->AddText(iconFont, eIconFsz,
|
||||||
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), exitCol, exitIcon);
|
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), exitCol, exitIcon);
|
||||||
if (exitHover) ImGui::SetTooltip("%s", TR("exit"));
|
if (exitHover) material::Tooltip("%s", TR("exit"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
113
src/ui/widgets/copy_field.h
Normal file
113
src/ui/widgets/copy_field.h
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Shared "click-to-copy field" widgets used by the key-export and QR-popup dialogs, so an address
|
||||||
|
// renders identically in both. A field is a bordered box of wrapping text whose whole area copies
|
||||||
|
// to the clipboard on click; long secrets/addresses are grouped into space-separated blocks.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/tooltip_style.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
namespace widgets {
|
||||||
|
|
||||||
|
// Draw `text` wrapped within wrapW with every visual line horizontally centered (returns the height
|
||||||
|
// used). ImGui has no center+wrap, so we walk the wrap points the way it would.
|
||||||
|
inline float DrawCenteredWrapped(ImDrawList* dl, ImFont* font, float fs, ImVec2 origin,
|
||||||
|
float wrapW, ImU32 col, const char* text)
|
||||||
|
{
|
||||||
|
const float scale = fs / font->LegacySize;
|
||||||
|
const float lineH = ImGui::GetTextLineHeight();
|
||||||
|
const char* s = text;
|
||||||
|
const char* e = text + std::strlen(text);
|
||||||
|
float y = origin.y;
|
||||||
|
while (s < e) {
|
||||||
|
const char* wrap = font->CalcWordWrapPositionA(scale, s, e, wrapW);
|
||||||
|
if (wrap <= s) wrap = s + 1;
|
||||||
|
const ImVec2 lw = font->CalcTextSizeA(fs, 1.0e30f, 0.0f, s, wrap);
|
||||||
|
float lx = origin.x + (wrapW - lw.x) * 0.5f;
|
||||||
|
if (lx < origin.x) lx = origin.x;
|
||||||
|
dl->AddText(font, fs, ImVec2(lx, y), col, s, wrap);
|
||||||
|
y += lineH;
|
||||||
|
s = wrap;
|
||||||
|
while (s < e && (*s == ' ' || *s == '\n')) s++; // ImGui consumes the break char
|
||||||
|
}
|
||||||
|
return y - origin.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group a long string into space-separated blocks so it reads in scannable chunks and wraps on
|
||||||
|
// block boundaries. Callers copy the ORIGINAL (un-grouped) string.
|
||||||
|
inline std::string ChunkString(const std::string& s, size_t group)
|
||||||
|
{
|
||||||
|
if (group == 0 || s.size() <= group) return s;
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size() + s.size() / group + 1);
|
||||||
|
for (size_t i = 0; i < s.size(); ++i) {
|
||||||
|
if (i && (i % group) == 0) out.push_back(' ');
|
||||||
|
out.push_back(s[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrapping, click-to-copy "field": draws `display` in a bordered box (the whole box is the copy
|
||||||
|
// affordance) and returns true when clicked. `center` centers each wrapped line. The caller is
|
||||||
|
// responsible for the actual clipboard write (so secrets can use an auto-clearing clipboard).
|
||||||
|
inline bool CopyField(const char* id, const std::string& display, float width, float minH, bool center)
|
||||||
|
{
|
||||||
|
namespace m = material;
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImGuiStyle& st = ImGui::GetStyle();
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
const float fs = ImGui::GetFontSize();
|
||||||
|
const float padX = st.FramePadding.x + 4.0f;
|
||||||
|
const float padY = st.FramePadding.y + 4.0f;
|
||||||
|
const float wrapW = (width - padX * 2.0f) > 1.0f ? (width - padX * 2.0f) : 1.0f;
|
||||||
|
const ImVec2 ts = ImGui::CalcTextSize(display.c_str(), nullptr, false, wrapW);
|
||||||
|
float h = ts.y + padY * 2.0f;
|
||||||
|
if (h < minH) h = minH;
|
||||||
|
const ImVec2 p0 = ImGui::GetCursorScreenPos();
|
||||||
|
const ImVec2 p1 = ImVec2(p0.x + width, p0.y + h);
|
||||||
|
ImGui::InvisibleButton(id, ImVec2(width, h));
|
||||||
|
const bool hovered = ImGui::IsItemHovered();
|
||||||
|
const bool clicked = ImGui::IsItemClicked();
|
||||||
|
dl->AddRectFilled(p0, p1, m::WithAlpha(m::OnSurface(), hovered ? 18 : 8), 6.0f);
|
||||||
|
dl->AddRect(p0, p1, m::WithAlpha(m::OnSurface(), hovered ? 70 : 30), 6.0f, 0, 1.0f);
|
||||||
|
const float textY = p0.y + (h - ts.y) * 0.5f; // vertical center
|
||||||
|
if (center)
|
||||||
|
DrawCenteredWrapped(dl, font, fs, ImVec2(p0.x + padX, textY), wrapW, m::OnSurface(), display.c_str());
|
||||||
|
else
|
||||||
|
dl->AddText(font, fs, ImVec2(p0.x + padX, textY), m::OnSurface(), display.c_str(), nullptr, wrapW);
|
||||||
|
if (hovered) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
material::Tooltip("%s", TR("copy"));
|
||||||
|
}
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An address field: chunked by 4 for readability, width clamped to its text, box centered in the
|
||||||
|
// available region. Copies the raw address to the clipboard on click; returns true when clicked.
|
||||||
|
inline bool AddressCopyField(const char* id, const std::string& address)
|
||||||
|
{
|
||||||
|
const std::string at = ChunkString(address, 4);
|
||||||
|
const float avail = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float padX = ImGui::GetStyle().FramePadding.x + 4.0f;
|
||||||
|
float boxW = ImGui::CalcTextSize(at.c_str()).x + padX * 2.0f + 2.0f; // clamp to text width
|
||||||
|
if (boxW > avail) boxW = avail;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail > boxW ? (avail - boxW) * 0.5f : 0.0f));
|
||||||
|
const bool clicked = CopyField(id, at, boxW, 0.0f, /*center=*/true);
|
||||||
|
if (clicked) ImGui::SetClipboardText(address.c_str());
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace widgets
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -258,7 +258,7 @@ void AddressBookDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
ImGui::TextDisabled("%s", addr_display.c_str());
|
ImGui::TextDisabled("%s", addr_display.c_str());
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", entry.address.c_str());
|
material::Tooltip("%s", entry.address.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ public:
|
|||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
||||||
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
||||||
if (hov) ImGui::SetTooltip("%s", iconName);
|
if (hov) material::Tooltip("%s", iconName);
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
|
|
||||||
col = (col + 1) % cols;
|
col = (col + 1) % cols;
|
||||||
|
|||||||
@@ -385,13 +385,13 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
|||||||
// edge of a row = reorder (move here).
|
// edge of a row = reorder (move here).
|
||||||
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size() && s_dropMode == 0) {
|
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size() && s_dropMode == 0) {
|
||||||
const auto& target = rows[s_dropTargetIdx];
|
const auto& target = rows[s_dropTargetIdx];
|
||||||
ImGui::SetTooltip("%s\n%s\n\n%s %s",
|
material::Tooltip("%s\n%s\n\n%s %s",
|
||||||
truncateAddress(addr.address, 32).c_str(),
|
truncateAddress(addr.address, 32).c_str(),
|
||||||
row.isZ ? TR("shielded") : TR("transparent"),
|
row.isZ ? TR("shielded") : TR("transparent"),
|
||||||
TR("transfer_to"),
|
TR("transfer_to"),
|
||||||
truncateAddress(target.info->address, 32).c_str());
|
truncateAddress(target.info->address, 32).c_str());
|
||||||
} else {
|
} else {
|
||||||
ImGui::SetTooltip("%s\n%s\n\n%s",
|
material::Tooltip("%s\n%s\n\n%s",
|
||||||
truncateAddress(addr.address, 32).c_str(),
|
truncateAddress(addr.address, 32).c_str(),
|
||||||
row.isZ ? TR("shielded") : TR("transparent"),
|
row.isZ ? TR("shielded") : TR("transparent"),
|
||||||
TR("address_reorder_hint"));
|
TR("address_reorder_hint"));
|
||||||
@@ -474,7 +474,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
|||||||
else app->favoriteAddress(addr.address);
|
else app->favoriteAddress(addr.address);
|
||||||
btnClicked = true;
|
btnClicked = true;
|
||||||
}
|
}
|
||||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
if (bHov) material::Tooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eye button (zero balance or hidden)
|
// Eye button (zero balance or hidden)
|
||||||
@@ -500,7 +500,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
|||||||
else app->hideAddress(addr.address);
|
else app->hideAddress(addr.address);
|
||||||
btnClicked = true;
|
btnClicked = true;
|
||||||
}
|
}
|
||||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
if (bHov) material::Tooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Type icon or custom icon ----
|
// ---- Type icon or custom icon ----
|
||||||
@@ -624,7 +624,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
|
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
|
||||||
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
||||||
if (ImGui::IsItemHovered() && !s_dragActive) {
|
if (ImGui::IsItemHovered() && !s_dragActive) {
|
||||||
ImGui::SetTooltip("%s", addr.address.c_str());
|
material::Tooltip("%s", addr.address.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context menu
|
// Context menu
|
||||||
|
|||||||
@@ -904,7 +904,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
else app->favoriteAddress(addr.address);
|
else app->favoriteAddress(addr.address);
|
||||||
btnClicked = true;
|
btnClicked = true;
|
||||||
}
|
}
|
||||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
if (bHov) material::Tooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eye button (zero balance or hidden)
|
// Eye button (zero balance or hidden)
|
||||||
@@ -926,7 +926,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
else app->hideAddress(addr.address);
|
else app->hideAddress(addr.address);
|
||||||
btnClicked = true;
|
btnClicked = true;
|
||||||
}
|
}
|
||||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
if (bHov) material::Tooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content zone ends before buttons
|
// Content zone ends before buttons
|
||||||
@@ -1023,7 +1023,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
|
|
||||||
// Tooltip with full address
|
// Tooltip with full address
|
||||||
if (ImGui::IsItemHovered() && !btnClicked) {
|
if (ImGui::IsItemHovered() && !btnClicked) {
|
||||||
ImGui::SetTooltip("%s", addr.address.c_str());
|
material::Tooltip("%s", addr.address.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right-click context menu
|
// Right-click context menu
|
||||||
@@ -1180,7 +1180,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Show the full, untruncated address — two z-addresses can truncate to the
|
// Show the full, untruncated address — two z-addresses can truncate to the
|
||||||
// same first/last window, so the truncated text alone can't disambiguate.
|
// same first/last window, so the truncated text alone can't disambiguate.
|
||||||
if (!tx.address.empty())
|
if (!tx.address.empty())
|
||||||
ImGui::SetTooltip("%s", tx.address.c_str());
|
material::Tooltip("%s", tx.address.c_str());
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(NavPage::History);
|
app->setCurrentPage(NavPage::History);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("click_to_copy"));
|
material::Tooltip("%s", TR("click_to_copy"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked()) {
|
if (ImGui::IsItemClicked()) {
|
||||||
ImGui::SetClipboardText(s_block_hash.c_str());
|
ImGui::SetClipboardText(s_block_hash.c_str());
|
||||||
@@ -257,7 +257,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("block_click_prev"));
|
material::Tooltip("%s", TR("block_click_prev"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked() && s_height > 1) {
|
if (ImGui::IsItemClicked() && s_height > 1) {
|
||||||
s_height--;
|
s_height--;
|
||||||
@@ -279,7 +279,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("block_click_next"));
|
material::Tooltip("%s", TR("block_click_next"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked()) {
|
if (ImGui::IsItemClicked()) {
|
||||||
s_height++;
|
s_height++;
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ private:
|
|||||||
startDownload(mirrorUrl);
|
startDownload(mirrorUrl);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("bootstrap_mirror_tooltip"));
|
material::Tooltip("%s", TR("bootstrap_mirror_tooltip"));
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("cancel"), ImVec2(btnSm, 0))) {
|
if (TactileButton(TR("cancel"), ImVec2(btnSm, 0))) {
|
||||||
@@ -226,6 +226,8 @@ private:
|
|||||||
if (s_bootstrap->isDone()) {
|
if (s_bootstrap->isDone()) {
|
||||||
auto finalProg = s_bootstrap->getProgress();
|
auto finalProg = s_bootstrap->getProgress();
|
||||||
if (finalProg.state == util::Bootstrap::State::Completed) {
|
if (finalProg.state == util::Bootstrap::State::Completed) {
|
||||||
|
// Reconcile the preserved wallet.dat against the new chain once the daemon is back up.
|
||||||
|
s_app->markPostBootstrapRescanPending();
|
||||||
s_state = State::Done;
|
s_state = State::Done;
|
||||||
} else {
|
} else {
|
||||||
s_errorMsg = finalProg.error;
|
s_errorMsg = finalProg.error;
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
s_prev_daemon_enabled = s_daemon_messages_enabled;
|
s_prev_daemon_enabled = s_daemon_messages_enabled;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("console_show_daemon_output"));
|
material::Tooltip("%s", TR("console_show_daemon_output"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -511,7 +511,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
s_prev_errors_only = s_errors_only_enabled;
|
s_prev_errors_only = s_errors_only_enabled;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("console_show_errors_only"));
|
material::Tooltip("%s", TR("console_show_errors_only"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -530,7 +530,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
s_prev_rpc_trace_enabled = s_rpc_trace_enabled;
|
s_prev_rpc_trace_enabled = s_rpc_trace_enabled;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("console_show_rpc_trace"));
|
material::Tooltip("%s", TR("console_show_rpc_trace"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -547,7 +547,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
s_prev_app_enabled = s_app_messages_enabled;
|
s_prev_app_enabled = s_app_messages_enabled;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("console_show_app_output"));
|
material::Tooltip("%s", TR("console_show_app_output"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -582,7 +582,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
|
material::Tooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -592,7 +592,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
show_commands_popup_ = true;
|
show_commands_popup_ = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("console_show_rpc_ref"));
|
material::Tooltip("%s", TR("console_show_rpc_ref"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -629,14 +629,14 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
|
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
|
material::Tooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
|
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
|
||||||
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
|
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
|
material::Tooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1404,9 +1404,9 @@ void ConsoleTab::renderCommandsPopup()
|
|||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (cmd.params[0] != '\0')
|
if (cmd.params[0] != '\0')
|
||||||
ImGui::SetTooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
|
material::Tooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
|
||||||
else
|
else
|
||||||
ImGui::SetTooltip(TR("console_click_insert"), cmd.name);
|
material::Tooltip(TR("console_click_insert"), cmd.name);
|
||||||
}
|
}
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
if (showParams) {
|
if (showParams) {
|
||||||
|
|||||||
254
src/ui/windows/daemon_download_dialog.h
Normal file
254
src/ui/windows/daemon_download_dialog.h
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Modal dialog to download / update the dragonxd full node from the project Gitea, driven by
|
||||||
|
// util::DaemonUpdater. Sibling of XmrigDownloadDialog (the miner updater). States follow the
|
||||||
|
// updater: Checking -> UpToDate/UpdateAvailable -> Downloading/Verifying/Extracting -> Done /
|
||||||
|
// Failed. Header-only (static inline state). The new binary is installed into the daemon directory
|
||||||
|
// without touching the running node; on Done the user is offered a daemon restart to apply it.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../resources/embedded_resources.h" // resources::getDaemonDirectory()
|
||||||
|
#include "../../util/daemon_updater.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../theme.h"
|
||||||
|
#include "release_list_view.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
class DaemonUpdateDialog {
|
||||||
|
public:
|
||||||
|
// `installedVersion` is the version scanned from the installed dragonxd (may be empty/unknown),
|
||||||
|
// used to tell "up to date" from "update available".
|
||||||
|
static void show(App* app, const std::string& installedVersion) {
|
||||||
|
if (!app) return;
|
||||||
|
s_app = app;
|
||||||
|
s_open = true;
|
||||||
|
s_notified = false;
|
||||||
|
s_installed_flag = false; // start each session clean (don't leak a prior install's flag)
|
||||||
|
s_installed_version = installedVersion;
|
||||||
|
s_rows.clear();
|
||||||
|
s_releases.clear();
|
||||||
|
s_updater = std::make_unique<util::DaemonUpdater>();
|
||||||
|
s_updater->startCheck(installedVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isOpen() { return s_open; }
|
||||||
|
|
||||||
|
// True (once) after an install succeeded, so the Settings panel can refresh its cached daemon
|
||||||
|
// info. Clears on read.
|
||||||
|
static bool consumeInstalled() {
|
||||||
|
const bool v = s_installed_flag;
|
||||||
|
s_installed_flag = false;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render() {
|
||||||
|
if (!s_open || !s_app || !s_updater) {
|
||||||
|
if (!s_open) s_updater.reset(); // closed: drop the updater (dtor joins the worker)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using namespace material;
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
if (BeginOverlayDialog(TR("daemon_update_title"), &s_open, 480.0f, 0.94f)) {
|
||||||
|
const auto p = s_updater->getProgress();
|
||||||
|
using St = util::DaemonUpdater::State;
|
||||||
|
switch (p.state) {
|
||||||
|
case St::Checking: renderChecking(dp, p); break;
|
||||||
|
case St::UpToDate:
|
||||||
|
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
||||||
|
case St::Unavailable: renderUnavailable(dp, p); break;
|
||||||
|
case St::Listing: renderListing(dp, p); break;
|
||||||
|
case St::ReleaseList: renderReleaseList(dp, p); break;
|
||||||
|
case St::Downloading:
|
||||||
|
case St::Verifying:
|
||||||
|
case St::Extracting: renderProgress(dp, p); break;
|
||||||
|
case St::Done: renderDone(dp, p); break;
|
||||||
|
case St::Failed: renderFailed(dp, p); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
EndOverlayDialog();
|
||||||
|
}
|
||||||
|
if (!s_open) s_updater.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Progress = util::DaemonUpdater::Progress;
|
||||||
|
using St = util::DaemonUpdater::State;
|
||||||
|
|
||||||
|
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
||||||
|
|
||||||
|
static void installAction(const char* label) {
|
||||||
|
using namespace material;
|
||||||
|
if (TactileButton(label, ImVec2(fullW(), 0)))
|
||||||
|
s_updater->startInstall(resources::getDaemonDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderChecking(float, const Progress&) {
|
||||||
|
using namespace material;
|
||||||
|
Type().text(TypeStyle::Body2, TR("daemon_update_checking"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderUnavailable(float, const Progress& p) {
|
||||||
|
using namespace material;
|
||||||
|
Type().text(TypeStyle::Subtitle2, TR("daemon_update_unavailable_title"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", p.status_text.empty()
|
||||||
|
? TR("daemon_update_unavailable_body")
|
||||||
|
: p.status_text.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderPrompt(float, const Progress& p) {
|
||||||
|
using namespace material;
|
||||||
|
const std::string installed = p.installed_tag.empty() ? TR("xmrig_none") : p.installed_tag;
|
||||||
|
if (p.state == St::UpdateAvailable) {
|
||||||
|
Type().text(TypeStyle::Subtitle2, TR("daemon_update_available"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Text("%s %s", TR("daemon_update_latest"), p.latest_tag.c_str());
|
||||||
|
ImGui::Text("%s %s", TR("daemon_update_installed"), installed.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_verify_note"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
installAction(TR("daemon_update_download_install"));
|
||||||
|
} else {
|
||||||
|
Type().text(TypeStyle::Subtitle2, TR("daemon_update_up_to_date"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Text("%s %s", TR("daemon_update_installed"), p.latest_tag.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
installAction(TR("daemon_update_reinstall"));
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
// Browse all releases (pre-releases / older versions).
|
||||||
|
if (TactileButton(TR("daemon_update_browse"), ImVec2(fullW(), 0))) {
|
||||||
|
s_rows.clear();
|
||||||
|
s_updater->startListReleases();
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderListing(float, const Progress&) {
|
||||||
|
using namespace material;
|
||||||
|
Type().text(TypeStyle::Body2, TR("daemon_update_loading"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderReleaseList(float dp, const Progress&) {
|
||||||
|
using namespace material;
|
||||||
|
// Snapshot the worker's list + build the rows once per listing (not every frame — the
|
||||||
|
// release bodies are large). Caches are cleared when a new listing starts (browse / show).
|
||||||
|
if (s_rows.empty()) {
|
||||||
|
s_releases = s_updater->getReleases();
|
||||||
|
const std::string token = util::currentDaemonPlatformToken();
|
||||||
|
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
||||||
|
s_rows.reserve(s_releases.size());
|
||||||
|
for (const auto& r : s_releases) {
|
||||||
|
ReleaseRow row;
|
||||||
|
row.tag = r.tag;
|
||||||
|
row.title = r.name;
|
||||||
|
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
||||||
|
row.prerelease = r.prerelease;
|
||||||
|
row.hasAsset = util::selectDaemonAsset(r, token) >= 0;
|
||||||
|
row.installed = !instCore.empty() && util::daemonVersionCore(r.tag) == instCore;
|
||||||
|
s_rows.push_back(std::move(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Downgrade caution: an older node binary may not match the current chain data.
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_downgrade_note"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
bool back = false;
|
||||||
|
const int idx = RenderReleaseList(s_rows, dp, &back);
|
||||||
|
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_version); return; }
|
||||||
|
if (idx >= 0 && idx < static_cast<int>(s_releases.size())) {
|
||||||
|
const util::DaemonRelease rel = s_releases[idx];
|
||||||
|
s_rows.clear();
|
||||||
|
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderProgress(float dp, const Progress& p) {
|
||||||
|
using namespace material;
|
||||||
|
const char* title = p.state == St::Downloading ? TR("daemon_update_downloading")
|
||||||
|
: p.state == St::Verifying ? TR("daemon_update_verifying")
|
||||||
|
: TR("daemon_update_installing");
|
||||||
|
Type().text(TypeStyle::Subtitle2, title);
|
||||||
|
ImGui::Spacing();
|
||||||
|
const float barH = 8.0f * dp;
|
||||||
|
const float barW = fullW();
|
||||||
|
const ImVec2 bMin = ImGui::GetCursorScreenPos();
|
||||||
|
const ImVec2 bMax(bMin.x + barW, bMin.y + barH);
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
|
||||||
|
const float fillW = barW * (p.percent / 100.0f);
|
||||||
|
if (fillW > 0)
|
||||||
|
dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp);
|
||||||
|
ImGui::Dummy(ImVec2(0, barH));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Text("%s", p.status_text.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
// Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort).
|
||||||
|
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
|
||||||
|
s_updater->cancel();
|
||||||
|
s_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderDone(float, const Progress& p) {
|
||||||
|
using namespace material;
|
||||||
|
// Tell the Settings panel to refresh its cached daemon info — once, not every frame (a
|
||||||
|
// re-scan reads the whole daemon binary).
|
||||||
|
if (!s_notified) { s_installed_flag = true; s_notified = true; }
|
||||||
|
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Text("%s %s", TR("daemon_update_version"), p.latest_tag.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_restart_note"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (s_app->isUsingEmbeddedDaemon()) {
|
||||||
|
if (TactileButton(TR("daemon_update_restart_now"), ImVec2(fullW(), 0))) {
|
||||||
|
s_app->restartDaemon();
|
||||||
|
s_open = false;
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (TactileButton(TR("daemon_update_later"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
|
} else {
|
||||||
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderFailed(float, const Progress& p) {
|
||||||
|
using namespace material;
|
||||||
|
Type().textColored(TypeStyle::Subtitle2, Error(), TR("daemon_update_failed"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str());
|
||||||
|
ImGui::Spacing();
|
||||||
|
installAction(TR("retry"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool s_open = false;
|
||||||
|
static inline bool s_installed_flag = false;
|
||||||
|
static inline bool s_notified = false;
|
||||||
|
static inline std::string s_installed_version;
|
||||||
|
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||||
|
static inline std::vector<util::DaemonRelease> s_releases; // matching releases (install by index)
|
||||||
|
static inline App* s_app = nullptr;
|
||||||
|
static inline std::unique_ptr<util::DaemonUpdater> s_updater;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -205,7 +205,7 @@ static void copyButton(const char* id, const std::string& text, float x, float y
|
|||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
ImVec2(x + Layout::spacingSm(), y),
|
ImVec2(x + Layout::spacingSm(), y),
|
||||||
hovered ? Primary() : OnSurfaceMedium(), ICON_MD_CONTENT_COPY);
|
hovered ? Primary() : OnSurfaceMedium(), ICON_MD_CONTENT_COPY);
|
||||||
if (hovered) ImGui::SetTooltip("%s", TR("click_to_copy"));
|
if (hovered) material::Tooltip("%s", TR("click_to_copy"));
|
||||||
if (clicked) {
|
if (clicked) {
|
||||||
ImGui::SetClipboardText(text.c_str());
|
ImGui::SetClipboardText(text.c_str());
|
||||||
Notifications::instance().success(TR("copied_to_clipboard"));
|
Notifications::instance().success(TR("copied_to_clipboard"));
|
||||||
@@ -1066,7 +1066,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
if (app->rpc() && app->rpc()->isConnected())
|
if (app->rpc() && app->rpc()->isConnected())
|
||||||
fetchBlockDetail(app, s_detail_height - 1);
|
fetchBlockDetail(app, s_detail_height - 1);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Previous block");
|
if (ImGui::IsItemHovered()) material::Tooltip("Previous block");
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -1080,7 +1080,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
if (app->rpc() && app->rpc()->isConnected())
|
if (app->rpc() && app->rpc()->isConnected())
|
||||||
fetchBlockDetail(app, s_detail_height + 1);
|
fetchBlockDetail(app, s_detail_height + 1);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Next block");
|
if (ImGui::IsItemHovered()) material::Tooltip("Next block");
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
@@ -1117,7 +1117,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y);
|
ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y);
|
||||||
|
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", s_detail_hash.c_str());
|
material::Tooltip("%s", s_detail_hash.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -1218,7 +1218,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
WithAlpha(OnSurface(), 10),
|
WithAlpha(OnSurface(), 10),
|
||||||
S.drawElement("tabs.explorer", "row-rounding").size);
|
S.drawElement("tabs.explorer", "row-rounding").size);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", txid.c_str());
|
material::Tooltip("%s", txid.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw icon + text
|
// Draw icon + text
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ void ImportKeyDialog::render(App* app)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("(?)");
|
ImGui::TextDisabled("(?)");
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("import_key_tooltip"));
|
material::Tooltip("%s", TR("import_key_tooltip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation indicator inline with title — check preview text during hover,
|
// Validation indicator inline with title — check preview text during hover,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include "key_export_dialog.h"
|
#include "key_export_dialog.h"
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
|
#include "../../wallet/lite_wallet_controller.h"
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
#include "../../rpc/rpc_client.h"
|
#include "../../rpc/rpc_client.h"
|
||||||
#include "../../rpc/rpc_worker.h"
|
#include "../../rpc/rpc_worker.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
@@ -11,7 +13,10 @@
|
|||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
|
#include "../layout.h"
|
||||||
#include "../widgets/qr_code.h"
|
#include "../widgets/qr_code.h"
|
||||||
|
#include "../widgets/copy_field.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -61,15 +66,20 @@ void KeyExportDialog::render(App* app)
|
|||||||
TR("export_private_key") : TR("export_viewing_key");
|
TR("export_private_key") : TR("export_viewing_key");
|
||||||
|
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
auto win = S.window("dialogs.key-export");
|
|
||||||
auto warningBox = S.drawElement("dialogs.key-export", "warning-box");
|
auto warningBox = S.drawElement("dialogs.key-export", "warning-box");
|
||||||
auto addrInput = S.input("dialogs.key-export", "address-input");
|
auto addrInput = S.input("dialogs.key-export", "address-input");
|
||||||
auto revealBtn = S.button("dialogs.key-export", "reveal-button");
|
auto revealBtn = S.button("dialogs.key-export", "reveal-button");
|
||||||
auto keyDisplay = S.drawElement("dialogs.key-export", "key-display");
|
auto keyDisplay = S.drawElement("dialogs.key-export", "key-display");
|
||||||
auto copyBtn = S.button("dialogs.key-export", "copy-button");
|
auto copyBtn = S.button("dialogs.key-export", "copy-button");
|
||||||
auto closeBtn = S.button("dialogs.key-export", "close-button");
|
auto closeBtn = S.button("dialogs.key-export", "close-button");
|
||||||
|
(void)addrInput;
|
||||||
|
(void)keyDisplay;
|
||||||
|
(void)copyBtn;
|
||||||
|
|
||||||
if (material::BeginOverlayDialog(title, &s_open, win.width, 0.94f)) {
|
// Modal scales to 85% of the window width. BeginOverlayDialog multiplies the width by
|
||||||
|
// Layout::dpiScale(); divide it out here so the final card is exactly 85% at any font scale.
|
||||||
|
const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale();
|
||||||
|
if (material::BeginOverlayDialog(title, &s_open, cardW, 0.94f)) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Warning section with colored background
|
// Warning section with colored background
|
||||||
@@ -79,13 +89,37 @@ void KeyExportDialog::render(App* app)
|
|||||||
ImGui::BeginChild("WarningBox", ImVec2(-1, 0),
|
ImGui::BeginChild("WarningBox", ImVec2(-1, 0),
|
||||||
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders);
|
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders);
|
||||||
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " %s", TR("warning_upper"));
|
// Heading — warning icon (drawn with the icon font, not the text font, so it isn't tofu) +
|
||||||
|
// larger h6 text, centered, for emphasis.
|
||||||
|
{
|
||||||
|
ImFont* iconF = material::Type().iconMed();
|
||||||
|
ImFont* textF = material::Type().h6();
|
||||||
|
const char* icon = ICON_MD_WARNING;
|
||||||
|
const char* txt = TR("warning_upper");
|
||||||
|
ImGui::PushFont(iconF); const float iw = ImGui::CalcTextSize(icon).x; ImGui::PopFont();
|
||||||
|
ImGui::PushFont(textF); const float tw = ImGui::CalcTextSize(txt).x; ImGui::PopFont();
|
||||||
|
const float sp = ImGui::GetStyle().ItemSpacing.x;
|
||||||
|
const float wa = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float total = iw + sp + tw;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (wa > total ? (wa - total) * 0.5f : 0.0f));
|
||||||
|
const ImVec4 red(1.0f, 0.42f, 0.42f, 1.0f);
|
||||||
|
ImGui::PushFont(iconF); ImGui::TextColored(red, "%s", icon); ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, sp);
|
||||||
|
ImGui::PushFont(textF); ImGui::TextColored(red, "%s", txt); ImGui::PopFont();
|
||||||
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
if (s_key_type == KeyType::Private) {
|
// Body — centered wrapped text.
|
||||||
ImGui::TextWrapped(" %s", TR("key_export_private_warning"));
|
{
|
||||||
} else {
|
const char* body = (s_key_type == KeyType::Private)
|
||||||
ImGui::TextWrapped(" %s", TR("key_export_viewing_warning"));
|
? TR("key_export_private_warning") : TR("key_export_viewing_warning");
|
||||||
|
ImDrawList* wdl = ImGui::GetWindowDrawList();
|
||||||
|
ImFont* wf = ImGui::GetFont();
|
||||||
|
const float wfs = ImGui::GetFontSize();
|
||||||
|
const float wa = ImGui::GetContentRegionAvail().x;
|
||||||
|
const ImVec2 wp = ImGui::GetCursorScreenPos();
|
||||||
|
const float bh = widgets::DrawCenteredWrapped(wdl, wf, wfs, wp, wa, material::OnSurface(), body);
|
||||||
|
ImGui::Dummy(ImVec2(wa, bh));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
@@ -95,32 +129,9 @@ void KeyExportDialog::render(App* app)
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Address display
|
// Address — click-to-copy field (chunked, width-clamped, centered).
|
||||||
ImGui::Text("%s", TR("address_label"));
|
ImGui::Text("%s", TR("address_label"));
|
||||||
|
widgets::AddressCopyField("##addrcopy", s_address);
|
||||||
// Determine if it's a z-address (longer) or t-address
|
|
||||||
bool is_zaddr = s_address.length() > 50;
|
|
||||||
|
|
||||||
if (is_zaddr) {
|
|
||||||
// Use multiline for z-addresses
|
|
||||||
char addr_buf[512];
|
|
||||||
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
|
||||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
|
||||||
// Fit the field to the wrapped address (no excess empty space below it).
|
|
||||||
(void)addrInput;
|
|
||||||
const float addrFieldH =
|
|
||||||
ImGui::CalcTextSize(addr_buf, nullptr, false,
|
|
||||||
ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2.0f).y
|
|
||||||
+ ImGui::GetStyle().FramePadding.y * 2.0f + 4.0f;
|
|
||||||
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
|
|
||||||
ImVec2(-1, addrFieldH), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
} else {
|
|
||||||
char addr_buf[128];
|
|
||||||
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
|
||||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
@@ -140,7 +151,44 @@ void KeyExportDialog::render(App* app)
|
|||||||
// Check if z-address or t-address
|
// Check if z-address or t-address
|
||||||
bool is_zaddress = (s_address.length() > 50 || s_address[0] == 'z');
|
bool is_zaddress = (s_address.length() > 50 || s_address[0] == 'z');
|
||||||
|
|
||||||
if (s_key_type == KeyType::Private) {
|
if (auto* lite = app->liteWallet()) {
|
||||||
|
// Lite wallet: there is no daemon RPC — export the key locally via the SDXL
|
||||||
|
// backend (a local, network-free op) instead of the z_exportkey/dumpprivkey
|
||||||
|
// RPCs, which fail with "Not connected" when no full node is present.
|
||||||
|
const bool wantViewing = (s_key_type != KeyType::Private);
|
||||||
|
if (wantViewing && !is_zaddress) {
|
||||||
|
s_error = TR("key_export_viewing_keys_zonly");
|
||||||
|
s_fetching = false;
|
||||||
|
} else {
|
||||||
|
auto r = lite->exportPrivateKeys(s_address);
|
||||||
|
std::string found;
|
||||||
|
if (r.ok) {
|
||||||
|
try {
|
||||||
|
auto j = nlohmann::json::parse(r.privateKeysJson);
|
||||||
|
const nlohmann::json* entry = nullptr;
|
||||||
|
if (j.is_array()) {
|
||||||
|
for (auto& e : j)
|
||||||
|
if (e.contains("address") && e["address"] == s_address) { entry = &e; break; }
|
||||||
|
if (!entry && !j.empty()) entry = &j.front();
|
||||||
|
} else if (j.is_object()) {
|
||||||
|
entry = &j;
|
||||||
|
}
|
||||||
|
const char* field = wantViewing ? "viewing_key" : "private_key";
|
||||||
|
if (entry && entry->contains(field) && (*entry)[field].is_string())
|
||||||
|
found = (*entry)[field].get<std::string>();
|
||||||
|
} catch (...) {}
|
||||||
|
wallet::secureWipeLiteSecret(r.privateKeysJson);
|
||||||
|
}
|
||||||
|
if (!found.empty()) {
|
||||||
|
s_key = found;
|
||||||
|
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||||
|
} else {
|
||||||
|
s_error = r.ok ? std::string("Key not available for this address") : r.error;
|
||||||
|
}
|
||||||
|
wallet::secureWipeLiteSecret(found);
|
||||||
|
s_fetching = false;
|
||||||
|
}
|
||||||
|
} else if (s_key_type == KeyType::Private) {
|
||||||
// Export private key
|
// Export private key
|
||||||
std::string addr = s_address;
|
std::string addr = s_address;
|
||||||
std::string method = is_zaddress ? "z_exportkey" : "dumpprivkey";
|
std::string method = is_zaddress ? "z_exportkey" : "dumpprivkey";
|
||||||
@@ -201,51 +249,52 @@ void KeyExportDialog::render(App* app)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
||||||
} else {
|
} else {
|
||||||
// Key has been fetched - display it
|
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
|
||||||
|
const float gap = 12.0f;
|
||||||
|
const float avail = ImGui::GetContentRegionAvail().x;
|
||||||
|
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
|
||||||
|
float qrSize = avail * 0.30f;
|
||||||
|
if (qrSize < 200.0f) qrSize = 200.0f;
|
||||||
|
if (qrSize > 340.0f) qrSize = 340.0f;
|
||||||
|
float keyW = avail - qrSize - gap;
|
||||||
|
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
|
||||||
|
if (!sideBySide) keyW = avail;
|
||||||
|
|
||||||
// Fit the field to the wrapped key (same length whether shown or masked).
|
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
|
||||||
(void)keyDisplay;
|
// rather than slicing it into 4s — only the part from the '1' separator on is chunked.
|
||||||
char key_buf[1024];
|
auto chunkKey = [](const std::string& s) -> std::string {
|
||||||
if (s_show_key) {
|
const size_t one = s.find('1');
|
||||||
strncpy(key_buf, s_key.c_str(), sizeof(key_buf) - 1);
|
if (one != std::string::npos && one > 0) {
|
||||||
} else {
|
bool isHrp = true;
|
||||||
std::string masked(s_key.length(), '*');
|
for (size_t i = 0; i < one; ++i) {
|
||||||
strncpy(key_buf, masked.c_str(), sizeof(key_buf) - 1);
|
const char c = s[i];
|
||||||
}
|
if (!((c >= 'a' && c <= 'z') || c == '-')) { isHrp = false; break; }
|
||||||
key_buf[sizeof(key_buf) - 1] = '\0';
|
}
|
||||||
const float keyFieldH =
|
if (isHrp) return s.substr(0, one) + " " + widgets::ChunkString(s.substr(one), 4);
|
||||||
ImGui::CalcTextSize(key_buf, nullptr, false,
|
|
||||||
ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2.0f).y
|
|
||||||
+ ImGui::GetStyle().FramePadding.y * 2.0f + 4.0f;
|
|
||||||
ImGui::InputTextMultiline("##Key", key_buf, sizeof(key_buf),
|
|
||||||
ImVec2(-1, keyFieldH), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
|
|
||||||
// Action row: Show/Hide · Copy · QR. Auto-width buttons (size 0) so the label text never
|
|
||||||
// clips at the user's font scale, and ONE shared font so they all match.
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImFont* actionFont = S.resolveFont(copyBtn.font);
|
|
||||||
|
|
||||||
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(0, 0), actionFont)) {
|
|
||||||
s_show_key = !s_show_key;
|
|
||||||
if (!s_show_key) s_show_qr = false; // hiding the key also hides its QR
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
if (material::StyledButton(TR("copy_to_clipboard"), ImVec2(0, 0), actionFont)) {
|
|
||||||
// Auto-clearing clipboard: the key (as sensitive as the seed) is wiped after ~45s.
|
|
||||||
app->copySecretToClipboard(s_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// QR (only once revealed) — for scanning the key into another wallet.
|
|
||||||
if (s_show_key) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (material::StyledButton(s_show_qr ? TR("hide_qr") : TR("show_qr"), ImVec2(0, 0), actionFont)) {
|
|
||||||
s_show_qr = !s_show_qr;
|
|
||||||
}
|
}
|
||||||
}
|
return widgets::ChunkString(s, 4);
|
||||||
|
};
|
||||||
|
const std::string shown = s_show_key ? s_key : std::string(s_key.size(), '*');
|
||||||
|
const std::string keyText = chunkKey(shown);
|
||||||
|
|
||||||
if (s_show_qr && s_show_key && !s_key.empty()) {
|
// Left column: key text (click-to-copy), with the Show/Hide button just below it.
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
if (widgets::CopyField("##keycopy", keyText, keyW, 0.0f, /*center=*/true)) {
|
||||||
|
app->copySecretToClipboard(s_key); // auto-clearing clipboard
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(0, 0),
|
||||||
|
material::Type().subtitle1())) {
|
||||||
|
s_show_key = !s_show_key;
|
||||||
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
|
||||||
|
if (sideBySide) ImGui::SameLine(0, gap);
|
||||||
|
else ImGui::Spacing();
|
||||||
|
|
||||||
|
// QR to the right of the key when revealed; an empty placeholder square while hidden.
|
||||||
|
const ImVec2 sq0 = ImGui::GetCursorScreenPos();
|
||||||
|
if (s_show_key && !s_key.empty()) {
|
||||||
if (s_qr_cached != s_key) { // (re)generate the QR texture when the key changes
|
if (s_qr_cached != s_key) { // (re)generate the QR texture when the key changes
|
||||||
if (s_qr_tex) { FreeQRTexture(s_qr_tex); s_qr_tex = 0; }
|
if (s_qr_tex) { FreeQRTexture(s_qr_tex); s_qr_tex = 0; }
|
||||||
int qw = 0, qh = 0;
|
int qw = 0, qh = 0;
|
||||||
@@ -253,11 +302,20 @@ void KeyExportDialog::render(App* app)
|
|||||||
s_qr_cached = s_key;
|
s_qr_cached = s_key;
|
||||||
}
|
}
|
||||||
if (s_qr_tex) {
|
if (s_qr_tex) {
|
||||||
ImGui::Spacing();
|
// White quiet-zone backing so the code stays scannable on dark themes.
|
||||||
const float qrSize = 180.0f;
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - qrSize) * 0.5f);
|
sq0, ImVec2(sq0.x + qrSize, sq0.y + qrSize), IM_COL32_WHITE, 4.0f);
|
||||||
RenderQRCode(s_qr_tex, qrSize);
|
RenderQRCode(s_qr_tex, qrSize);
|
||||||
|
} else {
|
||||||
|
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Empty placeholder square — same footprint the QR will occupy once revealed.
|
||||||
|
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
const ImVec2 sq1(sq0.x + qrSize, sq0.y + qrSize);
|
||||||
|
dl->AddRectFilled(sq0, sq1, material::WithAlpha(material::OnSurface(), 8), 6.0f);
|
||||||
|
dl->AddRect(sq0, sq1, material::WithAlpha(material::OnSurface(), 45), 6.0f, 0, 1.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ void RenderMarketTab(App* app)
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip(TR("market_trade_on"), currentExchange.name.c_str());
|
material::Tooltip(TR("market_trade_on"), currentExchange.name.c_str());
|
||||||
}
|
}
|
||||||
ImGui::SetCursorScreenPos(savedCur);
|
ImGui::SetCursorScreenPos(savedCur);
|
||||||
}
|
}
|
||||||
@@ -490,7 +490,7 @@ void RenderMarketTab(App* app)
|
|||||||
s_history_initialized = false;
|
s_history_initialized = false;
|
||||||
s_last_refresh_time = ImGui::GetTime();
|
s_last_refresh_time = ImGui::GetTime();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("market_refresh_price"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("market_refresh_price"));
|
||||||
|
|
||||||
// Timestamp text to the left of refresh button
|
// Timestamp text to the left of refresh button
|
||||||
if (s_last_refresh_time > 0.0) {
|
if (s_last_refresh_time > 0.0) {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
|
material::Tooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
idleRightEdge = btnX - 4.0f * dp;
|
idleRightEdge = btnX - 4.0f * dp;
|
||||||
@@ -178,7 +178,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", threadScaling
|
material::Tooltip("%s", threadScaling
|
||||||
? TR("mining_idle_scale_on_tooltip")
|
? TR("mining_idle_scale_on_tooltip")
|
||||||
: TR("mining_idle_scale_off_tooltip"));
|
: TR("mining_idle_scale_off_tooltip"));
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", gpuAware
|
material::Tooltip("%s", gpuAware
|
||||||
? TR("mining_idle_gpu_on_tooltip")
|
? TR("mining_idle_gpu_on_tooltip")
|
||||||
: TR("mining_idle_gpu_off_tooltip"));
|
: TR("mining_idle_gpu_off_tooltip"));
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
material::Tooltip("%s", TR("tt_idle_delay"));
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
material::Tooltip("%s", TR("tt_idle_delay"));
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
|
material::Tooltip("%s", TR("mining_idle_threads_idle_tooltip"));
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
|
material::Tooltip("%s", TR("mining_idle_threads_active_tooltip"));
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,7 +439,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_benchmark_cancel"));
|
material::Tooltip("%s", TR("mining_benchmark_cancel"));
|
||||||
}
|
}
|
||||||
const char* cancelIcon = ICON_MD_CLOSE;
|
const char* cancelIcon = ICON_MD_CLOSE;
|
||||||
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
|
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
|
||||||
@@ -471,7 +471,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
s_benchmark.reset();
|
s_benchmark.reset();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss"));
|
material::Tooltip("%s", TR("mining_benchmark_dismiss"));
|
||||||
}
|
}
|
||||||
const char* okIcon = ICON_MD_CHECK;
|
const char* okIcon = ICON_MD_CHECK;
|
||||||
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
|
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
|
||||||
@@ -498,7 +498,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
||||||
StateHover(), btnSz * 0.5f);
|
StateHover(), btnSz * 0.5f);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip"));
|
material::Tooltip("%s", TR("mining_benchmark_tooltip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* benchIcon = ICON_MD_SPEED;
|
const char* benchIcon = ICON_MD_SPEED;
|
||||||
@@ -556,7 +556,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
|
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
|
||||||
if (xhov) {
|
if (xhov) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
|
material::Tooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
|
||||||
: TR("xmrig_update_title"));
|
: TR("xmrig_update_title"));
|
||||||
}
|
}
|
||||||
if (xclk && !minerBusy) XmrigDownloadDialog::show(app);
|
if (xclk && !minerBusy) XmrigDownloadDialog::show(app);
|
||||||
@@ -864,13 +864,13 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
if (!disabled)
|
if (!disabled)
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (isToggling)
|
if (isToggling)
|
||||||
ImGui::SetTooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
material::Tooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
||||||
else if (isSyncing && !s_pool_mode)
|
else if (isSyncing && !s_pool_mode)
|
||||||
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
material::Tooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
||||||
else if (poolBlockedBySolo)
|
else if (poolBlockedBySolo)
|
||||||
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||||
else
|
else
|
||||||
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
material::Tooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click action — pool or solo
|
// Click action — pool or solo
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
soloMiningAvailable ? TR("mining_filter_tip_solo") : TR("mining_filter_tip_pool"),
|
soloMiningAvailable ? TR("mining_filter_tip_solo") : TR("mining_filter_tip_pool"),
|
||||||
TR("mining_filter_tip_pool")
|
TR("mining_filter_tip_pool")
|
||||||
};
|
};
|
||||||
ImGui::SetTooltip("%s", tips[s_earnings_filter]);
|
material::Tooltip("%s", tips[s_earnings_filter]);
|
||||||
}
|
}
|
||||||
ImGui::SetCursorScreenPos(savedCur);
|
ImGui::SetCursorScreenPos(savedCur);
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_click_copy_difficulty"));
|
material::Tooltip("%s", TR("mining_click_copy_difficulty"));
|
||||||
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||||
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
|
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
@@ -308,7 +308,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_click_copy_block"));
|
material::Tooltip("%s", TR("mining_click_copy_block"));
|
||||||
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||||
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
|
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
@@ -346,7 +346,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_click_copy_address"));
|
material::Tooltip("%s", TR("mining_click_copy_address"));
|
||||||
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||||
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
|
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
@@ -518,7 +518,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(barX, barY));
|
ImGui::SetCursorScreenPos(ImVec2(barX, barY));
|
||||||
ImGui::InvisibleButton("##rambar", ImVec2(barW, barH));
|
ImGui::InvisibleButton("##rambar", ImVec2(barW, barH));
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::BeginTooltip();
|
material::BeginTooltip();
|
||||||
if (selfRAM >= 1024.0)
|
if (selfRAM >= 1024.0)
|
||||||
ImGui::Text(TR("ram_wallet_gb"), selfRAM / 1024.0);
|
ImGui::Text(TR("ram_wallet_gb"), selfRAM / 1024.0);
|
||||||
else
|
else
|
||||||
@@ -529,7 +529,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str());
|
ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str());
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Text(TR("ram_system_gb"), usedRAM / 1024.0, totalRAM / 1024.0);
|
ImGui::Text(TR("ram_system_gb"), usedRAM / 1024.0, totalRAM / 1024.0);
|
||||||
ImGui::EndTooltip();
|
material::EndTooltip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,7 +662,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
dragonx::util::Platform::openUrl(url);
|
dragonx::util::Platform::openUrl(url);
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
|
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
|
||||||
ImGui::SetTooltip("%s", TR("mining_open_in_explorer"));
|
material::Tooltip("%s", TR("mining_open_in_explorer"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
}
|
}
|
||||||
if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (poolHov && soloMiningActive && !s_pool_mode) {
|
if (poolHov && soloMiningActive && !s_pool_mode) {
|
||||||
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||||
@@ -188,7 +188,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
StateHover(), 4.0f * dp);
|
StateHover(), 4.0f * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_saved_pools"));
|
material::Tooltip("%s", TR("mining_saved_pools"));
|
||||||
}
|
}
|
||||||
ImFont* icoFont = Type().iconSmall();
|
ImFont* icoFont = Type().iconSmall();
|
||||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||||
@@ -217,7 +217,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
StateHover(), 4.0f * dp);
|
StateHover(), 4.0f * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
|
material::Tooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
|
||||||
}
|
}
|
||||||
ImFont* icoFont = Type().iconSmall();
|
ImFont* icoFont = Type().iconSmall();
|
||||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||||
@@ -293,7 +293,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
if (inXZone) {
|
if (inXZone) {
|
||||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_remove"));
|
material::Tooltip("%s", TR("mining_remove"));
|
||||||
} else if (rowHov) {
|
} else if (rowHov) {
|
||||||
// Show faint X when row is hovered
|
// Show faint X when row is hovered
|
||||||
ImFont* icoF = Type().iconSmall();
|
ImFont* icoF = Type().iconSmall();
|
||||||
@@ -350,9 +350,9 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
std::string currentWorkerStr(s_pool_worker);
|
std::string currentWorkerStr(s_pool_worker);
|
||||||
if (currentWorkerStr.empty()) {
|
if (currentWorkerStr.empty()) {
|
||||||
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
|
material::Tooltip("%s", TR("mining_generate_z_address_hint"));
|
||||||
} else {
|
} else {
|
||||||
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
|
material::Tooltip("%s", TR("mining_payout_tooltip"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
StateHover(), 4.0f * dp);
|
StateHover(), 4.0f * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_saved_addresses"));
|
material::Tooltip("%s", TR("mining_saved_addresses"));
|
||||||
}
|
}
|
||||||
ImFont* icoFont = Type().iconSmall();
|
ImFont* icoFont = Type().iconSmall();
|
||||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||||
@@ -399,7 +399,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
StateHover(), 4.0f * dp);
|
StateHover(), 4.0f * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
|
material::Tooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
|
||||||
}
|
}
|
||||||
ImFont* icoFont = Type().iconSmall();
|
ImFont* icoFont = Type().iconSmall();
|
||||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||||
@@ -471,7 +471,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
||||||
// Tooltip for long addresses
|
// Tooltip for long addresses
|
||||||
if (rowHov && !inXZone)
|
if (rowHov && !inXZone)
|
||||||
ImGui::SetTooltip("%s", addr.c_str());
|
material::Tooltip("%s", addr.c_str());
|
||||||
// X button — flush with right edge, icon centered
|
// X button — flush with right edge, icon centered
|
||||||
{
|
{
|
||||||
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
|
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
|
||||||
@@ -479,7 +479,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
if (inXZone) {
|
if (inXZone) {
|
||||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_remove"));
|
material::Tooltip("%s", TR("mining_remove"));
|
||||||
} else if (rowHov) {
|
} else if (rowHov) {
|
||||||
ImFont* icoF = Type().iconSmall();
|
ImFont* icoF = Type().iconSmall();
|
||||||
const char* xIcon = ICON_MD_CLOSE;
|
const char* xIcon = ICON_MD_CLOSE;
|
||||||
@@ -540,7 +540,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
StateHover(), 4.0f * dp);
|
StateHover(), 4.0f * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", TR("mining_reset_defaults"));
|
material::Tooltip("%s", TR("mining_reset_defaults"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
|||||||
if (hov) {
|
if (hov) {
|
||||||
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", toggleTip);
|
material::Tooltip("%s", toggleTip);
|
||||||
}
|
}
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||||
|
|||||||
@@ -230,17 +230,16 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
spec.rounding = rnd;
|
spec.rounding = rnd;
|
||||||
spec.fillAlpha = selected ? 34 : 18;
|
spec.fillAlpha = selected ? 34 : 18;
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, spec);
|
DrawGlassPanel(dl, cardMin, cardMax, spec);
|
||||||
if (official) {
|
// Glow only the ACTIVE (selected/in-use) node — officials are distinguished by their pill.
|
||||||
|
if (selected) {
|
||||||
auto& fx = effects::ThemeEffects::instance();
|
auto& fx = effects::ThemeEffects::instance();
|
||||||
if (fx.isEnabled()) {
|
if (fx.isEnabled()) {
|
||||||
fx.drawGlowPulse(dl, cardMin, cardMax, rnd);
|
fx.drawGlowPulse(dl, cardMin, cardMax, rnd);
|
||||||
}
|
}
|
||||||
// Always-visible static outline so officials are distinguishable even without effects.
|
// Always-visible pulsing outline so the active node stands out even without effects.
|
||||||
float pulse = 0.75f + 0.25f * (float)std::sin(ImGui::GetTime() * 2.0);
|
float pulse = 0.75f + 0.25f * (float)std::sin(ImGui::GetTime() * 2.0);
|
||||||
dl->AddRect(cardMin, cardMax, WithAlpha(Primary(), (int)(150 * pulse)), rnd, 0, 1.6f * dp);
|
dl->AddRect(cardMin, cardMax, WithAlpha(Primary(), (int)(150 * pulse)), rnd, 0, 1.6f * dp);
|
||||||
}
|
}
|
||||||
if (selected)
|
|
||||||
dl->AddRectFilled(cardMin, ImVec2(cardMin.x + 3.0f * dp, cardMax.y), Primary(), rnd);
|
|
||||||
|
|
||||||
// Main click area selects the server (left of the hide strip).
|
// Main click area selects the server (left of the hide strip).
|
||||||
ImGui::SetCursorScreenPos(cardMin);
|
ImGui::SetCursorScreenPos(cardMin);
|
||||||
@@ -302,7 +301,7 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
bool hideHov = ImGui::IsItemHovered();
|
bool hideHov = ImGui::IsItemHovered();
|
||||||
if (hideHov) {
|
if (hideHov) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"));
|
material::Tooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked()) {
|
if (ImGui::IsItemClicked()) {
|
||||||
if (hiddenList) st->unhideLiteServer(sv.url);
|
if (hiddenList) st->unhideLiteServer(sv.url);
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ void RenderPeersTab(App* app)
|
|||||||
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
|
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s %s", TR("peers_click_copy"), hash.c_str());
|
material::Tooltip("%s %s", TR("peers_click_copy"), hash.c_str());
|
||||||
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
|
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
|
||||||
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
|
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
|
||||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
@@ -657,7 +657,7 @@ void RenderPeersTab(App* app)
|
|||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (!isRefreshing)
|
if (!isRefreshing)
|
||||||
ImGui::SetTooltip("%s", TR("peers_refresh_tooltip"));
|
material::Tooltip("%s", TR("peers_refresh_tooltip"));
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
@@ -808,7 +808,7 @@ void RenderPeersTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::BeginTooltip();
|
material::BeginTooltip();
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(8, 3));
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(8, 3));
|
||||||
if (ImGui::BeginTable("##PeerTT", 2, ImGuiTableFlags_SizingFixedFit)) {
|
if (ImGui::BeginTable("##PeerTT", 2, ImGuiTableFlags_SizingFixedFit)) {
|
||||||
auto TTRow = [](const char* label, const char* value) {
|
auto TTRow = [](const char* label, const char* value) {
|
||||||
@@ -836,7 +836,7 @@ void RenderPeersTab(App* app)
|
|||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::EndTooltip();
|
material::EndTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < state.peers.size() - 1) {
|
if (i < state.peers.size() - 1) {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../widgets/qr_code.h"
|
#include "../widgets/qr_code.h"
|
||||||
|
#include "../widgets/copy_field.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../layout.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
@@ -59,12 +61,13 @@ void QRPopupDialog::render(App* app)
|
|||||||
if (!s_open) return;
|
if (!s_open) return;
|
||||||
|
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
auto win = S.window("dialogs.qr-popup");
|
|
||||||
auto qr = S.drawElement("dialogs.qr-popup", "qr-code");
|
auto qr = S.drawElement("dialogs.qr-popup", "qr-code");
|
||||||
auto addrInput = S.input("dialogs.qr-popup", "address-input");
|
|
||||||
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
|
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
|
||||||
|
|
||||||
if (material::BeginOverlayDialog(TR("qr_title"), &s_open, win.width, 0.94f)) {
|
// Match the key-export modal: 85% of the window width (divide out the dpiScale that
|
||||||
|
// BeginOverlayDialog re-applies, so the final card is exactly 85% at any font scale).
|
||||||
|
const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale();
|
||||||
|
if (material::BeginOverlayDialog(TR("qr_title"), &s_open, cardW, 0.94f)) {
|
||||||
|
|
||||||
// Label if present
|
// Label if present
|
||||||
if (!s_label.empty()) {
|
if (!s_label.empty()) {
|
||||||
@@ -74,10 +77,14 @@ void QRPopupDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center the QR code
|
// Center the QR code (responsive — larger, to suit the wider modal).
|
||||||
float qr_size = qr.size > 0 ? qr.size : 280;
|
|
||||||
float window_width = ImGui::GetWindowWidth();
|
float window_width = ImGui::GetWindowWidth();
|
||||||
|
float qr_size = qr.size > 0 ? (float)qr.size : 280.0f;
|
||||||
|
const float responsive = window_width * 0.5f;
|
||||||
|
if (responsive > qr_size) qr_size = responsive;
|
||||||
|
if (qr_size > 420.0f) qr_size = 420.0f;
|
||||||
float padding = (window_width - qr_size) / 2.0f;
|
float padding = (window_width - qr_size) / 2.0f;
|
||||||
|
if (padding < 0.0f) padding = 0.0f;
|
||||||
|
|
||||||
ImGui::SetCursorPosX(padding);
|
ImGui::SetCursorPosX(padding);
|
||||||
|
|
||||||
@@ -95,39 +102,29 @@ void QRPopupDialog::render(App* app)
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Address display
|
// Address — click-to-copy field (same formatting as the key-export dialog).
|
||||||
ImGui::Text("%s", TR("address_label"));
|
ImGui::Text("%s", TR("address_label"));
|
||||||
|
widgets::AddressCopyField("##QRAddress", s_address);
|
||||||
// Use multiline for z-addresses
|
|
||||||
if (s_address.length() > 50) {
|
|
||||||
char addr_buf[512];
|
|
||||||
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
|
||||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
|
||||||
ImGui::InputTextMultiline("##QRAddress", addr_buf, sizeof(addr_buf),
|
|
||||||
ImVec2(-1, addrInput.height > 0 ? addrInput.height : 60), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
} else {
|
|
||||||
char addr_buf[128];
|
|
||||||
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
|
||||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##QRAddress", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Buttons
|
// Buttons — size each to its label (so "Copy address" never clips), then center the pair.
|
||||||
float button_width = actionBtn.width;
|
ImFont* btnFont = S.resolveFont(actionBtn.font);
|
||||||
float total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x;
|
ImGui::PushFont(btnFont);
|
||||||
float start_x = (window_width - total_width) / 2.0f;
|
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f;
|
||||||
ImGui::SetCursorPosX(start_x);
|
float w_copy = ImGui::CalcTextSize(TR("copy_address")).x + btnPad;
|
||||||
|
float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad;
|
||||||
|
ImGui::PopFont();
|
||||||
|
if (w_copy < actionBtn.width) w_copy = actionBtn.width;
|
||||||
|
if (w_close < actionBtn.width) w_close = actionBtn.width;
|
||||||
|
const float total_width = w_copy + w_close + ImGui::GetStyle().ItemSpacing.x;
|
||||||
|
ImGui::SetCursorPosX((window_width - total_width) / 2.0f);
|
||||||
|
|
||||||
if (material::StyledButton(TR("copy_address"), ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
|
if (material::StyledButton(TR("copy_address"), ImVec2(w_copy, 0), btnFont)) {
|
||||||
ImGui::SetClipboardText(s_address.c_str());
|
ImGui::SetClipboardText(s_address.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
if (material::StyledButton(TR("close"), ImVec2(w_close, 0), btnFont)) {
|
||||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
|
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
material::EndOverlayDialog();
|
material::EndOverlayDialog();
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
s_cached_qr_data.clear(); // Force QR regeneration
|
s_cached_qr_data.clear(); // Force QR regeneration
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s\nBalance: %.8f %s%s",
|
material::Tooltip("%s\nBalance: %.8f %s%s",
|
||||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||||
isCurrent ? "\n(selected)" : "");
|
isCurrent ? "\n(selected)" : "");
|
||||||
}
|
}
|
||||||
@@ -842,7 +842,7 @@ void RenderReceiveTab(App* app)
|
|||||||
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
|
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
ImGui::SetTooltip("%s",
|
material::Tooltip("%s",
|
||||||
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
|
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked()) {
|
if (ImGui::IsItemClicked()) {
|
||||||
|
|||||||
87
src/ui/windows/release_list_view.h
Normal file
87
src/ui/windows/release_list_view.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Shared "Browse all releases" picker, used by both the miner updater (XmrigDownloadDialog) and the
|
||||||
|
// node updater (DaemonUpdateDialog). Renders a scrollable list of releases (tag, title, date, with
|
||||||
|
// pre-release / installed badges) inside the current overlay dialog; the caller maps its updater's
|
||||||
|
// release list into ReleaseRow values and acts on the clicked index.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
struct ReleaseRow {
|
||||||
|
std::string tag; // version tag, e.g. "v1.0.2"
|
||||||
|
std::string title; // human title (release "name"), shown dimmed
|
||||||
|
std::string date; // YYYY-MM-DD (already trimmed)
|
||||||
|
bool prerelease = false; // show a pre-release badge
|
||||||
|
bool hasAsset = true; // a build exists for this platform (else the row's Install is disabled)
|
||||||
|
bool installed = false; // matches the currently-installed version (Install -> Reinstall)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Renders the picker. Returns the row index whose Install button was clicked this frame, or -1.
|
||||||
|
// Sets *back when the Back button is clicked. If `globalDisabledTooltip` is non-null, every Install
|
||||||
|
// button is disabled and shows that tooltip (e.g. "stop mining before updating the miner").
|
||||||
|
inline int RenderReleaseList(const std::vector<ReleaseRow>& rows, float dp, bool* back,
|
||||||
|
const char* globalDisabledTooltip = nullptr) {
|
||||||
|
using namespace material;
|
||||||
|
int clicked = -1;
|
||||||
|
|
||||||
|
Type().text(TypeStyle::Subtitle2, TR("upd_select_version"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
const float listH = 300.0f * dp;
|
||||||
|
if (ImGui::BeginChild("##release_list", ImVec2(0, listH), true)) {
|
||||||
|
const ImVec4 dim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
||||||
|
const ImVec4 warn = ImVec4(1.0f, 0.78f, 0.25f, 1.0f);
|
||||||
|
const ImVec4 succ = ImGui::ColorConvertU32ToFloat4(Success());
|
||||||
|
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||||
|
const ReleaseRow& r = rows[i];
|
||||||
|
ImGui::PushID(i);
|
||||||
|
|
||||||
|
ImGui::TextUnformatted(r.tag.c_str());
|
||||||
|
if (r.prerelease) { ImGui::SameLine(); ImGui::TextColored(warn, "[%s]", TR("upd_prerelease")); }
|
||||||
|
if (r.installed) { ImGui::SameLine(); ImGui::TextColored(succ, "[%s]", TR("upd_installed_badge")); }
|
||||||
|
|
||||||
|
if (!r.title.empty() || !r.date.empty()) {
|
||||||
|
std::string meta = r.title;
|
||||||
|
if (!r.title.empty() && !r.date.empty()) meta += " · ";
|
||||||
|
meta += r.date;
|
||||||
|
ImGui::TextColored(dim, "%s", meta.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* lbl = r.installed ? TR("upd_reinstall") : TR("upd_install");
|
||||||
|
const bool disabled = !r.hasAsset || globalDisabledTooltip != nullptr;
|
||||||
|
ImGui::BeginDisabled(disabled);
|
||||||
|
if (TactileButton(lbl, ImVec2(140.0f * dp, 0))) clicked = i;
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
material::Tooltip("%s", globalDisabledTooltip ? globalDisabledTooltip
|
||||||
|
: TR("upd_no_build_platform"));
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (TactileButton(TR("upd_back"), ImVec2(ImGui::GetContentRegionAvail().x, 0))) *back = true;
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -296,7 +296,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
addr.address.c_str());
|
addr.address.c_str());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s\nBalance: %.8f %s",
|
material::Tooltip("%s\nBalance: %.8f %s",
|
||||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER);
|
addr.address.c_str(), addr.balance, DRAGONX_TICKER);
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -351,7 +351,7 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
|||||||
snprintf(s_to_address, sizeof(s_to_address), "%s", suggestions[si].c_str());
|
snprintf(s_to_address, sizeof(s_to_address), "%s", suggestions[si].c_str());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", suggestions[si].c_str());
|
material::Tooltip("%s", suggestions[si].c_str());
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
@@ -472,7 +472,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
|
|||||||
snprintf(tipBuf, sizeof(tipBuf), "%.8f / %.8f %s (%.1f%%)",
|
snprintf(tipBuf, sizeof(tipBuf), "%.8f / %.8f %s (%.1f%%)",
|
||||||
s_amount, maxAmount > 0 ? maxAmount : 0.0, DRAGONX_TICKER,
|
s_amount, maxAmount > 0 ? maxAmount : 0.0, DRAGONX_TICKER,
|
||||||
progress * 100.0f);
|
progress * 100.0f);
|
||||||
ImGui::SetTooltip("%s", tipBuf);
|
material::Tooltip("%s", tipBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glass thumb circle at the fill edge
|
// Glass thumb circle at the fill edge
|
||||||
@@ -925,19 +925,19 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
|
|
||||||
if (!can_send && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
if (!can_send && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||||
if (!app->isConnected())
|
if (!app->isConnected())
|
||||||
ImGui::SetTooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
||||||
else if (state.sync.syncing)
|
else if (state.sync.syncing)
|
||||||
ImGui::SetTooltip("%s", TR("send_tooltip_syncing"));
|
material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||||
else if (s_from_address[0] == '\0')
|
else if (s_from_address[0] == '\0')
|
||||||
ImGui::SetTooltip("%s", TR("send_tooltip_select_source"));
|
material::Tooltip("%s", TR("send_tooltip_select_source"));
|
||||||
else if (!is_valid_address)
|
else if (!is_valid_address)
|
||||||
ImGui::SetTooltip("%s", TR("send_tooltip_invalid_address"));
|
material::Tooltip("%s", TR("send_tooltip_invalid_address"));
|
||||||
else if (s_amount <= 0)
|
else if (s_amount <= 0)
|
||||||
ImGui::SetTooltip("%s", TR("send_tooltip_enter_amount"));
|
material::Tooltip("%s", TR("send_tooltip_enter_amount"));
|
||||||
else if (total > available)
|
else if (total > available)
|
||||||
ImGui::SetTooltip("%s", TR("send_tooltip_exceeds_balance"));
|
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
|
||||||
else if (s_sending)
|
else if (s_sending)
|
||||||
ImGui::SetTooltip("%s", TR("send_tooltip_in_progress"));
|
material::Tooltip("%s", TR("send_tooltip_in_progress"));
|
||||||
}
|
}
|
||||||
if (!can_send) ImGui::PopStyleColor(3);
|
if (!can_send) ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||||
ImGui::SetTooltip("%s", skin.validationError.c_str());
|
material::Tooltip("%s", skin.validationError.c_str());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
std::string label = skin.name;
|
std::string label = skin.name;
|
||||||
@@ -244,7 +244,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
|
material::Tooltip("%s", TR("tt_theme_hotkey"));
|
||||||
|
|
||||||
// Show indicator if custom theme is active
|
// Show indicator if custom theme is active
|
||||||
if (active_is_custom) {
|
if (active_is_custom) {
|
||||||
@@ -253,7 +253,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s", TR("tt_custom_theme"));
|
material::Tooltip("%s", TR("tt_custom_theme"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
}
|
}
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip(TR("tt_scan_themes"),
|
material::Tooltip(TR("tt_scan_themes"),
|
||||||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -842,7 +842,7 @@ void RenderTransactionsTab(App* app)
|
|||||||
|
|
||||||
// Tooltip
|
// Tooltip
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("%s\n%s\n%s", tx.address.c_str(),
|
material::Tooltip("%s\n%s\n%s", tx.address.c_str(),
|
||||||
tx.txid.c_str(), tx.getTimeString().c_str());
|
tx.txid.c_str(), tx.getTimeString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../config/settings.h"
|
#include "../../config/settings.h"
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
|
#include "release_list_view.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -33,8 +35,11 @@ public:
|
|||||||
s_app = app;
|
s_app = app;
|
||||||
s_open = true;
|
s_open = true;
|
||||||
s_persisted = false;
|
s_persisted = false;
|
||||||
|
s_installed_tag = app->settings() ? app->settings()->getXmrigVersion() : std::string();
|
||||||
|
s_rows.clear();
|
||||||
|
s_releases.clear();
|
||||||
s_updater = std::make_unique<util::XmrigUpdater>();
|
s_updater = std::make_unique<util::XmrigUpdater>();
|
||||||
s_updater->startCheck(app->settings() ? app->settings()->getXmrigVersion() : std::string());
|
s_updater->startCheck(s_installed_tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isOpen() { return s_open; }
|
static bool isOpen() { return s_open; }
|
||||||
@@ -54,6 +59,8 @@ public:
|
|||||||
case St::UpToDate:
|
case St::UpToDate:
|
||||||
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
||||||
case St::Unavailable: renderUnavailable(dp, p); break;
|
case St::Unavailable: renderUnavailable(dp, p); break;
|
||||||
|
case St::Listing: renderListing(dp, p); break;
|
||||||
|
case St::ReleaseList: renderReleaseList(dp, p); break;
|
||||||
case St::Downloading:
|
case St::Downloading:
|
||||||
case St::Verifying:
|
case St::Verifying:
|
||||||
case St::Extracting: renderProgress(dp, p); break;
|
case St::Extracting: renderProgress(dp, p); break;
|
||||||
@@ -121,9 +128,50 @@ private:
|
|||||||
installAction(TR("xmrig_reinstall"));
|
installAction(TR("xmrig_reinstall"));
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
// Browse all releases (pre-releases / older versions).
|
||||||
|
if (TactileButton(TR("xmrig_browse_releases"), ImVec2(fullW(), 0))) {
|
||||||
|
s_rows.clear();
|
||||||
|
s_updater->startListReleases();
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void renderListing(float, const Progress&) {
|
||||||
|
using namespace material;
|
||||||
|
Type().text(TypeStyle::Body2, TR("xmrig_loading_releases"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderReleaseList(float dp, const Progress&) {
|
||||||
|
using namespace material;
|
||||||
|
// Snapshot + build rows once per listing (not every frame). Cleared on browse / show.
|
||||||
|
if (s_rows.empty()) {
|
||||||
|
s_releases = s_updater->getReleases();
|
||||||
|
const std::string token = util::currentXmrigPlatformToken();
|
||||||
|
s_rows.reserve(s_releases.size());
|
||||||
|
for (const auto& r : s_releases) {
|
||||||
|
ReleaseRow row;
|
||||||
|
row.tag = r.tag;
|
||||||
|
row.title = r.name;
|
||||||
|
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
||||||
|
row.prerelease = r.prerelease;
|
||||||
|
row.hasAsset = util::selectXmrigAsset(r, token) >= 0;
|
||||||
|
row.installed = !s_installed_tag.empty() && r.tag == s_installed_tag;
|
||||||
|
s_rows.push_back(std::move(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Can't replace a running miner binary — disable installs while mining (TOCTOU guard).
|
||||||
|
const char* disabledNote = s_app->isPoolMinerRunning() ? TR("xmrig_stop_mining_first") : nullptr;
|
||||||
|
bool back = false;
|
||||||
|
const int idx = RenderReleaseList(s_rows, dp, &back, disabledNote);
|
||||||
|
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_tag); return; }
|
||||||
|
if (idx >= 0 && !s_app->isPoolMinerRunning() && idx < static_cast<int>(s_releases.size())) {
|
||||||
|
const util::XmrigRelease rel = s_releases[idx];
|
||||||
|
s_rows.clear();
|
||||||
|
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void renderProgress(float dp, const Progress& p) {
|
static void renderProgress(float dp, const Progress& p) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
const char* title = p.state == St::Downloading ? TR("xmrig_downloading")
|
const char* title = p.state == St::Downloading ? TR("xmrig_downloading")
|
||||||
@@ -180,6 +228,9 @@ private:
|
|||||||
|
|
||||||
static inline bool s_open = false;
|
static inline bool s_open = false;
|
||||||
static inline bool s_persisted = false;
|
static inline bool s_persisted = false;
|
||||||
|
static inline std::string s_installed_tag;
|
||||||
|
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||||
|
static inline std::vector<util::XmrigRelease> s_releases; // matching releases (install by index)
|
||||||
static inline App* s_app = nullptr;
|
static inline App* s_app = nullptr;
|
||||||
static inline std::unique_ptr<util::XmrigUpdater> s_updater;
|
static inline std::unique_ptr<util::XmrigUpdater> s_updater;
|
||||||
};
|
};
|
||||||
|
|||||||
478
src/util/daemon_updater.cpp
Normal file
478
src/util/daemon_updater.cpp
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// DaemonUpdater background worker (libcurl download + miniz extract). The pure, no-I/O helpers it
|
||||||
|
// calls (release parsing, asset/platform matching, the checksum-table parser, version core) live in
|
||||||
|
// daemon_updater_core.cpp; the generic SHA-256 / ed25519 verification is reused from the miner
|
||||||
|
// updater (xmrig_updater_core.cpp) via util::sha256Hex / util::verifyXmrigSignature.
|
||||||
|
|
||||||
|
#include "daemon_updater.h"
|
||||||
|
#include "xmrig_updater.h" // util::sha256Hex, util::verifyXmrigSignature (shared crypto)
|
||||||
|
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <miniz.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Bound the archive/member sizes before they can fill memory/disk — a defense even ahead of the
|
||||||
|
// checksum check (which only runs once the file is fully downloaded). The full-node archive bundles
|
||||||
|
// the daemon binaries plus Sapling params, so the caps are well above the miner updater's.
|
||||||
|
constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024; // 256 MiB
|
||||||
|
constexpr std::size_t kMaxMemberBytes = 128u * 1024 * 1024; // 128 MiB per extracted file
|
||||||
|
|
||||||
|
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||||
|
{
|
||||||
|
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
|
||||||
|
return size * nmemb;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||||
|
{
|
||||||
|
return std::fwrite(contents, size, nmemb, static_cast<FILE*>(userp));
|
||||||
|
}
|
||||||
|
|
||||||
|
// libcurl progress callback: publish live byte counts and abort the transfer on cancel().
|
||||||
|
int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t)
|
||||||
|
{
|
||||||
|
auto* up = static_cast<DaemonUpdater*>(clientp);
|
||||||
|
return up->onDownloadProgress(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string baseName(const std::string& path)
|
||||||
|
{
|
||||||
|
const auto slash = path.find_last_of("/\\");
|
||||||
|
return slash == std::string::npos ? path : path.substr(slash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toLower(std::string s)
|
||||||
|
{
|
||||||
|
std::transform(s.begin(), s.end(), s.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DaemonUpdater::~DaemonUpdater()
|
||||||
|
{
|
||||||
|
cancel_requested_ = true;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::setProgress(State state, const std::string& text, double done, double total)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.state = state;
|
||||||
|
progress_.status_text = text;
|
||||||
|
if (done > 0) progress_.downloaded_bytes = done;
|
||||||
|
if (total > 0) progress_.total_bytes = total;
|
||||||
|
progress_.percent = (progress_.total_bytes > 0)
|
||||||
|
? static_cast<float>(100.0 * progress_.downloaded_bytes / progress_.total_bytes)
|
||||||
|
: progress_.percent;
|
||||||
|
if (state == State::Failed) progress_.error = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonUpdater::Progress DaemonUpdater::getProgress() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
return progress_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonUpdater::onDownloadProgress(double downloadedBytes, double totalBytes)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.downloaded_bytes = downloadedBytes;
|
||||||
|
if (totalBytes > 0) progress_.total_bytes = totalBytes;
|
||||||
|
progress_.percent = (progress_.total_bytes > 0)
|
||||||
|
? static_cast<float>(100.0 * progress_.downloaded_bytes / progress_.total_bytes)
|
||||||
|
: progress_.percent;
|
||||||
|
}
|
||||||
|
return !cancel_requested_.load(); // false -> curl aborts the transfer
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonUpdater::isDone() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
return progress_.state == State::Done || progress_.state == State::Failed ||
|
||||||
|
progress_.state == State::Unavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::cancel()
|
||||||
|
{
|
||||||
|
cancel_requested_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DaemonUpdater::httpGet(const std::string& url)
|
||||||
|
{
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) return {};
|
||||||
|
std::string result;
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
const CURLcode res = curl_easy_perform(curl);
|
||||||
|
long httpCode = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
|
||||||
|
DEBUG_LOGF("[daemon-updater] GET %s failed: %s (HTTP %ld)\n",
|
||||||
|
url.c_str(), curl_easy_strerror(res), httpCode);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonUpdater::downloadToFile(const std::string& url, const std::string& destPath)
|
||||||
|
{
|
||||||
|
FILE* fp = std::fopen(destPath.c_str(), "wb");
|
||||||
|
if (!fp) return false;
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) { std::fclose(fp); return false; }
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, kMaxArchiveBytes); // refuse oversized bodies
|
||||||
|
// Live progress + cancellation: the callback publishes byte counts and aborts on cancel().
|
||||||
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
const CURLcode res = curl_easy_perform(curl);
|
||||||
|
long httpCode = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
std::fclose(fp);
|
||||||
|
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
|
||||||
|
DEBUG_LOGF("[daemon-updater] download %s failed: %s (HTTP %ld)\n",
|
||||||
|
url.c_str(), curl_easy_strerror(res), httpCode);
|
||||||
|
std::error_code ec; fs::remove(destPath, ec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::startCheck(const std::string& installedVersion)
|
||||||
|
{
|
||||||
|
if (worker_running_.exchange(true)) return;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_ = Progress{};
|
||||||
|
progress_.installed_tag = installedVersion;
|
||||||
|
progress_.state = State::Checking;
|
||||||
|
progress_.status_text = "Checking for the latest node…";
|
||||||
|
}
|
||||||
|
worker_ = std::thread([this, installedVersion] { runCheck(installedVersion); worker_running_ = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::startInstall(const std::string& targetDir)
|
||||||
|
{
|
||||||
|
if (worker_running_.exchange(true)) return;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.state = State::Downloading;
|
||||||
|
progress_.error.clear();
|
||||||
|
progress_.status_text = "Preparing…";
|
||||||
|
progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0%
|
||||||
|
progress_.total_bytes = 0;
|
||||||
|
progress_.percent = 0.0f;
|
||||||
|
}
|
||||||
|
worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::startListReleases()
|
||||||
|
{
|
||||||
|
if (worker_running_.exchange(true)) return;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.state = State::Listing;
|
||||||
|
progress_.error.clear();
|
||||||
|
progress_.status_text = "Loading releases…";
|
||||||
|
progress_.downloaded_bytes = 0; // clear any prior op's progress
|
||||||
|
progress_.total_bytes = 0;
|
||||||
|
progress_.percent = 0.0f;
|
||||||
|
}
|
||||||
|
worker_ = std::thread([this] { runListReleases(); worker_running_ = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::startInstallRelease(const std::string& targetDir, DaemonRelease release)
|
||||||
|
{
|
||||||
|
if (worker_running_.exchange(true)) return;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.state = State::Downloading;
|
||||||
|
progress_.error.clear();
|
||||||
|
progress_.status_text = "Preparing…";
|
||||||
|
progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0%
|
||||||
|
progress_.total_bytes = 0;
|
||||||
|
progress_.percent = 0.0f;
|
||||||
|
}
|
||||||
|
worker_ = std::thread([this, targetDir, release = std::move(release)] {
|
||||||
|
installResolved(targetDir, release);
|
||||||
|
worker_running_ = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DaemonRelease> DaemonUpdater::getReleases() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
return releases_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::runListReleases()
|
||||||
|
{
|
||||||
|
const std::string body = httpGet(kReleasesUrl);
|
||||||
|
if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
|
||||||
|
std::vector<DaemonRelease> list = parseDaemonReleaseList(body);
|
||||||
|
const bool empty = list.empty();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
releases_ = std::move(list);
|
||||||
|
}
|
||||||
|
if (empty) { setProgress(State::Failed, "No releases found."); return; }
|
||||||
|
setProgress(State::ReleaseList, "Select a version to install.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::runCheck(std::string installedVersion)
|
||||||
|
{
|
||||||
|
const std::string body = httpGet(kApiUrl);
|
||||||
|
if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
|
||||||
|
const DaemonRelease rel = parseDaemonRelease(body);
|
||||||
|
if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
|
||||||
|
|
||||||
|
const std::string token = currentDaemonPlatformToken();
|
||||||
|
const int idx = selectDaemonAsset(rel, token);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.latest_tag = rel.tag;
|
||||||
|
progress_.installed_tag = installedVersion;
|
||||||
|
}
|
||||||
|
if (idx < 0) {
|
||||||
|
setProgress(State::Unavailable, "No node build is available for this platform (" +
|
||||||
|
(token.empty() ? "unknown" : token) + ").");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Compare by vN.N.N core so an installed "v1.0.2-<commit>" matches a release "v1.0.2".
|
||||||
|
const std::string instCore = daemonVersionCore(installedVersion);
|
||||||
|
const bool updateAvailable = instCore.empty() || instCore != daemonVersionCore(rel.tag);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.update_available = updateAvailable;
|
||||||
|
}
|
||||||
|
if (updateAvailable)
|
||||||
|
setProgress(State::UpdateAvailable, "A new node version is available (" + rel.tag + ").");
|
||||||
|
else
|
||||||
|
setProgress(State::UpToDate, "The node is up to date (" + rel.tag + ").");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::runInstall(std::string targetDir)
|
||||||
|
{
|
||||||
|
setProgress(State::Checking, "Checking for the latest node…");
|
||||||
|
const std::string apiBody = httpGet(kApiUrl);
|
||||||
|
if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
|
||||||
|
const DaemonRelease rel = parseDaemonRelease(apiBody);
|
||||||
|
if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
|
||||||
|
installResolved(targetDir, rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonUpdater::installResolved(const std::string& targetDir, const DaemonRelease& rel)
|
||||||
|
{
|
||||||
|
const std::string token = currentDaemonPlatformToken();
|
||||||
|
const int idx = selectDaemonAsset(rel, token);
|
||||||
|
if (idx < 0) {
|
||||||
|
setProgress(State::Unavailable, "No node build is available for this platform (" +
|
||||||
|
(token.empty() ? "unknown" : token) + ").");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const DaemonReleaseAsset& asset = rel.assets[idx];
|
||||||
|
const auto checksums = parseDaemonChecksums(rel.body);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.latest_tag = rel.tag;
|
||||||
|
}
|
||||||
|
if (cancel_requested_) { setProgress(State::Failed, "Cancelled."); return; }
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(targetDir, ec);
|
||||||
|
|
||||||
|
// 1. Download the archive.
|
||||||
|
const std::string zipPath = (fs::path(targetDir) / ".dragonx-daemon-download.zip").string();
|
||||||
|
setProgress(State::Downloading, "Downloading " + asset.name + "…", 0,
|
||||||
|
static_cast<double>(asset.size));
|
||||||
|
if (!downloadToFile(asset.downloadUrl, zipPath)) {
|
||||||
|
setProgress(State::Failed, "Download failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cancel_requested_) { fs::remove(zipPath, ec); setProgress(State::Failed, "Cancelled."); return; }
|
||||||
|
|
||||||
|
// 2. Verify the downloaded archive. Read it once, then (a) compare its SHA-256 to the published
|
||||||
|
// checksum and (b) verify a detached ed25519 signature over the archive bytes against the
|
||||||
|
// pinned key, so a checksum rewritten in a tampered release body is not sufficient to install.
|
||||||
|
setProgress(State::Verifying, "Verifying download…");
|
||||||
|
{
|
||||||
|
std::ifstream f(zipPath, std::ios::binary);
|
||||||
|
if (!f) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "Could not read the downloaded archive.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::string bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
if (f.bad()) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "Could not read the downloaded archive.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2a. SHA-256 (integrity / transit corruption).
|
||||||
|
const std::string actual = sha256Hex(bytes.data(), bytes.size());
|
||||||
|
const auto it = checksums.find(toLower(asset.name)); // keys are lowercased
|
||||||
|
if (it == checksums.end()) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "No published checksum for " + asset.name + " — refusing to install.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actual != it->second) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "Archive checksum mismatch — refusing to install (possible tampering).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2b. Detached ed25519 signature (authenticity) against the pinned key.
|
||||||
|
const std::string pubKey = kDaemonSignaturePublicKeyBase64;
|
||||||
|
if (!pubKey.empty()) {
|
||||||
|
const int sigIdx = selectDaemonSignatureAsset(rel, asset.name);
|
||||||
|
if (sigIdx < 0) {
|
||||||
|
if (kDaemonRequireSignature) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "No signature published for this release — refusing to install.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DEBUG_LOGF("[daemon-updater] no signature asset for %s; proceeding on checksum only\n",
|
||||||
|
asset.name.c_str());
|
||||||
|
} else {
|
||||||
|
setProgress(State::Verifying, "Verifying signature…");
|
||||||
|
const std::string sigContent = httpGet(rel.assets[sigIdx].downloadUrl);
|
||||||
|
if (sigContent.empty() || !verifyXmrigSignature(bytes, sigContent, pubKey)) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "Signature verification failed — refusing to install.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (kDaemonRequireSignature) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "No signing key is pinned in this build — refusing to install.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Extract the daemon binaries (flatten the versioned subdir). No per-member hash check is
|
||||||
|
// needed: the whole archive was already verified above (SHA-256 + ed25519 signature), so
|
||||||
|
// every member is authentic by transitivity. The archive also carries params/asmap, which
|
||||||
|
// this updater deliberately leaves to the wallet's own resource extraction.
|
||||||
|
setProgress(State::Extracting, "Installing node…");
|
||||||
|
const std::vector<std::string> wanted = daemonExtractBasenames(token);
|
||||||
|
const std::string daemonName = wanted.front(); // "dragonxd" / "dragonxd.exe"
|
||||||
|
|
||||||
|
mz_zip_archive zip{};
|
||||||
|
if (!mz_zip_reader_init_file(&zip, zipPath.c_str(), 0)) {
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
setProgress(State::Failed, "Could not open the downloaded archive.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool daemonInstalled = false;
|
||||||
|
bool failed = false;
|
||||||
|
const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip));
|
||||||
|
for (int i = 0; i < numFiles && !failed; ++i) {
|
||||||
|
mz_zip_archive_file_stat st;
|
||||||
|
if (!mz_zip_reader_file_stat(&zip, i, &st)) continue;
|
||||||
|
if (mz_zip_reader_is_file_a_directory(&zip, i)) continue;
|
||||||
|
const std::string base = baseName(st.m_filename);
|
||||||
|
if (std::find(wanted.begin(), wanted.end(), base) == wanted.end()) continue; // skip params/asmap/etc.
|
||||||
|
|
||||||
|
// Reject an implausibly large member before decompressing it into memory.
|
||||||
|
if (st.m_uncomp_size > kMaxMemberBytes) { failed = true; break; }
|
||||||
|
|
||||||
|
size_t outSize = 0;
|
||||||
|
void* mem = mz_zip_reader_extract_to_heap(&zip, i, &outSize, 0);
|
||||||
|
if (!mem) { failed = true; break; }
|
||||||
|
|
||||||
|
const std::string finalPath = (fs::path(targetDir) / base).string();
|
||||||
|
const std::string tmpPath = finalPath + ".tmp";
|
||||||
|
// Tidy any leftover from a previous update (the old binary moved aside, freed after restart).
|
||||||
|
fs::remove(finalPath + ".old", ec);
|
||||||
|
{
|
||||||
|
std::ofstream of(tmpPath, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!of) { mz_free(mem); failed = true; break; }
|
||||||
|
of.write(static_cast<const char*>(mem), static_cast<std::streamsize>(outSize));
|
||||||
|
}
|
||||||
|
mz_free(mem);
|
||||||
|
|
||||||
|
// Atomic install that also works while the daemon is running: POSIX rename() replaces the
|
||||||
|
// path even if the old binary is in use (the running process keeps the unlinked inode). On
|
||||||
|
// Windows a running .exe cannot be renamed-over, so move it aside to ".old" first, then put
|
||||||
|
// the new file in place; the ".old" is cleaned up on a later update once the daemon restarts.
|
||||||
|
fs::rename(tmpPath, finalPath, ec);
|
||||||
|
if (ec) {
|
||||||
|
std::error_code mec;
|
||||||
|
fs::rename(finalPath, finalPath + ".old", mec);
|
||||||
|
ec.clear();
|
||||||
|
fs::rename(tmpPath, finalPath, ec);
|
||||||
|
}
|
||||||
|
if (ec) { fs::remove(tmpPath, ec); failed = true; break; }
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Every wanted member is an executable in the daemon set — make them all runnable.
|
||||||
|
fs::permissions(finalPath,
|
||||||
|
fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec |
|
||||||
|
fs::perms::others_read | fs::perms::others_exec,
|
||||||
|
fs::perm_options::replace, ec);
|
||||||
|
#endif
|
||||||
|
if (base == daemonName) daemonInstalled = true;
|
||||||
|
}
|
||||||
|
mz_zip_reader_end(&zip);
|
||||||
|
fs::remove(zipPath, ec);
|
||||||
|
|
||||||
|
if (failed) { setProgress(State::Failed, "Could not verify/install the node binaries."); return; }
|
||||||
|
if (!daemonInstalled) { setProgress(State::Failed, "Daemon binary not found in the archive."); return; }
|
||||||
|
|
||||||
|
setProgress(State::Done, "Node installed (" + rel.tag + ").");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace dragonx
|
||||||
189
src/util/daemon_updater.h
Normal file
189
src/util/daemon_updater.h
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// DaemonUpdater — fetch + verify + install the latest dragonxd full node from the DragonX Gitea.
|
||||||
|
//
|
||||||
|
// Sibling of util::XmrigUpdater (the miner updater): same query → download → verify → extract →
|
||||||
|
// install pipeline on a background thread, but for the full-node binaries instead of the miner.
|
||||||
|
// Flow: query the Gitea releases API for the latest dragonx release, pick the archive matching this
|
||||||
|
// platform (dragonx-<ver>-linux-amd64.zip / -win64.zip / -macos.zip), download it, verify it, then
|
||||||
|
// extract the daemon executables (dragonxd / dragonx-cli / dragonx-tx, flattening the versioned
|
||||||
|
// subdir the archive nests them in) into the daemon directory and chmod them executable.
|
||||||
|
//
|
||||||
|
// Security: download-and-execute, so verification is mandatory. TLS is verified (libcurl defaults),
|
||||||
|
// the host is the project's own Gitea over HTTPS, the archive's SHA-256 is checked against the
|
||||||
|
// checksum table published in the release body, AND a detached ed25519 signature over the archive
|
||||||
|
// bytes is verified against the key pinned below (kDaemonRequireSignature enforces it: an install
|
||||||
|
// is refused when no valid .sig is published). The inner binaries are trusted by transitivity once
|
||||||
|
// the whole archive is authenticated. The generic SHA-256 / ed25519 primitives are shared with the
|
||||||
|
// miner updater (util::sha256Hex / util::verifyXmrigSignature in xmrig_updater_core.cpp).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
struct DaemonReleaseAsset {
|
||||||
|
std::string name;
|
||||||
|
std::string downloadUrl;
|
||||||
|
long long size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DaemonRelease {
|
||||||
|
bool ok = false;
|
||||||
|
std::string tag; // e.g. "v1.0.2"
|
||||||
|
std::string name; // human title (e.g. "Dragonx v1.0.2")
|
||||||
|
std::string body; // release notes markdown (holds the checksum table)
|
||||||
|
bool prerelease = false; // marked pre-release on the Gitea release
|
||||||
|
std::string publishedAt; // ISO-8601 publish timestamp (date shown in the UI)
|
||||||
|
std::vector<DaemonReleaseAsset> assets;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Release signature (supply-chain hardening, mandatory for the daemon) ──────
|
||||||
|
//
|
||||||
|
// Pinned ed25519 public key (base64, 32 bytes) used to verify a detached signature over the
|
||||||
|
// downloaded archive. With kDaemonRequireSignature=true an install is refused unless the release
|
||||||
|
// publishes a "<archive-name>.sig" asset (base64 of, or raw, a 64-byte ed25519 signature over the
|
||||||
|
// archive bytes) that verifies against this key — see scripts/sign-daemon-release.sh. To rotate the
|
||||||
|
// key, regenerate with that script and replace the value below. If the key is set empty AND
|
||||||
|
// kDaemonRequireSignature=false, verification falls back to TLS + the published SHA-256 only.
|
||||||
|
inline constexpr const char* kDaemonSignaturePublicKeyBase64 =
|
||||||
|
"d59hosHzh2LtHypgGvsMBlgrRGBhOfS0Xl40D/fEjzQ=";
|
||||||
|
inline constexpr bool kDaemonRequireSignature = true; // enforced: refuse installs without a valid .sig
|
||||||
|
|
||||||
|
// ── Pure helpers (no I/O; unit-tested) ───────────────────────────────────────
|
||||||
|
|
||||||
|
// Parse the Gitea GET /releases/latest JSON into a DaemonRelease (ok=false + error on failure).
|
||||||
|
DaemonRelease parseDaemonRelease(const std::string& json);
|
||||||
|
|
||||||
|
// Parse the Gitea GET /releases (array) JSON into the list of releases, newest first, skipping
|
||||||
|
// drafts. Empty on parse failure. Lets the user browse + pin a specific (or pre-release) version.
|
||||||
|
std::vector<DaemonRelease> parseDaemonReleaseList(const std::string& json);
|
||||||
|
|
||||||
|
// The asset-name token for the host platform: "linux-amd64", "win64", "macos", or "" if
|
||||||
|
// unknown/unsupported (e.g. linux-arm64, for which no build is published -> Unavailable).
|
||||||
|
std::string currentDaemonPlatformToken();
|
||||||
|
|
||||||
|
// Index of the asset whose name matches the platform token (e.g. ends with "-linux-amd64.zip"),
|
||||||
|
// or -1 if none.
|
||||||
|
int selectDaemonAsset(const DaemonRelease& release, const std::string& platformToken);
|
||||||
|
|
||||||
|
// Parse the release-body markdown checksum table ( "| <archive>.zip | `<sha256hex>` |" rows ) into
|
||||||
|
// { archive-name -> lowercase-hex }. Header/separator/prose rows (no 64-hex token) are ignored.
|
||||||
|
std::map<std::string, std::string> parseDaemonChecksums(const std::string& body);
|
||||||
|
|
||||||
|
// The binary file basenames to extract for a platform: {"dragonxd","dragonx-cli","dragonx-tx"} on
|
||||||
|
// POSIX, the ".exe" variants on Windows. The first entry is always the daemon itself.
|
||||||
|
std::vector<std::string> daemonExtractBasenames(const std::string& platformToken);
|
||||||
|
|
||||||
|
// Index of the detached-signature asset for a given archive (name "<archive>.sig" or
|
||||||
|
// "<archive>.minisig"), or -1 if none is published.
|
||||||
|
int selectDaemonSignatureAsset(const DaemonRelease& release, const std::string& archiveName);
|
||||||
|
|
||||||
|
// Reduce a version string to its "vMAJOR.MINOR.PATCH" core, dropping any "-<commit>" suffix the
|
||||||
|
// binary scanner appends, so an installed "v1.0.2-ddd851dc1" compares equal to a release "v1.0.2".
|
||||||
|
// Returns the input unchanged if it holds no vN.N.N pattern.
|
||||||
|
std::string daemonVersionCore(const std::string& version);
|
||||||
|
|
||||||
|
// ── Background worker ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class DaemonUpdater {
|
||||||
|
public:
|
||||||
|
enum class State {
|
||||||
|
Idle,
|
||||||
|
Checking,
|
||||||
|
UpToDate,
|
||||||
|
UpdateAvailable,
|
||||||
|
Unavailable, // no daemon build is published for this platform (terminal, not an error)
|
||||||
|
Listing, // fetching the full release list (Browse all releases)
|
||||||
|
ReleaseList, // release list fetched; awaiting the user's pick
|
||||||
|
Downloading,
|
||||||
|
Verifying,
|
||||||
|
Extracting,
|
||||||
|
Done,
|
||||||
|
Failed
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Progress {
|
||||||
|
State state = State::Idle;
|
||||||
|
double downloaded_bytes = 0;
|
||||||
|
double total_bytes = 0;
|
||||||
|
float percent = 0.0f;
|
||||||
|
std::string status_text;
|
||||||
|
std::string error; // non-empty on Failed
|
||||||
|
std::string latest_tag; // tag reported by the API (once checked)
|
||||||
|
std::string installed_tag; // caller-supplied current install (for update detection)
|
||||||
|
bool update_available = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gitea releases API for the dragonx full node.
|
||||||
|
static constexpr const char* kApiUrl =
|
||||||
|
"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest";
|
||||||
|
// Full release list (newest first, includes pre-releases) for the "Browse all releases" picker.
|
||||||
|
static constexpr const char* kReleasesUrl =
|
||||||
|
"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases?limit=50";
|
||||||
|
|
||||||
|
DaemonUpdater() = default;
|
||||||
|
~DaemonUpdater();
|
||||||
|
DaemonUpdater(const DaemonUpdater&) = delete;
|
||||||
|
DaemonUpdater& operator=(const DaemonUpdater&) = delete;
|
||||||
|
|
||||||
|
// Query the latest release on a background thread. `installedVersion` (may be empty/unknown) is
|
||||||
|
// compared (by vN.N.N core) to the API tag to set Progress.update_available. End state:
|
||||||
|
// UpToDate / UpdateAvailable / Failed.
|
||||||
|
void startCheck(const std::string& installedVersion);
|
||||||
|
|
||||||
|
// Download → verify archive → extract (flatten) → install into `targetDir` on a background
|
||||||
|
// thread. Re-fetches the latest release so it is self-contained. End state: Done / Failed. The
|
||||||
|
// new binary takes effect on the next daemon start (the caller offers a restart).
|
||||||
|
void startInstall(const std::string& targetDir);
|
||||||
|
|
||||||
|
// Fetch the full release list on a background thread (for "Browse all releases"). End state:
|
||||||
|
// ReleaseList (then getReleases() holds the list, newest first) / Failed.
|
||||||
|
void startListReleases();
|
||||||
|
|
||||||
|
// Snapshot of the release list fetched by startListReleases().
|
||||||
|
std::vector<DaemonRelease> getReleases() const;
|
||||||
|
|
||||||
|
// Install a SPECIFIC release (chosen from the browse list) into `targetDir` — same verify/extract
|
||||||
|
// path as startInstall, but pinned to `release` instead of latest. End state: Done / Failed.
|
||||||
|
void startInstallRelease(const std::string& targetDir, DaemonRelease release);
|
||||||
|
|
||||||
|
void cancel();
|
||||||
|
Progress getProgress() const;
|
||||||
|
bool isDone() const; // true once the worker reached a terminal state (Done/Failed/Unavailable)
|
||||||
|
|
||||||
|
// Internal: called by the libcurl progress callback. Publishes live download bytes and returns
|
||||||
|
// false to ask curl to abort (when cancel() was requested). Public only so the C callback in the
|
||||||
|
// .cpp can reach it without leaking curl types into this header.
|
||||||
|
bool onDownloadProgress(double downloadedBytes, double totalBytes);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void runCheck(std::string installedVersion);
|
||||||
|
void runListReleases();
|
||||||
|
void runInstall(std::string targetDir);
|
||||||
|
void installResolved(const std::string& targetDir, const DaemonRelease& rel); // shared install body
|
||||||
|
void setProgress(State state, const std::string& text, double done = 0, double total = 0);
|
||||||
|
bool downloadToFile(const std::string& url, const std::string& destPath);
|
||||||
|
std::string httpGet(const std::string& url);
|
||||||
|
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
Progress progress_;
|
||||||
|
std::vector<DaemonRelease> releases_;
|
||||||
|
std::atomic<bool> cancel_requested_{false};
|
||||||
|
std::atomic<bool> worker_running_{false};
|
||||||
|
std::thread worker_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace dragonx
|
||||||
206
src/util/daemon_updater_core.cpp
Normal file
206
src/util/daemon_updater_core.cpp
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Pure (no-I/O) core of the daemon updater: release-JSON parsing, platform/asset matching, the
|
||||||
|
// markdown checksum-table parser, version normalization, and signature-asset selection. Split from
|
||||||
|
// daemon_updater.cpp (the libcurl/miniz worker) so it can be unit-tested without curl/miniz. The
|
||||||
|
// generic SHA-256 / ed25519 primitives are reused from the miner updater (xmrig_updater_core.cpp).
|
||||||
|
|
||||||
|
#include "daemon_updater.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string toLower(std::string s)
|
||||||
|
{
|
||||||
|
std::transform(s.begin(), s.end(), s.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isHex64(const std::string& s)
|
||||||
|
{
|
||||||
|
if (s.size() != 64) return false;
|
||||||
|
for (unsigned char c : s)
|
||||||
|
if (!std::isxdigit(c)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Fill a DaemonRelease from one Gitea release JSON object (ok=true iff it has a tag).
|
||||||
|
DaemonRelease parseOneDaemonRelease(const json& j)
|
||||||
|
{
|
||||||
|
DaemonRelease r;
|
||||||
|
if (!j.is_object()) return r;
|
||||||
|
if (j.contains("tag_name") && j["tag_name"].is_string())
|
||||||
|
r.tag = j["tag_name"].get<std::string>();
|
||||||
|
if (j.contains("name") && j["name"].is_string())
|
||||||
|
r.name = j["name"].get<std::string>();
|
||||||
|
if (j.contains("body") && j["body"].is_string())
|
||||||
|
r.body = j["body"].get<std::string>();
|
||||||
|
if (j.contains("prerelease") && j["prerelease"].is_boolean())
|
||||||
|
r.prerelease = j["prerelease"].get<bool>();
|
||||||
|
if (j.contains("published_at") && j["published_at"].is_string())
|
||||||
|
r.publishedAt = j["published_at"].get<std::string>();
|
||||||
|
if (j.contains("assets") && j["assets"].is_array()) {
|
||||||
|
for (const auto& a : j["assets"]) {
|
||||||
|
if (!a.is_object()) continue;
|
||||||
|
DaemonReleaseAsset asset;
|
||||||
|
if (a.contains("name") && a["name"].is_string())
|
||||||
|
asset.name = a["name"].get<std::string>();
|
||||||
|
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
|
||||||
|
asset.downloadUrl = a["browser_download_url"].get<std::string>();
|
||||||
|
if (a.contains("size") && a["size"].is_number_integer())
|
||||||
|
asset.size = a["size"].get<long long>();
|
||||||
|
if (!asset.name.empty() && !asset.downloadUrl.empty())
|
||||||
|
r.assets.push_back(std::move(asset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!r.tag.empty()) r.ok = true;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DaemonRelease parseDaemonRelease(const std::string& jsonStr)
|
||||||
|
{
|
||||||
|
DaemonRelease r;
|
||||||
|
try {
|
||||||
|
r = parseOneDaemonRelease(json::parse(jsonStr));
|
||||||
|
if (!r.ok) r.error = "release JSON has no tag_name";
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
r.error = std::string("failed to parse release JSON: ") + e.what();
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DaemonRelease> parseDaemonReleaseList(const std::string& jsonStr)
|
||||||
|
{
|
||||||
|
std::vector<DaemonRelease> out;
|
||||||
|
try {
|
||||||
|
const json j = json::parse(jsonStr);
|
||||||
|
if (!j.is_array()) return out;
|
||||||
|
for (const auto& e : j) {
|
||||||
|
if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get<bool>())
|
||||||
|
continue; // skip drafts (not meant for end users)
|
||||||
|
DaemonRelease r = parseOneDaemonRelease(e);
|
||||||
|
if (r.ok) out.push_back(std::move(r));
|
||||||
|
}
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// malformed list -> empty (caller treats as "could not load releases")
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string currentDaemonPlatformToken()
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return "win64";
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
return "macos"; // single macOS archive (no arm/x86 split in the release naming)
|
||||||
|
#elif defined(__linux__)
|
||||||
|
#if defined(__aarch64__)
|
||||||
|
return "linux-arm64"; // no arm64 build published yet -> resolves to Unavailable
|
||||||
|
#else
|
||||||
|
return "linux-amd64";
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return "";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int selectDaemonAsset(const DaemonRelease& release, const std::string& platformToken)
|
||||||
|
{
|
||||||
|
if (platformToken.empty()) return -1;
|
||||||
|
const std::string needle = "-" + toLower(platformToken) + ".zip";
|
||||||
|
for (std::size_t i = 0; i < release.assets.size(); ++i) {
|
||||||
|
const std::string n = toLower(release.assets[i].name);
|
||||||
|
if (n.size() >= needle.size() &&
|
||||||
|
n.compare(n.size() - needle.size(), needle.size(), needle) == 0)
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, std::string> parseDaemonChecksums(const std::string& body)
|
||||||
|
{
|
||||||
|
// The release body publishes checksums as a markdown table:
|
||||||
|
// | File | SHA-256 |
|
||||||
|
// |------|---------|
|
||||||
|
// | dragonx-1.0.2-linux-amd64.zip | `85f1dd…16` |
|
||||||
|
// Per line: blank out the table/code delimiters ('|' and '`'), then find the 64-hex token (the
|
||||||
|
// hash) and a token ending in ".zip" (the archive name). Header/separator/prose rows lack one
|
||||||
|
// or the other and are skipped, so this is robust to surrounding text and column order.
|
||||||
|
std::map<std::string, std::string> out;
|
||||||
|
std::istringstream in(body);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
for (char& c : line)
|
||||||
|
if (c == '|' || c == '`') c = ' ';
|
||||||
|
std::istringstream ls(line);
|
||||||
|
std::string tok, hash, name;
|
||||||
|
while (ls >> tok) {
|
||||||
|
if (hash.empty() && isHex64(tok)) { hash = toLower(tok); continue; }
|
||||||
|
if (name.empty()) {
|
||||||
|
const std::string low = toLower(tok);
|
||||||
|
// Key by the lowercased name so the lookup (also lowercased) is case-insensitive,
|
||||||
|
// in case the markdown table and the JSON asset names differ in case.
|
||||||
|
if (low.size() >= 4 && low.compare(low.size() - 4, 4, ".zip") == 0) name = low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hash.empty() && !name.empty()) out[name] = hash;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> daemonExtractBasenames(const std::string& platformToken)
|
||||||
|
{
|
||||||
|
if (platformToken.rfind("win", 0) == 0)
|
||||||
|
return {"dragonxd.exe", "dragonx-cli.exe", "dragonx-tx.exe"};
|
||||||
|
return {"dragonxd", "dragonx-cli", "dragonx-tx"};
|
||||||
|
}
|
||||||
|
|
||||||
|
int selectDaemonSignatureAsset(const DaemonRelease& release, const std::string& archiveName)
|
||||||
|
{
|
||||||
|
if (archiveName.empty()) return -1;
|
||||||
|
for (std::size_t i = 0; i < release.assets.size(); ++i) {
|
||||||
|
const std::string& n = release.assets[i].name;
|
||||||
|
if (n == archiveName + ".sig" || n == archiveName + ".minisig")
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string daemonVersionCore(const std::string& version)
|
||||||
|
{
|
||||||
|
// Extract a leading "v?MAJOR.MINOR.PATCH" run, ignoring any "-<commit>" / build suffix. Hand-
|
||||||
|
// rolled (no <regex>) to stay light and dependency-free.
|
||||||
|
const std::size_t start = version.find_first_of("0123456789");
|
||||||
|
if (start == std::string::npos) return version;
|
||||||
|
std::size_t i = start;
|
||||||
|
int dots = 0;
|
||||||
|
for (; i < version.size(); ++i) {
|
||||||
|
const char c = version[i];
|
||||||
|
if (c >= '0' && c <= '9') continue;
|
||||||
|
if (c == '.' && dots < 2) { ++dots; continue; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (dots < 2) return version; // not a full N.N.N — leave as-is
|
||||||
|
const bool hasV = start > 0 && (version[start - 1] == 'v' || version[start - 1] == 'V');
|
||||||
|
return (hasV ? "v" : "") + version.substr(start, i - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace dragonx
|
||||||
@@ -418,6 +418,27 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["confirm_rescan_title"] = "Rescan Blockchain";
|
strings_["confirm_rescan_title"] = "Rescan Blockchain";
|
||||||
strings_["confirm_rescan_msg"] = "This restarts the daemon and re-scans the entire blockchain for your wallet's transactions. It can take a long time and the wallet stays offline until it finishes.";
|
strings_["confirm_rescan_msg"] = "This restarts the daemon and re-scans the entire blockchain for your wallet's transactions. It can take a long time and the wallet stays offline until it finishes.";
|
||||||
strings_["confirm_rescan_safe"] = "Your wallet.dat and blockchain data are not deleted — only re-scanned.";
|
strings_["confirm_rescan_safe"] = "Your wallet.dat and blockchain data are not deleted — only re-scanned.";
|
||||||
|
strings_["rescan_detecting"] = "Checking which blocks your node has on disk…";
|
||||||
|
strings_["rescan_bootstrapped_msg"] = "Your node was bootstrapped, so blocks below the snapshot aren't on disk and a rescan from genesis would fail. Rescan from a height your snapshot includes to reconcile your wallet's spent balance. Your wallet.dat and chain data are not deleted.";
|
||||||
|
strings_["rescan_from_height"] = "Rescan from block height:";
|
||||||
|
strings_["repair_wallet"] = "Repair Wallet";
|
||||||
|
strings_["tt_repair_wallet"] = "Wipe and rebuild the wallet's transaction records from the blockchain (fixes notes that fail to send after a rescan)";
|
||||||
|
strings_["confirm_repair_wallet_title"] = "Repair Wallet";
|
||||||
|
strings_["confirm_repair_wallet_msg"] = "This restarts the daemon with -zapwallettxes=2: it deletes all of the wallet's transaction and note records, then rebuilds them from the blockchain. Use this when transactions fail to build (\"Invalid sapling spend proof\" / \"shielded requirements not met\") even after a full rescan. It takes a long time and the wallet stays offline until it finishes.";
|
||||||
|
strings_["confirm_repair_wallet_safe"] = "Your keys, addresses and balance are preserved — only the cached transaction records are rebuilt.";
|
||||||
|
strings_["daemon_binary"] = "Daemon binary";
|
||||||
|
strings_["daemon_installed"] = "Installed";
|
||||||
|
strings_["daemon_bundled"] = "Bundled";
|
||||||
|
strings_["daemon_not_installed"] = "not installed";
|
||||||
|
strings_["daemon_none_bundled"] = "none in this build";
|
||||||
|
strings_["daemon_status_match"] = "Installed binary matches the bundled version.";
|
||||||
|
strings_["daemon_status_differ"] = "Installed binary differs from the bundled version.";
|
||||||
|
strings_["daemon_status_missing"] = "No daemon installed — install the bundled version.";
|
||||||
|
strings_["daemon_install_bundled"] = "Install bundled";
|
||||||
|
strings_["tt_daemon_install_bundled"] = "Stop the node, overwrite the installed dragonxd with the version bundled in this wallet build, then restart";
|
||||||
|
strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon";
|
||||||
|
strings_["confirm_reinstall_daemon_msg"] = "This stops the daemon, overwrites the installed dragonxd (and dragonx-cli/dragonx-tx) with the versions bundled in this wallet build, then restarts the node. Use this to recover or update the node binary.";
|
||||||
|
strings_["confirm_reinstall_daemon_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced.";
|
||||||
strings_["confirm_restart_daemon_title"] = "Restart Daemon";
|
strings_["confirm_restart_daemon_title"] = "Restart Daemon";
|
||||||
strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect.";
|
strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect.";
|
||||||
strings_["lite_maintenance"] = "Maintenance";
|
strings_["lite_maintenance"] = "Maintenance";
|
||||||
@@ -796,6 +817,9 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)";
|
strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)";
|
||||||
strings_["sb_rescanning_pct"] = "Rescanning %.0f%%";
|
strings_["sb_rescanning_pct"] = "Rescanning %.0f%%";
|
||||||
strings_["sb_rescanning"] = "Rescanning";
|
strings_["sb_rescanning"] = "Rescanning";
|
||||||
|
strings_["sb_building_witnesses_pct"] = "Rebuilding witnesses %.0f%%";
|
||||||
|
strings_["sb_building_witnesses"] = "Setting witnesses";
|
||||||
|
strings_["sb_witness_cache"] = "Rebuilding witnesses";
|
||||||
strings_["sb_importing_keys"] = "Importing keys";
|
strings_["sb_importing_keys"] = "Importing keys";
|
||||||
strings_["sb_daemon_not_found"] = "Daemon not found";
|
strings_["sb_daemon_not_found"] = "Daemon not found";
|
||||||
strings_["sb_loading_config"] = "Loading configuration...";
|
strings_["sb_loading_config"] = "Loading configuration...";
|
||||||
@@ -1142,6 +1166,45 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["xmrig_installed_ok"] = "Miner installed";
|
strings_["xmrig_installed_ok"] = "Miner installed";
|
||||||
strings_["xmrig_update_failed"] = "Update failed";
|
strings_["xmrig_update_failed"] = "Update failed";
|
||||||
strings_["xmrig_unknown_error"] = "Unknown error.";
|
strings_["xmrig_unknown_error"] = "Unknown error.";
|
||||||
|
strings_["xmrig_browse_releases"] = "Browse all releases…";
|
||||||
|
strings_["xmrig_loading_releases"] = "Loading releases…";
|
||||||
|
|
||||||
|
// --- Shared release picker ("Browse all releases") ---
|
||||||
|
strings_["upd_select_version"] = "Select a version to install";
|
||||||
|
strings_["upd_install"] = "Install";
|
||||||
|
strings_["upd_reinstall"] = "Reinstall";
|
||||||
|
strings_["upd_prerelease"] = "pre-release";
|
||||||
|
strings_["upd_installed_badge"] = "installed";
|
||||||
|
strings_["upd_no_build_platform"] = "No build for this platform";
|
||||||
|
strings_["upd_back"] = "Back";
|
||||||
|
|
||||||
|
// --- Daemon (full node) updater — Settings → daemon binary panel ---
|
||||||
|
strings_["daemon_update_check"] = "Check for updates…";
|
||||||
|
strings_["tt_daemon_update_check"] = "Download and verify the latest dragonxd full node from the project Gitea, then restart to apply";
|
||||||
|
strings_["daemon_update_title"] = "Update Node";
|
||||||
|
strings_["daemon_update_checking"] = "Checking for the latest node…";
|
||||||
|
strings_["daemon_update_unavailable_title"]= "Node updates unavailable";
|
||||||
|
strings_["daemon_update_unavailable_body"] = "No node build is available for this platform.";
|
||||||
|
strings_["daemon_update_available"] = "A new node version is available";
|
||||||
|
strings_["daemon_update_up_to_date"] = "The node is up to date";
|
||||||
|
strings_["daemon_update_latest"] = "Latest:";
|
||||||
|
strings_["daemon_update_installed"] = "Installed:";
|
||||||
|
strings_["daemon_update_version"] = "Version:";
|
||||||
|
strings_["daemon_update_verify_note"] = "The download is verified against the release's published SHA-256 and a pinned ed25519 signature before install.";
|
||||||
|
strings_["daemon_update_download_install"] = "Download & install";
|
||||||
|
strings_["daemon_update_reinstall"] = "Reinstall";
|
||||||
|
strings_["daemon_update_downloading"] = "Downloading…";
|
||||||
|
strings_["daemon_update_verifying"] = "Verifying…";
|
||||||
|
strings_["daemon_update_installing"] = "Installing…";
|
||||||
|
strings_["daemon_update_installed_ok"] = "Node installed";
|
||||||
|
strings_["daemon_update_restart_note"] = "Restart the daemon to start running the new version.";
|
||||||
|
strings_["daemon_update_restart_now"] = "Restart daemon now";
|
||||||
|
strings_["daemon_update_later"] = "Later";
|
||||||
|
strings_["daemon_update_failed"] = "Update failed";
|
||||||
|
strings_["daemon_update_unknown_error"] = "Unknown error.";
|
||||||
|
strings_["daemon_update_browse"] = "Browse all releases…";
|
||||||
|
strings_["daemon_update_loading"] = "Loading releases…";
|
||||||
|
strings_["daemon_update_downgrade_note"] = "Older versions may be incompatible with your current chain data. Installing a different version takes effect after a daemon restart.";
|
||||||
|
|
||||||
// --- Lite Network tab (server browser) ---
|
// --- Lite Network tab (server browser) ---
|
||||||
strings_["lite_console_title"] = "Console";
|
strings_["lite_console_title"] = "Console";
|
||||||
@@ -1293,6 +1356,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_tx_sent"] = "Transaction sent!";
|
strings_["send_tx_sent"] = "Transaction sent!";
|
||||||
strings_["send_tx_success"] = "Transaction sent successfully!";
|
strings_["send_tx_success"] = "Transaction sent successfully!";
|
||||||
strings_["send_status_unconfirmed"] = "Transaction status could not be confirmed";
|
strings_["send_status_unconfirmed"] = "Transaction status could not be confirmed";
|
||||||
|
strings_["send_err_needs_rescan"] = "Your wallet's shielded note data is out of date with the blockchain (this happens after a bootstrap or reindex). Run a full rescan via Settings -> Rescan Blockchain and let it finish completely, then try sending again.";
|
||||||
strings_["send_txid_copied"] = "TxID copied to clipboard";
|
strings_["send_txid_copied"] = "TxID copied to clipboard";
|
||||||
strings_["send_txid_label"] = "TxID: %s";
|
strings_["send_txid_label"] = "TxID: %s";
|
||||||
strings_["send_valid_shielded"] = "Valid shielded address";
|
strings_["send_valid_shielded"] = "Valid shielded address";
|
||||||
|
|||||||
@@ -194,10 +194,70 @@ void XmrigUpdater::startInstall(const std::string& targetDir)
|
|||||||
progress_.state = State::Downloading;
|
progress_.state = State::Downloading;
|
||||||
progress_.error.clear();
|
progress_.error.clear();
|
||||||
progress_.status_text = "Preparing…";
|
progress_.status_text = "Preparing…";
|
||||||
|
progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0%
|
||||||
|
progress_.total_bytes = 0;
|
||||||
|
progress_.percent = 0.0f;
|
||||||
}
|
}
|
||||||
worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; });
|
worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void XmrigUpdater::startListReleases()
|
||||||
|
{
|
||||||
|
if (worker_running_.exchange(true)) return;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.state = State::Listing;
|
||||||
|
progress_.error.clear();
|
||||||
|
progress_.status_text = "Loading releases…";
|
||||||
|
progress_.downloaded_bytes = 0; // clear any prior op's progress
|
||||||
|
progress_.total_bytes = 0;
|
||||||
|
progress_.percent = 0.0f;
|
||||||
|
}
|
||||||
|
worker_ = std::thread([this] { runListReleases(); worker_running_ = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void XmrigUpdater::startInstallRelease(const std::string& targetDir, XmrigRelease release)
|
||||||
|
{
|
||||||
|
if (worker_running_.exchange(true)) return;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
if (worker_.joinable()) worker_.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
progress_.state = State::Downloading;
|
||||||
|
progress_.error.clear();
|
||||||
|
progress_.status_text = "Preparing…";
|
||||||
|
progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0%
|
||||||
|
progress_.total_bytes = 0;
|
||||||
|
progress_.percent = 0.0f;
|
||||||
|
}
|
||||||
|
worker_ = std::thread([this, targetDir, release = std::move(release)] {
|
||||||
|
installResolved(targetDir, release);
|
||||||
|
worker_running_ = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<XmrigRelease> XmrigUpdater::getReleases() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
return releases_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XmrigUpdater::runListReleases()
|
||||||
|
{
|
||||||
|
const std::string body = httpGet(kReleasesUrl);
|
||||||
|
if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
|
||||||
|
std::vector<XmrigRelease> list = parseXmrigReleaseList(body);
|
||||||
|
const bool empty = list.empty();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
releases_ = std::move(list);
|
||||||
|
}
|
||||||
|
if (empty) { setProgress(State::Failed, "No releases found."); return; }
|
||||||
|
setProgress(State::ReleaseList, "Select a version to install.");
|
||||||
|
}
|
||||||
|
|
||||||
void XmrigUpdater::runCheck(std::string installedTag)
|
void XmrigUpdater::runCheck(std::string installedTag)
|
||||||
{
|
{
|
||||||
const std::string body = httpGet(kApiUrl);
|
const std::string body = httpGet(kApiUrl);
|
||||||
@@ -235,7 +295,11 @@ void XmrigUpdater::runInstall(std::string targetDir)
|
|||||||
if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
|
if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
|
||||||
const XmrigRelease rel = parseXmrigRelease(apiBody);
|
const XmrigRelease rel = parseXmrigRelease(apiBody);
|
||||||
if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
|
if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
|
||||||
|
installResolved(targetDir, rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRelease& rel)
|
||||||
|
{
|
||||||
const std::string token = currentXmrigPlatformToken();
|
const std::string token = currentXmrigPlatformToken();
|
||||||
const int idx = selectXmrigAsset(rel, token);
|
const int idx = selectXmrigAsset(rel, token);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ struct XmrigReleaseAsset {
|
|||||||
struct XmrigRelease {
|
struct XmrigRelease {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
std::string tag; // e.g. "v1.0.0"
|
std::string tag; // e.g. "v1.0.0"
|
||||||
|
std::string name; // human title (e.g. "DRG-XMRig v6.25.3")
|
||||||
std::string body; // release notes markdown (holds the checksum blocks)
|
std::string body; // release notes markdown (holds the checksum blocks)
|
||||||
|
bool prerelease = false; // marked pre-release on the Gitea release
|
||||||
|
std::string publishedAt; // ISO-8601 publish timestamp (date shown in the UI)
|
||||||
std::vector<XmrigReleaseAsset> assets;
|
std::vector<XmrigReleaseAsset> assets;
|
||||||
std::string error;
|
std::string error;
|
||||||
};
|
};
|
||||||
@@ -61,6 +64,10 @@ inline constexpr bool kXmrigRequireSignature = true; // enforced: refus
|
|||||||
// Parse the Gitea GET /releases/latest JSON into an XmrigRelease (ok=false + error on failure).
|
// Parse the Gitea GET /releases/latest JSON into an XmrigRelease (ok=false + error on failure).
|
||||||
XmrigRelease parseXmrigRelease(const std::string& json);
|
XmrigRelease parseXmrigRelease(const std::string& json);
|
||||||
|
|
||||||
|
// Parse the Gitea GET /releases (array) JSON into the list of releases, newest first, skipping
|
||||||
|
// drafts. Empty on parse failure. Lets the user browse + pin a specific (or pre-release) version.
|
||||||
|
std::vector<XmrigRelease> parseXmrigReleaseList(const std::string& json);
|
||||||
|
|
||||||
// The asset-name token for the host platform: "linux-x64", "win-x64", "macos-x64",
|
// The asset-name token for the host platform: "linux-x64", "win-x64", "macos-x64",
|
||||||
// "macos-arm64", or "" if unknown/unsupported.
|
// "macos-arm64", or "" if unknown/unsupported.
|
||||||
std::string currentXmrigPlatformToken();
|
std::string currentXmrigPlatformToken();
|
||||||
@@ -102,6 +109,8 @@ public:
|
|||||||
UpToDate,
|
UpToDate,
|
||||||
UpdateAvailable,
|
UpdateAvailable,
|
||||||
Unavailable, // no miner build is published for this platform (terminal, not an error)
|
Unavailable, // no miner build is published for this platform (terminal, not an error)
|
||||||
|
Listing, // fetching the full release list (Browse all releases)
|
||||||
|
ReleaseList, // release list fetched; awaiting the user's pick
|
||||||
Downloading,
|
Downloading,
|
||||||
Verifying,
|
Verifying,
|
||||||
Extracting,
|
Extracting,
|
||||||
@@ -124,6 +133,9 @@ public:
|
|||||||
// Gitea releases API for the DRG-XMRig fork.
|
// Gitea releases API for the DRG-XMRig fork.
|
||||||
static constexpr const char* kApiUrl =
|
static constexpr const char* kApiUrl =
|
||||||
"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest";
|
"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest";
|
||||||
|
// Full release list (newest first, includes pre-releases) for the "Browse all releases" picker.
|
||||||
|
static constexpr const char* kReleasesUrl =
|
||||||
|
"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases?limit=50";
|
||||||
|
|
||||||
XmrigUpdater() = default;
|
XmrigUpdater() = default;
|
||||||
~XmrigUpdater();
|
~XmrigUpdater();
|
||||||
@@ -136,10 +148,21 @@ public:
|
|||||||
void startCheck(const std::string& installedTag);
|
void startCheck(const std::string& installedTag);
|
||||||
|
|
||||||
// Download → verify archive → extract (flatten) → verify binary → install into `targetDir` on a
|
// Download → verify archive → extract (flatten) → verify binary → install into `targetDir` on a
|
||||||
// background thread. Re-fetches the release so it is self-contained. End state: Done / Failed.
|
// background thread. Re-fetches the latest release so it is self-contained. End state: Done /
|
||||||
// On Done, getProgress().latest_tag is the version that should be persisted as the installed tag.
|
// Failed. On Done, getProgress().latest_tag is the version that should be persisted as installed.
|
||||||
void startInstall(const std::string& targetDir);
|
void startInstall(const std::string& targetDir);
|
||||||
|
|
||||||
|
// Fetch the full release list on a background thread (for "Browse all releases"). End state:
|
||||||
|
// ReleaseList (then getReleases() holds the list, newest first) / Failed.
|
||||||
|
void startListReleases();
|
||||||
|
|
||||||
|
// Snapshot of the release list fetched by startListReleases().
|
||||||
|
std::vector<XmrigRelease> getReleases() const;
|
||||||
|
|
||||||
|
// Install a SPECIFIC release (chosen from the browse list) into `targetDir` — same verify/extract
|
||||||
|
// path as startInstall, but pinned to `release` instead of latest. End state: Done / Failed.
|
||||||
|
void startInstallRelease(const std::string& targetDir, XmrigRelease release);
|
||||||
|
|
||||||
void cancel();
|
void cancel();
|
||||||
Progress getProgress() const;
|
Progress getProgress() const;
|
||||||
bool isDone() const; // true once the worker reached a terminal state (Done/Failed/Unavailable)
|
bool isDone() const; // true once the worker reached a terminal state (Done/Failed/Unavailable)
|
||||||
@@ -151,13 +174,16 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void runCheck(std::string installedTag);
|
void runCheck(std::string installedTag);
|
||||||
|
void runListReleases();
|
||||||
void runInstall(std::string targetDir);
|
void runInstall(std::string targetDir);
|
||||||
|
void installResolved(const std::string& targetDir, const XmrigRelease& rel); // shared install body
|
||||||
void setProgress(State state, const std::string& text, double done = 0, double total = 0);
|
void setProgress(State state, const std::string& text, double done = 0, double total = 0);
|
||||||
bool downloadToFile(const std::string& url, const std::string& destPath);
|
bool downloadToFile(const std::string& url, const std::string& destPath);
|
||||||
std::string httpGet(const std::string& url);
|
std::string httpGet(const std::string& url);
|
||||||
|
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
Progress progress_;
|
Progress progress_;
|
||||||
|
std::vector<XmrigRelease> releases_;
|
||||||
std::atomic<bool> cancel_requested_{false};
|
std::atomic<bool> cancel_requested_{false};
|
||||||
std::atomic<bool> worker_running_{false};
|
std::atomic<bool> worker_running_{false};
|
||||||
std::thread worker_;
|
std::thread worker_;
|
||||||
|
|||||||
@@ -40,37 +40,71 @@ bool isHex64(const std::string& s)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Fill an XmrigRelease from one Gitea release JSON object (ok=true iff it has a tag).
|
||||||
|
XmrigRelease parseOneXmrigRelease(const json& j)
|
||||||
|
{
|
||||||
|
XmrigRelease r;
|
||||||
|
if (!j.is_object()) return r;
|
||||||
|
if (j.contains("tag_name") && j["tag_name"].is_string())
|
||||||
|
r.tag = j["tag_name"].get<std::string>();
|
||||||
|
if (j.contains("name") && j["name"].is_string())
|
||||||
|
r.name = j["name"].get<std::string>();
|
||||||
|
if (j.contains("body") && j["body"].is_string())
|
||||||
|
r.body = j["body"].get<std::string>();
|
||||||
|
if (j.contains("prerelease") && j["prerelease"].is_boolean())
|
||||||
|
r.prerelease = j["prerelease"].get<bool>();
|
||||||
|
if (j.contains("published_at") && j["published_at"].is_string())
|
||||||
|
r.publishedAt = j["published_at"].get<std::string>();
|
||||||
|
if (j.contains("assets") && j["assets"].is_array()) {
|
||||||
|
for (const auto& a : j["assets"]) {
|
||||||
|
if (!a.is_object()) continue;
|
||||||
|
XmrigReleaseAsset asset;
|
||||||
|
if (a.contains("name") && a["name"].is_string())
|
||||||
|
asset.name = a["name"].get<std::string>();
|
||||||
|
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
|
||||||
|
asset.downloadUrl = a["browser_download_url"].get<std::string>();
|
||||||
|
if (a.contains("size") && a["size"].is_number_integer())
|
||||||
|
asset.size = a["size"].get<long long>();
|
||||||
|
if (!asset.name.empty() && !asset.downloadUrl.empty())
|
||||||
|
r.assets.push_back(std::move(asset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!r.tag.empty()) r.ok = true;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
XmrigRelease parseXmrigRelease(const std::string& jsonStr)
|
XmrigRelease parseXmrigRelease(const std::string& jsonStr)
|
||||||
{
|
{
|
||||||
XmrigRelease r;
|
XmrigRelease r;
|
||||||
try {
|
try {
|
||||||
const json j = json::parse(jsonStr);
|
r = parseOneXmrigRelease(json::parse(jsonStr));
|
||||||
if (j.contains("tag_name") && j["tag_name"].is_string())
|
if (!r.ok) r.error = "release JSON has no tag_name";
|
||||||
r.tag = j["tag_name"].get<std::string>();
|
|
||||||
if (j.contains("body") && j["body"].is_string())
|
|
||||||
r.body = j["body"].get<std::string>();
|
|
||||||
if (j.contains("assets") && j["assets"].is_array()) {
|
|
||||||
for (const auto& a : j["assets"]) {
|
|
||||||
if (!a.is_object()) continue;
|
|
||||||
XmrigReleaseAsset asset;
|
|
||||||
if (a.contains("name") && a["name"].is_string())
|
|
||||||
asset.name = a["name"].get<std::string>();
|
|
||||||
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
|
|
||||||
asset.downloadUrl = a["browser_download_url"].get<std::string>();
|
|
||||||
if (a.contains("size") && a["size"].is_number_integer())
|
|
||||||
asset.size = a["size"].get<long long>();
|
|
||||||
if (!asset.name.empty() && !asset.downloadUrl.empty())
|
|
||||||
r.assets.push_back(std::move(asset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (r.tag.empty()) { r.error = "release JSON has no tag_name"; return r; }
|
|
||||||
r.ok = true;
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
r.error = std::string("failed to parse release JSON: ") + e.what();
|
r.error = std::string("failed to parse release JSON: ") + e.what();
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<XmrigRelease> parseXmrigReleaseList(const std::string& jsonStr)
|
||||||
|
{
|
||||||
|
std::vector<XmrigRelease> out;
|
||||||
|
try {
|
||||||
|
const json j = json::parse(jsonStr);
|
||||||
|
if (!j.is_array()) return out;
|
||||||
|
for (const auto& e : j) {
|
||||||
|
if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get<bool>())
|
||||||
|
continue; // skip drafts (not meant for end users)
|
||||||
|
XmrigRelease r = parseOneXmrigRelease(e);
|
||||||
|
if (r.ok) out.push_back(std::move(r));
|
||||||
|
}
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// malformed list -> empty (caller treats as "could not load releases")
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
std::string currentXmrigPlatformToken()
|
std::string currentXmrigPlatformToken()
|
||||||
{
|
{
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
|
|||||||
1
tests/fixtures/daemon/release_latest.json
vendored
Normal file
1
tests/fixtures/daemon/release_latest.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":15,"tag_name":"v1.0.2","target_commitish":"dragonx","name":"Dragonx v1.0.2","body":"## What is DragonX?\r\n\r\nDragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. It uses the\r\nSapling protocol for shielded transactions and enforces mandatory z2z (shielded-to-shielded)\r\ntransactions after block 340,000, meaning funds can only be sent to shielded z-addresses at\r\nthe consensus level.\r\n\r\n### Bug Fixes\r\n * Fix sapling pool persistence — pool total no longer resets to 0 on node restart. Explorer nodes should reindex once after upgrading.\r\n\r\n### New Features\r\n * Add `subsidy` and `fees` fields to the `getblock` RPC response so explorers can display the correct 3 DRGX block reward separately from fees\r\n\r\n### Key Features\r\n\r\n * **RandomX Proof-of-Work** — CPU-mineable, ASIC-resistant mining algorithm\r\n * **Sapling zk-SNARKs** — zero-knowledge proofs for fully private transactions\r\n * **Mandatory shielded transactions** — z2z enforced at consensus after block 340,000\r\n * **Encrypted P2P** — all connections secured with TLS 1.3 via WolfSSL (AES-256-GCM and ChaCha20-Poly1305)\r\n * **Anonymous networking** — built-in Tor, i2p, and cjdns support\r\n * **Passive network spy protection** — prevents ISPs and observers from identifying transaction origins\r\n\r\n## Checksums\r\n\r\n| File | SHA-256 |\r\n|------|---------|\r\n| dragonx-1.0.2-linux-amd64.zip | `85f1dd908bfbdee6aaebabdc74848dc8963d45e7510172d84d0230c0fad6cc16` |\r\n| dragonx-1.0.2-macos.zip | `102e1f1ecab05def25465ad4867b19d46c7cc00a9fbb018ffb405bcb82cc6432` |\r\n| dragonx-1.0.2-win64.zip | `dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a` |\r\n\r\n## License\r\n\r\nDragonX is released under the [GNU General Public License v3 (GPLv3)](https://git.dragonx.is/DragonX/dragonx/src/branch/dragonx/COPYING).\r\n\r\nCopyright © 2024-2026 The DragonX Developers\r\n","url":"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/15","html_url":"https://git.dragonx.is/DragonX/dragonx/releases/tag/v1.0.2","tarball_url":"https://git.dragonx.is/DragonX/dragonx/archive/v1.0.2.tar.gz","zipball_url":"https://git.dragonx.is/DragonX/dragonx/archive/v1.0.2.zip","upload_url":"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/15/assets","draft":false,"prerelease":false,"created_at":"2026-03-19T10:09:18-05:00","published_at":"2026-03-19T10:09:18-05:00","author":{"id":1,"login":"DanS","login_name":"","source_id":0,"full_name":"","email":"dans@noreply.localhost","avatar_url":"https://git.dragonx.is/avatars/ac3f843e96162174082a0b0e2b02b4d894ea793139c2e181e5971483e233a946","html_url":"https://git.dragonx.is/DanS","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-02-27T13:12:08-06:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":1,"username":"DanS"},"assets":[{"id":53,"name":"dragonx-1.0.2-linux-amd64.zip","size":61792264,"download_count":98,"created_at":"2026-03-19T10:21:03-05:00","uuid":"ff53b136-e128-438b-82d8-76b9f33b830a","browser_download_url":"https://git.dragonx.is/DragonX/dragonx/releases/download/v1.0.2/dragonx-1.0.2-linux-amd64.zip"},{"id":52,"name":"dragonx-1.0.2-macos.zip","size":59382241,"download_count":35,"created_at":"2026-03-19T10:21:03-05:00","uuid":"aefc7f79-ae87-4da9-a168-8b731166c080","browser_download_url":"https://git.dragonx.is/DragonX/dragonx/releases/download/v1.0.2/dragonx-1.0.2-macos.zip"},{"id":54,"name":"dragonx-1.0.2-win64.zip","size":62453466,"download_count":150,"created_at":"2026-03-19T10:21:06-05:00","uuid":"ab44576e-e354-4019-8f5a-a5536257e854","browser_download_url":"https://git.dragonx.is/DragonX/dragonx/releases/download/v1.0.2/dragonx-1.0.2-win64.zip"}]}
|
||||||
1
tests/fixtures/daemon/releases_list.json
vendored
Normal file
1
tests/fixtures/daemon/releases_list.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/xmrig/releases_list.json
vendored
Normal file
1
tests/fixtures/xmrig/releases_list.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@
|
|||||||
#include "util/payment_uri.h"
|
#include "util/payment_uri.h"
|
||||||
#include "util/platform.h"
|
#include "util/platform.h"
|
||||||
#include "util/xmrig_updater.h"
|
#include "util/xmrig_updater.h"
|
||||||
|
#include "util/daemon_updater.h"
|
||||||
#include "util/lite_server_probe.h"
|
#include "util/lite_server_probe.h"
|
||||||
#include "wallet/lite_connection_service.h"
|
#include "wallet/lite_connection_service.h"
|
||||||
#include "wallet/lite_diagnostics.h"
|
#include "wallet/lite_diagnostics.h"
|
||||||
@@ -4604,6 +4605,190 @@ void testXmrigSignatureVerify()
|
|||||||
EXPECT_FALSE(verifyXmrigSignature(data, "", pkB64));
|
EXPECT_FALSE(verifyXmrigSignature(data, "", pkB64));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reads any fixture file by repo-relative path under tests/fixtures/.
|
||||||
|
static std::string readFixtureFile(const std::string& rel)
|
||||||
|
{
|
||||||
|
std::ifstream f(std::string(DRAGONX_TEST_FIXTURE_DIR) + "/" + rel, std::ios::binary);
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
void testXmrigReleaseListParsing()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
const auto list = parseXmrigReleaseList(readFixtureFile("xmrig/releases_list.json"));
|
||||||
|
EXPECT_TRUE(list.size() >= 2);
|
||||||
|
EXPECT_EQ(list.front().tag, std::string("v6.25.3")); // newest first (Gitea order preserved)
|
||||||
|
for (const auto& r : list) {
|
||||||
|
EXPECT_TRUE(r.ok);
|
||||||
|
EXPECT_TRUE(!r.tag.empty());
|
||||||
|
EXPECT_TRUE(!r.assets.empty());
|
||||||
|
}
|
||||||
|
// Fail closed on non-array / garbage.
|
||||||
|
EXPECT_TRUE(parseXmrigReleaseList("not json").empty());
|
||||||
|
EXPECT_TRUE(parseXmrigReleaseList("{}").empty());
|
||||||
|
// Drafts are skipped; the pre-release flag is captured.
|
||||||
|
const std::string inj = R"([
|
||||||
|
{"tag_name":"v9.9.9","prerelease":true,"assets":[{"name":"a-linux-x64.zip","browser_download_url":"https://x/a","size":1}]},
|
||||||
|
{"tag_name":"v9.9.8","draft":true,"assets":[]}
|
||||||
|
])";
|
||||||
|
const auto injList = parseXmrigReleaseList(inj);
|
||||||
|
EXPECT_EQ(injList.size(), static_cast<std::size_t>(1)); // draft dropped
|
||||||
|
EXPECT_EQ(injList.front().tag, std::string("v9.9.9"));
|
||||||
|
EXPECT_TRUE(injList.front().prerelease);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── daemon updater pure core (util/daemon_updater.h) — driven by the real v1.0.2 release fixture ──
|
||||||
|
static std::string readDaemonFixture()
|
||||||
|
{
|
||||||
|
return readFixtureFile("daemon/release_latest.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonReleaseParsing()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
const auto rel = parseDaemonRelease(readDaemonFixture());
|
||||||
|
EXPECT_TRUE(rel.ok);
|
||||||
|
EXPECT_EQ(rel.tag, std::string("v1.0.2"));
|
||||||
|
EXPECT_EQ(rel.assets.size(), static_cast<std::size_t>(3)); // linux-amd64 / macos / win64
|
||||||
|
for (const auto& a : rel.assets) {
|
||||||
|
EXPECT_TRUE(!a.name.empty());
|
||||||
|
EXPECT_TRUE(a.downloadUrl.rfind("https://", 0) == 0);
|
||||||
|
EXPECT_TRUE(a.size > 0);
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(parseDaemonRelease("not json at all").ok);
|
||||||
|
EXPECT_FALSE(parseDaemonRelease("{}").ok); // no tag_name
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonAssetSelection()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
const auto rel = parseDaemonRelease(readDaemonFixture());
|
||||||
|
const int lin = selectDaemonAsset(rel, "linux-amd64");
|
||||||
|
const int mac = selectDaemonAsset(rel, "macos");
|
||||||
|
const int win = selectDaemonAsset(rel, "win64");
|
||||||
|
EXPECT_TRUE(lin >= 0 && mac >= 0 && win >= 0);
|
||||||
|
EXPECT_TRUE(lin != mac && mac != win && lin != win);
|
||||||
|
EXPECT_TRUE(rel.assets[lin].name.find("linux-amd64.zip") != std::string::npos);
|
||||||
|
EXPECT_TRUE(rel.assets[mac].name.find("macos.zip") != std::string::npos);
|
||||||
|
EXPECT_TRUE(rel.assets[win].name.find("win64.zip") != std::string::npos);
|
||||||
|
// Wrong/foreign tokens (e.g. the miner's naming) must NOT match the daemon archives.
|
||||||
|
EXPECT_EQ(selectDaemonAsset(rel, "linux-x64"), -1);
|
||||||
|
EXPECT_EQ(selectDaemonAsset(rel, "linux-arm64"), -1);
|
||||||
|
EXPECT_EQ(selectDaemonAsset(rel, ""), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonChecksumParsing()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
const auto rel = parseDaemonRelease(readDaemonFixture());
|
||||||
|
const auto sums = parseDaemonChecksums(rel.body);
|
||||||
|
// The markdown checksum table ( "| <archive>.zip | `<sha256>` |" ) is parsed by archive name.
|
||||||
|
EXPECT_EQ(sums.at("dragonx-1.0.2-linux-amd64.zip"),
|
||||||
|
std::string("85f1dd908bfbdee6aaebabdc74848dc8963d45e7510172d84d0230c0fad6cc16"));
|
||||||
|
EXPECT_EQ(sums.at("dragonx-1.0.2-macos.zip"),
|
||||||
|
std::string("102e1f1ecab05def25465ad4867b19d46c7cc00a9fbb018ffb405bcb82cc6432"));
|
||||||
|
EXPECT_EQ(sums.at("dragonx-1.0.2-win64.zip"),
|
||||||
|
std::string("dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a"));
|
||||||
|
// Header/separator/prose rows are ignored.
|
||||||
|
EXPECT_TRUE(parseDaemonChecksums("| File | SHA-256 |\n|---|---|\njust prose, no hashes").empty());
|
||||||
|
// Keys are lowercased so the (also-lowercased) lookup is case-insensitive vs the JSON names.
|
||||||
|
const auto mixed = parseDaemonChecksums(
|
||||||
|
"| DragonX-1.0.2-Win64.ZIP | `dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a` |");
|
||||||
|
EXPECT_EQ(mixed.at("dragonx-1.0.2-win64.zip"),
|
||||||
|
std::string("dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonBasenamesAndVersionCore()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
const auto posix = daemonExtractBasenames("linux-amd64");
|
||||||
|
EXPECT_EQ(posix.size(), static_cast<std::size_t>(3));
|
||||||
|
EXPECT_EQ(posix.front(), std::string("dragonxd")); // daemon binary is always first
|
||||||
|
EXPECT_TRUE(std::find(posix.begin(), posix.end(), std::string("dragonx-cli")) != posix.end());
|
||||||
|
EXPECT_TRUE(std::find(posix.begin(), posix.end(), std::string("dragonx-tx")) != posix.end());
|
||||||
|
const auto win = daemonExtractBasenames("win64");
|
||||||
|
EXPECT_EQ(win.front(), std::string("dragonxd.exe"));
|
||||||
|
EXPECT_TRUE(std::find(win.begin(), win.end(), std::string("dragonx-cli.exe")) != win.end());
|
||||||
|
|
||||||
|
// Version-core normalization: a scanned "vX.Y.Z-<commit>" must compare equal to a release tag.
|
||||||
|
EXPECT_EQ(daemonVersionCore("v1.0.2-ddd851dc1"), std::string("v1.0.2"));
|
||||||
|
EXPECT_EQ(daemonVersionCore("v1.0.2"), std::string("v1.0.2"));
|
||||||
|
EXPECT_EQ(daemonVersionCore("1.0.2"), std::string("1.0.2"));
|
||||||
|
EXPECT_EQ(daemonVersionCore("DragonX v1.0.2 (abc)"), std::string("v1.0.2"));
|
||||||
|
EXPECT_EQ(daemonVersionCore("v1.2"), std::string("v1.2")); // not full N.N.N -> unchanged
|
||||||
|
EXPECT_EQ(daemonVersionCore(""), std::string(""));
|
||||||
|
EXPECT_EQ(daemonVersionCore("nightly"), std::string("nightly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonSignatureAssetSelection()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
DaemonRelease rel; rel.ok = true; rel.tag = "v1.0.2";
|
||||||
|
rel.assets.push_back({"dragonx-1.0.2-linux-amd64.zip", "https://x/zip", 100});
|
||||||
|
EXPECT_EQ(selectDaemonSignatureAsset(rel, "dragonx-1.0.2-linux-amd64.zip"), -1); // none published
|
||||||
|
rel.assets.push_back({"dragonx-1.0.2-linux-amd64.zip.sig", "https://x/sig", 64});
|
||||||
|
EXPECT_TRUE(selectDaemonSignatureAsset(rel, "dragonx-1.0.2-linux-amd64.zip") >= 0);
|
||||||
|
EXPECT_EQ(selectDaemonSignatureAsset(rel, "other.zip"), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonPinnedKeyValidity()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
// The daemon updater REQUIRES signatures, so the pinned key must be a valid 32-byte ed25519 key.
|
||||||
|
const std::string pin = kDaemonSignaturePublicKeyBase64;
|
||||||
|
EXPECT_TRUE(kDaemonRequireSignature);
|
||||||
|
EXPECT_TRUE(!pin.empty());
|
||||||
|
EXPECT_TRUE(sodium_init() >= 0);
|
||||||
|
unsigned char pk[crypto_sign_PUBLICKEYBYTES];
|
||||||
|
std::size_t n = 0; const char* end = nullptr;
|
||||||
|
const int rc = sodium_base642bin(pk, sizeof(pk), pin.data(), pin.size(), " \t\r\n",
|
||||||
|
&n, &end, sodium_base64_VARIANT_ORIGINAL);
|
||||||
|
EXPECT_EQ(rc, 0);
|
||||||
|
EXPECT_EQ(n, static_cast<std::size_t>(crypto_sign_PUBLICKEYBYTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonSignatureInterop()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
// Known answer produced by scripts/sign-daemon-release.sh (OpenSSL ed25519) over the message
|
||||||
|
// below with the pinned key's secret half. Proves the pinned key + the release-signing flow
|
||||||
|
// produce signatures the wallet's libsodium verifier accepts (closes the OpenSSL<->libsodium
|
||||||
|
// interop loop), and that the pinned public key actually matches the signing key.
|
||||||
|
const std::string msg = "dragonx daemon archive payload bytes for signing test";
|
||||||
|
const std::string sigB64 =
|
||||||
|
"rmQ5qOw+W8vu56GeZrooD7Wh1N/WHRP4siD19Mxq/8WXQQuNrFY3DPCNU9C7jHB2jg/VfKrLVna57K/lkSDBDA==";
|
||||||
|
const std::string pin = kDaemonSignaturePublicKeyBase64;
|
||||||
|
EXPECT_TRUE(verifyXmrigSignature(msg, sigB64, pin));
|
||||||
|
EXPECT_TRUE(verifyXmrigSignature(msg, " " + sigB64 + "\n", pin)); // trailing newline tolerated
|
||||||
|
// Fails closed on tampered payload or wrong key.
|
||||||
|
EXPECT_FALSE(verifyXmrigSignature(msg + "x", sigB64, pin));
|
||||||
|
EXPECT_FALSE(verifyXmrigSignature(msg, sigB64, std::string(kXmrigSignaturePublicKeyBase64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDaemonReleaseListParsing()
|
||||||
|
{
|
||||||
|
using namespace dragonx::util;
|
||||||
|
const auto list = parseDaemonReleaseList(readFixtureFile("daemon/releases_list.json"));
|
||||||
|
EXPECT_TRUE(list.size() >= 2);
|
||||||
|
EXPECT_EQ(list.front().tag, std::string("v1.0.2")); // newest first
|
||||||
|
for (const auto& r : list) {
|
||||||
|
EXPECT_TRUE(r.ok);
|
||||||
|
EXPECT_TRUE(!r.assets.empty());
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(parseDaemonReleaseList("not json").empty());
|
||||||
|
EXPECT_TRUE(parseDaemonReleaseList("{}").empty());
|
||||||
|
// Draft skipped; pre-release + name + published_at captured.
|
||||||
|
const std::string inj = R"([
|
||||||
|
{"tag_name":"v2.0.0","prerelease":true,"name":"RC","published_at":"2026-09-01T00:00:00Z","assets":[{"name":"dragonx-2.0.0-linux-amd64.zip","browser_download_url":"https://x/a","size":1}]},
|
||||||
|
{"tag_name":"v1.9.9","draft":true,"assets":[]}
|
||||||
|
])";
|
||||||
|
const auto injList = parseDaemonReleaseList(inj);
|
||||||
|
EXPECT_EQ(injList.size(), static_cast<std::size_t>(1));
|
||||||
|
EXPECT_TRUE(injList.front().prerelease);
|
||||||
|
EXPECT_EQ(injList.front().name, std::string("RC"));
|
||||||
|
EXPECT_EQ(injList.front().publishedAt.substr(0, 10), std::string("2026-09-01"));
|
||||||
|
}
|
||||||
|
|
||||||
// Live end-to-end exercise of the XmrigUpdater WORKER (real network + curl + miniz). Env-gated so
|
// Live end-to-end exercise of the XmrigUpdater WORKER (real network + curl + miniz). Env-gated so
|
||||||
// CI / offline runs skip it; run with DRAGONX_TEST_NETWORK=1 to hit git.dragonx.is. Verifies the
|
// CI / offline runs skip it; run with DRAGONX_TEST_NETWORK=1 to hit git.dragonx.is. Verifies the
|
||||||
// full download -> archive-checksum -> extract/flatten -> inner-binary-checksum -> install path.
|
// full download -> archive-checksum -> extract/flatten -> inner-binary-checksum -> install path.
|
||||||
@@ -4707,6 +4892,15 @@ int main()
|
|||||||
testXmrigSignatureAssetSelection();
|
testXmrigSignatureAssetSelection();
|
||||||
testXmrigPinnedKeyValidity();
|
testXmrigPinnedKeyValidity();
|
||||||
testXmrigSignatureVerify();
|
testXmrigSignatureVerify();
|
||||||
|
testXmrigReleaseListParsing();
|
||||||
|
testDaemonReleaseParsing();
|
||||||
|
testDaemonAssetSelection();
|
||||||
|
testDaemonChecksumParsing();
|
||||||
|
testDaemonBasenamesAndVersionCore();
|
||||||
|
testDaemonSignatureAssetSelection();
|
||||||
|
testDaemonPinnedKeyValidity();
|
||||||
|
testDaemonSignatureInterop();
|
||||||
|
testDaemonReleaseListParsing();
|
||||||
testLiteServerHostParsing();
|
testLiteServerHostParsing();
|
||||||
testLiteOfficialServerDetection();
|
testLiteOfficialServerDetection();
|
||||||
testAtomicFileWrite();
|
testAtomicFileWrite();
|
||||||
|
|||||||
2
third_party/silentdragonxlite/lib/.cargo/config.toml
vendored
Normal file
2
third_party/silentdragonxlite/lib/.cargo/config.toml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[net]
|
||||||
|
git-fetch-with-cli = true
|
||||||
2
third_party/silentdragonxlite/lib/.gitignore
vendored
Normal file
2
third_party/silentdragonxlite/lib/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target/
|
||||||
|
|
||||||
2751
third_party/silentdragonxlite/lib/Cargo.lock
generated
vendored
Normal file
2751
third_party/silentdragonxlite/lib/Cargo.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
16
third_party/silentdragonxlite/lib/Cargo.toml
vendored
Normal file
16
third_party/silentdragonxlite/lib/Cargo.toml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "qtlib"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["zecwallet", "The Hush Developers"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "silentdragonxlite"
|
||||||
|
crate-type = ["staticlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libc = "0.2.58"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
blake3 = "0.3.4"
|
||||||
|
silentdragonxlitelib = { path = "../silentdragonxlite-cli/lib" }
|
||||||
|
socket2 = "0.3.11"
|
||||||
28
third_party/silentdragonxlite/lib/Makefile
vendored
Normal file
28
third_party/silentdragonxlite/lib/Makefile
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
ifeq ($(shell uname),Darwin)
|
||||||
|
EXT := dylib
|
||||||
|
CFLAGS := "-mmacosx-version-min=10.11"
|
||||||
|
else
|
||||||
|
EXT := a
|
||||||
|
CFLAGS :=
|
||||||
|
endif
|
||||||
|
|
||||||
|
PWD := $(shell pwd)
|
||||||
|
|
||||||
|
all: release
|
||||||
|
|
||||||
|
winrelease: target/x86_64-pc-windows-gnu/release/silentdragonxlite.lib
|
||||||
|
|
||||||
|
target/x86_64-pc-windows-gnu/release/silentdragonxlite.lib: src/lib.rs Cargo.toml
|
||||||
|
SODIUM_LIB_DIR="$(PWD)/libsodium-mingw/" cargo build --lib --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
release: target/release/silentdragonxlite.$(EXT)
|
||||||
|
debug: target/debug/silentdragonxlite.$(EXT)
|
||||||
|
|
||||||
|
target/release/silentdragonxlite.$(EXT): src/lib.rs Cargo.toml
|
||||||
|
LIBS="" CFLAGS=$(CFLAGS) cargo build --lib --release
|
||||||
|
|
||||||
|
target/debug/silentdragonxlite.$(EXT): src/lib.rs Cargo.toml
|
||||||
|
LIBS="" CFLAGS=$(CFLAGS) cargo build --lib
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf target
|
||||||
BIN
third_party/silentdragonxlite/lib/libsodium-mingw/libsodium.a
vendored
Normal file
BIN
third_party/silentdragonxlite/lib/libsodium-mingw/libsodium.a
vendored
Normal file
Binary file not shown.
29
third_party/silentdragonxlite/lib/silentdragonxlitelib.h
vendored
Normal file
29
third_party/silentdragonxlite/lib/silentdragonxlitelib.h
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef _hush_PAPER_RUST_H
|
||||||
|
#define _hush_PAPER_RUST_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern bool litelib_wallet_exists (const char* chain_name);
|
||||||
|
extern char * litelib_initialize_new (bool dangerous, const char* server);
|
||||||
|
extern char * litelib_initialize_new_from_phrase
|
||||||
|
(bool dangerous, const char* server, const char* seed,
|
||||||
|
unsigned long long birthday, unsigned long long number,
|
||||||
|
bool overwrite);
|
||||||
|
extern char * litelib_initialize_existing (bool dangerous,const char* server);
|
||||||
|
extern char * litelib_execute (const char* s, const char* args);
|
||||||
|
extern void litelib_rust_free_string (char* s);
|
||||||
|
extern char * blake3_PW (char* pw);
|
||||||
|
extern bool litelib_check_server_online (const char* server);
|
||||||
|
extern void litelib_shutdown (void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// This is a function implemented in connection.cpp that will process a string response from
|
||||||
|
// the litelib and turn into into a QString in a memory-safe way.
|
||||||
|
QString litelib_process_response(char* resp);
|
||||||
|
|
||||||
|
#endif
|
||||||
328
third_party/silentdragonxlite/lib/src/lib.rs
vendored
Normal file
328
third_party/silentdragonxlite/lib/src/lib.rs
vendored
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
use libc::{c_char};
|
||||||
|
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::sync::{Mutex, Arc};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ptr;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
use silentdragonxlitelib::{commands, lightclient::{LightClient, LightClientConfig}};
|
||||||
|
|
||||||
|
/// Helper to create a CString, replacing null bytes to avoid panics
|
||||||
|
fn safe_cstring(s: &str) -> CString {
|
||||||
|
let cleaned: String = s.replace('\0', "");
|
||||||
|
CString::new(cleaned).unwrap_or_else(|_| CString::new("Error: failed to create CString").unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create an error CString
|
||||||
|
fn error_cstring(msg: &str) -> *mut c_char {
|
||||||
|
safe_cstring(&format!("Error: {}", msg)).into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll use a MUTEX to store a global lightclient instance,
|
||||||
|
// so we don't have to keep creating it. We need to store it here, in rust
|
||||||
|
// because we can't return such a complex structure back to C++
|
||||||
|
lazy_static! {
|
||||||
|
static ref LIGHTCLIENT: Mutex<RefCell<Option<Arc<LightClient>>>> = Mutex::new(RefCell::new(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is an existing wallet
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern fn litelib_wallet_exists(chain_name: *const c_char) -> bool {
|
||||||
|
let chain_name_str = unsafe {
|
||||||
|
assert!(!chain_name.is_null());
|
||||||
|
|
||||||
|
CStr::from_ptr(chain_name).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = LightClientConfig::create_unconnected(chain_name_str, None);
|
||||||
|
|
||||||
|
println!("Wallet exists: {}", config.wallet_exists());
|
||||||
|
config.wallet_exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
//////hash blake3
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern fn blake3_PW(pw: *const c_char) -> *mut c_char{
|
||||||
|
|
||||||
|
let passwd = unsafe {
|
||||||
|
assert!(!pw.is_null());
|
||||||
|
|
||||||
|
CStr::from_ptr(pw).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = passwd.as_bytes();
|
||||||
|
// Hash an input all at once.
|
||||||
|
let hash1 = blake3::hash(data).to_hex();
|
||||||
|
// This is sensitive metadata, do not log it to stdout
|
||||||
|
//println!("\nBlake3 Hash: {}", hash1);
|
||||||
|
println!("\nBlake3 Hash calculated");
|
||||||
|
|
||||||
|
//let sttring = CString::new(hash1).unwrap();
|
||||||
|
let e_str = CString::new(format!("{}", hash1)).unwrap();
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new wallet and return the seed for the newly created wallet.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern fn litelib_initialize_new(dangerous: bool,server: *const c_char) -> *mut c_char {
|
||||||
|
let server_str = unsafe {
|
||||||
|
assert!(!server.is_null());
|
||||||
|
|
||||||
|
CStr::from_ptr(server).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = LightClientConfig::get_server_or_default(Some(server_str));
|
||||||
|
let (config, latest_block_height) = match LightClientConfig::create(server, dangerous) {
|
||||||
|
Ok((c, h)) => (c, h),
|
||||||
|
Err(e) => {
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap();
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lightclient = match LightClient::new(&config, latest_block_height) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap();
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
let _ = lightclient.init_logging();
|
||||||
|
|
||||||
|
let seed = match lightclient.do_seed_phrase() {
|
||||||
|
Ok(s) => s.dump(),
|
||||||
|
Err(e) => {
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap();
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lc = Arc::new(lightclient);
|
||||||
|
match LightClient::start_mempool_monitor(lc.clone()) {
|
||||||
|
Ok(_) => {println!("Starting Mempool")},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Couldnt start mempool {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match LIGHTCLIENT.lock() {
|
||||||
|
Ok(l) => { l.replace(Some(lc)); },
|
||||||
|
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the wallet's seed
|
||||||
|
let s_str = safe_cstring(&seed);
|
||||||
|
return s_str.into_raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore a wallet from the seed phrase
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn litelib_initialize_new_from_phrase(dangerous: bool, server: *const c_char,
|
||||||
|
seed: *const c_char, birthday: u64, number: u64, overwrite: bool) -> *mut c_char {
|
||||||
|
if server.is_null() || seed.is_null() {
|
||||||
|
println!("Server or seed is null");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_str = unsafe {
|
||||||
|
CStr::from_ptr(server).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
let seed_str = unsafe {
|
||||||
|
CStr::from_ptr(seed).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
//println!("Initializing with server: {}, seed: {}", server_str, seed_str);
|
||||||
|
|
||||||
|
// Shut down the existing client if one is running, to stop background threads
|
||||||
|
if overwrite {
|
||||||
|
let old_lc = match LIGHTCLIENT.lock() {
|
||||||
|
Ok(l) => l.borrow().clone(),
|
||||||
|
Err(poisoned) => poisoned.into_inner().borrow().clone(),
|
||||||
|
};
|
||||||
|
if let Some(lc) = old_lc {
|
||||||
|
lc.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = LightClientConfig::get_server_or_default(Some(server_str));
|
||||||
|
let (config, _latest_block_height) = match LightClientConfig::create(server, dangerous) {
|
||||||
|
Ok((c, h)) => {
|
||||||
|
println!("Config created successfully");
|
||||||
|
(c, h)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error creating config: {}", e);
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap_or_else(|_| CString::new("Error creating CString").unwrap());
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lightclient = match LightClient::new_from_phrase(seed_str, &config, birthday, number, overwrite) {
|
||||||
|
Ok(l) => {
|
||||||
|
println!("LightClient created successfully");
|
||||||
|
l
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error creating LightClient: {}", e);
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap_or_else(|_| CString::new("Error creating CString").unwrap());
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
let _ = lightclient.init_logging();
|
||||||
|
|
||||||
|
let lc = Arc::new(lightclient);
|
||||||
|
match LightClient::start_mempool_monitor(lc.clone()) {
|
||||||
|
Ok(_) => println!("Starting Mempool"),
|
||||||
|
Err(e) => println!("Could not start mempool: {}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
match LIGHTCLIENT.lock() {
|
||||||
|
Ok(l) => { l.replace(Some(lc)); },
|
||||||
|
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
|
||||||
|
};
|
||||||
|
|
||||||
|
let c_str = safe_cstring("OK");
|
||||||
|
return c_str.into_raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a new lightclient and store its value
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern fn litelib_initialize_existing(dangerous: bool, server: *const c_char) -> *mut c_char {
|
||||||
|
let server_str = unsafe {
|
||||||
|
assert!(!server.is_null());
|
||||||
|
|
||||||
|
CStr::from_ptr(server).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = LightClientConfig::get_server_or_default(Some(server_str));
|
||||||
|
let (config, _latest_block_height) = match LightClientConfig::create(server,dangerous) {
|
||||||
|
Ok((c, h)) => (c, h),
|
||||||
|
Err(e) => {
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap();
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lightclient = match LightClient::read_from_disk(&config) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
let e_str = CString::new(format!("Error: {}", e)).unwrap();
|
||||||
|
return e_str.into_raw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
let _ = lightclient.init_logging();
|
||||||
|
|
||||||
|
let lc = Arc::new(lightclient);
|
||||||
|
match LightClient::start_mempool_monitor(lc.clone()) {
|
||||||
|
Ok(_) => {println!("Starting Mempool")},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Couldnt start mempool {}",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match LIGHTCLIENT.lock() {
|
||||||
|
Ok(l) => { l.replace(Some(lc)); },
|
||||||
|
Err(poisoned) => { poisoned.into_inner().replace(Some(lc)); },
|
||||||
|
};
|
||||||
|
|
||||||
|
let c_str = safe_cstring("OK");
|
||||||
|
return c_str.into_raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern fn litelib_execute(cmd: *const c_char, args: *const c_char) -> *mut c_char {
|
||||||
|
let result = panic::catch_unwind(|| {
|
||||||
|
let cmd_str = unsafe {
|
||||||
|
assert!(!cmd.is_null());
|
||||||
|
CStr::from_ptr(cmd).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let arg_str = unsafe {
|
||||||
|
assert!(!args.is_null());
|
||||||
|
CStr::from_ptr(args).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp: String;
|
||||||
|
{
|
||||||
|
let lightclient: Arc<LightClient>;
|
||||||
|
{
|
||||||
|
let lc = match LIGHTCLIENT.lock() {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(poisoned) => poisoned.into_inner(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if lc.borrow().is_none() {
|
||||||
|
return error_cstring("Light Client is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
lightclient = lc.borrow().as_ref().unwrap().clone();
|
||||||
|
};
|
||||||
|
|
||||||
|
let args = if arg_str.is_empty() { vec![] } else { vec![arg_str.as_ref()] };
|
||||||
|
|
||||||
|
resp = commands::do_user_command(&cmd_str, &args, lightclient.as_ref()).clone();
|
||||||
|
};
|
||||||
|
|
||||||
|
safe_cstring(&resp).into_raw()
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(ptr) => ptr,
|
||||||
|
Err(_) => error_cstring("Rust panic in litelib_execute"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is Server Connection is fine
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn litelib_check_server_online(server: *const c_char) -> bool {
|
||||||
|
let server_str = unsafe {
|
||||||
|
assert!(!server.is_null());
|
||||||
|
|
||||||
|
CStr::from_ptr(server).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = LightClientConfig::get_server_or_default(Some(server_str));
|
||||||
|
let result = LightClientConfig::create(server, false);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cleanly shut down the light client, stopping mempool monitor threads.
|
||||||
|
/// Must be called before exit to prevent hangs.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn litelib_shutdown() {
|
||||||
|
let lc_option = match LIGHTCLIENT.lock() {
|
||||||
|
Ok(l) => l.borrow().clone(),
|
||||||
|
Err(poisoned) => poisoned.into_inner().borrow().clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(lc) = lc_option {
|
||||||
|
lc.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callers that receive string return values from other functions should call this to return the string
|
||||||
|
* back to rust, so it can be freed. Failure to call this function will result in a memory leak
|
||||||
|
*/
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern fn litelib_rust_free_string(s: *mut c_char) {
|
||||||
|
unsafe {
|
||||||
|
if s.is_null() { return }
|
||||||
|
CString::from_raw(s)
|
||||||
|
};
|
||||||
|
}
|
||||||
80
third_party/silentdragonxlite/silentdragonxlite-cli/lib/Cargo.toml
vendored
Normal file
80
third_party/silentdragonxlite/silentdragonxlite-cli/lib/Cargo.toml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
[package]
|
||||||
|
name = "silentdragonxlitelib"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["embed_params"]
|
||||||
|
embed_params = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base58 = "0.1.0"
|
||||||
|
bs58 = { version = "0.2", features = ["check"] }
|
||||||
|
log = "0.4"
|
||||||
|
log4rs = "0.8.3"
|
||||||
|
dirs = "2.0.2"
|
||||||
|
http = "0.2"
|
||||||
|
hex = "0.3"
|
||||||
|
protobuf = "2"
|
||||||
|
byteorder = "1"
|
||||||
|
json = "0.12.0"
|
||||||
|
tiny-bip39 = "0.6.2"
|
||||||
|
secp256k1 = "=0.15.0"
|
||||||
|
sha2 = "0.8.0"
|
||||||
|
ripemd160 = "0.8.0"
|
||||||
|
lazy_static = "1.2.0"
|
||||||
|
rust-embed = { version = "5.1.0", features = ["debug-embed"] }
|
||||||
|
rand = "0.7.2"
|
||||||
|
sodiumoxide = "0.2.5"
|
||||||
|
ring = "0.16.9"
|
||||||
|
libflate = "0.1"
|
||||||
|
subtle = "2"
|
||||||
|
threadpool = "1.8.0"
|
||||||
|
num_cpus = "1.13.0"
|
||||||
|
|
||||||
|
tonic = { version = "0.1.1", features = ["tls", "tls-roots"] }
|
||||||
|
bytes = "0.4"
|
||||||
|
prost = "0.6"
|
||||||
|
prost-types = "0.6"
|
||||||
|
tokio = { version = "0.2", features = ["rt-threaded", "time", "stream", "fs", "macros", "uds", "full"] }
|
||||||
|
tokio-rustls = { version = "0.12.1", features = ["dangerous_configuration"] }
|
||||||
|
webpki = "0.21.0"
|
||||||
|
webpki-roots = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bellman]
|
||||||
|
git = "https://git.dragonx.is/DragonX/librustzcash.git"
|
||||||
|
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
|
||||||
|
default-features = false
|
||||||
|
features = ["groth16"]
|
||||||
|
|
||||||
|
[dependencies.pairing]
|
||||||
|
git = "https://git.dragonx.is/DragonX/librustzcash.git"
|
||||||
|
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
|
||||||
|
|
||||||
|
[dependencies.zcash_client_backend]
|
||||||
|
git = "https://git.dragonx.is/DragonX/librustzcash.git"
|
||||||
|
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
|
||||||
|
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.zcash_primitives]
|
||||||
|
git = "https://git.dragonx.is/DragonX/librustzcash.git"
|
||||||
|
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
|
||||||
|
default-features = false
|
||||||
|
features = ["transparent-inputs"]
|
||||||
|
|
||||||
|
[dependencies.zcash_proofs]
|
||||||
|
git = "https://git.dragonx.is/DragonX/librustzcash.git"
|
||||||
|
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.ff]
|
||||||
|
git = "https://git.dragonx.is/DragonX/librustzcash.git"
|
||||||
|
rev= "acff1444ec373e9c3e37b47ca95bfd358e45255b"
|
||||||
|
features = ["ff_derive"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tonic-build = "0.1.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempdir = "0.3.7"
|
||||||
13
third_party/silentdragonxlite/silentdragonxlite-cli/lib/build.rs
vendored
Normal file
13
third_party/silentdragonxlite/silentdragonxlite-cli/lib/build.rs
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright The Hush Developers 2019-2022
|
||||||
|
// Released under the GPLv3
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tonic_build::configure()
|
||||||
|
.build_server(false)
|
||||||
|
.compile(
|
||||||
|
&["proto/service.proto", "proto/compact_formats.proto"],
|
||||||
|
&["proto"],
|
||||||
|
)?;
|
||||||
|
println!("cargo:rerun-if-changed=proto/service.proto");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
65
third_party/silentdragonxlite/silentdragonxlite-cli/lib/proto/compact_formats.proto
vendored
Normal file
65
third_party/silentdragonxlite/silentdragonxlite-cli/lib/proto/compact_formats.proto
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package cash.z.wallet.sdk.rpc;
|
||||||
|
option go_package = "lightwalletd/walletrpc";
|
||||||
|
option swift_prefix = "";
|
||||||
|
|
||||||
|
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
||||||
|
// bytes fields of hashes are in canonical little-endian format.
|
||||||
|
|
||||||
|
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
||||||
|
// 1. Detect a payment to your shielded Sapling address
|
||||||
|
// 2. Detect a spend of your shielded Sapling notes
|
||||||
|
// 3. Update your witnesses to generate new Sapling spend proofs.
|
||||||
|
message CompactBlock {
|
||||||
|
uint32 protoVersion = 1; // the version of this wire format, for storage
|
||||||
|
uint64 height = 2; // the height of this block
|
||||||
|
bytes hash = 3; // the ID (hash) of this block, same as in block explorers
|
||||||
|
bytes prevHash = 4; // the ID (hash) of this block's predecessor
|
||||||
|
uint32 time = 5; // Unix epoch time when the block was mined
|
||||||
|
bytes header = 6; // (hash, prevHash, and time) OR (full header)
|
||||||
|
repeated CompactTx vtx = 7; // compact transactions from this block
|
||||||
|
}
|
||||||
|
|
||||||
|
message CompactTx {
|
||||||
|
// Index and hash will allow the receiver to call out to chain
|
||||||
|
// explorers or other data structures to retrieve more information
|
||||||
|
// about this transaction.
|
||||||
|
uint64 index = 1; // the index within the full block
|
||||||
|
bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers
|
||||||
|
|
||||||
|
// The transaction fee: present if server can provide. In the case of a
|
||||||
|
// stateless server and a transaction with transparent inputs, this will be
|
||||||
|
// unset because the calculation requires reference to prior transactions.
|
||||||
|
// in a pure-Sapling context, the fee will be calculable as:
|
||||||
|
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
||||||
|
uint32 fee = 3;
|
||||||
|
|
||||||
|
repeated CompactSaplingSpend spends = 4;
|
||||||
|
repeated CompactSaplingOutput outputs = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash
|
||||||
|
// protocol specification.
|
||||||
|
message CompactSaplingSpend {
|
||||||
|
bytes nf = 1; // nullifier (see the Zcash protocol specification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// output is a Sapling Output Description as described in section 7.4 of the
|
||||||
|
// Zcash protocol spec. Total size is 948.
|
||||||
|
message CompactSaplingOutput {
|
||||||
|
bytes cmu = 1; // note commitment u-coordinate
|
||||||
|
bytes epk = 2; // ephemeral public key
|
||||||
|
bytes ciphertext = 3; // first 52 bytes of ciphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
message CompactSpend {
|
||||||
|
bytes nf = 1; // nullifier (see the Zcash protocol specification)
|
||||||
|
}
|
||||||
|
|
||||||
|
message CompactOutput {
|
||||||
|
bytes cmu = 1; // note commitment u-coordinate
|
||||||
|
bytes epk = 2; // ephemeral public key
|
||||||
|
bytes ciphertext = 3; // first 52 bytes of ciphertext
|
||||||
|
}
|
||||||
|
*/
|
||||||
170
third_party/silentdragonxlite/silentdragonxlite-cli/lib/proto/service.proto
vendored
Normal file
170
third_party/silentdragonxlite/silentdragonxlite-cli/lib/proto/service.proto
vendored
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package cash.z.wallet.sdk.rpc;
|
||||||
|
option go_package = "lightwalletd/walletrpc";
|
||||||
|
option swift_prefix = "";
|
||||||
|
import "compact_formats.proto";
|
||||||
|
|
||||||
|
// A BlockID message contains identifiers to select a block: a height or a
|
||||||
|
// hash. If the hash is present it takes precedence.
|
||||||
|
message BlockID {
|
||||||
|
uint64 height = 1;
|
||||||
|
bytes hash = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockRange technically allows ranging from hash to hash etc but this is not
|
||||||
|
// currently intended for support, though there is no reason you couldn't do
|
||||||
|
// it. Further permutations are left as an exercise.
|
||||||
|
message BlockRange {
|
||||||
|
BlockID start = 1;
|
||||||
|
BlockID end = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TxFilter contains the information needed to identify a particular
|
||||||
|
// transaction: either a block and an index, or a direct transaction hash.
|
||||||
|
message TxFilter {
|
||||||
|
BlockID block = 1;
|
||||||
|
uint64 index = 2;
|
||||||
|
bytes hash = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||||
|
// the block height in which the transaction was included
|
||||||
|
message RawTransaction {
|
||||||
|
bytes data = 1;
|
||||||
|
uint64 height = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendResponse {
|
||||||
|
int32 errorCode = 1;
|
||||||
|
string errorMessage = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
|
||||||
|
message ChainSpec {}
|
||||||
|
|
||||||
|
message Empty {}
|
||||||
|
|
||||||
|
message LightdInfo {
|
||||||
|
string version = 1;
|
||||||
|
string vendor = 2;
|
||||||
|
bool taddrSupport = 3;
|
||||||
|
string chainName = 4;
|
||||||
|
uint64 saplingActivationHeight = 5;
|
||||||
|
string consensusBranchId = 6; // This should really be u32 or []byte, but string for readability
|
||||||
|
uint64 blockHeight = 7;
|
||||||
|
uint64 difficulty = 8;
|
||||||
|
uint64 longestchain = 9;
|
||||||
|
uint64 notarized = 10;
|
||||||
|
}
|
||||||
|
message Coinsupply {
|
||||||
|
string result = 1;
|
||||||
|
string coin = 2;
|
||||||
|
uint64 height = 3;
|
||||||
|
uint64 supply = 4;
|
||||||
|
uint64 zfunds = 5;
|
||||||
|
uint64 total = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransparentAddress {
|
||||||
|
string address = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransparentAddressBlockFilter {
|
||||||
|
string address = 1;
|
||||||
|
BlockRange range = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Address {
|
||||||
|
string address = 1;
|
||||||
|
}
|
||||||
|
message AddressList {
|
||||||
|
repeated string addresses = 1;
|
||||||
|
}
|
||||||
|
message Balance {
|
||||||
|
int64 valueZat = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Exclude {
|
||||||
|
repeated bytes txid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The TreeState is derived from the Hush getblockmerkletree rpc.
|
||||||
|
// https://faq.hush.is/rpc/getblockmerkletree.html
|
||||||
|
message TreeState {
|
||||||
|
string network = 1; // "main" or "test"
|
||||||
|
uint64 height = 2; // block height
|
||||||
|
string hash = 3; // block id
|
||||||
|
uint32 time = 4; // Unix epoch time when the block was mined
|
||||||
|
string saplingTree = 5; // sapling commitment tree state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results are sorted by height, which makes it easy to issue another
|
||||||
|
// request that picks up from where the previous left off.
|
||||||
|
message GetAddressUtxosArg {
|
||||||
|
repeated string addresses = 1;
|
||||||
|
uint64 startHeight = 2;
|
||||||
|
uint32 maxEntries = 3; // zero means unlimited
|
||||||
|
}
|
||||||
|
message GetAddressUtxosReply {
|
||||||
|
string address = 6;
|
||||||
|
bytes txid = 1;
|
||||||
|
int32 index = 2;
|
||||||
|
bytes script = 3;
|
||||||
|
int64 valueZat = 4;
|
||||||
|
uint64 height = 5;
|
||||||
|
}
|
||||||
|
message GetAddressUtxosReplyList {
|
||||||
|
repeated GetAddressUtxosReply addressUtxos = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service CompactTxStreamer {
|
||||||
|
// Return the height of the tip of the best chain
|
||||||
|
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
|
||||||
|
// Return the compact block corresponding to the given block identifier
|
||||||
|
rpc GetBlock(BlockID) returns (CompactBlock) {}
|
||||||
|
// Return a list of consecutive compact blocks
|
||||||
|
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
|
||||||
|
|
||||||
|
// Return the requested full (not compact) transaction (as from zcashd)
|
||||||
|
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
|
||||||
|
// Submit the given transaction to the Zcash network
|
||||||
|
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
|
||||||
|
|
||||||
|
// Return the txids corresponding to the given t-address within the given block range
|
||||||
|
rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
|
||||||
|
// wrapper for GetTaddressTxids
|
||||||
|
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
|
||||||
|
rpc GetTaddressBalance(AddressList) returns (Balance) {}
|
||||||
|
rpc GetTaddressBalanceStream(stream Address) returns (Balance) {}
|
||||||
|
|
||||||
|
// Return the compact transactions currently in the mempool; the results
|
||||||
|
// can be a few seconds out of date. If the Exclude list is empty, return
|
||||||
|
// all transactions; otherwise return all *except* those in the Exclude list
|
||||||
|
// (if any); this allows the client to avoid receiving transactions that it
|
||||||
|
// already has (from an earlier call to this rpc). The transaction IDs in the
|
||||||
|
// Exclude list can be shortened to any number of bytes to make the request
|
||||||
|
// more bandwidth-efficient; if two or more transactions in the mempool
|
||||||
|
// match a shortened txid, they are all sent (none is excluded). Transactions
|
||||||
|
// in the exclude list that don't exist in the mempool are ignored.
|
||||||
|
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
||||||
|
|
||||||
|
// Return a stream of current Mempool transactions. This will keep the output stream open while
|
||||||
|
// there are mempool transactions. It will close the returned stream when a new block is mined.
|
||||||
|
rpc GetMempoolStream(Empty) returns (stream RawTransaction) {}
|
||||||
|
|
||||||
|
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
||||||
|
// See section 3.7 of the Zcash protocol specification. It returns several other useful
|
||||||
|
// values also (even though they can be obtained using GetBlock).
|
||||||
|
// The block can be specified by either height or hash.
|
||||||
|
rpc GetTreeState(BlockID) returns (TreeState) {}
|
||||||
|
rpc GetLatestTreeState(Empty) returns (TreeState) {}
|
||||||
|
|
||||||
|
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
|
||||||
|
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
|
||||||
|
|
||||||
|
// Return information about this lightwalletd instance and the blockchain
|
||||||
|
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
|
||||||
|
// Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production)
|
||||||
|
// rpc Ping(Duration) returns (PingResponse) {}
|
||||||
|
rpc GetCoinsupply(Empty) returns (Coinsupply) {}
|
||||||
|
}
|
||||||
65
third_party/silentdragonxlite/silentdragonxlite-cli/lib/res/lightwalletd-lite.myhush.pem
vendored
Normal file
65
third_party/silentdragonxlite/silentdragonxlite-cli/lib/res/lightwalletd-lite.myhush.pem
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFlTCCA32gAwIBAgIUCU7sjrIbYfA+bc2qWlyUo0dCU7swDQYJKoZIhvcNAQEL
|
||||||
|
BQAwWjELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAoM
|
||||||
|
BEh1c2gxDTALBgNVBAsMBEh1c2gxGDAWBgNVBAMMD2xpdGUubXlodXNoLm9yZzAe
|
||||||
|
Fw0xOTEyMDIxNDUyMzBaFw0yMDEyMDExNDUyMzBaMFoxCzAJBgNVBAYTAlVTMRMw
|
||||||
|
EQYDVQQIDApTb21lLVN0YXRlMQ0wCwYDVQQKDARIdXNoMQ0wCwYDVQQLDARIdXNo
|
||||||
|
MRgwFgYDVQQDDA9saXRlLm15aHVzaC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC
|
||||||
|
DwAwggIKAoICAQDhd3SLJuGQ3ivUWle6+Est+qNBghf2vMkcgj9NjaxeMjSMLPVq
|
||||||
|
Mxt0j/mJe3+z767yXbiBUKnaQyb7OcWtxUN3PusGmqnMAUuy/tdu9h+2ScYKThh2
|
||||||
|
JHQNdyN1y4c7sFbmntpMIIqm6/v95UXnnStQ+VBlS2/IhLYTgW31DEIiTpyx4jjW
|
||||||
|
xY+QD2+mqf4sSDm4Yq/r3Cxp6YWufEbhkXiHcF15JPk1d6jzkOkcjCJJCqwRMJ+5
|
||||||
|
60q31S1W0Ud/L7AqkOhAKFHmORfCXM0ae4Rive/ZgM688KYIXA9kQzA6ZMdD7VIL
|
||||||
|
4q4IoP1ZjlPhosFoUFB6lHORYp15+Gu43jbC2/SUPWJQbJ1XusjxysqngyJ53/Va
|
||||||
|
MN/iWhOmqBXjx1SqkyIV4W56GDezxT1MhM5zSSKgEHePyFzkGNYasEeHa1/hZoz+
|
||||||
|
zKG1oGLlMQe5TtI3AMZMfLz6t8qtRB+k+XW988mHJZ7BYOjW3KvdN16SOYdFF6K+
|
||||||
|
86MAQ8rNPgcTsnclhmDdjh7+PhQpkF3uqF1EeVTzb03s77Cx6nDc9GCnpXqg8tkE
|
||||||
|
HnJD0WFIXA29PCjWyebuksMBRahekYDR0kn8O0Km/eFAprH3v4qoSc1JLNJR2G20
|
||||||
|
eHVDnNFnR6QdwlM9+39NYUhJV43aj28wx1m0FXI+dSklblk4hkbGPPbFjQIDAQAB
|
||||||
|
o1MwUTAdBgNVHQ4EFgQUo5TCdrNgopYbxQSzSPFSlyCtE6YwHwYDVR0jBBgwFoAU
|
||||||
|
o5TCdrNgopYbxQSzSPFSlyCtE6YwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAgEAyPxq4ZyBtKJEQtzmqdkI+28Yw4qDSBE0dj4QQfOErgK5hX29Bk4e
|
||||||
|
Auh6j0eyuRA8gtwngsE8fAAg84kcH/b+hM2zFW4MqpgjigA7oqA51VkIg+8Q9zdF
|
||||||
|
IHweV2kuSunZ2ANcGrr7o/Qy5y8D7URWUpUu8J1ZNyLg7YYMtpyjllTYhbfKpAgM
|
||||||
|
HUX1STCRfSPTgM/JxIsnll8RgacfhUmoOzOsrvvZ39h8cZZo96ksBRL4gvVQ++Hm
|
||||||
|
gzNTbYQXukR26Xfv112AEj5Xo3z3fsLP1KxZxd2p6/24XYktpZf2J71Np2CONdV4
|
||||||
|
gFgxFfPwvPyDO5pKice018qlXz0euhvK5g++s+TrSeZwleDTW4spP3TdVXNB96iZ
|
||||||
|
rrFkTT0SEBtd6iKqeFAX89BpshCUqOlsFdrf10i2dDiKsqxMod6a8i17RrtZL3q7
|
||||||
|
S0nqCsnyc1QvfKIQi08vfMZHHMbSS0Cg/5H8ISexM/R4SQc6C//IhEd5hUJ7E9AC
|
||||||
|
Sepr0xu1JBrglech4N+brHpGZK3Crofzu+hV+qruY5Wg21bD99zigxrgi3YaQPlA
|
||||||
|
6TJVsGk68h00QSy+Ri9dyuvyPnIDyMQfZLLIKCwsxznYXLtmIp+UoA3otGTijhmc
|
||||||
|
ZxFdEhd3cJrSPwihb/IvQJICFp3ya1aI7dLsaZO9h9kPc8GGLi7tiq8=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFozCCA4ugAwIBAgIUaRW0/q8ERGZUZqv+TVkC5lOwgX4wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwYTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlVTMQswCQYDVQQHDAJVUzEPMA0G
|
||||||
|
A1UECgwGTXlIdXNoMQ0wCwYDVQQLDARIVVNIMRgwFgYDVQQDDA9saXRlLm15aHVz
|
||||||
|
aC5vcmcwHhcNMjAwMTAzMjAzMTAwWhcNMjEwMTAyMjAzMTAwWjBhMQswCQYDVQQG
|
||||||
|
EwJVUzELMAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMQ8wDQYDVQQKDAZNeUh1c2gx
|
||||||
|
DTALBgNVBAsMBEhVU0gxGDAWBgNVBAMMD2xpdGUubXlodXNoLm9yZzCCAiIwDQYJ
|
||||||
|
KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/oJh+MSxGOedvEthiqdfMEDfjzJ7AW
|
||||||
|
wwvuBtVULvnIvJq0WTMK8INjkMqnj+/ZE9a1aOOSpwN+9CuknhiySTdqHGl6EigM
|
||||||
|
/S19ZdN/7aC881VrMCncguHUOa8HLq5F4R0YCGis1Cor9vXf0GVFJ2mDfPB0C8iz
|
||||||
|
C+gMGnkDwuBy51fUcWbxZ5diRYh7YlOIUxpmb24On+X7sw+7nbmV12x8v644xRKJ
|
||||||
|
MazPIVrxIwXZ3tjz2NR7IMu+SrtCgMAW56M63NJ+iZBYDRoFVMRGEfAgGaFV2Hqi
|
||||||
|
Dx7q89sO2bhVg2lBBOW7403S+T8eK1rGFj7iGJoRLm/cgWwozZteXynHzicYEvX6
|
||||||
|
w1h0lu/OPQQk5AKRn+iI1kKLlT1auKIBXpfnpELnie3XgzzLyKt0pobVrWdMutlJ
|
||||||
|
83Zo7LmnhJYlcG7Qb0UczSyaIn+3dWo2HTbiyzJD23gmUbzFD4AVF1Ee4x5yT4Hb
|
||||||
|
Aw0FpQXDHX2MT04xleM2HdjE86ruZfNCegvQdRtgKRAiVNe4kP5ZzB7OeDX6pgeE
|
||||||
|
/e07tiHiFb2Im7J/IR4PmIh6SuYI/QObFXFXfvwCk/iJCps15/PryGfXZLdz+2z1
|
||||||
|
av1nwSglBRqeRX8HUBIgNcY0Lyq82BKfq4ZU3fKiIDuNV16OxCnFGZRw+ByRNrKR
|
||||||
|
KsgaJEi7qH6DAgMBAAGjUzBRMB0GA1UdDgQWBBRd+rxIkdKrpLsgz5/KiqZOYzUB
|
||||||
|
KDAfBgNVHSMEGDAWgBRd+rxIkdKrpLsgz5/KiqZOYzUBKDAPBgNVHRMBAf8EBTAD
|
||||||
|
AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBR9M0CnCvT1Zd5D/Wwy9ylH6CSFq6AEbdh
|
||||||
|
fMo8+NZl4J0FNji2Iv05xh+V3f+eyuf6oc8q0vsKeC5MZj/44AzqzxCSvMrUnh0V
|
||||||
|
GAiZQqLAVJIR/fi49bX9ku1yfQVJKzGFS93TcHMv9XYHJ4bSnQKlEOF5F6Wh9AO1
|
||||||
|
6GU/+vb0pSOfOv5+pUaj84AYCKQ8kp9nuNpOS12jyjc5hUogflPFAmeIpcIGjiso
|
||||||
|
Ln8+b+Xh0BZGNpdrvJ/wr8InxblMu5chtYrGAGo4mh4q3YWiwYJTkTIhoeTxF4eU
|
||||||
|
BOqMJa9lQgZE4T7V33jrIsuMPEZfACpj+gQNNzl8WQ+jzkZhBdYPTqhO9u1rlRXG
|
||||||
|
9VJfmuQ7+KLXAMFQgsHlX5Y5lux3CV36Knb5+1f/u0cdys1yb9mbQ/Ok3T8cuh6B
|
||||||
|
7Hs53JhAW47+CCnsNTaPzwti3wfzWhS2sjHz4IT1NcacsuDlxk7IykJ2U3auiufE
|
||||||
|
lRFpZoK81jsipEgRPBeF6OesXpldKxK9lnVJA/6ElApuo0amgg++fROQEpZSLyBM
|
||||||
|
lZdYrW6ZKnzCUZpNuwNHg1nfiit3RJ6hRLsk2jHrLDb5BVWzuJCNa7LK9bZ3IbUa
|
||||||
|
5jGb9Rplo+NgglQQYCflptksti9h5DN+GlVGxfJ9yzkg4/4ckmh75colcc1CTQAf
|
||||||
|
eER5vAF7og==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
1004
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/commands.rs
vendored
Normal file
1004
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/commands.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
363
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/grpcconnector.rs
vendored
Normal file
363
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/grpcconnector.rs
vendored
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
// Copyright The Hush Developers 2019-2022
|
||||||
|
// Released under the GPLv3
|
||||||
|
use log::{info,error};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use zcash_primitives::transaction::{TxId};
|
||||||
|
|
||||||
|
use crate::grpc_client::{ChainSpec, BlockId, BlockRange, RawTransaction, CompactBlock,
|
||||||
|
TransparentAddressBlockFilter, TxFilter, Empty, LightdInfo, Coinsupply};
|
||||||
|
use tonic::transport::{Channel, ClientTlsConfig};
|
||||||
|
use tokio_rustls::{rustls::ClientConfig};
|
||||||
|
use tonic::{Request};
|
||||||
|
|
||||||
|
use threadpool::ThreadPool;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
|
use crate::PubCertificate;
|
||||||
|
use crate::grpc_client::compact_tx_streamer_client::CompactTxStreamerClient;
|
||||||
|
|
||||||
|
mod danger {
|
||||||
|
use tokio_rustls::rustls;
|
||||||
|
use webpki;
|
||||||
|
|
||||||
|
pub struct NoCertificateVerification {}
|
||||||
|
|
||||||
|
impl rustls::ServerCertVerifier for NoCertificateVerification {
|
||||||
|
fn verify_server_cert(&self,
|
||||||
|
_roots: &rustls::RootCertStore,
|
||||||
|
_presented_certs: &[rustls::Certificate],
|
||||||
|
_dns_name: webpki::DNSNameRef<'_>,
|
||||||
|
_ocsp: &[u8]) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
|
||||||
|
Ok(rustls::ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_client(uri: &http::Uri, no_cert: bool) -> Result<CompactTxStreamerClient<Channel>, Box<dyn std::error::Error>> {
|
||||||
|
let channel = if uri.scheme_str() == Some("http") {
|
||||||
|
Channel::builder(uri.clone()).connect().await?
|
||||||
|
} else {
|
||||||
|
let mut config = ClientConfig::new();
|
||||||
|
|
||||||
|
config.alpn_protocols.push(b"h2".to_vec());
|
||||||
|
config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
|
||||||
|
config.root_store.add_pem_file(
|
||||||
|
&mut PubCertificate::get("lightwalletd-lite.myhush.pem").unwrap().as_ref()).unwrap();
|
||||||
|
|
||||||
|
if no_cert {
|
||||||
|
config.dangerous()
|
||||||
|
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tls = ClientTlsConfig::new()
|
||||||
|
.rustls_client_config(config)
|
||||||
|
.domain_name(uri.host().unwrap());
|
||||||
|
|
||||||
|
Channel::builder(uri.clone())
|
||||||
|
.tls_config(tls)
|
||||||
|
.connect()
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CompactTxStreamerClient::new(channel))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============
|
||||||
|
// GRPC code
|
||||||
|
// ==============
|
||||||
|
async fn get_lightd_info(uri: &http::Uri, no_cert: bool) -> Result<LightdInfo, Box<dyn std::error::Error>> {
|
||||||
|
let mut client = get_client(uri, no_cert).await?;
|
||||||
|
|
||||||
|
let request = Request::new(Empty {});
|
||||||
|
|
||||||
|
let response = client.get_lightd_info(request).await?;
|
||||||
|
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_info(uri: &http::Uri, no_cert: bool) -> Result<LightdInfo, String> {
|
||||||
|
let mut rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
rt.block_on(get_lightd_info(uri, no_cert)).map_err( |e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn get_coinsupply_info(uri: &http::Uri, no_cert: bool) -> Result<Coinsupply, Box<dyn std::error::Error>> {
|
||||||
|
let mut client = get_client(uri, no_cert).await?;
|
||||||
|
|
||||||
|
let request = Request::new(Empty {});
|
||||||
|
|
||||||
|
let response = client.get_coinsupply(request).await?;
|
||||||
|
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
pub fn get_coinsupply(uri: http::Uri, no_cert: bool) -> Result<Coinsupply, String> {
|
||||||
|
let mut rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
rt.block_on(get_coinsupply_info(&uri, no_cert)).map_err( |e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_block_range<F : 'static + std::marker::Send>(
|
||||||
|
uri: &http::Uri,
|
||||||
|
start_height: u64,
|
||||||
|
end_height: u64,
|
||||||
|
no_cert: bool,
|
||||||
|
pool: ThreadPool,
|
||||||
|
c: F
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where F : Fn(&[u8], u64) {
|
||||||
|
let mut client = get_client(uri, no_cert).await?;
|
||||||
|
|
||||||
|
let bs = BlockId { height: start_height, hash: vec![] };
|
||||||
|
let be = BlockId { height: end_height, hash: vec![] };
|
||||||
|
|
||||||
|
let request = Request::new(BlockRange { start: Some(bs), end: Some(be) });
|
||||||
|
|
||||||
|
let (tx, rx) = channel::<Option<CompactBlock>>();
|
||||||
|
let (ftx, frx) = channel();
|
||||||
|
|
||||||
|
pool.execute(move || {
|
||||||
|
while let Ok(Some(block)) = rx.recv() {
|
||||||
|
use prost::Message;
|
||||||
|
let mut encoded_buf = vec![];
|
||||||
|
|
||||||
|
if let Err(e) = block.encode(&mut encoded_buf) {
|
||||||
|
error!("Error encoding block: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
c(&encoded_buf, block.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = ftx.send(Ok(())) {
|
||||||
|
error!("Error sending completion signal: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut response = client.get_block_range(request).await?.into_inner();
|
||||||
|
|
||||||
|
while let Some(block) = response.message().await? {
|
||||||
|
if let Err(e) = tx.send(Some(block)) {
|
||||||
|
error!("Error sending block to channel: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = tx.send(None) {
|
||||||
|
error!("Error sending end signal to channel: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
frx.iter().take(1).collect::<Result<Vec<()>, String>>()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn fetch_blocks<F : 'static + std::marker::Send>(uri: &http::Uri, start_height: u64, end_height: u64, no_cert: bool, pool: ThreadPool, c: F) -> Result<(), String>
|
||||||
|
where F : Fn(&[u8], u64) {
|
||||||
|
let mut rt = tokio::runtime::Runtime::new().map_err(|e| format!("Error creating runtime {:?}", e))?;
|
||||||
|
fetch_blocks_with_runtime(&mut rt, uri, start_height, end_height, no_cert, pool, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_blocks_with_runtime<F : 'static + std::marker::Send>(rt: &mut tokio::runtime::Runtime, uri: &http::Uri, start_height: u64, end_height: u64, no_cert: bool, pool: ThreadPool, c: F) -> Result<(), String>
|
||||||
|
where F : Fn(&[u8], u64) {
|
||||||
|
|
||||||
|
match rt.block_on(get_block_range(uri, start_height, end_height, no_cert, pool, c)) {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => {
|
||||||
|
let e = format!("Error fetching blocks {:?}", e);
|
||||||
|
error!("{}", e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_address_txids GRPC call
|
||||||
|
async fn get_address_txids<F : 'static + std::marker::Send>(
|
||||||
|
uri: &http::Uri,
|
||||||
|
address: String,
|
||||||
|
start_height: u64,
|
||||||
|
end_height: u64,
|
||||||
|
no_cert: bool,
|
||||||
|
c: F
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where F : Fn(&[u8], u64) {
|
||||||
|
|
||||||
|
let mut client = match get_client(uri, no_cert).await {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error creating client: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = Some(BlockId{ height: start_height, hash: vec!()});
|
||||||
|
let end = Some(BlockId{ height: end_height, hash: vec!()});
|
||||||
|
|
||||||
|
let request = Request::new(TransparentAddressBlockFilter{ address, range: Some(BlockRange{ start, end }) });
|
||||||
|
|
||||||
|
let maybe_response = match client.get_address_txids(request).await {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error getting address txids: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response = maybe_response.into_inner();
|
||||||
|
|
||||||
|
while let Some(tx) = response.message().await? {
|
||||||
|
c(&tx.data, tx.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// function to monitor mempool transactions
|
||||||
|
pub async fn monitor_mempool<F: 'static + std::marker::Send>(
|
||||||
|
uri: &http::Uri,
|
||||||
|
no_cert: bool,
|
||||||
|
mut c: F
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
F: FnMut(RawTransaction) -> Result<(), Box<dyn std::error::Error>>,
|
||||||
|
{
|
||||||
|
|
||||||
|
let mut client = get_client(uri, no_cert)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Error getting client: {:?}", e))?;
|
||||||
|
|
||||||
|
|
||||||
|
let request = Request::new(Empty {});
|
||||||
|
|
||||||
|
let mut response = client
|
||||||
|
.get_mempool_stream(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{}", e))?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
|
||||||
|
while let Ok(Some(rtx)) = response.message().await {
|
||||||
|
|
||||||
|
if let Err(e) = c(rtx) {
|
||||||
|
info!("Error processing RawTransaction: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_transparent_txids<F : 'static + std::marker::Send>(
|
||||||
|
uri: &http::Uri,
|
||||||
|
address: String,
|
||||||
|
start_height: u64,
|
||||||
|
end_height: u64,
|
||||||
|
no_cert: bool,
|
||||||
|
c: F
|
||||||
|
) -> Result<(), String>
|
||||||
|
where F : Fn(&[u8], u64) {
|
||||||
|
|
||||||
|
let mut rt = match tokio::runtime::Runtime::new() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
let e = format!("Error creating runtime {:?}", e);
|
||||||
|
error!("{}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match rt.block_on(get_address_txids(uri, address.clone(), start_height, end_height, no_cert, c)) {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => {
|
||||||
|
let e = format!("Error with get_address_txids runtime {:?}", e);
|
||||||
|
error!("{}", e);
|
||||||
|
return Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_transaction GRPC call
|
||||||
|
async fn get_transaction(uri: &http::Uri, txid: TxId, no_cert: bool)
|
||||||
|
-> Result<RawTransaction, Box<dyn std::error::Error>> {
|
||||||
|
let mut client = get_client(uri, no_cert).await?;
|
||||||
|
let request = Request::new(TxFilter { block: None, index: 0, hash: txid.0.to_vec() });
|
||||||
|
|
||||||
|
let response = client.get_transaction(request).await?;
|
||||||
|
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_full_tx(uri: &http::Uri, txid: TxId, no_cert: bool) -> Result<Vec<u8>, String> {
|
||||||
|
let mut rt = match tokio::runtime::Runtime::new() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
let errstr = format!("Error creating runtime {}", e.to_string());
|
||||||
|
error!("{}", errstr);
|
||||||
|
return Err(errstr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match rt.block_on(get_transaction(uri, txid, no_cert)) {
|
||||||
|
Ok(rawtx) => Ok(rawtx.data.to_vec()),
|
||||||
|
Err(e) => {
|
||||||
|
let errstr = format!("Error in get_transaction runtime {}", e.to_string());
|
||||||
|
error!("{}", errstr);
|
||||||
|
Err(errstr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send_transaction GRPC call
|
||||||
|
async fn send_transaction(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let mut client = get_client(uri, no_cert).await?;
|
||||||
|
|
||||||
|
let request = Request::new(RawTransaction {data: tx_bytes.to_vec(), height: 0});
|
||||||
|
|
||||||
|
let response = client.send_transaction(request).await?;
|
||||||
|
|
||||||
|
let sendresponse = response.into_inner();
|
||||||
|
if sendresponse.error_code == 0 {
|
||||||
|
let mut txid = sendresponse.error_message;
|
||||||
|
if txid.starts_with("\"") && txid.ends_with("\"") {
|
||||||
|
txid = txid[1..txid.len()-1].to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(txid)
|
||||||
|
} else {
|
||||||
|
Err(Box::from(format!("Error: {:?}", sendresponse)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> Result<String, String> {
|
||||||
|
let mut rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
rt.block_on(send_transaction(uri, no_cert, tx_bytes)).map_err( |e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_latest_block GRPC call
|
||||||
|
async fn get_latest_block(uri: &http::Uri, no_cert: bool) -> Result<BlockId, Box<dyn std::error::Error>> {
|
||||||
|
let mut client = get_client(uri, no_cert).await?;
|
||||||
|
|
||||||
|
let request = Request::new(ChainSpec {});
|
||||||
|
|
||||||
|
let response = client.get_latest_block(request).await?;
|
||||||
|
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_latest_block(uri: &http::Uri, no_cert: bool) -> Result<BlockId, String> {
|
||||||
|
let mut rt = match tokio::runtime::Runtime::new() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
let errstr = format!("Error creating runtime {}", e.to_string());
|
||||||
|
error!("{}", errstr);
|
||||||
|
return Err(errstr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.block_on(get_latest_block(uri, no_cert)).map_err(|e| {
|
||||||
|
let errstr = format!("Error getting latest block {}", e.to_string());
|
||||||
|
error!("{}", errstr);
|
||||||
|
errstr
|
||||||
|
})
|
||||||
|
}
|
||||||
29
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lib.rs
vendored
Normal file
29
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lib.rs
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright The Hush Developers 2019-2022
|
||||||
|
// Released under the GPLv3
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rust_embed;
|
||||||
|
|
||||||
|
pub mod lightclient;
|
||||||
|
pub mod grpcconnector;
|
||||||
|
pub mod lightwallet;
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
|
#[cfg(feature = "embed_params")]
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "zcash-params/"]
|
||||||
|
pub struct SaplingParams;
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "res/"]
|
||||||
|
pub struct PubCertificate;
|
||||||
|
|
||||||
|
|
||||||
|
// Anchor depth back from the tip for shielded spends. MUST be > 0: with 0 the wallet anchors to
|
||||||
|
// the absolute chain tip, which races the node committing that anchor and intermittently triggers
|
||||||
|
// "bad-txns-shielded-requirements-not-met" (missing sapling anchor) on broadcast. 4 matches upstream
|
||||||
|
// zecwallet-lite and uses a confirmed, reorg-stable anchor.
|
||||||
|
pub const ANCHOR_OFFSET: u32 = 4;
|
||||||
|
|
||||||
|
pub mod grpc_client {
|
||||||
|
tonic::include_proto!("cash.z.wallet.sdk.rpc");
|
||||||
|
}
|
||||||
1920
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient.rs
vendored
Normal file
1920
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1021
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient/checkpoints.rs
vendored
Normal file
1021
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient/checkpoints.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2600
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet.rs
vendored
Normal file
2600
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
46
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/address.rs
vendored
Normal file
46
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/address.rs
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//! Structs for handling supported address types.
|
||||||
|
|
||||||
|
use pairing::bls12_381::Bls12;
|
||||||
|
use zcash_primitives::primitives::PaymentAddress;
|
||||||
|
use zcash_client_backend::encoding::{decode_payment_address, decode_transparent_address};
|
||||||
|
use zcash_primitives::legacy::TransparentAddress;
|
||||||
|
|
||||||
|
/// An address that funds can be sent to.
|
||||||
|
pub enum RecipientAddress {
|
||||||
|
Shielded(PaymentAddress<Bls12>),
|
||||||
|
Transparent(TransparentAddress),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PaymentAddress<Bls12>> for RecipientAddress {
|
||||||
|
fn from(addr: PaymentAddress<Bls12>) -> Self {
|
||||||
|
RecipientAddress::Shielded(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TransparentAddress> for RecipientAddress {
|
||||||
|
fn from(addr: TransparentAddress) -> Self {
|
||||||
|
RecipientAddress::Transparent(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecipientAddress {
|
||||||
|
pub fn from_str(s: &str, hrp_sapling_address: &str, b58_pubkey_address: [u8; 1], b58_script_address: [u8; 1]) -> Option<Self> {
|
||||||
|
// Try to match a sapling z address
|
||||||
|
if let Some(pa) = match decode_payment_address(hrp_sapling_address, s) {
|
||||||
|
Ok(ret) => ret,
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Some(RecipientAddress::Shielded(pa)) // Matched a shielded address
|
||||||
|
} else if let Some(addr) = match decode_transparent_address(
|
||||||
|
&b58_pubkey_address, &b58_script_address, s) {
|
||||||
|
Ok(ret) => ret,
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Some(RecipientAddress::Transparent(addr)) // Matched a transparent address
|
||||||
|
} else {
|
||||||
|
None // Didn't match anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
558
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/data.rs
vendored
Normal file
558
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/data.rs
vendored
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use pairing::bls12_381::{Bls12};
|
||||||
|
use ff::{PrimeField, PrimeFieldRepr};
|
||||||
|
|
||||||
|
use zcash_primitives::{
|
||||||
|
block::BlockHash,
|
||||||
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
|
sapling::Node,
|
||||||
|
serialize::{Vector, Optional},
|
||||||
|
transaction::{
|
||||||
|
components::{OutPoint},
|
||||||
|
TxId,
|
||||||
|
},
|
||||||
|
note_encryption::{Memo,},
|
||||||
|
zip32::{ExtendedFullViewingKey,},
|
||||||
|
JUBJUB,
|
||||||
|
primitives::{Diversifier, Note,},
|
||||||
|
jubjub::{
|
||||||
|
JubjubEngine,
|
||||||
|
fs::{Fs, FsRepr},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
use zcash_primitives::zip32::ExtendedSpendingKey;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct BlockData {
|
||||||
|
pub height: i32,
|
||||||
|
pub hash: BlockHash,
|
||||||
|
pub tree: CommitmentTree<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockData {
|
||||||
|
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let height = reader.read_i32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let mut hash_bytes = [0; 32];
|
||||||
|
reader.read_exact(&mut hash_bytes)?;
|
||||||
|
|
||||||
|
let tree = CommitmentTree::<Node>::read(&mut reader)?;
|
||||||
|
|
||||||
|
let endtag = reader.read_u64::<LittleEndian>()?;
|
||||||
|
if endtag != 11 {
|
||||||
|
println!("End tag for blockdata {}", endtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Ok(BlockData{
|
||||||
|
height,
|
||||||
|
hash: BlockHash{ 0: hash_bytes },
|
||||||
|
tree
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||||
|
writer.write_i32::<LittleEndian>(self.height)?;
|
||||||
|
writer.write_all(&self.hash.0)?;
|
||||||
|
self.tree.write(&mut writer)?;
|
||||||
|
writer.write_u64::<LittleEndian>(11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SaplingNoteData {
|
||||||
|
pub(super) account: usize,
|
||||||
|
pub(super) extfvk: ExtendedFullViewingKey, // Technically, this should be recoverable from the account number, but we're going to refactor this in the future, so I'll write it again here.
|
||||||
|
pub diversifier: Diversifier,
|
||||||
|
pub note: Note<Bls12>,
|
||||||
|
pub(super) witnesses: Vec<IncrementalWitness<Node>>,
|
||||||
|
pub(super) nullifier: [u8; 32],
|
||||||
|
pub spent: Option<TxId>, // If this note was confirmed spent
|
||||||
|
pub unconfirmed_spent: Option<TxId>, // If this note was spent in a send, but has not yet been confirmed.
|
||||||
|
pub memo: Option<Memo>,
|
||||||
|
pub is_change: bool,
|
||||||
|
// TODO: We need to remove the unconfirmed_spent (i.e., set it to None) if the Tx has expired
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Reads an FsRepr from [u8] of length 32
|
||||||
|
/// This will panic (abort) if length provided is
|
||||||
|
/// not correct
|
||||||
|
/// TODO: This is duplicate from rustzcash.rs
|
||||||
|
fn read_fs(from: &[u8]) -> FsRepr {
|
||||||
|
assert_eq!(from.len(), 32);
|
||||||
|
|
||||||
|
let mut f = <<Bls12 as JubjubEngine>::Fs as PrimeField>::Repr::default();
|
||||||
|
f.read_le(from).expect("length is 32 bytes");
|
||||||
|
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading a note also needs the corresponding address to read from.
|
||||||
|
pub fn read_note<R: Read>(mut reader: R) -> io::Result<(u64, Fs)> {
|
||||||
|
let value = reader.read_u64::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let mut r_bytes: [u8; 32] = [0; 32];
|
||||||
|
reader.read_exact(&mut r_bytes)?;
|
||||||
|
|
||||||
|
let r = match Fs::from_repr(read_fs(&r_bytes)) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput, "Couldn't parse randomness"))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((value, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaplingNoteData {
|
||||||
|
fn serialized_version() -> u64 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
extfvk: &ExtendedFullViewingKey,
|
||||||
|
output: zcash_client_backend::wallet::WalletShieldedOutput
|
||||||
|
) -> Self {
|
||||||
|
let witness = output.witness;
|
||||||
|
let nf = {
|
||||||
|
let mut nf = [0; 32];
|
||||||
|
nf.copy_from_slice(
|
||||||
|
&output
|
||||||
|
.note
|
||||||
|
.nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB),
|
||||||
|
);
|
||||||
|
nf
|
||||||
|
};
|
||||||
|
|
||||||
|
SaplingNoteData {
|
||||||
|
account: output.account,
|
||||||
|
extfvk: extfvk.clone(),
|
||||||
|
diversifier: output.to.diversifier,
|
||||||
|
note: output.note,
|
||||||
|
witnesses: vec![witness],
|
||||||
|
nullifier: nf,
|
||||||
|
spent: None,
|
||||||
|
unconfirmed_spent: None,
|
||||||
|
memo: None,
|
||||||
|
is_change: output.is_change,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading a note also needs the corresponding address to read from.
|
||||||
|
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let _version = reader.read_u64::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let account = reader.read_u64::<LittleEndian>()? as usize;
|
||||||
|
|
||||||
|
let extfvk = ExtendedFullViewingKey::read(&mut reader)?;
|
||||||
|
|
||||||
|
let mut diversifier_bytes = [0u8; 11];
|
||||||
|
reader.read_exact(&mut diversifier_bytes)?;
|
||||||
|
let diversifier = Diversifier{0: diversifier_bytes};
|
||||||
|
|
||||||
|
// To recover the note, read the value and r, and then use the payment address
|
||||||
|
// to recreate the note
|
||||||
|
let (value, r) = read_note(&mut reader)?; // TODO: This method is in a different package, because of some fields that are private
|
||||||
|
|
||||||
|
let maybe_note = extfvk.fvk.vk.into_payment_address(diversifier, &JUBJUB).unwrap().create_note(value, r, &JUBJUB);
|
||||||
|
|
||||||
|
let note = match maybe_note {
|
||||||
|
Some(n) => Ok(n),
|
||||||
|
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Couldn't create the note for the address"))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let witnesses = Vector::read(&mut reader, |r| IncrementalWitness::<Node>::read(r))?;
|
||||||
|
|
||||||
|
let mut nullifier = [0u8; 32];
|
||||||
|
reader.read_exact(&mut nullifier)?;
|
||||||
|
|
||||||
|
// Note that this is only the spent field, we ignore the unconfirmed_spent field.
|
||||||
|
// The reason is that unconfirmed spents are only in memory, and we need to get the actual value of spent
|
||||||
|
// from the blockchain anyway.
|
||||||
|
let spent = Optional::read(&mut reader, |r| {
|
||||||
|
let mut txid_bytes = [0u8; 32];
|
||||||
|
r.read_exact(&mut txid_bytes)?;
|
||||||
|
Ok(TxId{0: txid_bytes})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let memo = Optional::read(&mut reader, |r| {
|
||||||
|
let mut memo_bytes = [0u8; 512];
|
||||||
|
r.read_exact(&mut memo_bytes)?;
|
||||||
|
match Memo::from_bytes(&memo_bytes) {
|
||||||
|
Some(m) => Ok(m),
|
||||||
|
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Couldn't create the memo"))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let is_change: bool = reader.read_u8()? > 0;
|
||||||
|
|
||||||
|
Ok(SaplingNoteData {
|
||||||
|
account,
|
||||||
|
extfvk,
|
||||||
|
diversifier,
|
||||||
|
note,
|
||||||
|
witnesses,
|
||||||
|
nullifier,
|
||||||
|
spent,
|
||||||
|
unconfirmed_spent: None,
|
||||||
|
memo,
|
||||||
|
is_change,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||||
|
// Write a version number first, so we can later upgrade this if needed.
|
||||||
|
writer.write_u64::<LittleEndian>(SaplingNoteData::serialized_version())?;
|
||||||
|
|
||||||
|
writer.write_u64::<LittleEndian>(self.account as u64)?;
|
||||||
|
|
||||||
|
self.extfvk.write(&mut writer)?;
|
||||||
|
|
||||||
|
writer.write_all(&self.diversifier.0)?;
|
||||||
|
|
||||||
|
// Writing the note means writing the note.value and note.r. The Note is recoverable
|
||||||
|
// from these 2 values and the Payment address.
|
||||||
|
writer.write_u64::<LittleEndian>(self.note.value)?;
|
||||||
|
|
||||||
|
let mut rcm = [0; 32];
|
||||||
|
self.note.r.into_repr().write_le(&mut rcm[..])?;
|
||||||
|
writer.write_all(&rcm)?;
|
||||||
|
|
||||||
|
Vector::write(&mut writer, &self.witnesses, |wr, wi| wi.write(wr) )?;
|
||||||
|
|
||||||
|
writer.write_all(&self.nullifier)?;
|
||||||
|
Optional::write(&mut writer, &self.spent, |w, t| w.write_all(&t.0))?;
|
||||||
|
|
||||||
|
Optional::write(&mut writer, &self.memo, |w, m| w.write_all(m.as_bytes()))?;
|
||||||
|
|
||||||
|
writer.write_u8(if self.is_change {1} else {0})?;
|
||||||
|
|
||||||
|
// Note that we don't write the unconfirmed_spent field, because if the wallet is restarted,
|
||||||
|
// we don't want to be beholden to any expired txns
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Utxo {
|
||||||
|
pub address: String,
|
||||||
|
pub txid: TxId,
|
||||||
|
pub output_index: u64,
|
||||||
|
pub script: Vec<u8>,
|
||||||
|
pub value: u64,
|
||||||
|
pub height: i32,
|
||||||
|
|
||||||
|
pub spent: Option<TxId>, // If this utxo was confirmed spent
|
||||||
|
pub unconfirmed_spent: Option<TxId>, // If this utxo was spent in a send, but has not yet been confirmed.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Utxo {
|
||||||
|
pub fn serialized_version() -> u64 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_outpoint(&self) -> OutPoint {
|
||||||
|
OutPoint { hash: self.txid.0, n: self.output_index as u32 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let version = reader.read_u64::<LittleEndian>()?;
|
||||||
|
assert_eq!(version, Utxo::serialized_version());
|
||||||
|
|
||||||
|
let address_len = reader.read_i32::<LittleEndian>()?;
|
||||||
|
let mut address_bytes = vec![0; address_len as usize];
|
||||||
|
reader.read_exact(&mut address_bytes)?;
|
||||||
|
let address = String::from_utf8(address_bytes).unwrap();
|
||||||
|
assert_eq!(address.chars().take(1).collect::<Vec<char>>()[0], 'R');
|
||||||
|
|
||||||
|
let mut txid_bytes = [0; 32];
|
||||||
|
reader.read_exact(&mut txid_bytes)?;
|
||||||
|
let txid = TxId { 0: txid_bytes };
|
||||||
|
|
||||||
|
let output_index = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let value = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let height = reader.read_i32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let script = Vector::read(&mut reader, |r| {
|
||||||
|
let mut byte = [0; 1];
|
||||||
|
r.read_exact(&mut byte)?;
|
||||||
|
Ok(byte[0])
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let spent = Optional::read(&mut reader, |r| {
|
||||||
|
let mut txbytes = [0u8; 32];
|
||||||
|
r.read_exact(&mut txbytes)?;
|
||||||
|
Ok(TxId{0: txbytes})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Note that we don't write the unconfirmed spent field, because if the wallet is restarted, we'll reset any unconfirmed stuff.
|
||||||
|
|
||||||
|
Ok(Utxo {
|
||||||
|
address,
|
||||||
|
txid,
|
||||||
|
output_index,
|
||||||
|
script,
|
||||||
|
value,
|
||||||
|
height,
|
||||||
|
spent,
|
||||||
|
unconfirmed_spent: None::<TxId>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||||
|
writer.write_u64::<LittleEndian>(Utxo::serialized_version())?;
|
||||||
|
|
||||||
|
writer.write_u32::<LittleEndian>(self.address.as_bytes().len() as u32)?;
|
||||||
|
writer.write_all(self.address.as_bytes())?;
|
||||||
|
|
||||||
|
writer.write_all(&self.txid.0)?;
|
||||||
|
|
||||||
|
writer.write_u64::<LittleEndian>(self.output_index)?;
|
||||||
|
writer.write_u64::<LittleEndian>(self.value)?;
|
||||||
|
writer.write_i32::<LittleEndian>(self.height)?;
|
||||||
|
|
||||||
|
Vector::write(&mut writer, &self.script, |w, b| w.write_all(&[*b]))?;
|
||||||
|
|
||||||
|
Optional::write(&mut writer, &self.spent, |w, txid| w.write_all(&txid.0))?;
|
||||||
|
|
||||||
|
// Note that we don't write the unconfirmed spent field, because if the wallet is restarted, we'll reset any unconfirmed stuff.
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OutgoingTxMetadata {
|
||||||
|
pub address: String,
|
||||||
|
pub value : u64,
|
||||||
|
pub memo : Memo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutgoingTxMetadata {
|
||||||
|
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let address_len = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let mut address_bytes = vec![0; address_len as usize];
|
||||||
|
reader.read_exact(&mut address_bytes)?;
|
||||||
|
let address = String::from_utf8(address_bytes).unwrap();
|
||||||
|
|
||||||
|
let value = reader.read_u64::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let mut memo_bytes = [0u8; 512];
|
||||||
|
reader.read_exact(&mut memo_bytes)?;
|
||||||
|
let memo = Memo::from_bytes(&memo_bytes).unwrap();
|
||||||
|
|
||||||
|
Ok(OutgoingTxMetadata{
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
memo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||||
|
// Strings are written as len + utf8
|
||||||
|
writer.write_u64::<LittleEndian>(self.address.as_bytes().len() as u64)?;
|
||||||
|
writer.write_all(self.address.as_bytes())?;
|
||||||
|
|
||||||
|
writer.write_u64::<LittleEndian>(self.value)?;
|
||||||
|
writer.write_all(self.memo.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IncomingTxMetadata {
|
||||||
|
pub address: String,
|
||||||
|
pub value : u64,
|
||||||
|
pub memo : Memo,
|
||||||
|
pub incoming_mempool: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IncomingTxMetadata {
|
||||||
|
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let address_len = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let mut address_bytes = vec![0; address_len as usize];
|
||||||
|
reader.read_exact(&mut address_bytes)?;
|
||||||
|
let address = String::from_utf8(address_bytes).unwrap();
|
||||||
|
|
||||||
|
let value = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let incoming_mempool = true;
|
||||||
|
// let position = 0;
|
||||||
|
|
||||||
|
let mut memo_bytes = [0u8; 512];
|
||||||
|
reader.read_exact(&mut memo_bytes)?;
|
||||||
|
let memo = Memo::from_bytes(&memo_bytes).unwrap();
|
||||||
|
|
||||||
|
Ok(IncomingTxMetadata{
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
memo,
|
||||||
|
incoming_mempool,
|
||||||
|
// position,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||||
|
// Strings are written as len + utf8
|
||||||
|
writer.write_u64::<LittleEndian>(self.address.as_bytes().len() as u64)?;
|
||||||
|
writer.write_all(self.address.as_bytes())?;
|
||||||
|
|
||||||
|
writer.write_u64::<LittleEndian>(self.value)?;
|
||||||
|
writer.write_all(self.memo.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WalletTx {
|
||||||
|
// Block in which this tx was included
|
||||||
|
pub block: i32,
|
||||||
|
|
||||||
|
// Timestamp of Tx. Added in v4
|
||||||
|
pub datetime: u64,
|
||||||
|
|
||||||
|
// Txid of this transaction. It's duplicated here (It is also the Key in the HashMap that points to this
|
||||||
|
// WalletTx in LightWallet::txs)
|
||||||
|
pub txid: TxId,
|
||||||
|
|
||||||
|
// List of all notes received in this tx. Some of these might be change notes.
|
||||||
|
pub notes: Vec<SaplingNoteData>,
|
||||||
|
|
||||||
|
// List of all Utxos received in this Tx. Some of these might be change notes
|
||||||
|
pub utxos: Vec<Utxo>,
|
||||||
|
|
||||||
|
// Total shielded value spent in this Tx. Note that this is the value of the wallet's notes spent.
|
||||||
|
// Some change may be returned in one of the notes above. Subtract the two to get the actual value spent.
|
||||||
|
// Also note that even after subtraction, you might need to account for transparent inputs and outputs
|
||||||
|
// to make sure the value is accurate.
|
||||||
|
pub total_shielded_value_spent: u64,
|
||||||
|
|
||||||
|
// Total amount of transparent funds that belong to us that were spent in this Tx.
|
||||||
|
pub total_transparent_value_spent : u64,
|
||||||
|
|
||||||
|
// All outgoing sapling sends to addresses outside this wallet
|
||||||
|
pub outgoing_metadata: Vec<OutgoingTxMetadata>,
|
||||||
|
|
||||||
|
pub incoming_metadata: Vec<IncomingTxMetadata>,
|
||||||
|
|
||||||
|
// Whether this TxID was downloaded from the server and scanned for Memos
|
||||||
|
pub full_tx_scanned: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTx {
|
||||||
|
pub fn serialized_version() -> u64 {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(height: i32, datetime: u64, txid: &TxId) -> Self {
|
||||||
|
WalletTx {
|
||||||
|
block: height,
|
||||||
|
datetime,
|
||||||
|
txid: txid.clone(),
|
||||||
|
notes: vec![],
|
||||||
|
utxos: vec![],
|
||||||
|
total_shielded_value_spent: 0,
|
||||||
|
total_transparent_value_spent: 0,
|
||||||
|
outgoing_metadata: vec![],
|
||||||
|
incoming_metadata: vec![],
|
||||||
|
full_tx_scanned: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let version = reader.read_u64::<LittleEndian>()?;
|
||||||
|
assert!(version <= WalletTx::serialized_version(), "Version mismatch. Please restore with your Seed");
|
||||||
|
|
||||||
|
let block = reader.read_i32::<LittleEndian>()?;
|
||||||
|
let datetime = if version >= 4 {
|
||||||
|
reader.read_u64::<LittleEndian>()?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut txid_bytes = [0u8; 32];
|
||||||
|
reader.read_exact(&mut txid_bytes)?;
|
||||||
|
let txid = TxId{0: txid_bytes};
|
||||||
|
|
||||||
|
let notes = Vector::read(&mut reader, |r| SaplingNoteData::read(r))?;
|
||||||
|
let utxos = Vector::read(&mut reader, |r| Utxo::read(r))?;
|
||||||
|
let total_shielded_value_spent = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let total_transparent_value_spent = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let outgoing_metadata = Vector::read(&mut reader, |r| OutgoingTxMetadata::read(r))?;
|
||||||
|
|
||||||
|
// Read incoming_metadata only if version is 5 or higher
|
||||||
|
let incoming_metadata = if version >= 5 {
|
||||||
|
Vector::read(&mut reader, |r| IncomingTxMetadata::read(r))?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_tx_scanned = reader.read_u8()? > 0;
|
||||||
|
|
||||||
|
Ok(WalletTx {
|
||||||
|
block,
|
||||||
|
datetime,
|
||||||
|
txid,
|
||||||
|
notes,
|
||||||
|
utxos,
|
||||||
|
total_shielded_value_spent,
|
||||||
|
total_transparent_value_spent,
|
||||||
|
outgoing_metadata,
|
||||||
|
incoming_metadata,
|
||||||
|
full_tx_scanned
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||||
|
writer.write_u64::<LittleEndian>(WalletTx::serialized_version())?;
|
||||||
|
|
||||||
|
writer.write_i32::<LittleEndian>(self.block)?;
|
||||||
|
|
||||||
|
writer.write_u64::<LittleEndian>(self.datetime)?;
|
||||||
|
|
||||||
|
writer.write_all(&self.txid.0)?;
|
||||||
|
|
||||||
|
Vector::write(&mut writer, &self.notes, |w, nd| nd.write(w))?;
|
||||||
|
Vector::write(&mut writer, &self.utxos, |w, u| u.write(w))?;
|
||||||
|
|
||||||
|
writer.write_u64::<LittleEndian>(self.total_shielded_value_spent)?;
|
||||||
|
writer.write_u64::<LittleEndian>(self.total_transparent_value_spent)?;
|
||||||
|
|
||||||
|
// Write the outgoing metadata
|
||||||
|
Vector::write(&mut writer, &self.outgoing_metadata, |w, om| om.write(w))?;
|
||||||
|
Vector::write(&mut writer, &self.incoming_metadata, |w, om| om.write(w))?;
|
||||||
|
|
||||||
|
writer.write_u8(if self.full_tx_scanned {1} else {0})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpendableNote {
|
||||||
|
pub txid: TxId,
|
||||||
|
pub nullifier: [u8; 32],
|
||||||
|
pub diversifier: Diversifier,
|
||||||
|
pub note: Note<Bls12>,
|
||||||
|
pub witness: IncrementalWitness<Node>,
|
||||||
|
pub extsk: ExtendedSpendingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpendableNote {
|
||||||
|
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &Option<ExtendedSpendingKey>) -> Option<Self> {
|
||||||
|
// Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet.
|
||||||
|
if nd.spent.is_none() && nd.unconfirmed_spent.is_none() && extsk.is_some() &&
|
||||||
|
nd.witnesses.len() >= (anchor_offset + 1) {
|
||||||
|
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1);
|
||||||
|
|
||||||
|
witness.map(|w| SpendableNote {
|
||||||
|
txid,
|
||||||
|
nullifier: nd.nullifier,
|
||||||
|
diversifier: nd.diversifier,
|
||||||
|
note: nd.note.clone(),
|
||||||
|
witness: w.clone(),
|
||||||
|
extsk: extsk.clone().unwrap(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/extended_key.rs
vendored
Normal file
126
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/extended_key.rs
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
use ring::{
|
||||||
|
hmac::{self, Context, Key},
|
||||||
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use secp256k1::{PublicKey, Secp256k1, SecretKey, SignOnly, VerifyOnly, Error};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref SECP256K1_SIGN_ONLY: Secp256k1<SignOnly> = Secp256k1::signing_only();
|
||||||
|
static ref SECP256K1_VERIFY_ONLY: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
|
||||||
|
}
|
||||||
|
/// Random entropy, part of extended key.
|
||||||
|
type ChainCode = Vec<u8>;
|
||||||
|
|
||||||
|
|
||||||
|
const HARDENED_KEY_START_INDEX: u32 = 2_147_483_648; // 2 ** 31
|
||||||
|
|
||||||
|
/// KeyIndex indicates the key type and index of a child key.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum KeyIndex {
|
||||||
|
/// Normal key, index range is from 0 to 2 ** 31 - 1
|
||||||
|
Normal(u32),
|
||||||
|
/// Hardened key, index range is from 2 ** 31 to 2 ** 32 - 1
|
||||||
|
Hardened(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyIndex {
|
||||||
|
|
||||||
|
/// Check index range.
|
||||||
|
pub fn is_valid(self) -> bool {
|
||||||
|
match self {
|
||||||
|
KeyIndex::Normal(i) => i < HARDENED_KEY_START_INDEX,
|
||||||
|
KeyIndex::Hardened(i) => i >= HARDENED_KEY_START_INDEX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate Hardened KeyIndex from normalize index value.
|
||||||
|
pub fn hardened_from_normalize_index(i: u32) -> Result<KeyIndex, Error> {
|
||||||
|
if i < HARDENED_KEY_START_INDEX {
|
||||||
|
Ok(KeyIndex::Hardened(HARDENED_KEY_START_INDEX + i))
|
||||||
|
} else {
|
||||||
|
Ok(KeyIndex::Hardened(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate KeyIndex from raw index value.
|
||||||
|
pub fn from_index(i: u32) -> Result<Self, Error> {
|
||||||
|
if i < HARDENED_KEY_START_INDEX {
|
||||||
|
Ok(KeyIndex::Normal(i))
|
||||||
|
} else {
|
||||||
|
Ok(KeyIndex::Hardened(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for KeyIndex {
|
||||||
|
fn from(index: u32) -> Self {
|
||||||
|
KeyIndex::from_index(index).expect("KeyIndex")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// ExtendedPrivKey is used for child key derivation.
|
||||||
|
/// See [secp256k1 crate documentation](https://docs.rs/secp256k1) for SecretKey signatures usage.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtendedPrivKey {
|
||||||
|
pub private_key: SecretKey,
|
||||||
|
pub chain_code: ChainCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl ExtendedPrivKey {
|
||||||
|
|
||||||
|
/// Generate an ExtendedPrivKey from seed
|
||||||
|
pub fn with_seed(seed: &[u8]) -> Result<ExtendedPrivKey, Error> {
|
||||||
|
let signature = {
|
||||||
|
let signing_key = Key::new(hmac::HMAC_SHA512, b"Bitcoin seed");
|
||||||
|
let mut h = Context::with_key(&signing_key);
|
||||||
|
h.update(&seed);
|
||||||
|
h.sign()
|
||||||
|
};
|
||||||
|
let sig_bytes = signature.as_ref();
|
||||||
|
let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2);
|
||||||
|
let private_key = SecretKey::from_slice(key)?;
|
||||||
|
Ok(ExtendedPrivKey {
|
||||||
|
private_key,
|
||||||
|
chain_code: chain_code.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_hardended_key(&self, index: u32) -> ring::hmac::Tag {
|
||||||
|
let signing_key = Key::new(hmac::HMAC_SHA512, &self.chain_code);
|
||||||
|
let mut h = Context::with_key(&signing_key);
|
||||||
|
h.update(&[0x00]);
|
||||||
|
h.update(&self.private_key[..]);
|
||||||
|
h.update(&index.to_be_bytes());
|
||||||
|
h.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_normal_key(&self, index: u32) -> ring::hmac::Tag {
|
||||||
|
let signing_key = Key::new(hmac::HMAC_SHA512, &self.chain_code);
|
||||||
|
let mut h = Context::with_key(&signing_key);
|
||||||
|
let public_key = PublicKey::from_secret_key(&SECP256K1_SIGN_ONLY, &self.private_key);
|
||||||
|
h.update(&public_key.serialize());
|
||||||
|
h.update(&index.to_be_bytes());
|
||||||
|
h.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive a child key from ExtendedPrivKey.
|
||||||
|
pub fn derive_private_key(&self, key_index: KeyIndex) -> Result<ExtendedPrivKey, Error> {
|
||||||
|
if !key_index.is_valid() {
|
||||||
|
return Err(Error::InvalidTweak);
|
||||||
|
}
|
||||||
|
let signature = match key_index {
|
||||||
|
KeyIndex::Hardened(index) => self.sign_hardended_key(index),
|
||||||
|
KeyIndex::Normal(index) => self.sign_normal_key(index),
|
||||||
|
};
|
||||||
|
let sig_bytes = signature.as_ref();
|
||||||
|
let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2);
|
||||||
|
let mut private_key = SecretKey::from_slice(key)?;
|
||||||
|
private_key.add_assign(&self.private_key[..])?;
|
||||||
|
Ok(ExtendedPrivKey {
|
||||||
|
private_key,
|
||||||
|
chain_code: chain_code.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
123
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/prover.rs
vendored
Normal file
123
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/prover.rs
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//! Abstractions over the proving system and parameters for ease of use.
|
||||||
|
|
||||||
|
use bellman::groth16::{prepare_verifying_key, Parameters, PreparedVerifyingKey};
|
||||||
|
use pairing::bls12_381::{Bls12, Fr};
|
||||||
|
use zcash_primitives::{
|
||||||
|
jubjub::{edwards, fs::Fs, Unknown},
|
||||||
|
primitives::{Diversifier, PaymentAddress, ProofGenerationKey},
|
||||||
|
redjubjub::{PublicKey, Signature},
|
||||||
|
transaction::components::Amount
|
||||||
|
};
|
||||||
|
use zcash_primitives::{
|
||||||
|
merkle_tree::CommitmentTreeWitness, prover::TxProver, sapling::Node,
|
||||||
|
transaction::components::GROTH_PROOF_SIZE, JUBJUB,
|
||||||
|
};
|
||||||
|
use zcash_proofs::sapling::SaplingProvingContext;
|
||||||
|
|
||||||
|
/// An implementation of [`TxProver`] using Sapling Spend and Output parameters provided
|
||||||
|
/// in-memory.
|
||||||
|
pub struct InMemTxProver {
|
||||||
|
spend_params: Parameters<Bls12>,
|
||||||
|
spend_vk: PreparedVerifyingKey<Bls12>,
|
||||||
|
output_params: Parameters<Bls12>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemTxProver {
|
||||||
|
pub fn new(spend_params: &[u8], output_params: &[u8]) -> Self {
|
||||||
|
// Deserialize params
|
||||||
|
let spend_params = Parameters::<Bls12>::read(spend_params, false)
|
||||||
|
.expect("couldn't deserialize Sapling spend parameters file");
|
||||||
|
let output_params = Parameters::<Bls12>::read(output_params, false)
|
||||||
|
.expect("couldn't deserialize Sapling spend parameters file");
|
||||||
|
|
||||||
|
// Prepare verifying keys
|
||||||
|
let spend_vk = prepare_verifying_key(&spend_params.vk);
|
||||||
|
|
||||||
|
InMemTxProver {
|
||||||
|
spend_params,
|
||||||
|
spend_vk,
|
||||||
|
output_params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TxProver for InMemTxProver {
|
||||||
|
type SaplingProvingContext = SaplingProvingContext;
|
||||||
|
|
||||||
|
fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext {
|
||||||
|
SaplingProvingContext::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spend_proof(
|
||||||
|
&self,
|
||||||
|
ctx: &mut Self::SaplingProvingContext,
|
||||||
|
proof_generation_key: ProofGenerationKey<Bls12>,
|
||||||
|
diversifier: Diversifier,
|
||||||
|
rcm: Fs,
|
||||||
|
ar: Fs,
|
||||||
|
value: u64,
|
||||||
|
anchor: Fr,
|
||||||
|
witness: CommitmentTreeWitness<Node>,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
[u8; GROTH_PROOF_SIZE],
|
||||||
|
edwards::Point<Bls12, Unknown>,
|
||||||
|
PublicKey<Bls12>,
|
||||||
|
),
|
||||||
|
(),
|
||||||
|
> {
|
||||||
|
let (proof, cv, rk) = ctx.spend_proof(
|
||||||
|
proof_generation_key,
|
||||||
|
diversifier,
|
||||||
|
rcm,
|
||||||
|
ar,
|
||||||
|
value,
|
||||||
|
anchor,
|
||||||
|
witness,
|
||||||
|
&self.spend_params,
|
||||||
|
&self.spend_vk,
|
||||||
|
&JUBJUB,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
|
||||||
|
proof
|
||||||
|
.write(&mut zkproof[..])
|
||||||
|
.expect("should be able to serialize a proof");
|
||||||
|
|
||||||
|
Ok((zkproof, cv, rk))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_proof(
|
||||||
|
&self,
|
||||||
|
ctx: &mut Self::SaplingProvingContext,
|
||||||
|
esk: Fs,
|
||||||
|
payment_address: PaymentAddress<Bls12>,
|
||||||
|
rcm: Fs,
|
||||||
|
value: u64,
|
||||||
|
) -> ([u8; GROTH_PROOF_SIZE], edwards::Point<Bls12, Unknown>) {
|
||||||
|
let (proof, cv) = ctx.output_proof(
|
||||||
|
esk,
|
||||||
|
payment_address,
|
||||||
|
rcm,
|
||||||
|
value,
|
||||||
|
&self.output_params,
|
||||||
|
&JUBJUB,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
|
||||||
|
proof
|
||||||
|
.write(&mut zkproof[..])
|
||||||
|
.expect("should be able to serialize a proof");
|
||||||
|
|
||||||
|
(zkproof, cv)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binding_sig(
|
||||||
|
&self,
|
||||||
|
ctx: &mut Self::SaplingProvingContext,
|
||||||
|
value_balance: Amount,
|
||||||
|
sighash: &[u8; 32],
|
||||||
|
) -> Result<Signature, ()> {
|
||||||
|
ctx.binding_sig(value_balance, sighash, &JUBJUB)
|
||||||
|
}
|
||||||
|
}
|
||||||
2339
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/tests.rs
vendored
Normal file
2339
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/tests.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/utils.rs
vendored
Normal file
21
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/utils.rs
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
pub fn read_string<R: Read>(mut reader: R) -> io::Result<String> {
|
||||||
|
// Strings are written as <littleendian> len + bytes
|
||||||
|
let str_len = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let mut str_bytes = vec![0; str_len as usize];
|
||||||
|
reader.read_exact(&mut str_bytes)?;
|
||||||
|
|
||||||
|
let str = String::from_utf8(str_bytes).map_err(|e| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, e.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_string<W: Write>(mut writer: W, s: &String) -> io::Result<()> {
|
||||||
|
// Strings are written as len + utf8
|
||||||
|
writer.write_u64::<LittleEndian>(s.as_bytes().len() as u64)?;
|
||||||
|
writer.write_all(s.as_bytes())
|
||||||
|
}
|
||||||
585
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/walletzkey.rs
vendored
Normal file
585
third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/walletzkey.rs
vendored
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use pairing::bls12_381::{Bls12};
|
||||||
|
|
||||||
|
use sodiumoxide::crypto::secretbox;
|
||||||
|
|
||||||
|
use zcash_primitives::{
|
||||||
|
serialize::{Vector, Optional},
|
||||||
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||||
|
primitives::{PaymentAddress},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::lightclient::{LightClientConfig};
|
||||||
|
use crate::lightwallet::{LightWallet, utils};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum WalletTKeyType {
|
||||||
|
HdKey = 0,
|
||||||
|
ImportedKey = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A struct that holds z-address private keys or view keys
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct WalletTKey {
|
||||||
|
pub(super) keytype: WalletTKeyType,
|
||||||
|
locked: bool,
|
||||||
|
pub(super) address: String,
|
||||||
|
pub(super) tkey: Option<secp256k1::SecretKey>,
|
||||||
|
|
||||||
|
// If locked, the encrypted key is here
|
||||||
|
enc_key: Option<Vec<u8>>,
|
||||||
|
nonce: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTKey {
|
||||||
|
pub fn new_hdkey(key: secp256k1::SecretKey, address: String) -> Self {
|
||||||
|
WalletTKey {
|
||||||
|
keytype: WalletTKeyType::HdKey,
|
||||||
|
locked: false,
|
||||||
|
address,
|
||||||
|
tkey: Some(key),
|
||||||
|
|
||||||
|
enc_key: None,
|
||||||
|
nonce: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_hdkey(key: secp256k1::SecretKey, address: String) -> Self {
|
||||||
|
WalletTKey {
|
||||||
|
keytype: WalletTKeyType::ImportedKey,
|
||||||
|
locked: false,
|
||||||
|
address,
|
||||||
|
tkey: Some(key),
|
||||||
|
|
||||||
|
enc_key: None,
|
||||||
|
nonce: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialized_version() -> u8 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
|
||||||
|
let version = inp.read_u8()?;
|
||||||
|
assert!(version <= Self::serialized_version());
|
||||||
|
|
||||||
|
let keytype: WalletTKeyType = match inp.read_u32::<LittleEndian>()? {
|
||||||
|
0 => Ok(WalletTKeyType::HdKey),
|
||||||
|
1 => Ok(WalletTKeyType::ImportedKey),
|
||||||
|
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown tkey type {}", n)))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let locked = inp.read_u8()? > 0;
|
||||||
|
|
||||||
|
let address = utils::read_string(&mut inp)?;
|
||||||
|
let tkey = Optional::read(&mut inp, |r| {
|
||||||
|
let mut tpk_bytes = [0u8; 32];
|
||||||
|
r.read_exact(&mut tpk_bytes)?;
|
||||||
|
secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let enc_key = Optional::read(&mut inp, |r|
|
||||||
|
Vector::read(r, |r| r.read_u8()))?;
|
||||||
|
let nonce = Optional::read(&mut inp, |r|
|
||||||
|
Vector::read(r, |r| r.read_u8()))?;
|
||||||
|
|
||||||
|
Ok(WalletTKey {
|
||||||
|
keytype,
|
||||||
|
locked,
|
||||||
|
address,
|
||||||
|
tkey,
|
||||||
|
enc_key,
|
||||||
|
nonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
|
||||||
|
out.write_u8(Self::serialized_version())?;
|
||||||
|
|
||||||
|
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
|
||||||
|
|
||||||
|
out.write_u8(self.locked as u8)?;
|
||||||
|
|
||||||
|
utils::write_string(&mut out, &self.address)?;
|
||||||
|
Optional::write(&mut out, &self.tkey, |w, pk|
|
||||||
|
w.write_all(&pk[..])
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Write enc_key
|
||||||
|
Optional::write(&mut out, &self.enc_key, |o, v|
|
||||||
|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
|
||||||
|
|
||||||
|
// Write nonce
|
||||||
|
Optional::write(&mut out, &self.nonce, |o, v|
|
||||||
|
Vector::write(o, v, |o,n| o.write_u8(*n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn lock(&mut self) -> io::Result<()> {
|
||||||
|
// For keys, encrypt the key into enckey
|
||||||
|
// assert that we have the encrypted key.
|
||||||
|
if self.enc_key.is_none() {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when t-addr private key is not encrypted"));
|
||||||
|
}
|
||||||
|
self.tkey = None;
|
||||||
|
self.locked = true;
|
||||||
|
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(&mut self, key: &secretbox::Key) -> io::Result<()> {
|
||||||
|
// For imported keys, we need to decrypt from the encrypted key
|
||||||
|
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
|
||||||
|
let sk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tkey = Some(secp256k1::SecretKey::from_slice(&sk_bytes[..]).map_err(|e|
|
||||||
|
io::Error::new(ErrorKind::InvalidData, format!("{}", e))
|
||||||
|
)?);
|
||||||
|
|
||||||
|
self.locked = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
|
||||||
|
// For keys, encrypt the key into enckey
|
||||||
|
let nonce = secretbox::gen_nonce();
|
||||||
|
|
||||||
|
let sk_bytes = &self.tkey.unwrap()[..];
|
||||||
|
|
||||||
|
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
|
||||||
|
self.nonce = Some(nonce.as_ref().to_vec());
|
||||||
|
|
||||||
|
self.tkey = None;
|
||||||
|
|
||||||
|
// Also lock after encrypt
|
||||||
|
self.lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_encryption(&mut self) -> io::Result<()> {
|
||||||
|
if self.locked {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.enc_key = None;
|
||||||
|
self.nonce = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum WalletZKeyType {
|
||||||
|
HdKey = 0,
|
||||||
|
ImportedSpendingKey = 1,
|
||||||
|
ImportedViewKey = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// A struct that holds z-address private keys or view keys
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct WalletZKey {
|
||||||
|
pub(super) keytype: WalletZKeyType,
|
||||||
|
locked: bool,
|
||||||
|
pub(super) extsk: Option<ExtendedSpendingKey>,
|
||||||
|
pub(super) extfvk: ExtendedFullViewingKey,
|
||||||
|
pub(super) zaddress: PaymentAddress<Bls12>,
|
||||||
|
|
||||||
|
// If this is a HD key, what is the key number
|
||||||
|
pub(super) hdkey_num: Option<u32>,
|
||||||
|
|
||||||
|
// If locked, the encrypted private key is stored here
|
||||||
|
enc_key: Option<Vec<u8>>,
|
||||||
|
nonce: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletZKey {
|
||||||
|
pub fn new_hdkey(hdkey_num: u32, extsk: ExtendedSpendingKey) -> Self {
|
||||||
|
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||||
|
let zaddress = extfvk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
WalletZKey {
|
||||||
|
keytype: WalletZKeyType::HdKey,
|
||||||
|
locked: false,
|
||||||
|
extsk: Some(extsk),
|
||||||
|
extfvk,
|
||||||
|
zaddress,
|
||||||
|
hdkey_num: Some(hdkey_num),
|
||||||
|
enc_key: None,
|
||||||
|
nonce: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_locked_hdkey(hdkey_num: u32, extfvk: ExtendedFullViewingKey) -> Self {
|
||||||
|
let zaddress = extfvk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
WalletZKey {
|
||||||
|
keytype: WalletZKeyType::HdKey,
|
||||||
|
locked: true,
|
||||||
|
extsk: None,
|
||||||
|
extfvk,
|
||||||
|
zaddress,
|
||||||
|
hdkey_num: Some(hdkey_num),
|
||||||
|
enc_key: None,
|
||||||
|
nonce: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_imported_sk(extsk: ExtendedSpendingKey) -> Self {
|
||||||
|
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||||
|
let zaddress = extfvk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
WalletZKey {
|
||||||
|
keytype: WalletZKeyType::ImportedSpendingKey,
|
||||||
|
locked: false,
|
||||||
|
extsk: Some(extsk),
|
||||||
|
extfvk,
|
||||||
|
zaddress,
|
||||||
|
hdkey_num: None,
|
||||||
|
enc_key: None,
|
||||||
|
nonce: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_imported_viewkey(extfvk: ExtendedFullViewingKey) -> Self {
|
||||||
|
let zaddress = extfvk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
WalletZKey {
|
||||||
|
keytype: WalletZKeyType::ImportedViewKey,
|
||||||
|
locked: false,
|
||||||
|
extsk: None,
|
||||||
|
extfvk,
|
||||||
|
zaddress,
|
||||||
|
hdkey_num: None,
|
||||||
|
enc_key: None,
|
||||||
|
nonce: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialized_version() -> u8 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn have_spending_key(&self) -> bool {
|
||||||
|
self.extsk.is_some() || self.enc_key.is_some() || self.hdkey_num.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
|
||||||
|
let version = inp.read_u8()?;
|
||||||
|
assert!(version <= Self::serialized_version());
|
||||||
|
|
||||||
|
let keytype: WalletZKeyType = match inp.read_u32::<LittleEndian>()? {
|
||||||
|
0 => Ok(WalletZKeyType::HdKey),
|
||||||
|
1 => Ok(WalletZKeyType::ImportedSpendingKey),
|
||||||
|
2 => Ok(WalletZKeyType::ImportedViewKey),
|
||||||
|
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown zkey type {}", n)))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let locked = inp.read_u8()? > 0;
|
||||||
|
|
||||||
|
let extsk = Optional::read(&mut inp, |r| ExtendedSpendingKey::read(r))?;
|
||||||
|
let extfvk = ExtendedFullViewingKey::read(&mut inp)?;
|
||||||
|
let zaddress = extfvk.default_address().unwrap().1;
|
||||||
|
|
||||||
|
let hdkey_num = Optional::read(&mut inp, |r| r.read_u32::<LittleEndian>())?;
|
||||||
|
|
||||||
|
let enc_key = Optional::read(&mut inp, |r|
|
||||||
|
Vector::read(r, |r| r.read_u8()))?;
|
||||||
|
let nonce = Optional::read(&mut inp, |r|
|
||||||
|
Vector::read(r, |r| r.read_u8()))?;
|
||||||
|
|
||||||
|
Ok(WalletZKey {
|
||||||
|
keytype,
|
||||||
|
locked,
|
||||||
|
extsk,
|
||||||
|
extfvk,
|
||||||
|
zaddress,
|
||||||
|
hdkey_num,
|
||||||
|
enc_key,
|
||||||
|
nonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
|
||||||
|
out.write_u8(Self::serialized_version())?;
|
||||||
|
|
||||||
|
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
|
||||||
|
|
||||||
|
out.write_u8(self.locked as u8)?;
|
||||||
|
|
||||||
|
Optional::write(&mut out, &self.extsk, |w, sk| ExtendedSpendingKey::write(sk, w))?;
|
||||||
|
|
||||||
|
ExtendedFullViewingKey::write(&self.extfvk, &mut out)?;
|
||||||
|
|
||||||
|
Optional::write(&mut out, &self.hdkey_num, |o, n| o.write_u32::<LittleEndian>(*n))?;
|
||||||
|
|
||||||
|
// Write enc_key
|
||||||
|
Optional::write(&mut out, &self.enc_key, |o, v|
|
||||||
|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
|
||||||
|
|
||||||
|
// Write nonce
|
||||||
|
Optional::write(&mut out, &self.nonce, |o, v|
|
||||||
|
Vector::write(o, v, |o,n| o.write_u8(*n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lock(&mut self) -> io::Result<()> {
|
||||||
|
match self.keytype {
|
||||||
|
WalletZKeyType::HdKey => {
|
||||||
|
// For HD keys, just empty out the keys, since they will be reconstructed from the hdkey_num
|
||||||
|
self.extsk = None;
|
||||||
|
self.locked = true;
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedSpendingKey => {
|
||||||
|
// For imported keys, encrypt the key into enckey
|
||||||
|
// assert that we have the encrypted key.
|
||||||
|
if self.enc_key.is_none() {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when imported key is not encrypted"));
|
||||||
|
}
|
||||||
|
self.extsk = None;
|
||||||
|
self.locked = true;
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedViewKey => {
|
||||||
|
// For viewing keys, there is nothing to lock, so just return true
|
||||||
|
self.locked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(&mut self, config: &LightClientConfig, bip39_seed: &[u8], key: &secretbox::Key) -> io::Result<()> {
|
||||||
|
match self.keytype {
|
||||||
|
WalletZKeyType::HdKey => {
|
||||||
|
let (extsk, extfvk, address) =
|
||||||
|
LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed, self.hdkey_num.unwrap());
|
||||||
|
|
||||||
|
if address != self.zaddress {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidData,
|
||||||
|
format!("zaddress mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), address, self.zaddress)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if extfvk != self.extfvk {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidData,
|
||||||
|
format!("fvk mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), extfvk, self.extfvk)));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.extsk = Some(extsk);
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedSpendingKey => {
|
||||||
|
// For imported keys, we need to decrypt from the encrypted key
|
||||||
|
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
|
||||||
|
let extsk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.extsk = Some(ExtendedSpendingKey::read(&extsk_bytes[..])?);
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedViewKey => {
|
||||||
|
// Viewing key unlocking is basically a no op
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.locked = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
|
||||||
|
match self.keytype {
|
||||||
|
WalletZKeyType::HdKey => {
|
||||||
|
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedSpendingKey => {
|
||||||
|
// For imported keys, encrypt the key into enckey
|
||||||
|
let nonce = secretbox::gen_nonce();
|
||||||
|
|
||||||
|
let mut sk_bytes = vec![];
|
||||||
|
self.extsk.as_ref().unwrap().write(&mut sk_bytes)?;
|
||||||
|
|
||||||
|
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
|
||||||
|
self.nonce = Some(nonce.as_ref().to_vec());
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedViewKey => {
|
||||||
|
// Encrypting a viewing key is a no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also lock after encrypt
|
||||||
|
self.lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_encryption(&mut self) -> io::Result<()> {
|
||||||
|
if self.locked {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.keytype {
|
||||||
|
WalletZKeyType::HdKey => {
|
||||||
|
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedSpendingKey => {
|
||||||
|
self.enc_key = None;
|
||||||
|
self.nonce = None;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
WalletZKeyType::ImportedViewKey => {
|
||||||
|
// Removing encryption is a no-op for viewing keys
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use zcash_client_backend::{
|
||||||
|
encoding::{encode_payment_address, decode_extended_spending_key, decode_extended_full_viewing_key}
|
||||||
|
};
|
||||||
|
use sodiumoxide::crypto::secretbox;
|
||||||
|
|
||||||
|
use crate::lightclient::LightClientConfig;
|
||||||
|
use super::WalletZKey;
|
||||||
|
|
||||||
|
fn get_config() -> LightClientConfig {
|
||||||
|
LightClientConfig {
|
||||||
|
server: "0.0.0.0:0".parse().unwrap(),
|
||||||
|
chain_name: "main".to_string(),
|
||||||
|
sapling_activation_height: 0,
|
||||||
|
consensus_branch_id: "000000".to_string(),
|
||||||
|
anchor_offset: 0,
|
||||||
|
data_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize() {
|
||||||
|
let config = get_config();
|
||||||
|
|
||||||
|
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
|
||||||
|
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
|
||||||
|
|
||||||
|
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
|
||||||
|
let wzk = WalletZKey::new_imported_sk(esk);
|
||||||
|
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
|
||||||
|
|
||||||
|
let mut v: Vec<u8> = vec![];
|
||||||
|
// Serialize
|
||||||
|
wzk.write(&mut v).unwrap();
|
||||||
|
// Read it right back
|
||||||
|
let wzk2 = WalletZKey::read(&v[..]).unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
assert_eq!(wzk, wzk2);
|
||||||
|
assert_eq!(wzk.extsk, wzk2.extsk);
|
||||||
|
assert_eq!(wzk.extfvk, wzk2.extfvk);
|
||||||
|
assert_eq!(wzk.zaddress, wzk2.zaddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_decrypt_sk() {
|
||||||
|
let config = get_config();
|
||||||
|
|
||||||
|
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
|
||||||
|
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
|
||||||
|
|
||||||
|
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
|
||||||
|
let mut wzk = WalletZKey::new_imported_sk(esk);
|
||||||
|
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
|
||||||
|
|
||||||
|
// Can't lock without encryption
|
||||||
|
assert!(wzk.lock().is_err());
|
||||||
|
|
||||||
|
// Encryption key
|
||||||
|
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
|
||||||
|
|
||||||
|
// Encrypt, but save the extsk first
|
||||||
|
let orig_extsk = wzk.extsk.clone().unwrap();
|
||||||
|
wzk.encrypt(&key).unwrap();
|
||||||
|
{
|
||||||
|
assert!(wzk.enc_key.is_some());
|
||||||
|
assert!(wzk.nonce.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now lock
|
||||||
|
assert!(wzk.lock().is_ok());
|
||||||
|
{
|
||||||
|
assert!(wzk.extsk.is_none());
|
||||||
|
assert_eq!(wzk.locked, true);
|
||||||
|
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't remove encryption without unlocking
|
||||||
|
assert!(wzk.remove_encryption().is_err());
|
||||||
|
|
||||||
|
// Unlock
|
||||||
|
assert!(wzk.unlock(&config, &[], &key).is_ok());
|
||||||
|
{
|
||||||
|
assert_eq!(wzk.extsk, Some(orig_extsk));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove encryption
|
||||||
|
assert!(wzk.remove_encryption().is_ok());
|
||||||
|
{
|
||||||
|
assert_eq!(wzk.enc_key, None);
|
||||||
|
assert_eq!(wzk.nonce, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_decrypt_vk() {
|
||||||
|
let config = get_config();
|
||||||
|
|
||||||
|
// Priv Key's address is "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc"
|
||||||
|
let viewkey = "zxviews1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3d9rr3ydjm9zl9464majh4pa3ejkfy779dm38sfnkar67et7ykxkk0z9rfsmf9jclfj2k85xt2exkg4pu5xqyzyxzlqa6x3p9wrd7pwdq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3ycqtvvje";
|
||||||
|
|
||||||
|
let extfvk = decode_extended_full_viewing_key(config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap();
|
||||||
|
let mut wzk = WalletZKey::new_imported_viewkey(extfvk);
|
||||||
|
|
||||||
|
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc".to_string());
|
||||||
|
|
||||||
|
// Encryption key
|
||||||
|
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
wzk.encrypt(&key).unwrap();
|
||||||
|
{
|
||||||
|
assert!(wzk.enc_key.is_none());
|
||||||
|
assert!(wzk.nonce.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now lock
|
||||||
|
assert!(wzk.lock().is_ok());
|
||||||
|
{
|
||||||
|
assert!(wzk.extsk.is_none());
|
||||||
|
assert_eq!(wzk.locked, true);
|
||||||
|
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't remove encryption without unlocking
|
||||||
|
assert!(wzk.remove_encryption().is_err());
|
||||||
|
|
||||||
|
// Unlock
|
||||||
|
assert!(wzk.unlock(&config, &[], &key).is_ok());
|
||||||
|
{
|
||||||
|
assert_eq!(wzk.extsk, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove encryption
|
||||||
|
assert!(wzk.remove_encryption().is_ok());
|
||||||
|
{
|
||||||
|
assert_eq!(wzk.enc_key, None);
|
||||||
|
assert_eq!(wzk.nonce, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
133
tools/lite_send_smoke.cpp
Normal file
133
tools/lite_send_smoke.cpp
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Real-backend SEND smoke harness for the lite wallet. Links the actual SDXL litelib_* backend
|
||||||
|
// (same imported target the app uses) and drives the EXACT send path the GUI uses
|
||||||
|
// (LiteClientBridge::execute("send", [{address,amount,memo}])), so it surfaces the same errors a
|
||||||
|
// GUI send would. ALWAYS run with an isolated HOME — it reads/writes ~/.silentdragonxlite there.
|
||||||
|
//
|
||||||
|
// lite_send_smoke --newaddr [server] # create a new wallet, print receive addresses
|
||||||
|
// lite_send_smoke --status [server] # open + sync + print balance/addresses
|
||||||
|
// lite_send_smoke --send <drgx> [server] # open + sync + self-send <drgx> to own z-addr
|
||||||
|
//
|
||||||
|
// Receive addresses are PUBLIC (meant to be shared to receive funds) so they are printed; seeds and
|
||||||
|
// private keys are never touched/printed here.
|
||||||
|
|
||||||
|
#include "wallet/lite_client_bridge.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace dragonx::wallet;
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
|
// Recursively collect address-looking strings from any JSON shape the backend returns.
|
||||||
|
static void collectAddrs(const json& n, std::vector<std::string>& zs, std::vector<std::string>& ts)
|
||||||
|
{
|
||||||
|
if (n.is_string()) {
|
||||||
|
const std::string s = n.get<std::string>();
|
||||||
|
if (s.rfind("zs", 0) == 0) zs.push_back(s);
|
||||||
|
else if (!s.empty() && (s[0] == 'R' || s[0] == 't')) ts.push_back(s);
|
||||||
|
} else if (n.is_array()) {
|
||||||
|
for (const auto& e : n) collectAddrs(e, zs, ts);
|
||||||
|
} else if (n.is_object()) {
|
||||||
|
for (const auto& kv : n.items()) collectAddrs(kv.value(), zs, ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
std::setvbuf(stdout, nullptr, _IONBF, 0); // unbuffered so output survives a timeout kill
|
||||||
|
|
||||||
|
std::string server = "https://lite.dragonx.is";
|
||||||
|
std::string mode;
|
||||||
|
double sendDrgx = 0.0;
|
||||||
|
bool doRescan = false;
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const std::string a = argv[i];
|
||||||
|
if (a == "--newaddr" || a == "--status" || a == "--list" || a == "--tree") mode = a;
|
||||||
|
else if (a == "--send") { mode = a; if (i + 1 < argc) sendDrgx = std::stod(argv[++i]); }
|
||||||
|
else if (a == "--rescan") doRescan = true; // force witness rebuild from the closest checkpoint
|
||||||
|
else server = a;
|
||||||
|
}
|
||||||
|
if (mode.empty()) { std::printf("usage: lite_send_smoke --newaddr|--status|--send <drgx> [server]\n"); return 2; }
|
||||||
|
|
||||||
|
auto bridge = LiteClientBridge::linkedSdxl();
|
||||||
|
std::printf("[send-smoke] server = %s\n", server.c_str());
|
||||||
|
std::printf("[send-smoke] available() = %s\n", bridge.available() ? "true" : "false");
|
||||||
|
if (!bridge.available()) { std::printf("[send-smoke] FAIL: backend not linked (%s)\n",
|
||||||
|
bridge.unavailableReason().c_str()); return 2; }
|
||||||
|
std::printf("[send-smoke] serverOnline = %s\n", bridge.checkServerOnline(server) ? "true" : "false");
|
||||||
|
|
||||||
|
if (mode == "--newaddr") {
|
||||||
|
auto r = bridge.initializeNew(false, server);
|
||||||
|
std::printf("[send-smoke] initializeNew ok=%d %s\n", r.ok, r.ok ? "" : r.error.c_str());
|
||||||
|
if (!r.ok) return 1;
|
||||||
|
} else {
|
||||||
|
auto r = bridge.initializeExisting(false, server);
|
||||||
|
std::printf("[send-smoke] initializeExisting ok=%d %s\n", r.ok, r.ok ? "" : r.error.c_str());
|
||||||
|
if (!r.ok) return 1;
|
||||||
|
if (doRescan) {
|
||||||
|
std::printf("[send-smoke] rescanning (rebuild witnesses from closest checkpoint)...\n");
|
||||||
|
auto rs = bridge.execute("rescan", "");
|
||||||
|
std::printf("[send-smoke] rescan ok=%d %.160s\n", rs.ok, rs.value.c_str());
|
||||||
|
} else {
|
||||||
|
std::printf("[send-smoke] syncing (may take a while)...\n");
|
||||||
|
auto s = bridge.execute("sync", "");
|
||||||
|
std::printf("[send-smoke] sync ok=%d\n", s.ok);
|
||||||
|
}
|
||||||
|
auto b = bridge.execute("balance", "");
|
||||||
|
std::printf("[send-smoke] balance = %.400s\n", b.value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addresses (public — safe to print).
|
||||||
|
std::vector<std::string> zs, ts;
|
||||||
|
{
|
||||||
|
auto a = bridge.execute("addresses", "");
|
||||||
|
json j = json::parse(a.value, nullptr, /*allow_exceptions*/ false);
|
||||||
|
if (!j.is_discarded()) collectAddrs(j, zs, ts);
|
||||||
|
std::printf("[send-smoke] addresses: z=%zu t=%zu\n", zs.size(), ts.size());
|
||||||
|
for (const auto& z : zs) std::printf("[send-smoke] Z (fund this for z2z): %s\n", z.c_str());
|
||||||
|
for (const auto& t : ts) std::printf("[send-smoke] T (transparent) : %s\n", t.c_str());
|
||||||
|
if (zs.empty() && ts.empty()) std::printf("[send-smoke] addresses RAW = %.200s\n", a.value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == "--list") {
|
||||||
|
auto l = bridge.execute("list", "");
|
||||||
|
std::printf("[send-smoke] list = %.3000s\n", l.value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == "--tree") {
|
||||||
|
// Dump the wallet's latest Sapling commitment tree to diff against the node's
|
||||||
|
// `getblockmerkletree <height>` — a mismatch localizes a tree-build divergence.
|
||||||
|
auto t = bridge.execute("saplingtree", "");
|
||||||
|
std::printf("[send-smoke] saplingtree = %s\n", t.value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == "--newaddr") {
|
||||||
|
std::printf("[send-smoke] save ok=%d\n", bridge.execute("save", "").ok);
|
||||||
|
} else if (mode == "--send") {
|
||||||
|
if (zs.empty()) { std::printf("[send-smoke] FAIL: no z-address to self-send to\n"); bridge.shutdown(); return 1; }
|
||||||
|
const long long zat = static_cast<long long>(std::llround(sendDrgx * 1e8));
|
||||||
|
json arr = json::array();
|
||||||
|
json o; o["address"] = zs.front(); o["amount"] = zat; o["memo"] = "lite send smoke";
|
||||||
|
arr.push_back(std::move(o));
|
||||||
|
std::printf("[send-smoke] SEND %.8f DRGX (%lld zat) -> own z-addr ...\n", sendDrgx, zat);
|
||||||
|
std::printf("[send-smoke] send args = %s\n", arr.dump().c_str());
|
||||||
|
auto res = bridge.execute("send", arr.dump());
|
||||||
|
std::printf("[send-smoke] send bridge_ok=%d\n", res.ok);
|
||||||
|
std::printf("[send-smoke] send RESULT = %.600s\n", res.value.c_str());
|
||||||
|
if (!res.error.empty()) std::printf("[send-smoke] send ERROR = %s\n", res.error.c_str());
|
||||||
|
std::printf("[send-smoke] save ok=%d\n", bridge.execute("save", "").ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.shutdown();
|
||||||
|
std::printf("[send-smoke] done\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user