Compare commits
59 Commits
2c909d35ea
...
lite-v1.0.
| Author | SHA1 | Date | |
|---|---|---|---|
| fffee9f0b5 | |||
| 00ffc959e5 | |||
| b561406c23 | |||
| 5a0743a17b | |||
| a7b0770ad0 | |||
| 7d8323a622 | |||
| 320944fd18 | |||
| 6d5e0ac614 | |||
| 02554d523d | |||
| 21da9e75fc | |||
| c53b7f771e | |||
| bcee4bfe72 | |||
| e1870c3b23 | |||
| 7cf0bb8bc7 | |||
| 541a9e1fd5 | |||
| 07b8e0b2cb | |||
| de48414c65 | |||
| f0c681b0b5 | |||
| fdf2502ca8 | |||
| e393b0d847 | |||
| 3b041f20c1 | |||
| 77a0ad64b0 | |||
| d0ca2fdf52 | |||
| 9c07b6d33d | |||
| fedfe3d60c | |||
| 95ff9ce9ae | |||
| 203967411a | |||
| 3d0305a9ca | |||
| e7f38c2a45 | |||
| 3a2be661ea | |||
| 17525003c5 | |||
| 267d839d5b | |||
| 987d9b93c7 | |||
| 4c43f2e082 | |||
| 57fa6470b7 | |||
| c68889d276 | |||
| b0a4333cdf | |||
| 4471f54842 | |||
| e08121af2b | |||
| 31ebfc782e | |||
| f3776bcbe5 | |||
| 38f2aa2f8f | |||
| 40f8425d94 | |||
| 2157a30192 | |||
| bef3c0c47d | |||
| 5c570613c8 | |||
| 567732206e | |||
| 0dcc09fc5f | |||
| 82d3178119 | |||
| 7d47c6e14e | |||
| 973bc0d338 | |||
| e4c9b98ca2 | |||
| 1c38f68781 | |||
| 818c29fca7 | |||
| 0a43742897 | |||
| defe14d913 | |||
| 8b55a90102 | |||
| 0fd3d214a4 | |||
| 996e10ed02 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
|
||||
!prebuilt-binaries/dragonxd-win/.gitkeep
|
||||
prebuilt-binaries/dragonxd-mac/*
|
||||
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
||||
prebuilt-binaries/xmrig-hac/*
|
||||
!prebuilt-binaries/xmrig-hac/.gitkeep
|
||||
prebuilt-binaries/drg-xmrig/*
|
||||
!prebuilt-binaries/drg-xmrig/.gitkeep
|
||||
|
||||
|
||||
# External sources / toolchains (created by scripts/setup.sh)
|
||||
@@ -33,7 +33,7 @@ imgui.ini
|
||||
*.bak*
|
||||
*.params
|
||||
asmap.dat
|
||||
/external/xmrig-hac
|
||||
/external/drg-xmrig
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
|
||||
@@ -53,7 +53,6 @@ set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
|
||||
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
|
||||
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
|
||||
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
|
||||
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
|
||||
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
|
||||
litelib_wallet_exists
|
||||
litelib_initialize_new
|
||||
@@ -126,36 +125,24 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
endif()
|
||||
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
|
||||
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
|
||||
endif()
|
||||
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
|
||||
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
|
||||
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
|
||||
endif()
|
||||
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
|
||||
endif()
|
||||
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
|
||||
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
|
||||
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
|
||||
endif()
|
||||
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
|
||||
endif()
|
||||
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
|
||||
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
|
||||
endif()
|
||||
endif()
|
||||
# Note (F15-1): the former signature-metadata gate was removed. It trusted a
|
||||
# "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
|
||||
# self-attested with no cryptographic check (the "verified" SHA was just the artifact's
|
||||
# own SHA). The trust root is now build-from-source: that script builds the backend from
|
||||
# the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
|
||||
# is the one built from reviewed source. The required-symbol inventory check above stays.
|
||||
|
||||
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
||||
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
||||
)
|
||||
if(APPLE)
|
||||
# The Rust backend's TLS stack (security-framework / core-foundation crates)
|
||||
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
|
||||
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
|
||||
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
|
||||
endif()
|
||||
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
@@ -304,6 +291,15 @@ FetchContent_Declare(
|
||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||
GIT_TAG v1.4.0
|
||||
GIT_SHALLOW TRUE
|
||||
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
|
||||
# files when it can't probe SSE support. Under a macOS universal build
|
||||
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
|
||||
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
|
||||
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
|
||||
PATCH_COMMAND ${CMAKE_COMMAND}
|
||||
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
|
||||
)
|
||||
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
||||
@@ -555,6 +551,7 @@ set(APP_SOURCES
|
||||
src/util/platform.cpp
|
||||
src/util/payment_uri.cpp
|
||||
src/util/texture_loader.cpp
|
||||
src/util/svg_texture.cpp
|
||||
src/util/noise_texture.cpp
|
||||
src/daemon/embedded_daemon.cpp
|
||||
src/daemon/seed_wallet_creator.cpp
|
||||
@@ -1203,5 +1200,5 @@ message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
|
||||
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
|
||||
message(STATUS " Lite trust: built-from-source (vendored third_party/silentdragonxlite)")
|
||||
message(STATUS "")
|
||||
|
||||
@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
|
||||
- prebuilt-binaries/dragonxd-win/
|
||||
- prebuilt-binaries/dragonxd-mac/
|
||||
|
||||
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
||||
- prebuilt-binaries/xmrig-hac/
|
||||
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
|
||||
- prebuilt-binaries/drg-xmrig/
|
||||
|
||||
|
||||
## Build Steps
|
||||
|
||||
90
build.sh
90
build.sh
@@ -131,7 +131,7 @@ fi
|
||||
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
@@ -386,7 +386,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
@@ -419,7 +419,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||
|
||||
# Desktop entry
|
||||
@@ -478,18 +478,28 @@ APPRUN
|
||||
done
|
||||
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
|
||||
|
||||
# appimagetool
|
||||
# appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
|
||||
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
|
||||
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
local APPIMAGETOOL=""
|
||||
if command -v appimagetool &>/dev/null; then
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
else
|
||||
info "Downloading appimagetool ..."
|
||||
wget -q -O "$bd/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
local at="$bd/appimagetool-x86_64.AppImage"
|
||||
# Re-verify any cached copy too; a stale unverified download must not be trusted.
|
||||
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
info "Downloading appimagetool 1.9.0 (pinned) ..."
|
||||
wget -q -O "$at" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
err "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$at"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$at"
|
||||
fi
|
||||
APPIMAGETOOL="$at"
|
||||
fi
|
||||
|
||||
local ARCH
|
||||
@@ -628,8 +638,8 @@ HDR
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
|
||||
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
|
||||
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
|
||||
@@ -781,7 +791,7 @@ HDR
|
||||
fi
|
||||
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
|
||||
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
@@ -891,8 +901,26 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
MAC_ARCH="universal"
|
||||
# Native macOS: build universal (arm64 + x86_64) by default. Override with
|
||||
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
|
||||
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
|
||||
# When linking the real lite backend, the app can only include architectures
|
||||
# the backend static library actually provides. Its pinned ring 0.16.11 has no
|
||||
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
|
||||
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
|
||||
# otherwise the arm64 slice fails to link.
|
||||
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
|
||||
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
|
||||
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
|
||||
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
|
||||
MAC_ARCHS="$_backend_archs"
|
||||
fi
|
||||
fi
|
||||
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
|
||||
MAC_ARCH="universal"
|
||||
else
|
||||
MAC_ARCH="$MAC_ARCHS"
|
||||
fi
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
@@ -980,7 +1008,7 @@ TOOLCHAIN
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
@@ -991,13 +1019,13 @@ TOOLCHAIN
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native universal arm64+x86_64) ..."
|
||||
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -1027,8 +1055,12 @@ TOOLCHAIN
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# Clean only THIS variant's prior artifacts so full-node and lite releases can
|
||||
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
|
||||
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
|
||||
# and the ".app" names are exact.
|
||||
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
|
||||
|
||||
local APP="$out/${APP_BASENAME}.app"
|
||||
local CONTENTS="$APP/Contents"
|
||||
@@ -1078,8 +1110,8 @@ TOOLCHAIN
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
if [[ -f "$XMRIG_MAC" ]]; then
|
||||
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||
chmod +x "$MACOS/xmrig"
|
||||
@@ -1228,8 +1260,10 @@ PLIST
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
local DMG_BASENAME="DragonX_Wallet"
|
||||
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
|
||||
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
|
||||
# (APP_DISPLAY_NAME above).
|
||||
local DMG_BASENAME="${APP_BASENAME}"
|
||||
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||
|
||||
if command -v create-dmg &>/dev/null; then
|
||||
@@ -1311,3 +1345,9 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then
|
||||
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
|
||||
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
|
||||
fi
|
||||
|
||||
# Reaching here means the build completed (real failures exit 1 at their point of failure).
|
||||
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
|
||||
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
|
||||
# become the script's exit code and make a successful build report failure (e.g. to CI).
|
||||
exit 0
|
||||
|
||||
31
cmake/patch-libwebp-simd.cmake
Normal file
31
cmake/patch-libwebp-simd.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
|
||||
#
|
||||
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
|
||||
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
|
||||
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
|
||||
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
|
||||
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
|
||||
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
|
||||
# functions unconditionally — so any TU including <math.h> fails to compile with
|
||||
# "_Float16 is not supported on this target".
|
||||
#
|
||||
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
|
||||
# is unnecessary on every platform we target. Blanking the SSE entries (indices
|
||||
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
|
||||
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
|
||||
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
|
||||
endif()
|
||||
|
||||
file(READ "${CPU_CMAKE}" _contents)
|
||||
string(REPLACE
|
||||
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
|
||||
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
|
||||
_patched "${_contents}")
|
||||
|
||||
if(_patched STREQUAL _contents)
|
||||
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
|
||||
else()
|
||||
file(WRITE "${CPU_CMAKE}" "${_patched}")
|
||||
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
|
||||
endif()
|
||||
3278
libs/nanosvg/nanosvg.h
Normal file
3278
libs/nanosvg/nanosvg.h
Normal file
File diff suppressed because it is too large
Load Diff
1472
libs/nanosvg/nanosvgrast.h
Normal file
1472
libs/nanosvg/nanosvgrast.h
Normal file
File diff suppressed because it is too large
Load Diff
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
Binary file not shown.
BIN
res/img/backgrounds/texture/jade_bg.png
Normal file
BIN
res/img/backgrounds/texture/jade_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
23
res/img/logos/logo_dragonx.svg
Normal file
23
res/img/logos/logo_dragonx.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "Minimal",
|
||||
"chat_bubble_rounded": "Abgerundet",
|
||||
"chat_bubble_square": "Eckig",
|
||||
"chat_buffer_loading": "Chat-Puffer: …",
|
||||
"chat_buffer_preparing": "Chat-Puffer: bereite %d/%d vor…",
|
||||
"chat_buffer_ready": "Chat-Puffer: %d/%d bereit",
|
||||
"chat_buffer_sending": "Chat: sende %d Nachrichten…",
|
||||
"chat_buffer_sending_one": "Chat: sende %d Nachricht…",
|
||||
"chat_cancel": "Abbrechen",
|
||||
"chat_contact_added": "Kontakt hinzugefügt – benenne ihn in Kontakte um",
|
||||
"chat_contact_request": "kontaktanfrage",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "Unterhaltung exportiert",
|
||||
"chat_export_failed": "Exportdatei konnte nicht geschrieben werden.",
|
||||
"chat_export_warn": "Speichert die entschlüsselten Nachrichten als Klartext. Bewahre die Datei sicher auf.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Unterhaltung ausgeblendet – eine neue Nachricht holt sie zurück",
|
||||
"chat_hide": "Ausblenden",
|
||||
"chat_hide_hidden": "Ausgeblendete verbergen",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Automatisch scrollen",
|
||||
"console_available_commands": "Verfügbare Befehle:",
|
||||
"console_backend_reference": "Backend-Befehlsreferenz",
|
||||
"console_backend_unavailable": "Kein Backend",
|
||||
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||
"console_cat_advanced": "Erweitert",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Steuerung",
|
||||
"console_cat_keys": "Schlüssel & Sicherheit",
|
||||
"console_cat_mining": "Mining",
|
||||
"console_cat_network": "Netzwerk",
|
||||
"console_cat_raw_transactions": "Rohtransaktionen",
|
||||
"console_cat_send": "Senden",
|
||||
"console_cat_sync": "Synchronisierung",
|
||||
"console_cat_utility": "Dienstprogramme",
|
||||
"console_cat_wallet": "Wallet",
|
||||
"console_clear": "Leeren",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - Diese Hilfe anzeigen",
|
||||
"console_help_setgenerate": " setgenerate - Mining steuern",
|
||||
"console_help_stop": " stop - Daemon stoppen",
|
||||
"console_last_error": "Letzter Fehler:",
|
||||
"console_line_count": "%zu Zeilen",
|
||||
"console_matches": "Treffer",
|
||||
"console_new_lines": "%d neue Zeilen",
|
||||
"console_no_daemon": "Kein Daemon",
|
||||
"console_no_output": "(keine Ausgabe)",
|
||||
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
|
||||
"console_not_connected_lite": "Fehler: Keine Wallet geöffnet",
|
||||
"console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.",
|
||||
"console_ref_builds": "Ergibt",
|
||||
"console_ref_cancel": "Abbrechen",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
|
||||
"console_ref_search_hint": "Nach Name oder Aufgabe suchen…",
|
||||
"console_ref_select_hint": "Wählen Sie einen Befehl, um zu sehen, was er tut.",
|
||||
"console_ref_value": "Wert",
|
||||
"console_rpc_reference": "RPC-Befehlsreferenz",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Konsolen-Scanline",
|
||||
"console_search_commands": "Befehle suchen...",
|
||||
"console_select_all": "Alles auswählen",
|
||||
"console_show_app_output": "[App]-Wallet-Protokollzeilen anzeigen",
|
||||
"console_show_backend_ref": "Backend-Befehlsreferenz anzeigen",
|
||||
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
||||
"console_show_errors_only": "Nur Fehler anzeigen",
|
||||
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "Gestoppt",
|
||||
"console_status_stopping": "Stoppt",
|
||||
"console_status_unknown": "Unbekannt",
|
||||
"console_stop_confirm_node": "'stop' fährt den Node herunter und trennt die Wallet. Geben Sie zur Bestätigung erneut 'stop' ein.",
|
||||
"console_tab_completion": "Tab zur Vervollständigung",
|
||||
"console_text_colors": "Textfarben",
|
||||
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "Kontaktname",
|
||||
"contact_wallet_loading": "Die Wallet lädt noch — aktiviere „In jeder Wallet anzeigen“ oder versuche es gleich erneut.",
|
||||
"contacts": "Kontakte",
|
||||
"contacts_avatar_shape": "Avatarform",
|
||||
"contacts_list_scale": "Listengröße",
|
||||
"contacts_search_no_match": "Keine passenden Kontakte",
|
||||
"contacts_search_placeholder": "Kontakte durchsuchen...",
|
||||
"contacts_settings_tip": "Kontakte anpassen",
|
||||
"contacts_settings_title": "Kontakteinstellungen",
|
||||
"contacts_shape_circle": "Kreis",
|
||||
"contacts_shape_square": "Quadrat",
|
||||
"contacts_shape_tab": "Reiter",
|
||||
"copied": "Kopiert!",
|
||||
"copy": "Kopieren",
|
||||
"copy_address": "Vollständige Adresse kopieren",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "Geburtstag: %llu (auch diesen sichern)",
|
||||
"lite_birthday_hint": "Blockhöhe, ab der gescannt werden soll. Bei 0 belassen, falls unbekannt (langsamerer vollständiger Scan).",
|
||||
"lite_birthday_label": "Geburtsblock",
|
||||
"lite_console_backend_commands": "Backend-Befehle:",
|
||||
"lite_console_help_passthrough": "Jede andere Eingabe wird als Lite-Wallet-Konsolenbefehl ausgeführt.",
|
||||
"lite_copy": "Kopieren",
|
||||
"lite_could_not_write": "Konnte nicht schreiben ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://ihr-lite-server",
|
||||
"lite_net_checking": "wird geprüft…",
|
||||
"lite_net_connected": "Verbunden",
|
||||
"lite_net_connecting": "Verbinde…",
|
||||
"lite_net_custom": "Benutzerdefiniert",
|
||||
"lite_net_disconnected": "Nicht verbunden",
|
||||
"lite_net_hidden_section": "Ausgeblendete Server",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "Marktkapitalisierung",
|
||||
"market_cap_short": "Kap.",
|
||||
"market_chart_loading": "Preisverlauf wird geladen",
|
||||
"market_col_name": "Name",
|
||||
"market_col_trend": "Trend",
|
||||
"market_col_value": "Wert",
|
||||
"market_iv_1d": "1T",
|
||||
"market_iv_1h": "1S",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "Kein Preisverlauf verfügbar",
|
||||
"market_no_price": "Keine Preisdaten",
|
||||
"market_now": "Jetzt",
|
||||
"market_opt_chart_style": "Diagrammstil",
|
||||
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
||||
"market_portfolio": "PORTFOLIO",
|
||||
"market_price_loading": "Preisdaten werden geladen...",
|
||||
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||
"market_refresh_price": "Preisdaten aktualisieren",
|
||||
"market_settings_tip": "Marktoptionen",
|
||||
"market_settings_title": "Markteinstellungen",
|
||||
"market_style_candle": "Zu Kerzenchart wechseln",
|
||||
"market_style_candle_label": "Kerzen",
|
||||
"market_style_line": "Zum Liniendiagramm wechseln",
|
||||
"market_style_line_label": "Linie",
|
||||
"market_trade_on": "Handeln auf %s",
|
||||
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
|
||||
"market_vol_short": "Vol.",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minute",
|
||||
"portfolio_spark_month": "Monat",
|
||||
"portfolio_spark_week": "Woche",
|
||||
"portfolio_style_compact": "Kompakte Zeilen",
|
||||
"portfolio_style_detailed": "Detaillierte Zeilen",
|
||||
"portfolio_style_featured": "Hervorgehobene Zeilen",
|
||||
"portfolio_style_compact": "Tabelle",
|
||||
"portfolio_style_detailed": "Karten",
|
||||
"portfolio_style_featured": "Hervorgehoben",
|
||||
"portfolio_style_label": "Portfolio-Stil",
|
||||
"portfolio_untitled": "Ohne Titel",
|
||||
"portfolio_wallet_loading": "Warte, bis die Wallet fertig geladen ist, um eine Gruppe hinzuzufügen.",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "Unschärfe-Stärke (0%% = aus, 100%% = maximum)",
|
||||
"tt_change_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
|
||||
"tt_change_pin": "Ihre Entsperr-PIN ändern",
|
||||
"tt_chat_bubble_accent": "Akzentfarbe für deine ausgehenden Nachrichtenblasen (oder dem aktuellen Theme folgen)",
|
||||
"tt_chat_bubble_style": "Form der Nachrichtenblase: abgerundet, eckig oder minimal (flach, randlos)",
|
||||
"tt_chat_density": "Abstand zwischen Nachrichten: Komfortabel fügt mehr Abstand hinzu; Kompakt zeigt mehr auf dem Bildschirm",
|
||||
"tt_chat_emoji_style": "Emoji als einfarbige Umrisse oder in voller Farbe darstellen",
|
||||
"tt_chat_enter_sends": "Wenn aktiviert, sendet Enter die Nachricht und Shift+Enter fügt einen Zeilenumbruch ein; wenn deaktiviert, fügt Enter einen Zeilenumbruch ein",
|
||||
"tt_chat_font_size": "Skaliere den Chat-Nachrichtentext von 0.8x bis 1.5x. Betrifft nur den Chat-Tab, nicht den Rest der App",
|
||||
"tt_chat_poll_rate": "Wie oft auf neue und 0-conf-Nachrichten geprüft wird (0.5-15 s). Schneller ist reaktionsfreudiger, verbraucht aber mehr CPU",
|
||||
"tt_chat_timestamp": "Zeitstempelformat nur für diesen Tab: der app-weiten Uhr folgen oder 24-hour bzw. 12-hour erzwingen",
|
||||
"tt_clear_ztx": "Lokal zwischengespeicherten Z-Transaktionsverlauf löschen",
|
||||
"tt_clock_format": "24- oder 12-Stunden-Uhr, app-weit. Der Chat-Tab kann sie überschreiben.",
|
||||
"tt_custom_fees": "Manuelle Gebühreneingabe beim Senden von Transaktionen aktivieren",
|
||||
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
|
||||
"tt_daemon_install_bundled": "Node stoppen, den installierten dragonxd mit der in diesem Wallet-Build enthaltenen Version überschreiben und dann neu starten",
|
||||
"tt_daemon_refresh": "Version, Größe und Datum des installierten und mitgelieferten dragonxd (oben angezeigt) erneut einlesen",
|
||||
"tt_daemon_update_check": "Den neuesten dragonxd-Full-Node vom Projekt-Gitea herunterladen und verifizieren, dann zum Anwenden neu starten",
|
||||
"tt_debug_collapse": "Debug-Protokollierungsoptionen einklappen",
|
||||
"tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
||||
"tt_language": "Schnittstellensprache der Wallet-UI",
|
||||
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
||||
"tt_lite_copy": "Das angezeigte Geheimnis in die Zwischenablage kopieren",
|
||||
"tt_lite_decrypt_pass": "Gib deine Passphrase ein, um die Verschlüsselung von der Wallet zu entfernen",
|
||||
"tt_lite_encrypt": "Die Wallet mit der obigen Passphrase verschlüsseln; sie wird sofort gesperrt und benötigt die Passphrase zum Entsperren",
|
||||
"tt_lite_encrypt_pass": "Passphrase, mit der die Wallet verschlüsselt wird. Geht sie verloren, kann die Wallet nicht mehr entsperrt oder wiederhergestellt werden",
|
||||
"tt_lite_hide_wipe": "Das angezeigte Geheimnis ausblenden und sicher aus dem Speicher löschen",
|
||||
"tt_lite_import_key": "Einen privaten Ausgabe- oder Ansichtsschlüssel zum Importieren einfügen; dessen Verlauf erscheint nach der nächsten Synchronisierung",
|
||||
"tt_lite_import_key_btn": "Den eingegebenen privaten Schlüssel in diese Wallet importieren; Guthaben und Verlauf erscheinen nach der nächsten Synchronisierung",
|
||||
"tt_lite_lifecycle_op": "Wähle, ob eine neue Wallet erstellt, eine vorhandene geöffnet oder eine aus einer Seed-Phrase wiederhergestellt werden soll",
|
||||
"tt_lite_lifecycle_pass": "Passphrase, um die Wallet bei diesem Erstellen- / Öffnen- / Wiederherstellen-Vorgang zu entsperren oder zu setzen",
|
||||
"tt_lite_lifecycle_run": "Den ausgewählten Erstellen- / Öffnen- / Wiederherstellen-Vorgang mit den obigen Werten ausführen",
|
||||
"tt_lite_lifecycle_toggle": "Die Bedienelemente zum Erstellen / Öffnen / Wiederherstellen zur Verwaltung deiner Lite-Wallet-Datei ein- oder ausblenden",
|
||||
"tt_lite_lock": "Die Wallet jetzt sperren; zum Entsperren ist eine Passphrase erforderlich und jede Chat-Sitzung wird beendet",
|
||||
"tt_lite_redownload": "Alle Blöcke erneut vom Lite-Server herunterladen und neu scannen",
|
||||
"tt_lite_remove_encrypt": "Verschlüsselung entfernen und die Wallet ungeschützt speichern; zum Öffnen ist dann keine Passphrase mehr erforderlich",
|
||||
"tt_lite_restore_account": "HD-Konto-Index zum Wiederherstellen; belasse 0, sofern du nicht mehrere Konten unter diesem Seed verwendet hast",
|
||||
"tt_lite_restore_birthday": "Blockhöhe, bei der die Wallet erstellt wurde; das Scannen beginnt hier. Verwende 0 oder die früheste Höhe, falls unsicher",
|
||||
"tt_lite_restore_overwrite": "Eine vorhandene Wallet-Datei durch diese Wiederherstellung ersetzen. Warnung: überschreibt die aktuellen Wallet-Daten",
|
||||
"tt_lite_restore_seed": "Die 24-word-Wiederherstellungs-Seed-Phrase, aus der diese Wallet wiederhergestellt wird; bei der Eingabe ausgeblendet",
|
||||
"tt_lite_save_seed_file": "Seed und Erstellungsdatum in eine nur für den Eigentümer lesbare Datei (lite-seed-backup.txt) im Konfigurationsordner schreiben",
|
||||
"tt_lite_show_keys": "Die privaten Ausgabeschlüssel dieser Wallet anzeigen. Wer einen Schlüssel besitzt, kann das von ihm kontrollierte Guthaben ausgeben",
|
||||
"tt_lite_show_seed": "Die Wiederherstellungs-Seed-Phrase und das Erstellungsdatum dieser Wallet anzeigen. Wer den Seed besitzt, kann dein Guthaben ausgeben",
|
||||
"tt_lite_unlock": "Die verschlüsselte Wallet mit der obigen Passphrase entsperren",
|
||||
"tt_lite_unlock_pass": "Gib deine Passphrase ein, um die verschlüsselte Wallet zu entsperren",
|
||||
"tt_lite_wallet_path": "Pfad oder Name der Wallet-Datei, die geöffnet oder in die wiederhergestellt werden soll",
|
||||
"tt_lock": "Die Wallet sofort sperren",
|
||||
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
|
||||
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "Hostname des DragonX-Daemons",
|
||||
"tt_rpc_pass": "RPC-Authentifizierungspasswort",
|
||||
"tt_rpc_port": "Port für RPC-Verbindungen des Daemons",
|
||||
"tt_rpc_toggle": "Die schreibgeschützten RPC-Verbindungsdaten (Host, Port, Benutzer, Passwort) für den Daemon ein- oder ausblenden",
|
||||
"tt_rpc_user": "RPC-Authentifizierungsbenutzername",
|
||||
"tt_save_settings": "Alle Einstellungen auf der Festplatte speichern",
|
||||
"tt_save_ztx": "Z-Adresse-Transaktionsverlauf lokal für schnelleres Laden speichern",
|
||||
"tt_scan_themes": "Nach neuen Themes suchen.\\nTheme-Ordner ablegen in:\\n%s",
|
||||
"tt_scanline": "CRT-Scanlinieneffekt in der Konsole",
|
||||
"tt_screenshot_open_dir": "Den Screenshots-Ordner (unter dem Konfigurationsverzeichnis) im Dateimanager öffnen",
|
||||
"tt_screenshot_sweep": "Jedes Theme über jeden Tab durchlaufen und von jedem einen Screenshot in den Screenshots-Ordner der Konfiguration speichern (überschreibt den letzten Durchlauf)",
|
||||
"tt_screenshot_sweep_full": "Wie der Theme-Durchlauf, erfasst aber auch jedes Modal / jeden Dialog / jeden Ablauf mit temporären Offline-Demo-Wallet-Daten",
|
||||
"tt_seed_backup": "Die 24-Wort-Wiederherstellungsphrase Ihrer Wallet anzeigen und sichern",
|
||||
"tt_seed_demo_chat": "Beispielunterhaltungen in den Chat-Tab einfügen, damit ein Durchlauf dessen UI erfasst; nur im Speicher, beim Neustart verschwunden",
|
||||
"tt_seed_migrate": "Eine neue Wallet mit Wiederherstellungsphrase erstellen und Ihre Gelder dorthin übertragen",
|
||||
"tt_set_pin": "Eine 4-8-stellige PIN für schnelles Entsperren festlegen",
|
||||
"tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "Die DragonX-Website öffnen",
|
||||
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
||||
"tt_wizard": "Den Ersteinrichtungsassistenten erneut ausführen\\nDer Daemon wird neu gestartet",
|
||||
"tx_chat_badge": "Nachricht",
|
||||
"tx_confirmations": "%d Bestätigungen",
|
||||
"tx_details_title": "Transaktionsdetails",
|
||||
"tx_from_address": "Von Adresse:",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "Mínima",
|
||||
"chat_bubble_rounded": "Redondeada",
|
||||
"chat_bubble_square": "Cuadrada",
|
||||
"chat_buffer_loading": "Búfer de chat: …",
|
||||
"chat_buffer_preparing": "Búfer de chat: preparando %d/%d…",
|
||||
"chat_buffer_ready": "Búfer de chat: %d/%d listos",
|
||||
"chat_buffer_sending": "Chat: enviando %d mensajes…",
|
||||
"chat_buffer_sending_one": "Chat: enviando %d mensaje…",
|
||||
"chat_cancel": "Cancelar",
|
||||
"chat_contact_added": "Contacto añadido: renómbralo en Contactos",
|
||||
"chat_contact_request": "solicitud de contacto",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "Conversación exportada",
|
||||
"chat_export_failed": "No se pudo escribir el archivo de exportación.",
|
||||
"chat_export_warn": "Guarda los mensajes descifrados como texto sin cifrar. Guarda el archivo de forma segura.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Conversación oculta: un mensaje nuevo la recupera",
|
||||
"chat_hide": "Ocultar",
|
||||
"chat_hide_hidden": "Ocultar ocultos",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Auto-desplazamiento",
|
||||
"console_available_commands": "Comandos disponibles:",
|
||||
"console_backend_reference": "Referencia de Comandos del Backend",
|
||||
"console_backend_unavailable": "Sin backend",
|
||||
"console_capturing_output": "Capturando salida del daemon...",
|
||||
"console_cat_advanced": "Avanzado",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Control",
|
||||
"console_cat_keys": "Claves y seguridad",
|
||||
"console_cat_mining": "Minería",
|
||||
"console_cat_network": "Red",
|
||||
"console_cat_raw_transactions": "Transacciones sin procesar",
|
||||
"console_cat_send": "Enviar",
|
||||
"console_cat_sync": "Sincronización",
|
||||
"console_cat_utility": "Utilidades",
|
||||
"console_cat_wallet": "Cartera",
|
||||
"console_clear": "Limpiar",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - Mostrar este mensaje de ayuda",
|
||||
"console_help_setgenerate": " setgenerate - Controlar minería",
|
||||
"console_help_stop": " stop - Detener el daemon",
|
||||
"console_last_error": "Último error:",
|
||||
"console_line_count": "%zu líneas",
|
||||
"console_matches": "coincidencias",
|
||||
"console_new_lines": "%d nuevas líneas",
|
||||
"console_no_daemon": "Sin daemon",
|
||||
"console_no_output": "(sin salida)",
|
||||
"console_not_connected": "Error: No conectado al daemon",
|
||||
"console_not_connected_lite": "Error: No hay ninguna cartera abierta",
|
||||
"console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.",
|
||||
"console_ref_builds": "Genera",
|
||||
"console_ref_cancel": "Cancelar",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
|
||||
"console_ref_search_hint": "Buscar por nombre o tarea…",
|
||||
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
|
||||
"console_ref_value": "valor",
|
||||
"console_rpc_reference": "Referencia de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Líneas de consola",
|
||||
"console_search_commands": "Buscar comandos...",
|
||||
"console_select_all": "Seleccionar Todo",
|
||||
"console_show_app_output": "Mostrar las líneas de registro de la cartera [app]",
|
||||
"console_show_backend_ref": "Mostrar referencia de comandos del backend",
|
||||
"console_show_daemon_output": "Mostrar salida del daemon",
|
||||
"console_show_errors_only": "Mostrar solo errores",
|
||||
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "Detenido",
|
||||
"console_status_stopping": "Deteniendo",
|
||||
"console_status_unknown": "Desconocido",
|
||||
"console_stop_confirm_node": "'stop' apagará el nodo y desconectará la cartera. Escribe 'stop' de nuevo para confirmar.",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Colores de texto",
|
||||
"console_toggle_accents": "Alternar acentos de color de línea",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "Nombre del contacto",
|
||||
"contact_wallet_loading": "La cartera aún se está cargando: marca «Mostrar en todas las carteras» o inténtalo de nuevo en un momento.",
|
||||
"contacts": "Contactos",
|
||||
"contacts_avatar_shape": "Forma del avatar",
|
||||
"contacts_list_scale": "Escala de la lista",
|
||||
"contacts_search_no_match": "No hay contactos coincidentes",
|
||||
"contacts_search_placeholder": "Buscar contactos...",
|
||||
"contacts_settings_tip": "Personalizar contactos",
|
||||
"contacts_settings_title": "Ajustes de contactos",
|
||||
"contacts_shape_circle": "Círculo",
|
||||
"contacts_shape_square": "Cuadrado",
|
||||
"contacts_shape_tab": "Pestaña",
|
||||
"copied": "¡Copiado!",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Dirección Completa",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "Cumpleaños: %llu (respalda esto también)",
|
||||
"lite_birthday_hint": "Altura de bloque desde la que empezar a escanear. Deja 0 si se desconoce (escaneo completo más lento).",
|
||||
"lite_birthday_label": "Fecha de creación",
|
||||
"lite_console_backend_commands": "Comandos del backend:",
|
||||
"lite_console_help_passthrough": "Cualquier otra entrada se ejecuta como un comando de consola de la cartera lite.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_write": "No se pudo escribir ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://tu-servidor-lite",
|
||||
"lite_net_checking": "comprobando…",
|
||||
"lite_net_connected": "Conectado",
|
||||
"lite_net_connecting": "Conectando…",
|
||||
"lite_net_custom": "Personalizado",
|
||||
"lite_net_disconnected": "No conectado",
|
||||
"lite_net_hidden_section": "Servidores ocultos",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "Cap. de Mercado",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_chart_loading": "Cargando historial de precios",
|
||||
"market_col_name": "Nombre",
|
||||
"market_col_trend": "Tendencia",
|
||||
"market_col_value": "Valor",
|
||||
"market_iv_1d": "1D",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "No hay historial de precios disponible",
|
||||
"market_no_price": "Sin datos de precio",
|
||||
"market_now": "Ahora",
|
||||
"market_opt_chart_style": "Estilo de gráfico",
|
||||
"market_pct_shielded": "%.0f%% Protegido",
|
||||
"market_portfolio": "PORTAFOLIO",
|
||||
"market_price_loading": "Cargando datos de precio...",
|
||||
"market_price_unavailable": "Datos de precio no disponibles",
|
||||
"market_refresh_price": "Actualizar datos de precio",
|
||||
"market_settings_tip": "Opciones de mercado",
|
||||
"market_settings_title": "Ajustes de mercado",
|
||||
"market_style_candle": "Cambiar a velas",
|
||||
"market_style_candle_label": "Velas",
|
||||
"market_style_line": "Cambiar a gráfico de líneas",
|
||||
"market_style_line_label": "Línea",
|
||||
"market_trade_on": "Operar en %s",
|
||||
"market_updated": "\\xc2\\xb7 Actualizado %s",
|
||||
"market_vol_short": "Vol",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minuto",
|
||||
"portfolio_spark_month": "Mes",
|
||||
"portfolio_spark_week": "Semana",
|
||||
"portfolio_style_compact": "Filas compactas",
|
||||
"portfolio_style_detailed": "Filas detalladas",
|
||||
"portfolio_style_featured": "Filas destacadas",
|
||||
"portfolio_style_compact": "Tabla",
|
||||
"portfolio_style_detailed": "Tarjetas",
|
||||
"portfolio_style_featured": "Destacado",
|
||||
"portfolio_style_label": "Estilo de cartera",
|
||||
"portfolio_untitled": "Sin título",
|
||||
"portfolio_wallet_loading": "Espera a que la cartera termine de cargar para añadir un grupo.",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "Cantidad de desenfoque (0%% = apagado, 100%% = máximo)",
|
||||
"tt_change_pass": "Cambiar la contraseña de cifrado de la billetera",
|
||||
"tt_change_pin": "Cambiar su PIN de desbloqueo",
|
||||
"tt_chat_bubble_accent": "Color de acento para tus burbujas de mensaje salientes (o sigue el tema actual)",
|
||||
"tt_chat_bubble_style": "Forma de la burbuja de mensaje: redondeada, cuadrada o mínima (plana, sin borde)",
|
||||
"tt_chat_density": "Espaciado entre mensajes: Cómodo añade más relleno; Compacto muestra más en pantalla",
|
||||
"tt_chat_emoji_style": "Muestra los emoji con contorno monocromo o a todo color",
|
||||
"tt_chat_enter_sends": "Si está activado, Enter envía el mensaje y Shift+Enter añade un salto de línea; si está desactivado, Enter añade un salto de línea",
|
||||
"tt_chat_font_size": "Escala el texto de los mensajes de chat de 0.8x a 1.5x. Solo afecta a la pestaña de Chat, no al resto de la app",
|
||||
"tt_chat_poll_rate": "Con qué frecuencia se comprueban mensajes nuevos y de 0-conf (0.5-15 s). Más rápido responde mejor pero usa más CPU",
|
||||
"tt_chat_timestamp": "Formato de marca de tiempo solo para esta pestaña: seguir el reloj de toda la app, o forzar 24-hour o 12-hour",
|
||||
"tt_clear_ztx": "Eliminar historial de z-transacciones en caché local",
|
||||
"tt_clock_format": "Reloj de 24 o 12 horas, en toda la app. El chat puede anularlo.",
|
||||
"tt_custom_fees": "Habilitar entrada manual de comisiones al enviar transacciones",
|
||||
"tt_custom_theme": "Tema personalizado activo",
|
||||
"tt_daemon_install_bundled": "Detiene el nodo, sobrescribe el dragonxd instalado con la versión incluida en esta compilación de la cartera y luego lo reinicia",
|
||||
"tt_daemon_refresh": "Vuelve a leer la versión, el tamaño y la fecha de dragonxd instalado y del incluido que se muestran arriba",
|
||||
"tt_daemon_update_check": "Descarga y verifica el nodo completo dragonxd más reciente desde el Gitea del proyecto, y luego reinicia para aplicarlo",
|
||||
"tt_debug_collapse": "Colapsar opciones de registro de depuración",
|
||||
"tt_debug_expand": "Expandir opciones de registro de depuración",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
||||
"tt_language": "Idioma de la interfaz de la billetera",
|
||||
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
||||
"tt_lite_copy": "Copia el secreto revelado al portapapeles",
|
||||
"tt_lite_decrypt_pass": "Introduce tu frase de contraseña para quitar el cifrado de la cartera",
|
||||
"tt_lite_encrypt": "Cifra la cartera con la frase de contraseña de arriba; se bloquea de inmediato y requiere la frase para desbloquearse",
|
||||
"tt_lite_encrypt_pass": "Frase de contraseña con la que cifrar la cartera. Si se pierde, la cartera no se puede desbloquear ni recuperar",
|
||||
"tt_lite_hide_wipe": "Oculta el secreto revelado y lo borra de la memoria de forma segura",
|
||||
"tt_lite_import_key": "Pega una clave privada de gasto o de visualización para importar; su historial aparece tras la próxima sincronización",
|
||||
"tt_lite_import_key_btn": "Importa la clave privada introducida en esta cartera; los fondos y el historial aparecen tras la próxima sincronización",
|
||||
"tt_lite_lifecycle_op": "Elige si crear una cartera nueva, abrir una existente o restaurar una desde una frase de recuperación",
|
||||
"tt_lite_lifecycle_pass": "Frase de contraseña para desbloquear o establecer en la cartera durante esta operación de crear / abrir / restaurar",
|
||||
"tt_lite_lifecycle_run": "Ejecuta la operación de crear / abrir / restaurar seleccionada con los valores de arriba",
|
||||
"tt_lite_lifecycle_toggle": "Muestra u oculta los controles de crear / abrir / restaurar para gestionar tu archivo de cartera lite",
|
||||
"tt_lite_lock": "Bloquea la cartera ahora; se necesita una frase de contraseña para desbloquearla y se cierra cualquier sesión de chat",
|
||||
"tt_lite_redownload": "Volver a descargar y re-escanear todos los bloques del servidor lite",
|
||||
"tt_lite_remove_encrypt": "Quita el cifrado y guarda la cartera sin protección; no se requerirá ninguna frase de contraseña para abrirla",
|
||||
"tt_lite_restore_account": "Índice de cuenta HD a restaurar; deja 0 salvo que hayas usado varias cuentas con esta semilla",
|
||||
"tt_lite_restore_birthday": "Altura de bloque en la que se creó la cartera; el escaneo empieza aquí. Usa 0 o la altura más temprana si no estás seguro",
|
||||
"tt_lite_restore_overwrite": "Reemplaza un archivo de cartera existente con esta restauración. Advertencia: sobrescribe los datos de la cartera actual",
|
||||
"tt_lite_restore_seed": "La frase de recuperación de 24-word para restaurar esta cartera; se oculta mientras escribes",
|
||||
"tt_lite_save_seed_file": "Escribe la semilla y la fecha de creación en un archivo solo para el propietario (lite-seed-backup.txt) en la carpeta de configuración",
|
||||
"tt_lite_show_keys": "Revela las claves privadas de gasto de esta cartera. Cualquiera con una clave puede gastar los fondos que controla",
|
||||
"tt_lite_show_seed": "Revela la frase de recuperación y la fecha de creación de esta cartera. Cualquiera con la semilla puede gastar tus fondos",
|
||||
"tt_lite_unlock": "Desbloquea la cartera cifrada con la frase de contraseña de arriba",
|
||||
"tt_lite_unlock_pass": "Introduce tu frase de contraseña para desbloquear la cartera cifrada",
|
||||
"tt_lite_wallet_path": "Ruta o nombre del archivo de cartera que se abrirá o en el que se restaurará",
|
||||
"tt_lock": "Bloquear la billetera inmediatamente",
|
||||
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "Nombre de host del daemon DragonX",
|
||||
"tt_rpc_pass": "Contraseña de autenticación RPC",
|
||||
"tt_rpc_port": "Puerto para conexiones RPC del daemon",
|
||||
"tt_rpc_toggle": "Muestra u oculta los datos de conexión RPC de solo lectura (host, puerto, usuario, contraseña) del daemon",
|
||||
"tt_rpc_user": "Nombre de usuario de autenticación RPC",
|
||||
"tt_save_settings": "Guardar todas las configuraciones en disco",
|
||||
"tt_save_ztx": "Almacenar historial de transacciones de z-address localmente para carga más rápida",
|
||||
"tt_scan_themes": "Buscar nuevos temas.\\nColoque carpetas de temas en:\\n%s",
|
||||
"tt_scanline": "Efecto de líneas de escaneo CRT en la consola",
|
||||
"tt_screenshot_open_dir": "Abre la carpeta de capturas (dentro del directorio de configuración) en tu explorador de archivos",
|
||||
"tt_screenshot_sweep": "Recorre cada tema en cada pestaña y guarda una captura de cada uno en la carpeta de capturas de la configuración (sobrescribe el último recorrido)",
|
||||
"tt_screenshot_sweep_full": "Como el recorrido de temas, pero además captura cada modal / diálogo / flujo usando datos de cartera de demostración temporales y sin conexión",
|
||||
"tt_seed_backup": "Muestra y respalda la frase de recuperación de 24 palabras de tu cartera",
|
||||
"tt_seed_demo_chat": "Inserta conversaciones de ejemplo en la pestaña de Chat para que un recorrido capture su interfaz; solo en memoria, se pierde al reiniciar",
|
||||
"tt_seed_migrate": "Crea una nueva cartera con frase de recuperación y traslada tus fondos a ella",
|
||||
"tt_set_pin": "Establecer un PIN de 4-8 dígitos para desbloqueo rápido",
|
||||
"tt_shield_mining": "Mover recompensas de minería transparentes a una dirección blindada",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "Abrir el sitio web de DragonX",
|
||||
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
||||
"tt_wizard": "Volver a ejecutar el asistente de configuración inicial\\nEl daemon será reiniciado",
|
||||
"tx_chat_badge": "Mensaje",
|
||||
"tx_confirmations": "%d confirmaciones",
|
||||
"tx_details_title": "Detalles de Transacción",
|
||||
"tx_from_address": "Dirección Origen:",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "Minimale",
|
||||
"chat_bubble_rounded": "Arrondie",
|
||||
"chat_bubble_square": "Carrée",
|
||||
"chat_buffer_loading": "Tampon de chat: …",
|
||||
"chat_buffer_preparing": "Tampon de chat: préparation %d/%d…",
|
||||
"chat_buffer_ready": "Tampon de chat: %d/%d prêts",
|
||||
"chat_buffer_sending": "Chat: envoi de %d messages…",
|
||||
"chat_buffer_sending_one": "Chat: envoi de %d message…",
|
||||
"chat_cancel": "Annuler",
|
||||
"chat_contact_added": "Contact ajouté — renommez-le dans Contacts",
|
||||
"chat_contact_request": "demande de contact",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "Conversation exportée",
|
||||
"chat_export_failed": "Impossible d'écrire le fichier d'exportation.",
|
||||
"chat_export_warn": "Enregistre les messages déchiffrés en texte clair. Conservez le fichier en lieu sûr.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Conversation masquée — un nouveau message la fait réapparaître",
|
||||
"chat_hide": "Masquer",
|
||||
"chat_hide_hidden": "Masquer masqués",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Défilement auto",
|
||||
"console_available_commands": "Commandes disponibles :",
|
||||
"console_backend_reference": "Référence des commandes du backend",
|
||||
"console_backend_unavailable": "Aucun backend",
|
||||
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||
"console_cat_advanced": "Avancé",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Contrôle",
|
||||
"console_cat_keys": "Clés et sécurité",
|
||||
"console_cat_mining": "Minage",
|
||||
"console_cat_network": "Réseau",
|
||||
"console_cat_raw_transactions": "Transactions brutes",
|
||||
"console_cat_send": "Envoyer",
|
||||
"console_cat_sync": "Synchronisation",
|
||||
"console_cat_utility": "Utilitaires",
|
||||
"console_cat_wallet": "Portefeuille",
|
||||
"console_clear": "Effacer",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - Afficher ce message d'aide",
|
||||
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
||||
"console_help_stop": " stop - Arrêter le daemon",
|
||||
"console_last_error": "Dernière erreur :",
|
||||
"console_line_count": "%zu lignes",
|
||||
"console_matches": "correspondances",
|
||||
"console_new_lines": "%d nouvelles lignes",
|
||||
"console_no_daemon": "Pas de daemon",
|
||||
"console_no_output": "(aucune sortie)",
|
||||
"console_not_connected": "Erreur : Non connecté au daemon",
|
||||
"console_not_connected_lite": "Erreur : Aucun portefeuille ouvert",
|
||||
"console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.",
|
||||
"console_ref_builds": "Génère",
|
||||
"console_ref_cancel": "Annuler",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "Exécuter %s maintenant ? C'est une commande à conséquences.",
|
||||
"console_ref_search_hint": "Rechercher par nom ou tâche…",
|
||||
"console_ref_select_hint": "Sélectionnez une commande pour voir ce qu'elle fait.",
|
||||
"console_ref_value": "valeur",
|
||||
"console_rpc_reference": "Référence des commandes RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline de la console",
|
||||
"console_search_commands": "Rechercher des commandes...",
|
||||
"console_select_all": "Tout sélectionner",
|
||||
"console_show_app_output": "Afficher les lignes du journal du portefeuille [app]",
|
||||
"console_show_backend_ref": "Afficher la référence des commandes du backend",
|
||||
"console_show_daemon_output": "Afficher la sortie du daemon",
|
||||
"console_show_errors_only": "Afficher uniquement les erreurs",
|
||||
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "Arrêté",
|
||||
"console_status_stopping": "Arrêt",
|
||||
"console_status_unknown": "Inconnu",
|
||||
"console_stop_confirm_node": "'stop' arrêtera le nœud et déconnectera le portefeuille. Tapez à nouveau 'stop' pour confirmer.",
|
||||
"console_tab_completion": "Tab pour compléter",
|
||||
"console_text_colors": "Couleurs du texte",
|
||||
"console_toggle_accents": "Basculer les accents de couleur des lignes",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "Nom du contact",
|
||||
"contact_wallet_loading": "Le portefeuille se charge encore — cochez « Afficher dans chaque portefeuille » ou réessayez dans un instant.",
|
||||
"contacts": "Contacts",
|
||||
"contacts_avatar_shape": "Forme de l'avatar",
|
||||
"contacts_list_scale": "Échelle de la liste",
|
||||
"contacts_search_no_match": "Aucun contact correspondant",
|
||||
"contacts_search_placeholder": "Rechercher des contacts...",
|
||||
"contacts_settings_tip": "Personnaliser les contacts",
|
||||
"contacts_settings_title": "Paramètres des contacts",
|
||||
"contacts_shape_circle": "Cercle",
|
||||
"contacts_shape_square": "Carré",
|
||||
"contacts_shape_tab": "Onglet",
|
||||
"copied": "Copié !",
|
||||
"copy": "Copier",
|
||||
"copy_address": "Copier l'adresse complète",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "Date de création : %llu (à sauvegarder également)",
|
||||
"lite_birthday_hint": "Hauteur de bloc à partir de laquelle commencer l'analyse. Laissez 0 si inconnue (analyse complète plus lente).",
|
||||
"lite_birthday_label": "Bloc de création",
|
||||
"lite_console_backend_commands": "Commandes du backend :",
|
||||
"lite_console_help_passthrough": "Toute autre entrée est exécutée comme une commande de la console du portefeuille lite.",
|
||||
"lite_copy": "Copier",
|
||||
"lite_could_not_write": "Impossible d'écrire ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://votre-serveur-lite",
|
||||
"lite_net_checking": "vérification…",
|
||||
"lite_net_connected": "Connecté",
|
||||
"lite_net_connecting": "Connexion…",
|
||||
"lite_net_custom": "Personnalisé",
|
||||
"lite_net_disconnected": "Non connecté",
|
||||
"lite_net_hidden_section": "Serveurs masqués",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "Capitalisation",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_chart_loading": "Chargement de l'historique des prix",
|
||||
"market_col_name": "Nom",
|
||||
"market_col_trend": "Tendance",
|
||||
"market_col_value": "Valeur",
|
||||
"market_iv_1d": "1J",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "Aucun historique de prix disponible",
|
||||
"market_no_price": "Pas de données de prix",
|
||||
"market_now": "Maintenant",
|
||||
"market_opt_chart_style": "Style du graphique",
|
||||
"market_pct_shielded": "%.0f%% Blindé",
|
||||
"market_portfolio": "PORTEFEUILLE",
|
||||
"market_price_loading": "Chargement des données de prix...",
|
||||
"market_price_unavailable": "Données de prix indisponibles",
|
||||
"market_refresh_price": "Actualiser les données de prix",
|
||||
"market_settings_tip": "Options du marché",
|
||||
"market_settings_title": "Paramètres du marché",
|
||||
"market_style_candle": "Passer aux chandeliers",
|
||||
"market_style_candle_label": "Chandelier",
|
||||
"market_style_line": "Passer au graphique en ligne",
|
||||
"market_style_line_label": "Ligne",
|
||||
"market_trade_on": "Échanger sur %s",
|
||||
"market_updated": "\\xc2\\xb7 Mis à jour %s",
|
||||
"market_vol_short": "Vol",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minute",
|
||||
"portfolio_spark_month": "Mois",
|
||||
"portfolio_spark_week": "Semaine",
|
||||
"portfolio_style_compact": "Lignes compactes",
|
||||
"portfolio_style_detailed": "Lignes détaillées",
|
||||
"portfolio_style_featured": "Lignes en vedette",
|
||||
"portfolio_style_compact": "Tableau",
|
||||
"portfolio_style_detailed": "Cartes",
|
||||
"portfolio_style_featured": "En vedette",
|
||||
"portfolio_style_label": "Style du portefeuille",
|
||||
"portfolio_untitled": "Sans titre",
|
||||
"portfolio_wallet_loading": "Attendez la fin du chargement du portefeuille pour ajouter un groupe.",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "Quantité de flou (0%% = désactivé, 100%% = maximum)",
|
||||
"tt_change_pass": "Changer la phrase secrète de chiffrement du portefeuille",
|
||||
"tt_change_pin": "Changer votre PIN de déverrouillage",
|
||||
"tt_chat_bubble_accent": "Couleur d'accent de vos bulles de message sortantes (ou suivre le thème actuel)",
|
||||
"tt_chat_bubble_style": "Forme de la bulle de message : arrondie, carrée ou minimale (plate, sans bordure)",
|
||||
"tt_chat_density": "Espacement entre les messages : Confortable ajoute plus de marge ; Compact en affiche davantage à l'écran",
|
||||
"tt_chat_emoji_style": "Affiche les emoji en contour monochrome ou en couleur",
|
||||
"tt_chat_enter_sends": "Si activé, Enter envoie le message et Shift+Enter ajoute un saut de ligne ; si désactivé, Enter ajoute un saut de ligne",
|
||||
"tt_chat_font_size": "Met à l'échelle le texte des messages de chat de 0.8x à 1.5x. N'affecte que l'onglet Chat, pas le reste de l'application",
|
||||
"tt_chat_poll_rate": "Fréquence de vérification des messages nouveaux et 0-conf (0.5-15 s). Plus rapide est plus réactif mais utilise plus de CPU",
|
||||
"tt_chat_timestamp": "Format d'horodatage pour cet onglet uniquement : suivre l'horloge de l'application, ou forcer 24-hour ou 12-hour",
|
||||
"tt_clear_ztx": "Supprimer l'historique des z-transactions mis en cache localement",
|
||||
"tt_clock_format": "Horloge 24 h ou 12 h, dans toute l'app. Le chat peut la remplacer.",
|
||||
"tt_custom_fees": "Activer la saisie manuelle des frais lors de l'envoi de transactions",
|
||||
"tt_custom_theme": "Thème personnalisé actif",
|
||||
"tt_daemon_install_bundled": "Arrêter le nœud, remplacer le dragonxd installé par la version intégrée dans cette version du portefeuille, puis redémarrer",
|
||||
"tt_daemon_refresh": "Relit la version, la taille et la date de dragonxd installé et fourni affichées ci-dessus",
|
||||
"tt_daemon_update_check": "Télécharger et vérifier le dernier nœud complet dragonxd depuis le Gitea du projet, puis redémarrer pour l'appliquer",
|
||||
"tt_debug_collapse": "Réduire les options de journalisation de débogage",
|
||||
"tt_debug_expand": "Développer les options de journalisation de débogage",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
||||
"tt_language": "Langue de l'interface du portefeuille",
|
||||
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
||||
"tt_lite_copy": "Copie le secret révélé dans le presse-papiers",
|
||||
"tt_lite_decrypt_pass": "Saisissez votre phrase de passe pour retirer le chiffrement du portefeuille",
|
||||
"tt_lite_encrypt": "Chiffre le portefeuille avec la phrase de passe ci-dessus ; il se verrouille immédiatement et requiert la phrase pour se déverrouiller",
|
||||
"tt_lite_encrypt_pass": "Phrase de passe pour chiffrer le portefeuille. En cas de perte, le portefeuille ne peut être ni déverrouillé ni récupéré",
|
||||
"tt_lite_hide_wipe": "Masque le secret révélé et l'efface de la mémoire de façon sécurisée",
|
||||
"tt_lite_import_key": "Collez une clé privée de dépense ou de lecture à importer ; son historique apparaît après la prochaine synchronisation",
|
||||
"tt_lite_import_key_btn": "Importe la clé privée saisie dans ce portefeuille ; les fonds et l'historique apparaissent après la prochaine synchronisation",
|
||||
"tt_lite_lifecycle_op": "Choisissez de créer un nouveau portefeuille, d'en ouvrir un existant ou d'en restaurer un à partir d'une phrase de récupération",
|
||||
"tt_lite_lifecycle_pass": "Phrase de passe pour déverrouiller ou définir sur le portefeuille lors de cette opération de création / ouverture / restauration",
|
||||
"tt_lite_lifecycle_run": "Exécute l'opération de création / ouverture / restauration sélectionnée avec les valeurs ci-dessus",
|
||||
"tt_lite_lifecycle_toggle": "Affiche ou masque les commandes de création / ouverture / restauration pour gérer votre fichier de portefeuille lite",
|
||||
"tt_lite_lock": "Verrouille le portefeuille maintenant ; une phrase de passe est requise pour le déverrouiller et toute session de chat est fermée",
|
||||
"tt_lite_redownload": "Re-télécharger et re-scanner tous les blocs depuis le serveur lite",
|
||||
"tt_lite_remove_encrypt": "Retire le chiffrement et stocke le portefeuille sans protection ; aucune phrase de passe ne sera requise pour l'ouvrir",
|
||||
"tt_lite_restore_account": "Index de compte HD à restaurer ; laissez 0 sauf si vous avez utilisé plusieurs comptes avec cette graine",
|
||||
"tt_lite_restore_birthday": "Hauteur de bloc à laquelle le portefeuille a été créé ; l'analyse commence ici. Utilisez 0 ou la hauteur la plus ancienne en cas de doute",
|
||||
"tt_lite_restore_overwrite": "Remplace un fichier de portefeuille existant par cette restauration. Attention : écrase les données du portefeuille actuel",
|
||||
"tt_lite_restore_seed": "La phrase de récupération de 24-word pour restaurer ce portefeuille ; masquée pendant la saisie",
|
||||
"tt_lite_save_seed_file": "Écrit la graine et la date de création dans un fichier réservé au propriétaire (lite-seed-backup.txt) dans le dossier de configuration",
|
||||
"tt_lite_show_keys": "Révèle les clés privées de dépense de ce portefeuille. Quiconque possède une clé peut dépenser les fonds qu'elle contrôle",
|
||||
"tt_lite_show_seed": "Révèle la phrase de récupération et la date de création de ce portefeuille. Quiconque possède la graine peut dépenser vos fonds",
|
||||
"tt_lite_unlock": "Déverrouille le portefeuille chiffré à l'aide de la phrase de passe ci-dessus",
|
||||
"tt_lite_unlock_pass": "Saisissez votre phrase de passe pour déverrouiller le portefeuille chiffré",
|
||||
"tt_lite_wallet_path": "Chemin ou nom du fichier de portefeuille à ouvrir ou dans lequel restaurer",
|
||||
"tt_lock": "Verrouiller le portefeuille immédiatement",
|
||||
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "Nom d'hôte du daemon DragonX",
|
||||
"tt_rpc_pass": "Mot de passe d'authentification RPC",
|
||||
"tt_rpc_port": "Port pour les connexions RPC du daemon",
|
||||
"tt_rpc_toggle": "Affiche ou masque les informations de connexion RPC en lecture seule (hôte, port, utilisateur, mot de passe) du daemon",
|
||||
"tt_rpc_user": "Nom d'utilisateur d'authentification RPC",
|
||||
"tt_save_settings": "Enregistrer tous les paramètres sur le disque",
|
||||
"tt_save_ztx": "Stocker l'historique des transactions z-address localement pour un chargement plus rapide",
|
||||
"tt_scan_themes": "Rechercher de nouveaux thèmes.\\nPlacez les dossiers de thèmes dans :\\n%s",
|
||||
"tt_scanline": "Effet de lignes de balayage CRT dans la console",
|
||||
"tt_screenshot_open_dir": "Ouvre le dossier de captures (sous le répertoire de configuration) dans votre gestionnaire de fichiers",
|
||||
"tt_screenshot_sweep": "Parcourt chaque thème sur chaque onglet et enregistre une capture de chacun dans le dossier de captures de la configuration (écrase le dernier parcours)",
|
||||
"tt_screenshot_sweep_full": "Comme le parcours des thèmes, mais capture aussi chaque modale / boîte de dialogue / flux à l'aide de données de portefeuille de démonstration temporaires et hors ligne",
|
||||
"tt_seed_backup": "Afficher et sauvegarder la phrase de récupération de 24 mots de votre portefeuille",
|
||||
"tt_seed_demo_chat": "Injecte des conversations d'exemple dans l'onglet Chat pour qu'un parcours capture son interface ; en mémoire uniquement, perdu au redémarrage",
|
||||
"tt_seed_migrate": "Créer un nouveau portefeuille à phrase de récupération et y transférer vos fonds",
|
||||
"tt_set_pin": "Définir un PIN de 4-8 chiffres pour un déverrouillage rapide",
|
||||
"tt_shield_mining": "Déplacer les récompenses de minage transparentes vers une adresse blindée",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "Ouvrir le site web DragonX",
|
||||
"tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)",
|
||||
"tt_wizard": "Relancer l'assistant de configuration initiale\\nLe daemon sera redémarré",
|
||||
"tx_chat_badge": "Message",
|
||||
"tx_confirmations": "%d confirmations",
|
||||
"tx_details_title": "Détails de la transaction",
|
||||
"tx_from_address": "Adresse d'origine :",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "ミニマル",
|
||||
"chat_bubble_rounded": "角丸",
|
||||
"chat_bubble_square": "角ばった",
|
||||
"chat_buffer_loading": "チャットバッファ:…",
|
||||
"chat_buffer_preparing": "チャットバッファ:%d/%d を準備中…",
|
||||
"chat_buffer_ready": "チャットバッファ:%d/%d 準備完了",
|
||||
"chat_buffer_sending": "チャット:%d 件のメッセージを送信中…",
|
||||
"chat_buffer_sending_one": "チャット:%d 件のメッセージを送信中…",
|
||||
"chat_cancel": "キャンセル",
|
||||
"chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます",
|
||||
"chat_contact_request": "連絡リクエスト",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "会話をエクスポートしました",
|
||||
"chat_export_failed": "エクスポートファイルを書き込めませんでした。",
|
||||
"chat_export_warn": "復号したメッセージを平文で保存します。ファイルは安全に保管してください。",
|
||||
"chat_filter": "チャット",
|
||||
"chat_hidden_toast": "会話を非表示にしました — 新しいメッセージが届くと再表示されます",
|
||||
"chat_hide": "非表示",
|
||||
"chat_hide_hidden": "非表示を隠す",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "アプリ",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_backend_reference": "バックエンドコマンドリファレンス",
|
||||
"console_backend_unavailable": "バックエンドなし",
|
||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||
"console_cat_advanced": "詳細設定",
|
||||
"console_cat_blockchain": "ブロックチェーン",
|
||||
"console_cat_control": "制御",
|
||||
"console_cat_keys": "鍵とセキュリティ",
|
||||
"console_cat_mining": "マイニング",
|
||||
"console_cat_network": "ネットワーク",
|
||||
"console_cat_raw_transactions": "生トランザクション",
|
||||
"console_cat_send": "送金",
|
||||
"console_cat_sync": "同期",
|
||||
"console_cat_utility": "ユーティリティ",
|
||||
"console_cat_wallet": "ウォレット",
|
||||
"console_clear": "クリア",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - このヘルプを表示",
|
||||
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||
"console_help_stop": " stop - デーモンを停止",
|
||||
"console_last_error": "最後のエラー:",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "件一致",
|
||||
"console_new_lines": "%d 新しい行",
|
||||
"console_no_daemon": "デーモンなし",
|
||||
"console_no_output": "(出力なし)",
|
||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||
"console_not_connected_lite": "エラー:ウォレットが開かれていません",
|
||||
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
||||
"console_ref_builds": "生成",
|
||||
"console_ref_cancel": "キャンセル",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
|
||||
"console_ref_search_hint": "名前または用途で検索…",
|
||||
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
|
||||
"console_ref_value": "値",
|
||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "コンソールスキャンライン",
|
||||
"console_search_commands": "コマンドを検索...",
|
||||
"console_select_all": "すべて選択",
|
||||
"console_show_app_output": "[app] ウォレットのログ行を表示",
|
||||
"console_show_backend_ref": "バックエンドコマンドリファレンスを表示",
|
||||
"console_show_daemon_output": "デーモン出力を表示",
|
||||
"console_show_errors_only": "エラーのみ表示",
|
||||
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "停止済み",
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "不明",
|
||||
"console_stop_confirm_node": "'stop' はノードを停止し、ウォレットを切断します。確認するにはもう一度 'stop' と入力してください。",
|
||||
"console_tab_completion": "Tabで補完",
|
||||
"console_text_colors": "テキスト色",
|
||||
"console_toggle_accents": "行のカラーアクセントを切り替え",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "連絡先名",
|
||||
"contact_wallet_loading": "ウォレットを読み込み中です。「すべてのウォレットに表示」にチェックするか、少し待ってから再試行してください。",
|
||||
"contacts": "連絡先",
|
||||
"contacts_avatar_shape": "アバターの形",
|
||||
"contacts_list_scale": "リストの拡大率",
|
||||
"contacts_search_no_match": "一致する連絡先がありません",
|
||||
"contacts_search_placeholder": "連絡先を検索...",
|
||||
"contacts_settings_tip": "連絡先のカスタマイズ",
|
||||
"contacts_settings_title": "連絡先設定",
|
||||
"contacts_shape_circle": "円",
|
||||
"contacts_shape_square": "四角",
|
||||
"contacts_shape_tab": "左タブ",
|
||||
"copied": "コピーしました!",
|
||||
"copy": "コピー",
|
||||
"copy_address": "完全なアドレスをコピー",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||
"lite_birthday_label": "バースデー",
|
||||
"lite_console_backend_commands": "バックエンドコマンド:",
|
||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||
"lite_copy": "コピー",
|
||||
"lite_could_not_write": "書き込めませんでした ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "確認中…",
|
||||
"lite_net_connected": "接続済み",
|
||||
"lite_net_connecting": "接続中…",
|
||||
"lite_net_custom": "カスタム",
|
||||
"lite_net_disconnected": "未接続",
|
||||
"lite_net_hidden_section": "非表示のサーバー",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "時価総額",
|
||||
"market_cap_short": "時価総額",
|
||||
"market_chart_loading": "価格履歴を読み込み中",
|
||||
"market_col_name": "名前",
|
||||
"market_col_trend": "トレンド",
|
||||
"market_col_value": "価値",
|
||||
"market_iv_1d": "1日",
|
||||
"market_iv_1h": "1時間",
|
||||
"market_iv_1m": "1ヶ月",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "価格履歴がありません",
|
||||
"market_no_price": "価格データなし",
|
||||
"market_now": "現在",
|
||||
"market_opt_chart_style": "チャートスタイル",
|
||||
"market_pct_shielded": "%.0f%% シールド済み",
|
||||
"market_portfolio": "ポートフォリオ",
|
||||
"market_price_loading": "価格データを読み込み中...",
|
||||
"market_price_unavailable": "価格データが利用できません",
|
||||
"market_refresh_price": "価格データを更新",
|
||||
"market_settings_tip": "マーケットオプション",
|
||||
"market_settings_title": "マーケット設定",
|
||||
"market_style_candle": "ローソク足に切り替え",
|
||||
"market_style_candle_label": "ローソク足",
|
||||
"market_style_line": "折れ線チャートに切り替え",
|
||||
"market_style_line_label": "ライン",
|
||||
"market_trade_on": "%s で取引",
|
||||
"market_updated": "\\xc2\\xb7 更新: %s",
|
||||
"market_vol_short": "出来高",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "分",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "週",
|
||||
"portfolio_style_compact": "コンパクト行",
|
||||
"portfolio_style_detailed": "詳細行",
|
||||
"portfolio_style_featured": "注目行",
|
||||
"portfolio_style_compact": "テーブル",
|
||||
"portfolio_style_detailed": "カード",
|
||||
"portfolio_style_featured": "スポットライト",
|
||||
"portfolio_style_label": "ポートフォリオスタイル",
|
||||
"portfolio_untitled": "無題",
|
||||
"portfolio_wallet_loading": "ウォレットの読み込みが終わってからグループを追加してください。",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "ぼかし量(0%% = オフ、100%% = 最大)",
|
||||
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
|
||||
"tt_change_pin": "アンロック PIN を変更",
|
||||
"tt_chat_bubble_accent": "送信メッセージの吹き出しのアクセントカラー(または現在のテーマに従う)",
|
||||
"tt_chat_bubble_style": "メッセージの吹き出しの形:角丸、四角、またはミニマル(フラット、枠なし)",
|
||||
"tt_chat_density": "メッセージ間の間隔:ゆったりは余白を増やし、コンパクトは画面に多く表示します",
|
||||
"tt_chat_emoji_style": "絵文字をモノクロの輪郭またはフルカラーで表示します",
|
||||
"tt_chat_enter_sends": "オンのとき、Enterでメッセージを送信し、Shift+Enterで改行します。オフのとき、Enterで改行します",
|
||||
"tt_chat_font_size": "チャットのメッセージ文字を0.8xから1.5xで拡大縮小します。チャットタブのみに影響し、アプリの他の部分には影響しません",
|
||||
"tt_chat_poll_rate": "新着および0-confメッセージを確認する頻度(0.5-15 s)。速いほど反応が良くなりますが、CPUをより多く使います",
|
||||
"tt_chat_timestamp": "このタブのみのタイムスタンプ形式:アプリ全体の時計に従うか、24-hourまたは12-hourを強制します",
|
||||
"tt_clear_ztx": "ローカルにキャッシュされた z-トランザクション履歴を削除",
|
||||
"tt_clock_format": "24時間または12時間表示(アプリ全体)。チャットで上書きできます。",
|
||||
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
||||
"tt_custom_theme": "カスタムテーマがアクティブ",
|
||||
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
||||
"tt_daemon_refresh": "上に表示されているインストール済みおよび同梱のdragonxdのバージョン、サイズ、日付を再読み込みします",
|
||||
"tt_daemon_update_check": "プロジェクトの Gitea から最新の dragonxd フルノードをダウンロードして検証し、再起動して適用します",
|
||||
"tt_debug_collapse": "デバッグログオプションを折りたたむ",
|
||||
"tt_debug_expand": "デバッグログオプションを展開",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||
"tt_language": "ウォレット UI のインターフェース言語",
|
||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
||||
"tt_lite_copy": "表示された秘密情報をクリップボードにコピーします",
|
||||
"tt_lite_decrypt_pass": "ウォレットの暗号化を解除するためにパスフレーズを入力します",
|
||||
"tt_lite_encrypt": "上のパスフレーズでウォレットを暗号化します。すぐにロックされ、解除にはパスフレーズが必要です",
|
||||
"tt_lite_encrypt_pass": "ウォレットを暗号化するパスフレーズ。失うとウォレットのロック解除も復元もできなくなります",
|
||||
"tt_lite_hide_wipe": "表示された秘密情報を隠し、メモリから安全に消去します",
|
||||
"tt_lite_import_key": "インポートする秘密鍵(送金用または閲覧用)を貼り付けます。その履歴は次回の同期後に表示されます",
|
||||
"tt_lite_import_key_btn": "入力した秘密鍵をこのウォレットにインポートします。資金と履歴は次回の同期後に表示されます",
|
||||
"tt_lite_lifecycle_op": "新しいウォレットを作成するか、既存のものを開くか、シードフレーズから復元するかを選びます",
|
||||
"tt_lite_lifecycle_pass": "この作成/開く/復元の操作でウォレットのロック解除または設定に使うパスフレーズ",
|
||||
"tt_lite_lifecycle_run": "上の値で、選択した作成/開く/復元の操作を実行します",
|
||||
"tt_lite_lifecycle_toggle": "ライトウォレットファイルを管理する作成/開く/復元のコントロールを表示または非表示にします",
|
||||
"tt_lite_lock": "ウォレットを今すぐロックします。解除にはパスフレーズが必要で、チャットセッションはすべて終了します",
|
||||
"tt_lite_redownload": "ライトサーバーからすべてのブロックを再ダウンロードして再スキャン",
|
||||
"tt_lite_remove_encrypt": "暗号化を解除し、ウォレットを保護なしで保存します。開くのにパスフレーズは不要になります",
|
||||
"tt_lite_restore_account": "復元するHDアカウントのインデックス。このシードで複数のアカウントを使っていない限り0のままにします",
|
||||
"tt_lite_restore_birthday": "ウォレットが作成されたブロック高。スキャンはここから始まります。不明な場合は0または最も古い高さを使ってください",
|
||||
"tt_lite_restore_overwrite": "既存のウォレットファイルをこの復元で置き換えます。警告:現在のウォレットデータを上書きします",
|
||||
"tt_lite_restore_seed": "このウォレットを復元するための24-wordのリカバリーシードフレーズ。入力中は非表示になります",
|
||||
"tt_lite_save_seed_file": "シードと作成時期を、設定フォルダ内の所有者のみが読めるファイル(lite-seed-backup.txt)に書き出します",
|
||||
"tt_lite_show_keys": "このウォレットの秘密鍵(送金用)を表示します。鍵を持つ人は誰でもそれが管理する資金を使えます",
|
||||
"tt_lite_show_seed": "このウォレットのリカバリーシードフレーズと作成時期を表示します。シードを持つ人は誰でもあなたの資金を使えます",
|
||||
"tt_lite_unlock": "上のパスフレーズを使って暗号化されたウォレットのロックを解除します",
|
||||
"tt_lite_unlock_pass": "暗号化されたウォレットのロックを解除するためにパスフレーズを入力します",
|
||||
"tt_lite_wallet_path": "開く、または復元先となるウォレットファイルのパスまたは名前",
|
||||
"tt_lock": "ウォレットを即座にロック",
|
||||
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
||||
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "DragonX デーモンのホスト名",
|
||||
"tt_rpc_pass": "RPC 認証パスワード",
|
||||
"tt_rpc_port": "デーモン RPC 接続用ポート",
|
||||
"tt_rpc_toggle": "デーモンの読み取り専用のRPC接続情報(ホスト、ポート、ユーザー、パスワード)を表示または非表示にします",
|
||||
"tt_rpc_user": "RPC 認証ユーザー名",
|
||||
"tt_save_settings": "すべての設定をディスクに保存",
|
||||
"tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み",
|
||||
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s",
|
||||
"tt_scanline": "コンソールでの CRT スキャンライン効果",
|
||||
"tt_screenshot_open_dir": "スクリーンショットフォルダ(設定ディレクトリ内)をファイルマネージャーで開きます",
|
||||
"tt_screenshot_sweep": "すべてのタブですべてのテーマを順に切り替え、それぞれのスクリーンショットを設定のスクリーンショットフォルダに保存します(前回の実行を上書きします)",
|
||||
"tt_screenshot_sweep_full": "テーマの実行と同様ですが、一時的なオフラインのデモウォレットデータを使って、すべてのモーダル/ダイアログ/フローも撮影します",
|
||||
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
|
||||
"tt_seed_demo_chat": "実行でUIを撮影できるように、チャットタブにサンプルの会話を挿入します。メモリ上のみで、再起動で消えます",
|
||||
"tt_seed_migrate": "新しいシードフレーズウォレットを作成し、資金をそこへ移動します",
|
||||
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
|
||||
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "DragonX ウェブサイトを開く",
|
||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||
"tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます",
|
||||
"tx_chat_badge": "メッセージ",
|
||||
"tx_confirmations": "%d 確認",
|
||||
"tx_details_title": "取引の詳細",
|
||||
"tx_from_address": "送信元アドレス:",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "미니멀",
|
||||
"chat_bubble_rounded": "둥근",
|
||||
"chat_bubble_square": "각진",
|
||||
"chat_buffer_loading": "채팅 버퍼: …",
|
||||
"chat_buffer_preparing": "채팅 버퍼: %d/%d 준비 중…",
|
||||
"chat_buffer_ready": "채팅 버퍼: %d/%d 준비됨",
|
||||
"chat_buffer_sending": "채팅: 메시지 %d개 보내는 중…",
|
||||
"chat_buffer_sending_one": "채팅: 메시지 %d개 보내는 중…",
|
||||
"chat_cancel": "취소",
|
||||
"chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요",
|
||||
"chat_contact_request": "연락 요청",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "대화를 내보냈습니다",
|
||||
"chat_export_failed": "내보내기 파일을 쓸 수 없습니다.",
|
||||
"chat_export_warn": "복호화된 메시지를 일반 텍스트로 저장합니다. 파일을 안전하게 보관하세요.",
|
||||
"chat_filter": "채팅",
|
||||
"chat_hidden_toast": "대화를 숨겼습니다 — 새 메시지가 오면 다시 표시됩니다",
|
||||
"chat_hide": "숨기기",
|
||||
"chat_hide_hidden": "숨김 접기",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "앱",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_backend_reference": "백엔드 명령어 참조",
|
||||
"console_backend_unavailable": "백엔드 없음",
|
||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||
"console_cat_advanced": "고급",
|
||||
"console_cat_blockchain": "블록체인",
|
||||
"console_cat_control": "제어",
|
||||
"console_cat_keys": "키 및 보안",
|
||||
"console_cat_mining": "채굴",
|
||||
"console_cat_network": "네트워크",
|
||||
"console_cat_raw_transactions": "원시 트랜잭션",
|
||||
"console_cat_send": "보내기",
|
||||
"console_cat_sync": "동기화",
|
||||
"console_cat_utility": "유틸리티",
|
||||
"console_cat_wallet": "지갑",
|
||||
"console_clear": "지우기",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - 도움말 표시",
|
||||
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||
"console_help_stop": " stop - 데몬 중지",
|
||||
"console_last_error": "마지막 오류:",
|
||||
"console_line_count": "%zu줄",
|
||||
"console_matches": "일치",
|
||||
"console_new_lines": "%d 새 줄",
|
||||
"console_no_daemon": "데몬 없음",
|
||||
"console_no_output": "(출력 없음)",
|
||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||
"console_not_connected_lite": "오류: 열린 지갑 없음",
|
||||
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
||||
"console_ref_builds": "생성",
|
||||
"console_ref_cancel": "취소",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
|
||||
"console_ref_search_hint": "이름 또는 용도로 검색…",
|
||||
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
|
||||
"console_ref_value": "값",
|
||||
"console_rpc_reference": "RPC 명령어 참조",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "콘솔 스캔라인",
|
||||
"console_search_commands": "명령어 검색...",
|
||||
"console_select_all": "모두 선택",
|
||||
"console_show_app_output": "[app] 지갑 로그 줄 표시",
|
||||
"console_show_backend_ref": "백엔드 명령어 참조 표시",
|
||||
"console_show_daemon_output": "데몬 출력 표시",
|
||||
"console_show_errors_only": "오류만 표시",
|
||||
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "중지됨",
|
||||
"console_status_stopping": "중지 중",
|
||||
"console_status_unknown": "알 수 없음",
|
||||
"console_stop_confirm_node": "'stop'은 노드를 종료하고 지갑 연결을 끊습니다. 확인하려면 'stop'을 다시 입력하세요.",
|
||||
"console_tab_completion": "Tab으로 자동 완성",
|
||||
"console_text_colors": "텍스트 색상",
|
||||
"console_toggle_accents": "줄 색상 강조 전환",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "연락처 이름",
|
||||
"contact_wallet_loading": "지갑을 아직 불러오는 중입니다 — “모든 지갑에 표시”를 선택하거나 잠시 후 다시 시도하세요.",
|
||||
"contacts": "연락처",
|
||||
"contacts_avatar_shape": "아바타 모양",
|
||||
"contacts_list_scale": "목록 배율",
|
||||
"contacts_search_no_match": "일치하는 연락처 없음",
|
||||
"contacts_search_placeholder": "연락처 검색...",
|
||||
"contacts_settings_tip": "연락처 사용자 지정",
|
||||
"contacts_settings_title": "연락처 설정",
|
||||
"contacts_shape_circle": "원",
|
||||
"contacts_shape_square": "사각형",
|
||||
"contacts_shape_tab": "왼쪽 탭",
|
||||
"copied": "복사됨!",
|
||||
"copy": "복사",
|
||||
"copy_address": "전체 주소 복사",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||
"lite_birthday_label": "생일 블록",
|
||||
"lite_console_backend_commands": "백엔드 명령:",
|
||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||
"lite_copy": "복사",
|
||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "확인 중…",
|
||||
"lite_net_connected": "연결됨",
|
||||
"lite_net_connecting": "연결 중…",
|
||||
"lite_net_custom": "사용자 지정",
|
||||
"lite_net_disconnected": "연결되지 않음",
|
||||
"lite_net_hidden_section": "숨겨진 서버",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "시가총액",
|
||||
"market_cap_short": "시총",
|
||||
"market_chart_loading": "가격 기록 불러오는 중",
|
||||
"market_col_name": "이름",
|
||||
"market_col_trend": "추세",
|
||||
"market_col_value": "가치",
|
||||
"market_iv_1d": "1일",
|
||||
"market_iv_1h": "1시간",
|
||||
"market_iv_1m": "1개월",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "가격 내역 없음",
|
||||
"market_no_price": "가격 데이터 없음",
|
||||
"market_now": "현재",
|
||||
"market_opt_chart_style": "차트 스타일",
|
||||
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||
"market_portfolio": "포트폴리오",
|
||||
"market_price_loading": "가격 데이터를 불러오는 중...",
|
||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||
"market_refresh_price": "가격 데이터 새로고침",
|
||||
"market_settings_tip": "마켓 옵션",
|
||||
"market_settings_title": "마켓 설정",
|
||||
"market_style_candle": "캔들차트로 전환",
|
||||
"market_style_candle_label": "캔들",
|
||||
"market_style_line": "선형 차트로 전환",
|
||||
"market_style_line_label": "라인",
|
||||
"market_trade_on": "%s에서 거래",
|
||||
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
|
||||
"market_vol_short": "거래량",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "분",
|
||||
"portfolio_spark_month": "월",
|
||||
"portfolio_spark_week": "주",
|
||||
"portfolio_style_compact": "간결한 행",
|
||||
"portfolio_style_detailed": "상세 행",
|
||||
"portfolio_style_featured": "강조 행",
|
||||
"portfolio_style_compact": "테이블",
|
||||
"portfolio_style_detailed": "카드",
|
||||
"portfolio_style_featured": "스포트라이트",
|
||||
"portfolio_style_label": "포트폴리오 스타일",
|
||||
"portfolio_untitled": "제목 없음",
|
||||
"portfolio_wallet_loading": "지갑 로딩이 끝난 후 그룹을 추가하세요.",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "블러 양 (0%% = 끔, 100%% = 최대)",
|
||||
"tt_change_pass": "지갑 암호화 비밀번호 변경",
|
||||
"tt_change_pin": "잠금 해제 PIN 변경",
|
||||
"tt_chat_bubble_accent": "보내는 메시지 말풍선의 강조 색상(또는 현재 테마를 따름)",
|
||||
"tt_chat_bubble_style": "메시지 말풍선 모양: 둥근형, 사각형 또는 미니멀(평면, 테두리 없음)",
|
||||
"tt_chat_density": "메시지 간 간격: 편안함은 여백을 더 추가하고; 촘촘함은 화면에 더 많이 표시합니다",
|
||||
"tt_chat_emoji_style": "이모지를 단색 윤곽선 또는 전체 색상으로 렌더링합니다",
|
||||
"tt_chat_enter_sends": "켜면 Enter가 메시지를 보내고 Shift+Enter가 줄바꿈을 추가합니다; 끄면 Enter가 줄바꿈을 추가합니다",
|
||||
"tt_chat_font_size": "채팅 메시지 텍스트를 0.8x에서 1.5x까지 조정합니다. 채팅 탭에만 적용되며 앱의 나머지 부분에는 영향을 주지 않습니다",
|
||||
"tt_chat_poll_rate": "새 메시지 및 0-conf 메시지를 확인하는 빈도(0.5-15 s). 빠를수록 반응성이 좋지만 CPU를 더 사용합니다",
|
||||
"tt_chat_timestamp": "이 탭에만 적용되는 타임스탬프 형식: 앱 전체 시계를 따르거나 24-hour 또는 12-hour로 강제합니다",
|
||||
"tt_clear_ztx": "로컬에 캐시된 z-트랜잭션 기록 삭제",
|
||||
"tt_clock_format": "24시간 또는 12시간 형식(앱 전체). 채팅에서 재정의할 수 있습니다.",
|
||||
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
||||
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
||||
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
||||
"tt_daemon_refresh": "위에 표시된 설치 및 번들 dragonxd의 버전, 크기, 날짜를 다시 읽어옵니다",
|
||||
"tt_daemon_update_check": "프로젝트 Gitea에서 최신 dragonxd 풀 노드를 다운로드하고 검증한 다음, 재시작하여 적용합니다",
|
||||
"tt_debug_collapse": "디버그 로깅 옵션 접기",
|
||||
"tt_debug_expand": "디버그 로깅 옵션 펼치기",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||
"tt_language": "지갑 UI 인터페이스 언어",
|
||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
||||
"tt_lite_copy": "표시된 비밀을 클립보드에 복사합니다",
|
||||
"tt_lite_decrypt_pass": "지갑에서 암호화를 제거하려면 암호를 입력하세요",
|
||||
"tt_lite_encrypt": "위 암호로 지갑을 암호화합니다; 즉시 잠기며 잠금 해제하려면 암호가 필요합니다",
|
||||
"tt_lite_encrypt_pass": "지갑을 암호화할 암호. 분실하면 지갑을 잠금 해제하거나 복구할 수 없습니다",
|
||||
"tt_lite_hide_wipe": "표시된 비밀을 숨기고 메모리에서 안전하게 지웁니다",
|
||||
"tt_lite_import_key": "가져올 개인 지출 또는 조회 키를 붙여넣으세요; 다음 동기화 후 해당 내역이 나타납니다",
|
||||
"tt_lite_import_key_btn": "입력한 개인 키를 이 지갑으로 가져옵니다; 자금과 내역은 다음 동기화 후 나타납니다",
|
||||
"tt_lite_lifecycle_op": "새 지갑을 생성할지, 기존 지갑을 열지, 시드 문구로 복구할지 선택합니다",
|
||||
"tt_lite_lifecycle_pass": "이 생성 / 열기 / 복구 작업 중 지갑을 잠금 해제하거나 설정할 암호",
|
||||
"tt_lite_lifecycle_run": "위 값으로 선택한 생성 / 열기 / 복구 작업을 실행합니다",
|
||||
"tt_lite_lifecycle_toggle": "라이트 지갑 파일을 관리하기 위한 생성 / 열기 / 복구 컨트롤을 표시하거나 숨깁니다",
|
||||
"tt_lite_lock": "지금 지갑을 잠급니다; 잠금 해제하려면 암호가 필요하며 모든 채팅 세션이 종료됩니다",
|
||||
"tt_lite_redownload": "라이트 서버에서 모든 블록을 다시 다운로드하고 다시 스캔합니다",
|
||||
"tt_lite_remove_encrypt": "암호화를 제거하고 지갑을 보호되지 않은 상태로 저장합니다; 지갑을 열 때 암호가 필요하지 않습니다",
|
||||
"tt_lite_restore_account": "복구할 HD 계정 인덱스; 이 시드로 여러 계정을 사용하지 않았다면 0으로 두세요",
|
||||
"tt_lite_restore_birthday": "지갑이 생성된 블록 높이; 여기서부터 스캔이 시작됩니다. 확실하지 않으면 0 또는 가장 이른 높이를 사용하세요",
|
||||
"tt_lite_restore_overwrite": "기존 지갑 파일을 이 복구본으로 대체합니다. 경고: 현재 지갑 데이터를 덮어씁니다",
|
||||
"tt_lite_restore_seed": "이 지갑을 복구할 24-word 복구 시드 문구; 입력하는 동안 숨겨집니다",
|
||||
"tt_lite_save_seed_file": "시드와 생성 높이를 설정 폴더의 소유자 전용 파일(lite-seed-backup.txt)에 기록합니다",
|
||||
"tt_lite_show_keys": "이 지갑의 개인 지출 키를 표시합니다. 키를 가진 사람은 누구나 그 키가 제어하는 자금을 사용할 수 있습니다",
|
||||
"tt_lite_show_seed": "이 지갑의 복구 시드 문구와 생성 높이를 표시합니다. 시드를 가진 사람은 누구나 자금을 사용할 수 있습니다",
|
||||
"tt_lite_unlock": "위 암호를 사용하여 암호화된 지갑을 잠금 해제합니다",
|
||||
"tt_lite_unlock_pass": "암호화된 지갑을 잠금 해제하려면 암호를 입력하세요",
|
||||
"tt_lite_wallet_path": "열거나 복구할 지갑 파일의 경로 또는 이름",
|
||||
"tt_lock": "지갑 즉시 잠금",
|
||||
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
||||
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "DragonX 데몬 호스트 이름",
|
||||
"tt_rpc_pass": "RPC 인증 비밀번호",
|
||||
"tt_rpc_port": "데몬 RPC 연결 포트",
|
||||
"tt_rpc_toggle": "데몬의 읽기 전용 RPC 연결 정보(호스트, 포트, 사용자, 비밀번호)를 표시하거나 숨깁니다",
|
||||
"tt_rpc_user": "RPC 인증 사용자 이름",
|
||||
"tt_save_settings": "모든 설정을 디스크에 저장",
|
||||
"tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩",
|
||||
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
|
||||
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
|
||||
"tt_screenshot_open_dir": "파일 관리자에서 screenshots 폴더(설정 디렉터리 아래)를 엽니다",
|
||||
"tt_screenshot_sweep": "모든 탭에서 모든 테마를 순환하며 각각의 스크린샷을 설정 screenshots 폴더에 저장합니다(마지막 스윕을 덮어씀)",
|
||||
"tt_screenshot_sweep_full": "테마 스윕과 유사하지만 임시 오프라인 데모 지갑 데이터를 사용하여 모든 모달 / 대화 상자 / 흐름도 캡처합니다",
|
||||
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
|
||||
"tt_seed_demo_chat": "스윕이 UI를 캡처하도록 샘플 대화를 채팅 탭에 삽입합니다; 메모리에만 저장되며 재시작 시 사라집니다",
|
||||
"tt_seed_migrate": "새 시드 문구 지갑을 만들고 자금을 그곳으로 옮깁니다",
|
||||
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
|
||||
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "DragonX 웹사이트 열기",
|
||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||
"tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다",
|
||||
"tx_chat_badge": "메시지",
|
||||
"tx_confirmations": "%d 확인",
|
||||
"tx_details_title": "거래 상세",
|
||||
"tx_from_address": "보낸 주소:",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "Mínimo",
|
||||
"chat_bubble_rounded": "Arredondado",
|
||||
"chat_bubble_square": "Quadrado",
|
||||
"chat_buffer_loading": "Buffer de chat: …",
|
||||
"chat_buffer_preparing": "Buffer de chat: preparando %d/%d…",
|
||||
"chat_buffer_ready": "Buffer de chat: %d/%d prontos",
|
||||
"chat_buffer_sending": "Chat: enviando %d mensagens…",
|
||||
"chat_buffer_sending_one": "Chat: enviando %d mensagem…",
|
||||
"chat_cancel": "Cancelar",
|
||||
"chat_contact_added": "Contato adicionado — renomeie em Contatos",
|
||||
"chat_contact_request": "solicitação de contato",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "Conversa exportada",
|
||||
"chat_export_failed": "Não foi possível gravar o arquivo de exportação.",
|
||||
"chat_export_warn": "Salva as mensagens descriptografadas como texto simples. Guarde o arquivo com segurança.",
|
||||
"chat_filter": "Chat",
|
||||
"chat_hidden_toast": "Conversa ocultada — uma nova mensagem a traz de volta",
|
||||
"chat_hide": "Ocultar",
|
||||
"chat_hide_hidden": "Ocultar ocultas",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "App",
|
||||
"console_auto_scroll": "Rolagem automática",
|
||||
"console_available_commands": "Comandos disponíveis:",
|
||||
"console_backend_reference": "Referência de Comandos do Backend",
|
||||
"console_backend_unavailable": "Sem backend",
|
||||
"console_capturing_output": "Capturando saída do daemon...",
|
||||
"console_cat_advanced": "Avançado",
|
||||
"console_cat_blockchain": "Blockchain",
|
||||
"console_cat_control": "Controle",
|
||||
"console_cat_keys": "Chaves e segurança",
|
||||
"console_cat_mining": "Mineração",
|
||||
"console_cat_network": "Rede",
|
||||
"console_cat_raw_transactions": "Transações brutas",
|
||||
"console_cat_send": "Enviar",
|
||||
"console_cat_sync": "Sincronização",
|
||||
"console_cat_utility": "Utilitários",
|
||||
"console_cat_wallet": "Carteira",
|
||||
"console_clear": "Limpar",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - Mostrar esta mensagem de ajuda",
|
||||
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
||||
"console_help_stop": " stop - Parar o daemon",
|
||||
"console_last_error": "Último erro:",
|
||||
"console_line_count": "%zu linhas",
|
||||
"console_matches": "correspondências",
|
||||
"console_new_lines": "%d novas linhas",
|
||||
"console_no_daemon": "Sem daemon",
|
||||
"console_no_output": "(sem saída)",
|
||||
"console_not_connected": "Erro: Não conectado ao daemon",
|
||||
"console_not_connected_lite": "Erro: Nenhuma carteira aberta",
|
||||
"console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.",
|
||||
"console_ref_builds": "Gera",
|
||||
"console_ref_cancel": "Cancelar",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
|
||||
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
|
||||
"console_ref_select_hint": "Selecione um comando para ver o que ele faz.",
|
||||
"console_ref_value": "valor",
|
||||
"console_rpc_reference": "Referência de Comandos RPC",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Scanline do console",
|
||||
"console_search_commands": "Pesquisar comandos...",
|
||||
"console_select_all": "Selecionar Tudo",
|
||||
"console_show_app_output": "Mostrar linhas do log da carteira [app]",
|
||||
"console_show_backend_ref": "Mostrar referência de comandos do backend",
|
||||
"console_show_daemon_output": "Mostrar saída do daemon",
|
||||
"console_show_errors_only": "Mostrar apenas erros",
|
||||
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "Parado",
|
||||
"console_status_stopping": "Parando",
|
||||
"console_status_unknown": "Desconhecido",
|
||||
"console_stop_confirm_node": "'stop' irá desligar o nó e desconectar a carteira. Digite 'stop' novamente para confirmar.",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_text_colors": "Cores do texto",
|
||||
"console_toggle_accents": "Alternar destaques de cor das linhas",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "Nome do contato",
|
||||
"contact_wallet_loading": "A carteira ainda está carregando — marque “Mostrar em todas as carteiras” ou tente novamente em um momento.",
|
||||
"contacts": "Contatos",
|
||||
"contacts_avatar_shape": "Forma do avatar",
|
||||
"contacts_list_scale": "Escala da lista",
|
||||
"contacts_search_no_match": "Nenhum contato correspondente",
|
||||
"contacts_search_placeholder": "Pesquisar contatos...",
|
||||
"contacts_settings_tip": "Personalizar contatos",
|
||||
"contacts_settings_title": "Configurações de contatos",
|
||||
"contacts_shape_circle": "Círculo",
|
||||
"contacts_shape_square": "Quadrado",
|
||||
"contacts_shape_tab": "Aba",
|
||||
"copied": "Copiado!",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Endereço Completo",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "Aniversário: %llu (faça o backup disto também)",
|
||||
"lite_birthday_hint": "Altura do bloco a partir da qual começar a escanear. Deixe 0 se desconhecida (escaneamento completo mais lento).",
|
||||
"lite_birthday_label": "Data de nascimento",
|
||||
"lite_console_backend_commands": "Comandos do backend:",
|
||||
"lite_console_help_passthrough": "Qualquer outra entrada é executada como um comando de console da carteira leve.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_write": "Não foi possível gravar ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://seu-servidor-lite",
|
||||
"lite_net_checking": "verificando…",
|
||||
"lite_net_connected": "Conectado",
|
||||
"lite_net_connecting": "Conectando…",
|
||||
"lite_net_custom": "Personalizado",
|
||||
"lite_net_disconnected": "Não conectado",
|
||||
"lite_net_hidden_section": "Servidores ocultos",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "Capitalização",
|
||||
"market_cap_short": "Cap.",
|
||||
"market_chart_loading": "Carregando histórico de preços",
|
||||
"market_col_name": "Nome",
|
||||
"market_col_trend": "Tendência",
|
||||
"market_col_value": "Valor",
|
||||
"market_iv_1d": "1D",
|
||||
"market_iv_1h": "1H",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "Nenhum histórico de preços disponível",
|
||||
"market_no_price": "Sem dados de preço",
|
||||
"market_now": "Agora",
|
||||
"market_opt_chart_style": "Estilo do gráfico",
|
||||
"market_pct_shielded": "%.0f%% Blindado",
|
||||
"market_portfolio": "PORTFÓLIO",
|
||||
"market_price_loading": "Carregando dados de preço...",
|
||||
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||
"market_refresh_price": "Atualizar dados de preço",
|
||||
"market_settings_tip": "Opções de mercado",
|
||||
"market_settings_title": "Configurações de mercado",
|
||||
"market_style_candle": "Mudar para velas",
|
||||
"market_style_candle_label": "Velas",
|
||||
"market_style_line": "Mudar para gráfico de linhas",
|
||||
"market_style_line_label": "Linha",
|
||||
"market_trade_on": "Negociar no %s",
|
||||
"market_updated": "\\xc2\\xb7 Atualizado %s",
|
||||
"market_vol_short": "Vol",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Minuto",
|
||||
"portfolio_spark_month": "Mês",
|
||||
"portfolio_spark_week": "Semana",
|
||||
"portfolio_style_compact": "Linhas compactas",
|
||||
"portfolio_style_detailed": "Linhas detalhadas",
|
||||
"portfolio_style_featured": "Linhas em destaque",
|
||||
"portfolio_style_compact": "Tabela",
|
||||
"portfolio_style_detailed": "Cartões",
|
||||
"portfolio_style_featured": "Destaque",
|
||||
"portfolio_style_label": "Estilo do portfólio",
|
||||
"portfolio_untitled": "Sem título",
|
||||
"portfolio_wallet_loading": "Aguarde a carteira terminar de carregar para adicionar um grupo.",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "Quantidade de desfoque (0%% = desligado, 100%% = máximo)",
|
||||
"tt_change_pass": "Alterar a frase secreta de encriptação da carteira",
|
||||
"tt_change_pin": "Alterar seu PIN de desbloqueio",
|
||||
"tt_chat_bubble_accent": "Cor de destaque para seus balões de mensagem enviados (ou seguir o tema atual)",
|
||||
"tt_chat_bubble_style": "Formato do balão de mensagem: arredondado, quadrado ou minimalista (plano, sem borda)",
|
||||
"tt_chat_density": "Espaçamento entre mensagens: Confortável adiciona mais espaçamento; Compacto exibe mais na tela",
|
||||
"tt_chat_emoji_style": "Renderizar emojis em contorno monocromático ou colorido",
|
||||
"tt_chat_enter_sends": "Quando ativado, Enter envia a mensagem e Shift+Enter adiciona uma nova linha; quando desativado, Enter adiciona uma nova linha",
|
||||
"tt_chat_font_size": "Dimensionar o texto das mensagens de chat de 0.8x a 1.5x. Afeta apenas a aba Chat, não o restante do aplicativo",
|
||||
"tt_chat_poll_rate": "Com que frequência verificar mensagens novas e 0-conf (0.5-15 s). Mais rápido é mais responsivo, mas usa mais CPU",
|
||||
"tt_chat_timestamp": "Formato de horário apenas para esta aba: seguir o relógio geral do aplicativo, ou forçar 24-hour ou 12-hour",
|
||||
"tt_clear_ztx": "Excluir histórico de z-transações em cache local",
|
||||
"tt_clock_format": "Relógio de 24 ou 12 horas, em todo o app. O chat pode substituí-lo.",
|
||||
"tt_custom_fees": "Ativar entrada manual de taxas ao enviar transações",
|
||||
"tt_custom_theme": "Tema personalizado ativo",
|
||||
"tt_daemon_install_bundled": "Parar o nó, sobrescrever o dragonxd instalado com a versão incluída nesta compilação da carteira e reiniciar",
|
||||
"tt_daemon_refresh": "Reler a versão, o tamanho e a data do dragonxd instalado e do incorporado, mostrados acima",
|
||||
"tt_daemon_update_check": "Baixe e verifique o nó completo dragonxd mais recente do Gitea do projeto e, em seguida, reinicie para aplicar",
|
||||
"tt_debug_collapse": "Recolher opções de registro de depuração",
|
||||
"tt_debug_expand": "Expandir opções de registro de depuração",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
||||
"tt_language": "Idioma da interface da carteira",
|
||||
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
||||
"tt_lite_copy": "Copiar o segredo revelado para a área de transferência",
|
||||
"tt_lite_decrypt_pass": "Digite sua frase-senha para remover a criptografia da carteira",
|
||||
"tt_lite_encrypt": "Criptografar a carteira com a frase-senha acima; ela é bloqueada imediatamente e exige a frase-senha para desbloquear",
|
||||
"tt_lite_encrypt_pass": "Frase-senha com a qual criptografar a carteira. Se perdida, a carteira não pode ser desbloqueada nem recuperada",
|
||||
"tt_lite_hide_wipe": "Ocultar o segredo revelado e apagá-lo com segurança da memória",
|
||||
"tt_lite_import_key": "Cole uma chave privada de gasto ou de visualização para importar; seu histórico aparece após a próxima sincronização",
|
||||
"tt_lite_import_key_btn": "Importar a chave privada informada para esta carteira; os fundos e o histórico aparecem após a próxima sincronização",
|
||||
"tt_lite_lifecycle_op": "Escolha entre criar uma nova carteira, abrir uma existente ou restaurar uma a partir de uma frase de recuperação",
|
||||
"tt_lite_lifecycle_pass": "Frase-senha para desbloquear ou definir na carteira durante esta operação de criar / abrir / restaurar",
|
||||
"tt_lite_lifecycle_run": "Executar a operação selecionada de criar / abrir / restaurar com os valores acima",
|
||||
"tt_lite_lifecycle_toggle": "Mostrar ou ocultar os controles de criar / abrir / restaurar para gerenciar o arquivo da sua carteira lite",
|
||||
"tt_lite_lock": "Bloquear a carteira agora; uma frase-senha é necessária para desbloquear e qualquer sessão de chat é encerrada",
|
||||
"tt_lite_redownload": "Rebaixar e reescanear todos os blocos do servidor lite",
|
||||
"tt_lite_remove_encrypt": "Remover a criptografia e armazenar a carteira desprotegida; nenhuma frase-senha será necessária para abri-la",
|
||||
"tt_lite_restore_account": "Índice da conta HD a restaurar; deixe 0 a menos que você tenha usado várias contas sob esta frase de recuperação",
|
||||
"tt_lite_restore_birthday": "Altura de bloco em que a carteira foi criada; a varredura começa aqui. Use 0 ou a altura mais antiga se não tiver certeza",
|
||||
"tt_lite_restore_overwrite": "Substituir um arquivo de carteira existente por esta restauração. Aviso: sobrescreve os dados da carteira atual",
|
||||
"tt_lite_restore_seed": "A frase de recuperação de 24-word para restaurar esta carteira; oculta enquanto você digita",
|
||||
"tt_lite_save_seed_file": "Gravar a frase de recuperação e a data de criação em um arquivo restrito ao dono (lite-seed-backup.txt) na pasta de configuração",
|
||||
"tt_lite_show_keys": "Revelar as chaves privadas de gasto desta carteira. Qualquer pessoa com uma chave pode gastar os fundos que ela controla",
|
||||
"tt_lite_show_seed": "Revelar a frase de recuperação e a data de criação desta carteira. Qualquer pessoa com a frase de recuperação pode gastar seus fundos",
|
||||
"tt_lite_unlock": "Desbloquear a carteira criptografada usando a frase-senha acima",
|
||||
"tt_lite_unlock_pass": "Digite sua frase-senha para desbloquear a carteira criptografada",
|
||||
"tt_lite_wallet_path": "Caminho ou nome do arquivo da carteira a abrir ou para o qual restaurar",
|
||||
"tt_lock": "Bloquear a carteira imediatamente",
|
||||
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
|
||||
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "Nome do host do daemon DragonX",
|
||||
"tt_rpc_pass": "Senha de autenticação RPC",
|
||||
"tt_rpc_port": "Porta para conexões RPC do daemon",
|
||||
"tt_rpc_toggle": "Mostrar ou ocultar os detalhes de conexão RPC somente leitura (host, porta, usuário, senha) do daemon",
|
||||
"tt_rpc_user": "Nome de usuário de autenticação RPC",
|
||||
"tt_save_settings": "Salvar todas as configurações no disco",
|
||||
"tt_save_ztx": "Armazenar histórico de transações z-address localmente para carregamento mais rápido",
|
||||
"tt_scan_themes": "Procurar novos temas.\\nColoque pastas de temas em:\\n%s",
|
||||
"tt_scanline": "Efeito de linhas de varredura CRT no console",
|
||||
"tt_screenshot_open_dir": "Abrir a pasta screenshots (dentro do diretório de configuração) no seu gerenciador de arquivos",
|
||||
"tt_screenshot_sweep": "Percorrer cada tema em cada aba, salvando uma captura de tela de cada uma na pasta screenshots de configuração (sobrescreve a última varredura)",
|
||||
"tt_screenshot_sweep_full": "Como a varredura de temas, mas também captura cada modal / caixa de diálogo / fluxo usando dados temporários de carteira de demonstração offline",
|
||||
"tt_seed_backup": "Mostrar e fazer backup da frase de recuperação de 24 palavras da sua carteira",
|
||||
"tt_seed_demo_chat": "Injetar conversas de exemplo na aba Chat para que uma varredura capture sua interface; apenas na memória, some ao reiniciar",
|
||||
"tt_seed_migrate": "Criar uma nova carteira com frase de recuperação e mover seus fundos para ela",
|
||||
"tt_set_pin": "Definir um PIN de 4-8 dígitos para desbloqueio rápido",
|
||||
"tt_shield_mining": "Mover recompensas de mineração transparentes para um endereço blindado",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "Abrir o site do DragonX",
|
||||
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
||||
"tt_wizard": "Executar novamente o assistente de configuração inicial\\nO daemon será reiniciado",
|
||||
"tx_chat_badge": "Mensagem",
|
||||
"tx_confirmations": "%d confirmações",
|
||||
"tx_details_title": "Detalhes da Transação",
|
||||
"tx_from_address": "Endereço de Origem:",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "Минимальный",
|
||||
"chat_bubble_rounded": "Скруглённый",
|
||||
"chat_bubble_square": "Прямоугольный",
|
||||
"chat_buffer_loading": "Буфер чата: …",
|
||||
"chat_buffer_preparing": "Буфер чата: подготовка %d/%d…",
|
||||
"chat_buffer_ready": "Буфер чата: %d/%d готово",
|
||||
"chat_buffer_sending": "Чат: отправка %d сообщений…",
|
||||
"chat_buffer_sending_one": "Чат: отправка %d сообщения…",
|
||||
"chat_cancel": "Отмена",
|
||||
"chat_contact_added": "Контакт добавлен — переименуйте его в Контактах",
|
||||
"chat_contact_request": "запрос контакта",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "Разговор экспортирован",
|
||||
"chat_export_failed": "Не удалось записать файл экспорта.",
|
||||
"chat_export_warn": "Сохраняет расшифрованные сообщения в виде обычного текста. Храните файл в надёжном месте.",
|
||||
"chat_filter": "Чат",
|
||||
"chat_hidden_toast": "Разговор скрыт — новое сообщение вернёт его",
|
||||
"chat_hide": "Скрыть",
|
||||
"chat_hide_hidden": "Скрыть скрытые",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "Прил.",
|
||||
"console_auto_scroll": "Авто-прокрутка",
|
||||
"console_available_commands": "Доступные команды:",
|
||||
"console_backend_reference": "Справочник команд бэкенда",
|
||||
"console_backend_unavailable": "Нет бэкенда",
|
||||
"console_capturing_output": "Захват вывода daemon...",
|
||||
"console_cat_advanced": "Дополнительно",
|
||||
"console_cat_blockchain": "Блокчейн",
|
||||
"console_cat_control": "Управление",
|
||||
"console_cat_keys": "Ключи и безопасность",
|
||||
"console_cat_mining": "Майнинг",
|
||||
"console_cat_network": "Сеть",
|
||||
"console_cat_raw_transactions": "Сырые транзакции",
|
||||
"console_cat_send": "Отправка",
|
||||
"console_cat_sync": "Синхронизация",
|
||||
"console_cat_utility": "Утилиты",
|
||||
"console_cat_wallet": "Кошелёк",
|
||||
"console_clear": "Очистить",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - Показать эту справку",
|
||||
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
||||
"console_help_stop": " stop - Остановить daemon",
|
||||
"console_last_error": "Последняя ошибка:",
|
||||
"console_line_count": "%zu строк",
|
||||
"console_matches": "совпадений",
|
||||
"console_new_lines": "%d новых строк",
|
||||
"console_no_daemon": "Нет daemon",
|
||||
"console_no_output": "(нет вывода)",
|
||||
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||
"console_not_connected_lite": "Ошибка: Нет открытого кошелька",
|
||||
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
|
||||
"console_ref_builds": "Формирует",
|
||||
"console_ref_cancel": "Отмена",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
|
||||
"console_ref_search_hint": "Поиск по названию или задаче…",
|
||||
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
|
||||
"console_ref_value": "значение",
|
||||
"console_rpc_reference": "Справочник RPC-команд",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "Скан-линия консоли",
|
||||
"console_search_commands": "Поиск команд...",
|
||||
"console_select_all": "Выбрать всё",
|
||||
"console_show_app_output": "Показать строки журнала кошелька [app]",
|
||||
"console_show_backend_ref": "Показать справочник команд бэкенда",
|
||||
"console_show_daemon_output": "Показать вывод daemon",
|
||||
"console_show_errors_only": "Показать только ошибки",
|
||||
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "Остановлен",
|
||||
"console_status_stopping": "Остановка",
|
||||
"console_status_unknown": "Неизвестно",
|
||||
"console_stop_confirm_node": "'stop' остановит узел и отключит кошелёк. Введите 'stop' ещё раз для подтверждения.",
|
||||
"console_tab_completion": "Tab для дополнения",
|
||||
"console_text_colors": "Цвета текста",
|
||||
"console_toggle_accents": "Переключить цветовые акценты строк",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "Имя контакта",
|
||||
"contact_wallet_loading": "Кошелёк ещё загружается — отметьте «Показывать во всех кошельках» или повторите чуть позже.",
|
||||
"contacts": "Контакты",
|
||||
"contacts_avatar_shape": "Форма аватара",
|
||||
"contacts_list_scale": "Масштаб списка",
|
||||
"contacts_search_no_match": "Совпадающих контактов нет",
|
||||
"contacts_search_placeholder": "Поиск контактов...",
|
||||
"contacts_settings_tip": "Настройка контактов",
|
||||
"contacts_settings_title": "Настройки контактов",
|
||||
"contacts_shape_circle": "Круг",
|
||||
"contacts_shape_square": "Квадрат",
|
||||
"contacts_shape_tab": "Вкладка",
|
||||
"copied": "Скопировано!",
|
||||
"copy": "Копировать",
|
||||
"copy_address": "Копировать полный адрес",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
||||
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
||||
"lite_birthday_label": "Дата рождения",
|
||||
"lite_console_backend_commands": "Команды бэкенда:",
|
||||
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
||||
"lite_copy": "Копировать",
|
||||
"lite_could_not_write": "Не удалось записать ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "проверка…",
|
||||
"lite_net_connected": "Подключено",
|
||||
"lite_net_connecting": "Подключение…",
|
||||
"lite_net_custom": "Свой",
|
||||
"lite_net_disconnected": "Не подключено",
|
||||
"lite_net_hidden_section": "Скрытые серверы",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "Рыночная капитализация",
|
||||
"market_cap_short": "Кап.",
|
||||
"market_chart_loading": "Загрузка истории цен",
|
||||
"market_col_name": "Название",
|
||||
"market_col_trend": "Тренд",
|
||||
"market_col_value": "Стоимость",
|
||||
"market_iv_1d": "1Д",
|
||||
"market_iv_1h": "1Ч",
|
||||
"market_iv_1m": "1М",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "Нет истории цен",
|
||||
"market_no_price": "Нет данных о ценах",
|
||||
"market_now": "Сейчас",
|
||||
"market_opt_chart_style": "Стиль графика",
|
||||
"market_pct_shielded": "%.0f%% Экранировано",
|
||||
"market_portfolio": "ПОРТФЕЛЬ",
|
||||
"market_price_loading": "Загрузка данных о ценах...",
|
||||
"market_price_unavailable": "Данные о ценах недоступны",
|
||||
"market_refresh_price": "Обновить данные о ценах",
|
||||
"market_settings_tip": "Параметры рынка",
|
||||
"market_settings_title": "Настройки рынка",
|
||||
"market_style_candle": "Переключить на свечи",
|
||||
"market_style_candle_label": "Свечи",
|
||||
"market_style_line": "Переключить на линейный график",
|
||||
"market_style_line_label": "Линия",
|
||||
"market_trade_on": "Торговать на %s",
|
||||
"market_updated": "\\xc2\\xb7 Обновлено %s",
|
||||
"market_vol_short": "Объём",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "Минута",
|
||||
"portfolio_spark_month": "Месяц",
|
||||
"portfolio_spark_week": "Неделя",
|
||||
"portfolio_style_compact": "Компактные строки",
|
||||
"portfolio_style_detailed": "Подробные строки",
|
||||
"portfolio_style_featured": "Избранные строки",
|
||||
"portfolio_style_compact": "Таблица",
|
||||
"portfolio_style_detailed": "Карточки",
|
||||
"portfolio_style_featured": "Витрина",
|
||||
"portfolio_style_label": "Стиль портфеля",
|
||||
"portfolio_untitled": "Без названия",
|
||||
"portfolio_wallet_loading": "Дождитесь загрузки кошелька, чтобы добавить группу.",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "Степень размытия (0%% = выкл., 100%% = максимум)",
|
||||
"tt_change_pass": "Сменить пароль шифрования кошелька",
|
||||
"tt_change_pin": "Изменить PIN-код разблокировки",
|
||||
"tt_chat_bubble_accent": "Акцентный цвет для ваших исходящих пузырьков сообщений (или следовать текущей теме)",
|
||||
"tt_chat_bubble_style": "Форма пузырька сообщения: скруглённая, квадратная или минимальная (плоская, без границы)",
|
||||
"tt_chat_density": "Интервал между сообщениями: Комфортный добавляет больше отступов; Компактный вмещает больше на экране",
|
||||
"tt_chat_emoji_style": "Отображать эмодзи в монохромном контуре или полноцветно",
|
||||
"tt_chat_enter_sends": "Когда включено, Enter отправляет сообщение, а Shift+Enter добавляет новую строку; когда выключено, Enter добавляет новую строку",
|
||||
"tt_chat_font_size": "Масштаб текста сообщений чата от 0.8x до 1.5x. Влияет только на вкладку «Чат», не на остальную часть приложения",
|
||||
"tt_chat_poll_rate": "Как часто проверять новые и 0-conf сообщения (0.5-15 s). Быстрее — отзывчивее, но использует больше CPU",
|
||||
"tt_chat_timestamp": "Формат времени только для этой вкладки: следовать общим настройкам часов приложения либо принудительно 24-hour или 12-hour",
|
||||
"tt_clear_ztx": "Удалить локально кешированную историю z-транзакций",
|
||||
"tt_clock_format": "24- или 12-часовой формат для всего приложения. Чат может переопределить.",
|
||||
"tt_custom_fees": "Включить ручной ввод комиссий при отправке транзакций",
|
||||
"tt_custom_theme": "Пользовательская тема активна",
|
||||
"tt_daemon_install_bundled": "Остановить узел, перезаписать установленный dragonxd версией, встроенной в эту сборку кошелька, затем перезапустить",
|
||||
"tt_daemon_refresh": "Перечитать версию, размер и дату установленного и встроенного dragonxd, показанные выше",
|
||||
"tt_daemon_update_check": "Скачать и проверить последний полный узел dragonxd из проектного Gitea, затем перезапустить для применения",
|
||||
"tt_debug_collapse": "Свернуть параметры журнала отладки",
|
||||
"tt_debug_expand": "Развернуть параметры журнала отладки",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
||||
"tt_language": "Язык интерфейса кошелька",
|
||||
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",
|
||||
"tt_lite_copy": "Скопировать показанный секрет в буфер обмена",
|
||||
"tt_lite_decrypt_pass": "Введите пароль, чтобы снять шифрование с кошелька",
|
||||
"tt_lite_encrypt": "Зашифровать кошелёк паролем выше; он блокируется сразу же и требует пароль для разблокировки",
|
||||
"tt_lite_encrypt_pass": "Пароль для шифрования кошелька. При утрате кошелёк невозможно разблокировать или восстановить",
|
||||
"tt_lite_hide_wipe": "Скрыть показанный секрет и безопасно стереть его из памяти",
|
||||
"tt_lite_import_key": "Вставьте приватный ключ расходования или просмотра для импорта; его история появится после следующей синхронизации",
|
||||
"tt_lite_import_key_btn": "Импортировать введённый приватный ключ в этот кошелёк; средства и история появятся после следующей синхронизации",
|
||||
"tt_lite_lifecycle_op": "Выберите, создать новый кошелёк, открыть существующий или восстановить его из seed-фразы",
|
||||
"tt_lite_lifecycle_pass": "Пароль для разблокировки или установки на кошелёк во время этой операции создания / открытия / восстановления",
|
||||
"tt_lite_lifecycle_run": "Выполнить выбранную операцию создания / открытия / восстановления со значениями выше",
|
||||
"tt_lite_lifecycle_toggle": "Показать или скрыть элементы управления создания / открытия / восстановления для управления файлом лёгкого кошелька",
|
||||
"tt_lite_lock": "Заблокировать кошелёк сейчас; для разблокировки потребуется пароль, а любая сессия чата будет прервана",
|
||||
"tt_lite_redownload": "Заново загрузить и пересканировать все блоки с лёгкого сервера",
|
||||
"tt_lite_remove_encrypt": "Снять шифрование и хранить кошелёк без защиты; пароль для его открытия не потребуется",
|
||||
"tt_lite_restore_account": "Индекс HD-аккаунта для восстановления; оставьте 0, если только вы не использовали несколько аккаунтов с этой seed-фразой",
|
||||
"tt_lite_restore_birthday": "Высота блока, на которой был создан кошелёк; отсюда начинается сканирование. Если не уверены, используйте 0 или самую раннюю высоту",
|
||||
"tt_lite_restore_overwrite": "Заменить существующий файл кошелька этим восстановлением. Внимание: перезаписывает текущие данные кошелька",
|
||||
"tt_lite_restore_seed": "24-word seed-фраза для восстановления этого кошелька; скрывается по мере ввода",
|
||||
"tt_lite_save_seed_file": "Записать seed-фразу и дату создания в файл, доступный только владельцу (lite-seed-backup.txt), в папке конфигурации",
|
||||
"tt_lite_show_keys": "Показать приватные ключи расходования этого кошелька. Любой, у кого есть ключ, может потратить контролируемые им средства",
|
||||
"tt_lite_show_seed": "Показать seed-фразу восстановления и дату создания этого кошелька. Любой, у кого есть seed-фраза, может потратить ваши средства",
|
||||
"tt_lite_unlock": "Разблокировать зашифрованный кошелёк с помощью пароля выше",
|
||||
"tt_lite_unlock_pass": "Введите пароль для разблокировки зашифрованного кошелька",
|
||||
"tt_lite_wallet_path": "Путь или имя файла кошелька для открытия или восстановления",
|
||||
"tt_lock": "Немедленно заблокировать кошелёк",
|
||||
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
|
||||
"tt_merge": "Объединить несколько UTXO в один адрес",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "Имя хоста демона DragonX",
|
||||
"tt_rpc_pass": "Пароль аутентификации RPC",
|
||||
"tt_rpc_port": "Порт для RPC-подключений демона",
|
||||
"tt_rpc_toggle": "Показать или скрыть параметры RPC-подключения только для чтения (хост, порт, пользователь, пароль) для демона",
|
||||
"tt_rpc_user": "Имя пользователя аутентификации RPC",
|
||||
"tt_save_settings": "Сохранить все настройки на диск",
|
||||
"tt_save_ztx": "Хранить историю транзакций z-адреса локально для более быстрой загрузки",
|
||||
"tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s",
|
||||
"tt_scanline": "Эффект развёртки ЭЛТ в консоли",
|
||||
"tt_screenshot_open_dir": "Открыть папку скриншотов (в каталоге конфигурации) в вашем файловом менеджере",
|
||||
"tt_screenshot_sweep": "Перебрать каждую тему по всем вкладкам, сохраняя скриншот каждой в папку скриншотов конфигурации (перезаписывает предыдущий проход)",
|
||||
"tt_screenshot_sweep_full": "Как проход по темам, но также захватывает каждое модальное окно / диалог / поток, используя временные офлайн-данные демонстрационного кошелька",
|
||||
"tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов",
|
||||
"tt_seed_demo_chat": "Добавить примеры переписок во вкладку «Чат», чтобы проход захватил её интерфейс; только в памяти, исчезает при перезапуске",
|
||||
"tt_seed_migrate": "Создать новый кошелёк с сид-фразой и перевести в него ваши средства",
|
||||
"tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки",
|
||||
"tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "Открыть сайт DragonX",
|
||||
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
||||
"tt_wizard": "Повторно запустить мастер начальной настройки\\nДемон будет перезапущен",
|
||||
"tx_chat_badge": "Сообщение",
|
||||
"tx_confirmations": "%d подтверждений",
|
||||
"tx_details_title": "Детали транзакции",
|
||||
"tx_from_address": "Адрес отправителя:",
|
||||
|
||||
@@ -146,6 +146,11 @@
|
||||
"chat_bubble_minimal": "极简",
|
||||
"chat_bubble_rounded": "圆角",
|
||||
"chat_bubble_square": "方形",
|
||||
"chat_buffer_loading": "聊天缓冲:…",
|
||||
"chat_buffer_preparing": "聊天缓冲:正在准备 %d/%d…",
|
||||
"chat_buffer_ready": "聊天缓冲:%d/%d 已就绪",
|
||||
"chat_buffer_sending": "聊天:正在发送 %d 条消息…",
|
||||
"chat_buffer_sending_one": "聊天:正在发送 %d 条消息…",
|
||||
"chat_cancel": "取消",
|
||||
"chat_contact_added": "已添加联系人——可在联系人中重命名",
|
||||
"chat_contact_request": "联系人请求",
|
||||
@@ -162,6 +167,7 @@
|
||||
"chat_export_done": "会话已导出",
|
||||
"chat_export_failed": "无法写入导出文件。",
|
||||
"chat_export_warn": "将解密后的消息保存为纯文本。请妥善保管该文件。",
|
||||
"chat_filter": "聊天",
|
||||
"chat_hidden_toast": "会话已隐藏——收到新消息后会重新显示",
|
||||
"chat_hide": "隐藏",
|
||||
"chat_hide_hidden": "收起已隐藏",
|
||||
@@ -271,12 +277,18 @@
|
||||
"console_app": "应用",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_backend_reference": "后端命令参考",
|
||||
"console_backend_unavailable": "无后端",
|
||||
"console_capturing_output": "正在捕获守护进程输出...",
|
||||
"console_cat_advanced": "高级",
|
||||
"console_cat_blockchain": "区块链",
|
||||
"console_cat_control": "控制",
|
||||
"console_cat_keys": "密钥与安全",
|
||||
"console_cat_mining": "挖矿",
|
||||
"console_cat_network": "网络",
|
||||
"console_cat_raw_transactions": "原始交易",
|
||||
"console_cat_send": "发送",
|
||||
"console_cat_sync": "同步",
|
||||
"console_cat_utility": "实用工具",
|
||||
"console_cat_wallet": "钱包",
|
||||
"console_clear": "清除",
|
||||
@@ -310,11 +322,14 @@
|
||||
"console_help_help": " help - 显示此帮助信息",
|
||||
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||
"console_help_stop": " stop - 停止守护进程",
|
||||
"console_last_error": "上次错误:",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "个匹配",
|
||||
"console_new_lines": "%d 新行",
|
||||
"console_no_daemon": "无守护进程",
|
||||
"console_no_output": "(无输出)",
|
||||
"console_not_connected": "错误:未连接到守护进程",
|
||||
"console_not_connected_lite": "错误:没有打开的钱包",
|
||||
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
||||
"console_ref_builds": "生成",
|
||||
"console_ref_cancel": "取消",
|
||||
@@ -330,12 +345,14 @@
|
||||
"console_ref_run_confirm": "立即运行 %s?这是一个有重大影响的命令。",
|
||||
"console_ref_search_hint": "按名称或用途搜索…",
|
||||
"console_ref_select_hint": "选择一个命令以查看其功能。",
|
||||
"console_ref_value": "值",
|
||||
"console_rpc_reference": "RPC 命令参考",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "控制台扫描线",
|
||||
"console_search_commands": "搜索命令...",
|
||||
"console_select_all": "全选",
|
||||
"console_show_app_output": "显示[应用]钱包日志行",
|
||||
"console_show_backend_ref": "显示后端命令参考",
|
||||
"console_show_daemon_output": "显示守护进程输出",
|
||||
"console_show_errors_only": "仅显示错误",
|
||||
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||
@@ -348,6 +365,7 @@
|
||||
"console_status_stopped": "已停止",
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "未知",
|
||||
"console_stop_confirm_node": "'stop' 将关闭节点并断开钱包连接。再次输入 'stop' 以确认。",
|
||||
"console_tab_completion": "Tab 补全",
|
||||
"console_text_colors": "文本颜色",
|
||||
"console_toggle_accents": "切换行颜色强调",
|
||||
@@ -375,8 +393,15 @@
|
||||
"contact_preview_name": "联系人名称",
|
||||
"contact_wallet_loading": "钱包仍在加载——请选中“在每个钱包中显示”,或稍后再试。",
|
||||
"contacts": "联系人",
|
||||
"contacts_avatar_shape": "头像形状",
|
||||
"contacts_list_scale": "列表缩放",
|
||||
"contacts_search_no_match": "没有匹配的联系人",
|
||||
"contacts_search_placeholder": "搜索联系人...",
|
||||
"contacts_settings_tip": "联系人自定义",
|
||||
"contacts_settings_title": "联系人设置",
|
||||
"contacts_shape_circle": "圆形",
|
||||
"contacts_shape_square": "方形",
|
||||
"contacts_shape_tab": "左标签",
|
||||
"copied": "已复制!",
|
||||
"copy": "复制",
|
||||
"copy_address": "复制完整地址",
|
||||
@@ -619,6 +644,7 @@
|
||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||
"lite_birthday_label": "诞生区块",
|
||||
"lite_console_backend_commands": "后端命令:",
|
||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||
"lite_copy": "复制",
|
||||
"lite_could_not_write": "无法写入 ",
|
||||
@@ -636,6 +662,7 @@
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "检查中…",
|
||||
"lite_net_connected": "已连接",
|
||||
"lite_net_connecting": "连接中…",
|
||||
"lite_net_custom": "自定义",
|
||||
"lite_net_disconnected": "未连接",
|
||||
"lite_net_hidden_section": "隐藏的服务器",
|
||||
@@ -723,6 +750,9 @@
|
||||
"market_cap": "市值",
|
||||
"market_cap_short": "市值",
|
||||
"market_chart_loading": "正在加载价格历史",
|
||||
"market_col_name": "名称",
|
||||
"market_col_trend": "趋势",
|
||||
"market_col_value": "价值",
|
||||
"market_iv_1d": "1天",
|
||||
"market_iv_1h": "1时",
|
||||
"market_iv_1m": "1M",
|
||||
@@ -731,13 +761,18 @@
|
||||
"market_no_history": "无价格历史",
|
||||
"market_no_price": "无价格数据",
|
||||
"market_now": "现在",
|
||||
"market_opt_chart_style": "图表样式",
|
||||
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||
"market_portfolio": "投资组合",
|
||||
"market_price_loading": "正在加载价格数据...",
|
||||
"market_price_unavailable": "价格数据不可用",
|
||||
"market_refresh_price": "刷新价格数据",
|
||||
"market_settings_tip": "市场选项",
|
||||
"market_settings_title": "市场设置",
|
||||
"market_style_candle": "切换到蜡烛图",
|
||||
"market_style_candle_label": "K线",
|
||||
"market_style_line": "切换到折线图",
|
||||
"market_style_line_label": "折线",
|
||||
"market_trade_on": "在 %s 交易",
|
||||
"market_updated": "\\xc2\\xb7 已更新 %s",
|
||||
"market_vol_short": "成交量",
|
||||
@@ -1041,9 +1076,9 @@
|
||||
"portfolio_spark_min": "分钟",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "周",
|
||||
"portfolio_style_compact": "紧凑行",
|
||||
"portfolio_style_detailed": "详细行",
|
||||
"portfolio_style_featured": "特色行",
|
||||
"portfolio_style_compact": "表格",
|
||||
"portfolio_style_detailed": "卡片",
|
||||
"portfolio_style_featured": "聚焦",
|
||||
"portfolio_style_label": "投资组合样式",
|
||||
"portfolio_untitled": "未命名",
|
||||
"portfolio_wallet_loading": "请等待钱包加载完成后再添加分组。",
|
||||
@@ -1425,11 +1460,20 @@
|
||||
"tt_blur": "模糊程度(0%% = 关闭,100%% = 最大)",
|
||||
"tt_change_pass": "更改钱包加密密码",
|
||||
"tt_change_pin": "更改您的解锁 PIN",
|
||||
"tt_chat_bubble_accent": "你发出的消息气泡的强调色(或跟随当前主题)",
|
||||
"tt_chat_bubble_style": "消息气泡形状:圆角、方形或极简(扁平、无边框)",
|
||||
"tt_chat_density": "消息之间的间距:宽松增加更多留白;紧凑在屏幕上容纳更多内容",
|
||||
"tt_chat_emoji_style": "以单色轮廓或全彩渲染表情符号",
|
||||
"tt_chat_enter_sends": "开启时,Enter 发送消息,Shift+Enter 换行;关闭时,Enter 换行",
|
||||
"tt_chat_font_size": "将聊天消息文字从 0.8x 缩放到 1.5x。仅影响聊天标签页,不影响应用的其余部分",
|
||||
"tt_chat_poll_rate": "检查新消息和 0-conf 消息的频率(0.5-15 s)。越快响应越及时,但占用更多 CPU",
|
||||
"tt_chat_timestamp": "仅此标签页的时间戳格式:跟随全应用时钟,或强制使用 24-hour 或 12-hour",
|
||||
"tt_clear_ztx": "删除本地缓存的 z-交易历史",
|
||||
"tt_clock_format": "24 或 12 小时制,应用全局。聊天可覆盖。",
|
||||
"tt_custom_fees": "发送交易时启用手动费用输入",
|
||||
"tt_custom_theme": "自定义主题已激活",
|
||||
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
||||
"tt_daemon_refresh": "重新读取上方显示的已安装及内置 dragonxd 版本、大小和日期",
|
||||
"tt_daemon_update_check": "从项目 Gitea 下载并验证最新的 dragonxd 全节点,然后重启以应用",
|
||||
"tt_debug_collapse": "折叠调试日志选项",
|
||||
"tt_debug_expand": "展开调试日志选项",
|
||||
@@ -1447,7 +1491,30 @@
|
||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||
"tt_language": "钱包界面语言",
|
||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
||||
"tt_lite_copy": "将显示的机密复制到剪贴板",
|
||||
"tt_lite_decrypt_pass": "输入你的密码以移除钱包的加密",
|
||||
"tt_lite_encrypt": "用上方的密码加密钱包;加密后立即锁定,需要该密码才能解锁",
|
||||
"tt_lite_encrypt_pass": "用于加密钱包的密码。若丢失,钱包将无法解锁或恢复",
|
||||
"tt_lite_hide_wipe": "隐藏显示的机密并将其从内存中安全擦除",
|
||||
"tt_lite_import_key": "粘贴要导入的私有花费或查看密钥;其历史记录会在下次同步后出现",
|
||||
"tt_lite_import_key_btn": "将输入的私钥导入此钱包;资金和历史记录会在下次同步后出现",
|
||||
"tt_lite_lifecycle_op": "选择是创建新钱包、打开现有钱包,还是从助记词恢复钱包",
|
||||
"tt_lite_lifecycle_pass": "在此次创建 / 打开 / 恢复操作中用于解锁或设置钱包的密码",
|
||||
"tt_lite_lifecycle_run": "使用上方的值执行所选的创建 / 打开 / 恢复操作",
|
||||
"tt_lite_lifecycle_toggle": "显示或隐藏用于管理轻钱包文件的创建 / 打开 / 恢复控件",
|
||||
"tt_lite_lock": "立即锁定钱包;解锁需要密码,任何聊天会话都会被中断",
|
||||
"tt_lite_redownload": "从轻钱包服务器重新下载并重新扫描所有区块",
|
||||
"tt_lite_remove_encrypt": "移除加密并以未受保护的方式存储钱包;打开它将不再需要密码",
|
||||
"tt_lite_restore_account": "要恢复的 HD 账户索引;除非你在此助记词下使用了多个账户,否则保持为 0",
|
||||
"tt_lite_restore_birthday": "钱包创建时的区块高度;扫描从此处开始。不确定时请填 0 或最早的高度",
|
||||
"tt_lite_restore_overwrite": "用此次恢复替换现有的钱包文件。警告:这会覆盖当前的钱包数据",
|
||||
"tt_lite_restore_seed": "用于恢复此钱包的 24-word 助记词恢复短语;输入时会隐藏",
|
||||
"tt_lite_save_seed_file": "将助记词和创建高度写入配置文件夹中一个仅所有者可读的文件(lite-seed-backup.txt)",
|
||||
"tt_lite_show_keys": "显示此钱包的私有花费密钥。任何拥有密钥的人都能动用它所控制的资金",
|
||||
"tt_lite_show_seed": "显示此钱包的助记词恢复短语和创建高度。任何拥有助记词的人都能动用你的资金",
|
||||
"tt_lite_unlock": "使用上方的密码解锁已加密的钱包",
|
||||
"tt_lite_unlock_pass": "输入你的密码以解锁已加密的钱包",
|
||||
"tt_lite_wallet_path": "要打开或恢复到的钱包文件路径或名称",
|
||||
"tt_lock": "立即锁定钱包",
|
||||
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
||||
"tt_merge": "将多个 UTXO 合并到一个地址",
|
||||
@@ -1468,12 +1535,17 @@
|
||||
"tt_rpc_host": "DragonX 守护进程主机名",
|
||||
"tt_rpc_pass": "RPC 认证密码",
|
||||
"tt_rpc_port": "守护进程 RPC 连接端口",
|
||||
"tt_rpc_toggle": "显示或隐藏守护进程的只读 RPC 连接详情(主机、端口、用户、密码)",
|
||||
"tt_rpc_user": "RPC 认证用户名",
|
||||
"tt_save_settings": "将所有设置保存到磁盘",
|
||||
"tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度",
|
||||
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s",
|
||||
"tt_scanline": "控制台中的 CRT 扫描线效果",
|
||||
"tt_screenshot_open_dir": "在你的文件管理器中打开截图文件夹(位于配置目录下)",
|
||||
"tt_screenshot_sweep": "在每个标签页遍历每种主题,将每种主题的截图保存到配置文件夹的 screenshots 目录(覆盖上一次遍历)",
|
||||
"tt_screenshot_sweep_full": "与主题遍历类似,但同时捕获每个使用临时离线演示钱包数据的模态框 / 对话框 / 流程",
|
||||
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
|
||||
"tt_seed_demo_chat": "向聊天标签页注入示例对话,以便遍历能捕获其界面;仅在内存中,重启后消失",
|
||||
"tt_seed_migrate": "创建一个新的助记词钱包并将您的资金转入其中",
|
||||
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
|
||||
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
|
||||
@@ -1492,6 +1564,7 @@
|
||||
"tt_website": "打开 DragonX 网站",
|
||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||
"tt_wizard": "重新运行初始设置向导\\n守护进程将被重启",
|
||||
"tx_chat_badge": "消息",
|
||||
"tx_confirmations": "%d 次确认",
|
||||
"tx_details_title": "交易详情",
|
||||
"tx_from_address": "发送地址:",
|
||||
|
||||
190
res/themes/jade.toml
Normal file
190
res/themes/jade.toml
Normal file
@@ -0,0 +1,190 @@
|
||||
[theme]
|
||||
name = "Jade"
|
||||
author = "The Hush Developers"
|
||||
dark = true
|
||||
elevation = { --elevation-0 = "#071210", --elevation-1 = "#0C1A16", --elevation-2 = "#16261F", --elevation-3 = "#1D3128", --elevation-4 = "#243B30" }
|
||||
images = { background_image = "backgrounds/texture/jade_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||
|
||||
[theme.palette]
|
||||
--primary = "#2FA07A"
|
||||
--primary-variant = "#1E7357"
|
||||
--primary-light = "#7FD1B5"
|
||||
--secondary = "#C9A24E"
|
||||
--secondary-variant = "#A8842F"
|
||||
--secondary-light = "#E0C583"
|
||||
--background = "#071210"
|
||||
--surface = "#0C1A16"
|
||||
--surface-variant = "#16261F"
|
||||
--on-primary = "#FFFFFF"
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#DCEDE4"
|
||||
--on-surface = "#DCEDE4"
|
||||
--on-surface-medium = "rgba(220,237,228,0.85)"
|
||||
--on-surface-disabled = "rgba(220,237,228,0.58)"
|
||||
--error = "#CF6679"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
--on-success = "#000000"
|
||||
--warning = "#FFB74D"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(130,205,170,0.14)"
|
||||
--outline = "rgba(130,205,170,0.16)"
|
||||
--scrim = "rgba(0,0,0,0.6)"
|
||||
--surface-hover = "rgba(130,205,170,0.07)"
|
||||
--surface-alt = "rgba(130,205,170,0.05)"
|
||||
--surface-active = "rgba(130,205,170,0.10)"
|
||||
--glass-button = "rgba(130,205,170,0.06)"
|
||||
--glass-button-hover = "rgba(130,205,170,0.12)"
|
||||
--card-border = "rgba(130,205,170,0.26)"
|
||||
--text-shadow = "rgba(0,0,0,0.50)"
|
||||
--input-overlay-text = "rgba(220,237,228,0.30)"
|
||||
--slider-text = "rgba(220,237,228,0.85)"
|
||||
--thumb-fill = "rgba(130,205,170,0.15)"
|
||||
--thumb-border = "rgba(130,205,170,0.50)"
|
||||
--disabled-label = "rgba(130,205,170,0.18)"
|
||||
--chart-grid = "rgba(130,205,170,0.05)"
|
||||
--chart-crosshair = "rgba(130,205,170,0.15)"
|
||||
--chart-hover-ring = "rgba(130,205,170,0.30)"
|
||||
--tooltip-bg = "rgba(9,20,16,0.92)"
|
||||
--tooltip-border = "rgba(130,205,170,0.12)"
|
||||
--glass-fill = "rgba(130,205,170,0.08)"
|
||||
--glass-border = "rgba(47,160,122,0.30)"
|
||||
--glass-noise-tint = "rgba(130,205,170,0.03)"
|
||||
--tactile-top = "rgba(130,205,170,0.06)"
|
||||
--tactile-bottom = "rgba(130,205,170,0.0)"
|
||||
--hover-overlay = "rgba(130,205,170,0.05)"
|
||||
--active-overlay = "rgba(130,205,170,0.10)"
|
||||
--rim-light = "rgba(130,205,170,0.14)"
|
||||
--status-divider = "rgba(130,205,170,0.08)"
|
||||
--sidebar-hover = "rgba(130,205,170,0.10)"
|
||||
--sidebar-icon = "rgba(130,205,170,0.42)"
|
||||
--sidebar-badge = "rgba(220,237,228,1.0)"
|
||||
--sidebar-divider = "rgba(130,205,170,0.06)"
|
||||
--chart-line = "rgba(130,205,170,0.10)"
|
||||
--window-control = "rgba(220,237,228,0.78)"
|
||||
--window-control-hover = "rgba(130,205,170,0.12)"
|
||||
--window-close-hover = "rgba(232,17,35,0.78)"
|
||||
--spinner-track = "rgba(130,205,170,0.10)"
|
||||
--spinner-active = "rgba(79,184,154,0.85)"
|
||||
--shutdown-panel-bg = "rgba(7,18,14,0.90)"
|
||||
--shutdown-panel-border = "rgba(130,205,170,0.07)"
|
||||
--ram-bar-app = "#2FA07A"
|
||||
--ram-bar-system = "rgba(255,255,255,0.18)"
|
||||
--accent-total = "#7FD1B5"
|
||||
--accent-shielded = "#4FB89A"
|
||||
--accent-transparent = "#C9A24E"
|
||||
--accent-action = "#2FA07A"
|
||||
--accent-market = "#4FB89A"
|
||||
--accent-portfolio = "#7FD1B5"
|
||||
--toast-info-accent = "#2FA07A"
|
||||
--toast-info-text = "#7FD1B5"
|
||||
--toast-success-accent = "rgba(50,180,80,1.0)"
|
||||
--toast-success-text = "rgba(180,255,180,1.0)"
|
||||
--toast-warning-accent = "rgba(204,166,50,1.0)"
|
||||
--toast-warning-text = "rgba(255,230,130,1.0)"
|
||||
--toast-error-accent = "rgba(204,64,64,1.0)"
|
||||
--toast-error-text = "rgba(255,153,153,1.0)"
|
||||
--snackbar-bg = "rgba(24,40,34,0.95)"
|
||||
--snackbar-text = "rgba(220,237,228,0.87)"
|
||||
--snackbar-action = "rgba(79,184,154,1.0)"
|
||||
--snackbar-action-hover = "rgba(127,209,181,1.0)"
|
||||
--switch-track-off = "rgba(130,205,170,0.12)"
|
||||
--switch-track-on = "rgba(47,160,122,0.50)"
|
||||
--switch-thumb-off = "#A0C0B4"
|
||||
--switch-thumb-on = "#DCEDE4"
|
||||
--control-shadow = "rgba(0,0,0,0.24)"
|
||||
--checkbox-check = "#000000"
|
||||
--app-bar-shadow = "rgba(0,0,0,0.25)"
|
||||
|
||||
[backdrop]
|
||||
base-color-top = "rgba(14,32,26,210)"
|
||||
base-color-bottom = "rgba(6,18,14,210)"
|
||||
texture-tint-alpha = 120
|
||||
gradient-top-r = 10
|
||||
gradient-top-g = 30
|
||||
gradient-top-b = 22
|
||||
gradient-top-a = 90
|
||||
gradient-bottom-r = 5
|
||||
gradient-bottom-g = 16
|
||||
gradient-bottom-b = 12
|
||||
gradient-bottom-a = 70
|
||||
background-alpha = 0.42
|
||||
surface-alpha = 0.56
|
||||
frame-alpha = 0.78
|
||||
surface-inline-alpha = 0.58
|
||||
background-inline-alpha = 0.40
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Theme Visual Effects — Jade (veins of gold shifting through the stone)
|
||||
# Jade's signature is a slow jade→gold color-shifting border on every glass
|
||||
# panel + the active nav button — a vein of gold surfacing through nephrite.
|
||||
# It's drawn via AddRect so it hugs the real rounded corners (no polygonal
|
||||
# edge-trace). Sparse jade motes drift up the viewport. No other theme turns
|
||||
# gradient-border-panels on, so the panel-wide vein is Jade's own —
|
||||
# deliberately NOT Obsidian's specular glare.
|
||||
# ---------------------------------------------------------------------------
|
||||
[effects]
|
||||
hue-cycle-enabled = { size = 0.0 }
|
||||
rainbow-border-enabled = { size = 0.0 }
|
||||
|
||||
# No shimmer sweep — replaced by specular glare
|
||||
shimmer-enabled = { size = 0.0 }
|
||||
|
||||
positional-hue-enabled = { size = 0.0 }
|
||||
|
||||
glow-pulse-enabled = { size = 0.0 }
|
||||
|
||||
# Edge-trace OFF — its hand-walked perimeter chamfers rounded corners.
|
||||
# Jade's vein is the gradient-border below (corner-clean via AddRect).
|
||||
edge-trace-enabled = { size = 0.0 }
|
||||
edge-trace-speed = { size = 0.16 }
|
||||
edge-trace-length = { size = 0.34 }
|
||||
edge-trace-thickness = { size = 1.6 }
|
||||
edge-trace-alpha = { size = 0.55 }
|
||||
edge-trace-color = { color = "#C9A24E" }
|
||||
|
||||
# Specular glare OFF — that's Obsidian's signature; Jade shouldn't echo it.
|
||||
specular-glare-enabled = { size = 0.0 }
|
||||
specular-glare-speed = { size = 0.018 }
|
||||
specular-glare-intensity = { size = 0.008 }
|
||||
specular-glare-radius = { size = 0.65 }
|
||||
specular-glare-count = { size = 1.0 }
|
||||
specular-glare-color = { color = "rgba(150,220,180,1.0)" }
|
||||
|
||||
# HERO — vein of gold: a slow jade→gold color-shifting border on the active
|
||||
# nav button AND (via gradient-border-panels) every glass panel. Drawn with
|
||||
# AddRect so it follows the rounded corners exactly. Panels drift at a softer
|
||||
# alpha and position-phased offset, so a screenful reads like veins at
|
||||
# different depths rather than one synchronized pulse.
|
||||
gradient-border-enabled = { size = 1.0 }
|
||||
gradient-border-panels = { size = 1.0 }
|
||||
gradient-border-speed = { size = 0.10 }
|
||||
gradient-border-thickness = { size = 1.5 }
|
||||
gradient-border-alpha = { size = 0.55 }
|
||||
gradient-border-color-a = { color = "#7FD1B5" }
|
||||
gradient-border-color-b = { color = "#C9A24E" }
|
||||
|
||||
# Ambient jade motes — sparse, slow, cool green particles drifting up the
|
||||
# viewport (recolored ember-rise; a different mood from dragonx's fire embers).
|
||||
ember-rise-enabled = { size = 1.0 }
|
||||
ember-rise-count = { size = 5.0 }
|
||||
ember-rise-speed = { size = 0.18 }
|
||||
ember-rise-particle-size = { size = 1.4 }
|
||||
ember-rise-alpha = { size = 0.26 }
|
||||
ember-rise-color = { color = "#7FD1B5" }
|
||||
|
||||
# Shader-like viewport overlay — deep green stone atmosphere
|
||||
viewport-wash-enabled = { size = 1.0 }
|
||||
viewport-wash-alpha = { size = 0.05 }
|
||||
viewport-wash-tl = { color = "#12402E" }
|
||||
viewport-wash-tr = { color = "#0E3828" }
|
||||
viewport-wash-bl = { color = "#16442E" }
|
||||
viewport-wash-br = { color = "#1A4A34" }
|
||||
viewport-wash-rotate = { size = 0.015 }
|
||||
viewport-wash-pulse = { size = 0.0 }
|
||||
viewport-wash-pulse-depth = { size = 0.0 }
|
||||
|
||||
viewport-vignette-enabled = { size = 1.0 }
|
||||
viewport-vignette-color = { color = "#04140D" }
|
||||
viewport-vignette-radius = { size = 0.22 }
|
||||
viewport-vignette-alpha = { size = 0.15 }
|
||||
@@ -1,5 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
|
||||
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
|
||||
# present (Homebrew), and fail with a clear message otherwise.
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
|
||||
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
|
||||
done
|
||||
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
|
||||
echo " On macOS: brew install bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -67,25 +79,9 @@ Options:
|
||||
--backend-dir PATH SilentDragonXLite/lib source directory.
|
||||
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
|
||||
--out-dir PATH Output directory for copied artifact and metadata.
|
||||
--artifact PATH Inventory an existing artifact instead of building.
|
||||
--no-build Do not run cargo; requires --artifact.
|
||||
--reproducible Add deterministic Rust path remaps for clean builds.
|
||||
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
|
||||
--builder NAME Redacted builder/provenance label. Default: local.
|
||||
--signature-required Fail if verified signature metadata is not supplied.
|
||||
--signature-file PATH Existing sidecar signature file to record.
|
||||
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
|
||||
--signature-verification-tool T Verification tool and version used by the release builder.
|
||||
--signature-verification-command C
|
||||
Verification command already run by the release builder.
|
||||
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
|
||||
--signature-certificate-identity ID
|
||||
Reviewed certificate identity, when applicable.
|
||||
--signature-certificate-issuer I
|
||||
Reviewed certificate issuer, when applicable.
|
||||
--signature-transparency-log-url URL
|
||||
Transparency log entry, when applicable.
|
||||
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
|
||||
-j, --jobs N Cargo parallel jobs.
|
||||
--cargo-arg ARG Extra argument forwarded to cargo build.
|
||||
-h, --help Show this help.
|
||||
@@ -95,9 +91,13 @@ Outputs:
|
||||
<out>/<platform>/lite-backend-symbols.txt
|
||||
<out>/<platform>/lite-backend-artifact-manifest.json
|
||||
|
||||
The script captures symbols, checksums, and optional read-only signature
|
||||
verification metadata only. It does not load the library, resolve function
|
||||
pointers, call SDXL, sign, upload, or publish artifacts.
|
||||
The lite backend is always built from the vendored in-tree source
|
||||
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
|
||||
and self-attested signature metadata are NOT accepted (F15-1) — the previous
|
||||
scheme only recorded an unverified "verified" claim. The script captures the
|
||||
freshly-built artifact's symbols and checksum, and records build provenance.
|
||||
It does not load the library, resolve function pointers, call SDXL, sign,
|
||||
upload, or publish artifacts.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -166,15 +166,8 @@ while [[ $# -gt 0 ]]; do
|
||||
OUT_DIR="$(absolute_path "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--artifact)
|
||||
[[ $# -ge 2 ]] || die "--artifact requires a value"
|
||||
ARTIFACT_PATH="$(absolute_path "$2")"
|
||||
BUILD_ARTIFACT=false
|
||||
shift 2
|
||||
;;
|
||||
--no-build)
|
||||
BUILD_ARTIFACT=false
|
||||
shift
|
||||
--artifact|--no-build)
|
||||
die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
|
||||
;;
|
||||
--reproducible)
|
||||
REPRODUCIBLE=true
|
||||
@@ -191,54 +184,11 @@ while [[ $# -gt 0 ]]; do
|
||||
BUILDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-required)
|
||||
SIGNATURE_REQUIRED=true
|
||||
shift
|
||||
;;
|
||||
--signature-file|--signature-path)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
SIGNATURE_FILE="$(absolute_path "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--signature-format)
|
||||
[[ $# -ge 2 ]] || die "--signature-format requires a value"
|
||||
SIGNATURE_FORMAT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-verification-tool|--signature-tool)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
SIGNATURE_VERIFICATION_TOOL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-verification-command)
|
||||
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
|
||||
SIGNATURE_VERIFICATION_COMMAND="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-key-fingerprint)
|
||||
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
|
||||
SIGNATURE_KEY_FINGERPRINT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-certificate-identity)
|
||||
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
|
||||
SIGNATURE_CERTIFICATE_IDENTITY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-certificate-issuer)
|
||||
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
|
||||
SIGNATURE_CERTIFICATE_ISSUER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-transparency-log-url)
|
||||
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
|
||||
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-verified-sha256)
|
||||
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
|
||||
SIGNATURE_VERIFIED_SHA256="$2"
|
||||
shift 2
|
||||
--signature-required|--signature-file|--signature-path|--signature-format|\
|
||||
--signature-verification-tool|--signature-tool|--signature-verification-command|\
|
||||
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
|
||||
--signature-transparency-log-url|--signature-verified-sha256)
|
||||
die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
|
||||
;;
|
||||
-j|--jobs)
|
||||
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
||||
@@ -374,6 +324,9 @@ prepare_backend_source() {
|
||||
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
||||
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
|
||||
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
|
||||
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
|
||||
# "vendor" relative to the build root, so expose it inside the prepared root too.
|
||||
@@ -766,6 +719,7 @@ MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
|
||||
printf ' },\n'
|
||||
printf ' "provenance": {\n'
|
||||
printf ' "owner_ready": true,\n'
|
||||
printf ' "built_from_source": true,\n'
|
||||
printf ' "metadata_provided": true,\n'
|
||||
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
||||
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
||||
|
||||
@@ -24,18 +24,27 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for appimagetool
|
||||
# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a moving, unverified network download that runs on the release
|
||||
# builder; verify it or refuse to package.
|
||||
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
APPIMAGETOOL=""
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
else
|
||||
print_status "Downloading appimagetool..."
|
||||
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_status "Downloading appimagetool 1.9.0 (pinned)..."
|
||||
wget -q -O "$AT" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_error "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$AT"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$AT"
|
||||
fi
|
||||
APPIMAGETOOL="$AT"
|
||||
fi
|
||||
|
||||
print_status "Creating AppDir structure..."
|
||||
|
||||
@@ -256,8 +256,8 @@ HEADER_START
|
||||
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
||||
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
||||
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
|
||||
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
|
||||
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
|
||||
#
|
||||
# The wallet verifies a detached ed25519 signature over the 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.
|
||||
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
|
||||
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
|
||||
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
|
||||
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
|
||||
# checksum table in the release body, so `release` prints that table for you to paste in.
|
||||
#
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl 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).
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
|
||||
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
|
||||
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
|
||||
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
|
||||
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
|
||||
# # binary staged are skipped.
|
||||
#
|
||||
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
|
||||
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||
|
||||
@@ -27,6 +33,26 @@ command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25
|
||||
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
|
||||
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
||||
}
|
||||
|
||||
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
|
||||
sign_file() {
|
||||
local key="$1" f="$2" raw
|
||||
raw="$(mktemp)"
|
||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||
openssl base64 -A -in "$raw" > "$f.sig"
|
||||
printf '\n' >> "$f.sig"
|
||||
rm -f "$raw"
|
||||
}
|
||||
|
||||
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
|
||||
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
|
||||
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
|
||||
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
keygen)
|
||||
@@ -42,26 +68,90 @@ case "$cmd" in
|
||||
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"
|
||||
sign_file "$key" "$f"
|
||||
echo "signed: $f -> $f.sig"
|
||||
done
|
||||
echo "Upload each .sig as a release asset next to its archive."
|
||||
;;
|
||||
|
||||
release)
|
||||
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
|
||||
key="$1"; version="$2"; shift 2
|
||||
src="$PROJECT_ROOT/prebuilt-binaries"
|
||||
out="$PROJECT_ROOT/release/daemon"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
|
||||
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
[ -d "$src" ] || die "no such source dir: $src"
|
||||
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
|
||||
mkdir -p "$out"
|
||||
|
||||
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
|
||||
# then reject every signature made with it — only expected when deliberately rotating the key).
|
||||
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
|
||||
mine="$(pubkey_b64 "$key")"
|
||||
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
|
||||
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
|
||||
echo " signing key -> $mine" >&2
|
||||
echo " pinned key -> $pinned" >&2
|
||||
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
|
||||
echo >&2
|
||||
fi
|
||||
|
||||
made=0
|
||||
table=""
|
||||
for plat in linux mac win; do
|
||||
d="$src/$(plat_dir "$plat")"
|
||||
daemon="$d/$(plat_daemon "$plat")"
|
||||
if [ ! -f "$daemon" ]; then
|
||||
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
|
||||
continue
|
||||
fi
|
||||
archive="dragonx-$version-$(plat_token "$plat").zip"
|
||||
apath="$out/$archive"
|
||||
rm -f "$apath"
|
||||
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
|
||||
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
|
||||
files=()
|
||||
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
|
||||
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
|
||||
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
|
||||
sign_file "$key" "$apath"
|
||||
sum="$(sha256_of "$apath")"
|
||||
table+="| $archive | \`$sum\` |"$'\n'
|
||||
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
|
||||
made=$((made + 1))
|
||||
done
|
||||
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
|
||||
|
||||
echo
|
||||
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
|
||||
echo "| Archive | SHA-256 |"
|
||||
echo "|---|---|"
|
||||
printf '%s' "$table"
|
||||
echo
|
||||
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
|
||||
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
|
||||
;;
|
||||
|
||||
*)
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
|
||||
;;
|
||||
esac
|
||||
|
||||
90
setup.sh
90
setup.sh
@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
|
||||
libxkbcommon wayland libsodium curl
|
||||
autoconf automake libtool wget python xxd"
|
||||
|
||||
pkgs_core_macos="cmake python xxd"
|
||||
pkgs_core_macos="bash cmake python xxd"
|
||||
|
||||
# Windows cross-compile (from Linux)
|
||||
pkgs_win_debian="mingw-w64 zip"
|
||||
@@ -284,18 +284,27 @@ fi
|
||||
header "Windows Cross-Compile"
|
||||
|
||||
if $SETUP_WIN; then
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
fi
|
||||
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
|
||||
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
|
||||
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
|
||||
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
|
||||
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
|
||||
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
||||
ok "Windows cross-compile toolchain already present — skipping apt install"
|
||||
else
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
fi
|
||||
|
||||
# Set posix thread model if available
|
||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
# Set posix thread model if available
|
||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -391,11 +400,26 @@ elif $SETUP_SAPLING; then
|
||||
|
||||
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
||||
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
|
||||
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
|
||||
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
|
||||
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
|
||||
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
|
||||
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
|
||||
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
|
||||
ok "Downloaded sapling-spend.params"
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
||||
ok "Downloaded sapling-output.params"
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
|
||||
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-spend.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-spend.params"
|
||||
err "sapling-spend.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
|
||||
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-output.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-output.params"
|
||||
err "sapling-output.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
||||
@@ -684,11 +708,13 @@ if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||
fi
|
||||
|
||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||
header "xmrig-hac Mining Binary"
|
||||
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
|
||||
header "drg-xmrig Mining Binary"
|
||||
|
||||
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
|
||||
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
|
||||
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
|
||||
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||
# Only clean the binary for the platform(s) we are actually building,
|
||||
@@ -700,14 +726,14 @@ if ! $CHECK_ONLY; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper: clone xmrig-hac if not present
|
||||
# Helper: clone drg-xmrig if not present
|
||||
clone_xmrig_if_needed() {
|
||||
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||
info "Cloning xmrig-hac..."
|
||||
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
||||
info "Cloning drg-xmrig..."
|
||||
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
|
||||
else
|
||||
ok "xmrig-hac source already present"
|
||||
info "Pulling latest xmrig-hac..."
|
||||
ok "drg-xmrig source already present"
|
||||
info "Pulling latest drg-xmrig..."
|
||||
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||
fi
|
||||
}
|
||||
@@ -728,15 +754,15 @@ else
|
||||
rm -rf "$XMRIG_SRC/build"
|
||||
|
||||
# Build dependencies (libuv, hwloc, openssl)
|
||||
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
|
||||
info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
sh build_deps.sh
|
||||
)
|
||||
ok "xmrig-hac dependencies built"
|
||||
ok "drg-xmrig dependencies built"
|
||||
|
||||
# Build xmrig
|
||||
info "Building xmrig-hac (Linux)..."
|
||||
info "Building drg-xmrig (Linux)..."
|
||||
mkdir -p "$XMRIG_SRC/build"
|
||||
(
|
||||
cd "$XMRIG_SRC/build"
|
||||
@@ -753,7 +779,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig (Linux) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -777,7 +803,7 @@ else
|
||||
# Clean previous Windows build
|
||||
rm -rf "$XMRIG_SRC/build-windows"
|
||||
|
||||
info "Building xmrig-hac (Windows cross-compile)..."
|
||||
info "Building drg-xmrig (Windows cross-compile)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
bash build_windows.sh
|
||||
@@ -787,7 +813,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig.exe (Windows) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -797,7 +823,7 @@ fi
|
||||
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||
header "Binary Directories"
|
||||
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
|
||||
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||
if [[ -d "$dir" ]]; then
|
||||
# Count actual files (not .gitkeep)
|
||||
|
||||
270
src/app.cpp
270
src/app.cpp
@@ -73,6 +73,9 @@
|
||||
#include "util/text_format.h"
|
||||
#include "util/payment_uri.h"
|
||||
#include "util/texture_loader.h"
|
||||
#include "util/svg_texture.h"
|
||||
#include "ui/material/colors.h"
|
||||
#include "logo_dragonx_svg.h"
|
||||
#include "util/bootstrap.h"
|
||||
#include "util/secure_vault.h"
|
||||
#include "resources/embedded_resources.h"
|
||||
@@ -735,20 +738,40 @@ void App::update()
|
||||
// HushChat (lite): harvest chat memos from the refreshed transactions and thread them
|
||||
// (no-op when the feature is off or no identity; the store dedups across refreshes).
|
||||
ingestLiteChatMemos(liteModel);
|
||||
// Chat note-buffer: recompute the verified/maturing self-note estimates from the fresh model.
|
||||
refreshChatNoteBudget(liteModel);
|
||||
}
|
||||
// Deliver a completed async send/shield result to the waiting send_tab callback.
|
||||
// Deliver a completed async send/shield result. Route by the in-flight op that owns the single
|
||||
// broadcast channel: a user Send-tab send goes to lite_send_callback_; a chat send / contact
|
||||
// request resolves its echo (real status — not the old optimistic "Sent"); a note-buffer split
|
||||
// just logs. Because the controller runs one broadcast at a time, inflight_op_ correlates the
|
||||
// one global result with no txid matching.
|
||||
wallet::LiteBroadcastResult broadcast;
|
||||
if (lite_wallet_->takeBroadcastResult(broadcast)) {
|
||||
// Mirror failures into the lite Console (copyable) in addition to the toast the send UI
|
||||
// shows — transient toasts are easy to miss and impossible to copy.
|
||||
if (!broadcast.ok)
|
||||
wallet::liteLog("Send/shield failed: " + broadcast.error);
|
||||
if (lite_send_callback_) {
|
||||
lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : broadcast.error);
|
||||
lite_send_callback_ = nullptr;
|
||||
const LiteInflightOp finished = inflight_op_;
|
||||
inflight_op_ = LiteInflightOp{}; // channel is free again
|
||||
switch (finished.kind) {
|
||||
case LiteOpKind::ChatSend:
|
||||
case LiteOpKind::ContactRequest:
|
||||
onChatBroadcastResult(finished, broadcast.ok, broadcast.error);
|
||||
break;
|
||||
case LiteOpKind::Split:
|
||||
// Buffer split done; the next refresh re-counts the new (maturing) notes.
|
||||
break;
|
||||
case LiteOpKind::UserSend:
|
||||
case LiteOpKind::None:
|
||||
default:
|
||||
if (lite_send_callback_) {
|
||||
lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : broadcast.error);
|
||||
lite_send_callback_ = nullptr;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Startup lock screen: once the first refresh reveals the (auto-opened) wallet is
|
||||
// encrypted, prompt to unlock if it's locked. Soft by design — balances stay viewable via
|
||||
// viewing keys while locked; only spending needs the passphrase, so the user may dismiss
|
||||
@@ -765,6 +788,13 @@ void App::update()
|
||||
// it can act; compiled away when the feature is off (constexpr gate inside).
|
||||
maybeProvisionChatIdentity();
|
||||
|
||||
// Chat note-buffer coordinator (BOTH variants). Full-node only: refresh the per-note counts via a
|
||||
// rate-limited z_listunspent worker scan (lite gets its counts from the refresh model, above). Then
|
||||
// pump: drain queued chat sends against verified notes and build/refill the buffer during idle. Both
|
||||
// self-gate to no-ops unless chat is engaged; the pump serializes to one send outstanding at a time.
|
||||
refreshChatNoteBudgetNode();
|
||||
pumpChatNoteBuffer();
|
||||
|
||||
// One-time reminder to back up the wallet's seed phrase (mnemonic wallets only).
|
||||
maybeRemindSeedBackup();
|
||||
|
||||
@@ -1326,6 +1356,103 @@ void App::handleGlobalShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
// Load (and recolor per theme) the DragonX header logo texture. Called at the top of render() — BEFORE
|
||||
// the first-run-wizard / lock-screen paths — so every screen shows the mark. The SVG is recolored to the
|
||||
// theme accent, so it re-rasterizes whenever the accent or the dark↔light variant changes.
|
||||
void App::ensureLogoTexture()
|
||||
{
|
||||
const bool wantDark = ui::material::IsDarkTheme();
|
||||
const ImU32 logoAccent = ui::material::Primary();
|
||||
if (logo_loaded_ && wantDark == logo_is_dark_variant_ && logoAccent == logo_accent_)
|
||||
return; // already current
|
||||
logo_loaded_ = true;
|
||||
logo_is_dark_variant_ = wantDark;
|
||||
logo_accent_ = logoAccent;
|
||||
// The SVG's white highlight (detail) stays white on dark skins, but darkens to the theme's on-surface
|
||||
// colour on light skins so it doesn't wash out against a light card/background.
|
||||
const ImU32 detailCol = wantDark ? IM_COL32(255, 255, 255, 255) : ui::material::OnSurface();
|
||||
|
||||
// ":drgx:" custom chat emoji — themed to the accent like the logo (re-rasterized on theme change).
|
||||
if (drgx_emoji_tex_) util::DestroyTexture(drgx_emoji_tex_);
|
||||
drgx_emoji_tex_ = 0; drgx_emoji_w_ = 0; drgx_emoji_h_ = 0;
|
||||
util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 96, logoAccent, detailCol,
|
||||
&drgx_emoji_tex_, &drgx_emoji_w_, &drgx_emoji_h_);
|
||||
|
||||
if (logo_tex_) util::DestroyTexture(logo_tex_); // free the previous texture before replacing
|
||||
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
||||
|
||||
// Coin/currency icon (the big DragonX mark on the balance card) — the SAME themed SVG recolored to
|
||||
// the accent, rasterized here so it changes with the skin too (done first, since the header block
|
||||
// below early-returns on success). Falls back to the PNG coin icon if rasterization fails.
|
||||
if (coin_logo_tex_) util::DestroyTexture(coin_logo_tex_);
|
||||
coin_logo_tex_ = 0; coin_logo_w_ = 0; coin_logo_h_ = 0;
|
||||
coin_logo_loaded_ = true;
|
||||
if (!util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 128, logoAccent, detailCol,
|
||||
&coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
||||
auto coinElem = ui::schema::UI().drawElement("components.main-window", "coin-icon");
|
||||
auto cit = coinElem.extraColors.find("icon");
|
||||
std::string coinFile = (cit != coinElem.extraColors.end() && !cit->second.empty())
|
||||
? cit->second : "logos/logo_dragonx_128.png";
|
||||
std::string coinPath = util::getExecutableDirectory() + "/res/img/" + coinFile;
|
||||
std::error_code coinEc;
|
||||
if (!(std::filesystem::exists(coinPath, coinEc) &&
|
||||
util::LoadTextureFromFile(coinPath.c_str(), &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_))) {
|
||||
std::string coinBasename = std::filesystem::path(coinFile).filename().string();
|
||||
const auto* coinRes = resources::getEmbeddedResource(coinBasename);
|
||||
if (coinRes && coinRes->data && coinRes->size > 0)
|
||||
util::LoadTextureFromMemory(coinRes->data, coinRes->size, &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_);
|
||||
}
|
||||
}
|
||||
|
||||
// 0) DragonX mark — rasterize the embedded SVG recolored to the theme (body = accent, detail = white)
|
||||
// at ~2x the 128px viewBox for crisp downscaling. This is the branding on every skin; the per-skin
|
||||
// PNG path below is only a fallback if rasterization ever fails.
|
||||
if (util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 256, logoAccent,
|
||||
detailCol, &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Rendered DragonX SVG logo (%dx%d, accent %08X)\n", logo_w_, logo_h_, logoAccent);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Fallback — theme-override logo from the active skin
|
||||
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
||||
ui::schema::SkinManager::instance().activeSkinId());
|
||||
std::string logoPath;
|
||||
if (activeSkin && !activeSkin->logoPath.empty()) {
|
||||
logoPath = activeSkin->logoPath;
|
||||
} else {
|
||||
// 2) Read icon filename from ui.toml (dark/light variant)
|
||||
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
||||
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
||||
auto it = iconElem.extraColors.find(iconKey);
|
||||
std::string iconFile;
|
||||
if (it != iconElem.extraColors.end() && !it->second.empty())
|
||||
iconFile = it->second;
|
||||
else
|
||||
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
||||
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
||||
}
|
||||
// Only attempt the disk read when the file is actually present (dev build / theme drop-in). The
|
||||
// portable single-file build has no res/img/ beside it, so skip straight to the embedded copy.
|
||||
std::error_code logoEc;
|
||||
if (std::filesystem::exists(logoPath, logoEc) &&
|
||||
util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
||||
} else {
|
||||
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
||||
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
||||
if (!logoRes || !logoRes->data || logoRes->size == 0)
|
||||
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
||||
if (logoRes && logoRes->data && logoRes->size > 0) {
|
||||
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_))
|
||||
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
||||
else
|
||||
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::render()
|
||||
{
|
||||
// Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the
|
||||
@@ -1333,6 +1460,9 @@ void App::render()
|
||||
// (Pins current_page_ too, before the sidebar reads it further down.)
|
||||
updateScreenshotSweep();
|
||||
|
||||
// DragonX logo — load/recolor before the wizard/lock early-returns so every screen shows it.
|
||||
ensureLogoTexture();
|
||||
|
||||
// First-run wizard gate — blocks all normal UI
|
||||
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
||||
renderFirstRunWizard();
|
||||
@@ -1533,94 +1663,9 @@ void App::render()
|
||||
sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running;
|
||||
sbStatus.chatUnreadCount = chatUnreadCount(); // unread badge on the Chat nav item (Q1)
|
||||
|
||||
// Load logo texture lazily on first frame (or after theme change)
|
||||
// Also reload when dark↔light mode changes so the correct variant shows
|
||||
{
|
||||
bool wantDark = ui::material::IsDarkTheme();
|
||||
if (!logo_loaded_ || (wantDark != logo_is_dark_variant_)) {
|
||||
logo_loaded_ = true;
|
||||
logo_is_dark_variant_ = wantDark;
|
||||
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
||||
// (DragonX logo is loaded at the top of render() via ensureLogoTexture().)
|
||||
|
||||
// 1) Check for theme-override logo from active skin
|
||||
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
||||
ui::schema::SkinManager::instance().activeSkinId());
|
||||
std::string logoPath;
|
||||
if (activeSkin && !activeSkin->logoPath.empty()) {
|
||||
logoPath = activeSkin->logoPath;
|
||||
} else {
|
||||
// 2) Read icon filename from ui.toml (dark/light variant)
|
||||
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
||||
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
||||
auto it = iconElem.extraColors.find(iconKey);
|
||||
std::string iconFile;
|
||||
if (it != iconElem.extraColors.end() && !it->second.empty()) {
|
||||
iconFile = it->second;
|
||||
} else {
|
||||
// Fallback filenames
|
||||
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
||||
}
|
||||
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
||||
}
|
||||
// Only attempt the disk read when the file is actually present (dev build / theme drop-in).
|
||||
// The portable single-file build has no res/img/ beside it, so skip straight to the
|
||||
// embedded copy instead of logging a spurious "failed to read".
|
||||
std::error_code logoEc;
|
||||
if (std::filesystem::exists(logoPath, logoEc) &&
|
||||
util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
||||
} else {
|
||||
// Try embedded data fallback — use actual filename from path
|
||||
// so light/dark variants resolve correctly on Windows single-file
|
||||
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
||||
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
||||
if (!logoRes || !logoRes->data || logoRes->size == 0) {
|
||||
// Final fallback: try the default dark logo constant
|
||||
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
||||
}
|
||||
if (logoRes && logoRes->data && logoRes->size > 0) {
|
||||
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_)) {
|
||||
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load coin logo texture lazily (DragonX currency icon for balance tab)
|
||||
if (!coin_logo_loaded_) {
|
||||
coin_logo_loaded_ = true;
|
||||
coin_logo_tex_ = 0; coin_logo_w_ = 0; coin_logo_h_ = 0;
|
||||
|
||||
// Read coin icon filename from ui.toml
|
||||
auto coinElem = ui::schema::UI().drawElement("components.main-window", "coin-icon");
|
||||
auto cit = coinElem.extraColors.find("icon");
|
||||
std::string coinFile = (cit != coinElem.extraColors.end() && !cit->second.empty())
|
||||
? cit->second : "logos/logo_dragonx_128.png";
|
||||
std::string coinPath = util::getExecutableDirectory() + "/res/img/" + coinFile;
|
||||
std::error_code coinEc;
|
||||
if (std::filesystem::exists(coinPath, coinEc) &&
|
||||
util::LoadTextureFromFile(coinPath.c_str(), &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
||||
DEBUG_LOGF("Loaded coin logo from %s (%dx%d)\n", coinPath.c_str(), coin_logo_w_, coin_logo_h_);
|
||||
} else {
|
||||
// Try embedded resource fallback (Windows single-file distribution)
|
||||
std::string coinBasename = std::filesystem::path(coinFile).filename().string();
|
||||
const auto* coinRes = resources::getEmbeddedResource(coinBasename);
|
||||
if (coinRes && coinRes->data && coinRes->size > 0) {
|
||||
if (util::LoadTextureFromMemory(coinRes->data, coinRes->size, &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
||||
DEBUG_LOGF("Loaded coin logo from embedded: %s (%dx%d)\n", coinBasename.c_str(), coin_logo_w_, coin_logo_h_);
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Failed to decode embedded coin logo\n");
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Note: Coin logo not found at %s\n", coinPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
// (coin logo is loaded/themed at the top of render() via ensureLogoTexture().)
|
||||
|
||||
if (logo_tex_ != 0) {
|
||||
sbStatus.logoTexID = logo_tex_;
|
||||
@@ -2005,8 +2050,8 @@ void App::render()
|
||||
// Send confirm popup
|
||||
ui::RenderSendConfirmPopup(this);
|
||||
|
||||
// Console RPC Command Reference popup
|
||||
console_tab_.renderCommandsPopupModal();
|
||||
// Console command-reference popup (full-node RPC reference / lite backend verbs).
|
||||
console_tab_.renderCommandsPopupModal(console_exec_.get());
|
||||
|
||||
// Key export dialog (triggered from balance tab context menu)
|
||||
ui::KeyExportDialog::render(this);
|
||||
@@ -2324,17 +2369,32 @@ void App::renderStatusBar()
|
||||
// Connection / daemon status sits to the left of the version string
|
||||
// with a small gap.
|
||||
float gap = sbSectionGap;
|
||||
float occupiedX = versionX; // leftmost X used by the version + connection status so far
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
||||
float statusW = ImGui::CalcTextSize(connection_status_.c_str()).x;
|
||||
float statusX = versionX - statusW - gap;
|
||||
ImGui::SameLine(statusX);
|
||||
ImGui::TextDisabled("%s", connection_status_.c_str());
|
||||
occupiedX = statusX;
|
||||
} else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) {
|
||||
const char* errText = TR("sb_daemon_not_found");
|
||||
float statusW = ImGui::CalcTextSize(errText).x;
|
||||
float statusX = versionX - statusW - gap;
|
||||
ImGui::SameLine(statusX);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "%s", errText);
|
||||
occupiedX = statusX;
|
||||
}
|
||||
|
||||
// Chat note-buffer status — only while the Chat tab is active. Sits left of the version/connection
|
||||
// block. Surfaces the buffer filling/draining (and doubles as a diagnostic for send readiness).
|
||||
if (current_page_ == ui::NavPage::Chat) {
|
||||
const std::string cb = chatBufferStatusText();
|
||||
if (!cb.empty()) {
|
||||
float cbW = ImGui::CalcTextSize(cb.c_str()).x;
|
||||
float cbX = occupiedX - cbW - gap;
|
||||
ImGui::SameLine(cbX);
|
||||
ImGui::TextUnformatted(cb.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Version always at far right
|
||||
@@ -2387,12 +2447,15 @@ void App::reloadThemeImages(const std::string& bgPath, const std::string& logoPa
|
||||
gradient_tex_ = 0;
|
||||
}
|
||||
|
||||
// Reset logo loaded flags — will reload on next render frame
|
||||
// Reset logo loaded flags — will reload on next render frame (ensureLogoTexture re-rasterizes the
|
||||
// SVG for the new theme). Free the current texture first so a theme switch doesn't leak it.
|
||||
if (logo_tex_) util::DestroyTexture(logo_tex_);
|
||||
logo_loaded_ = false;
|
||||
logo_tex_ = 0;
|
||||
logo_w_ = 0;
|
||||
logo_h_ = 0;
|
||||
coin_logo_loaded_ = false;
|
||||
if (coin_logo_tex_) util::DestroyTexture(coin_logo_tex_);
|
||||
coin_logo_tex_ = 0;
|
||||
coin_logo_w_ = 0;
|
||||
coin_logo_h_ = 0;
|
||||
@@ -5257,6 +5320,11 @@ void App::renderLoadingOverlay(float contentH)
|
||||
|
||||
void App::shutdown()
|
||||
{
|
||||
// Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer
|
||||
// never fires if the user quits sooner, which would otherwise leave a key/seed resident.
|
||||
// (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().)
|
||||
clearSecretClipboardIfArmed();
|
||||
|
||||
// Clean up bootstrap if running
|
||||
if (bootstrap_) {
|
||||
bootstrap_->cancel();
|
||||
@@ -5459,10 +5527,9 @@ void App::copySecretToClipboard(const std::string& secret)
|
||||
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
|
||||
}
|
||||
|
||||
void App::pumpSecretClipboardClear()
|
||||
void App::clearSecretClipboardIfArmed()
|
||||
{
|
||||
if (clipboard_clear_deadline_ <= 0.0) return;
|
||||
if (ImGui::GetTime() < clipboard_clear_deadline_) return;
|
||||
if (clipboard_secret_hash_ == 0) return;
|
||||
// Only clear if the clipboard STILL holds our secret (the user may have copied something else).
|
||||
if (const char* cb = ImGui::GetClipboardText()) {
|
||||
std::uint64_t h = 1469598103934665603ULL;
|
||||
@@ -5473,6 +5540,13 @@ void App::pumpSecretClipboardClear()
|
||||
clipboard_secret_hash_ = 0;
|
||||
}
|
||||
|
||||
void App::pumpSecretClipboardClear()
|
||||
{
|
||||
if (clipboard_clear_deadline_ <= 0.0) return;
|
||||
if (ImGui::GetTime() < clipboard_clear_deadline_) return;
|
||||
clearSecretClipboardIfArmed();
|
||||
}
|
||||
|
||||
void App::maybeFinishTransactionSendProgress()
|
||||
{
|
||||
using Job = services::NetworkRefreshService::Job;
|
||||
|
||||
86
src/app.h
86
src/app.h
@@ -13,6 +13,7 @@
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <deque>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
@@ -478,6 +479,10 @@ public:
|
||||
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
||||
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
||||
|
||||
// DragonX custom chat emoji (the ":drgx:" shortcode) — the mark recolored to the theme accent (like
|
||||
// the logo), re-rasterized on theme change. Used by the emoji picker tile + inline in chat bubbles.
|
||||
ImTextureID getDrgxEmojiTexture() const { return drgx_emoji_tex_; }
|
||||
|
||||
/**
|
||||
* @brief Reload theme images (background gradient + logo) from new paths
|
||||
* @param bgPath Path to background image override (empty = use default)
|
||||
@@ -485,6 +490,10 @@ public:
|
||||
*/
|
||||
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
||||
|
||||
// Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
|
||||
// the top of render() so the wizard, lock screen, and main header all show it.
|
||||
void ensureLogoTexture();
|
||||
|
||||
// Wizard / first-run
|
||||
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
||||
bool isFirstRun() const;
|
||||
@@ -564,6 +573,9 @@ public:
|
||||
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||
void copySecretToClipboard(const std::string& secret);
|
||||
void pumpSecretClipboardClear();
|
||||
// Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
|
||||
// Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
|
||||
void clearSecretClipboardIfArmed();
|
||||
bool isTransactionRefreshInProgress() const {
|
||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||
}
|
||||
@@ -605,10 +617,13 @@ private:
|
||||
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
|
||||
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
|
||||
// retry of a retry is reported as a real error.
|
||||
// background=true (autonomous chat sends / note-buffer splits): keep the single-flight + opid
|
||||
// accounting but DON'T raise the global "transaction in progress" UI (status is on the chat message).
|
||||
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||
const std::string& memo, const nlohmann::json& recipients,
|
||||
const char* traceLabel, bool markFeeGapRetry,
|
||||
std::function<void(bool, const std::string&)> callback);
|
||||
std::function<void(bool, const std::string&)> callback,
|
||||
bool background = false);
|
||||
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||
const std::string& txid);
|
||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||
@@ -695,6 +710,72 @@ private:
|
||||
// thread is viewed (markChatConversationSeen); wiped in resetChatSession so unread doesn't leak across
|
||||
// wallets. In-memory only (resets on app restart).
|
||||
std::map<std::string, std::int64_t> chat_seen_watermark_;
|
||||
|
||||
// ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
|
||||
// Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
|
||||
// it's spendable again (lite: backend ANCHOR_OFFSET+1 = 5; full node: z_sendmany minconf = 1), so
|
||||
// rapid sends run out of verified funds. We keep a buffer of ~kChatBufferTarget small self-notes so a
|
||||
// burst of messages each spends a separate verified note, refilled from change + background self-
|
||||
// splits. Chat sends, self-splits and user sends share the wallet's single send channel; inflight_op_
|
||||
// + (lite) lite_send_callback_ / (full node) send_submissions_in_flight_+pending_opids_ serialize them
|
||||
// so exactly one send is ever outstanding. Implemented in app_network.cpp; pumped from update().
|
||||
enum class LiteOpKind { None, ChatSend, ContactRequest, Split, UserSend };
|
||||
struct LiteInflightOp {
|
||||
LiteOpKind kind = LiteOpKind::None;
|
||||
std::string echoLocalId; // ChatSend/ContactRequest: the echo to resolve when it completes
|
||||
int sessionGen = 0; // chat_session_generation_ snapshot at submit (stale-guard)
|
||||
double submittedAt = 0.0;
|
||||
};
|
||||
struct QueuedChatOp {
|
||||
LiteOpKind kind = LiteOpKind::ChatSend;
|
||||
chat::OutgoingChatMemos memos; // kept so a transient-funds retry re-broadcasts, never recomposes
|
||||
std::string echoLocalId;
|
||||
int sessionGen = 0;
|
||||
int retries = 0;
|
||||
};
|
||||
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
|
||||
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
|
||||
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
|
||||
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
|
||||
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
|
||||
int chat_verified_note_budget_ = 0;
|
||||
int chat_pipeline_note_count_ = 0; // verified + maturing self-notes (drives shouldSplit)
|
||||
std::uint64_t chat_verified_shielded_zat_ = 0; // verified shielded balance (split affordability)
|
||||
bool chat_note_model_seen_ = false; // saw a refresh/scan carrying per-note visibility
|
||||
// Single-split-in-flight guard: a self-split's OUTPUT notes are invisible until mined (~1 block), far
|
||||
// longer than any wall-clock cooldown — so we permit only ONE outstanding split and clear the flag when
|
||||
// the pipeline recovers (outputs mined) or a watchdog expires (a split that never mines mustn't wedge
|
||||
// refill forever). Prevents runaway splitting that would drain balance into fees.
|
||||
bool chat_split_outstanding_ = false;
|
||||
double chat_split_submitted_at_ = 0.0; // ImGui time the outstanding split was submitted (watchdog)
|
||||
// Full-node only: a coordinator-owned z_listunspent worker scan feeds the per-note counts (the shared
|
||||
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
|
||||
bool chat_note_scan_in_flight_ = false;
|
||||
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
|
||||
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
|
||||
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
|
||||
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
|
||||
std::int64_t chat_last_chain_height_ = 0;
|
||||
int chat_fast_scan_last_seen_ = -1; // dedup: last memo-note count logged by the 0-conf scan
|
||||
|
||||
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
||||
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
||||
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
|
||||
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
||||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
bool shouldSplitChatBuffer();
|
||||
bool broadcastSelfSplitLite(int noteCount); // lite: self-send minting noteCount reply-address notes
|
||||
bool broadcastSelfSplitNode(int noteCount); // full node: z_sendmany self-send minting noteCount notes
|
||||
void enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);
|
||||
void onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error);
|
||||
static bool isTransientVerifiedFundsError(const std::string& error);
|
||||
int chatConfsRequired() const; // verified-note confs threshold: 5 (lite) / 1 (full node)
|
||||
public:
|
||||
// Status-bar summary of the chat note buffer (empty when not applicable). Shown while the Chat tab is
|
||||
// active so the buffer's state (ready / building / sending) is visible.
|
||||
std::string chatBufferStatusText();
|
||||
private:
|
||||
public:
|
||||
// Total unread incoming chat messages across all conversations (for the sidebar badge). 0 when the
|
||||
// feature is off / no identity.
|
||||
@@ -1010,6 +1091,9 @@ private:
|
||||
int logo_h_ = 0;
|
||||
bool logo_loaded_ = false;
|
||||
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
||||
ImU32 logo_accent_ = 0; // theme accent the SVG logo was last rasterized with (re-render on change)
|
||||
ImTextureID drgx_emoji_tex_ = 0; // ":drgx:" custom chat emoji (themed to the accent, like the logo)
|
||||
int drgx_emoji_w_ = 0, drgx_emoji_h_ = 0;
|
||||
|
||||
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
||||
ImTextureID coin_logo_tex_ = 0;
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "config/settings.h"
|
||||
#include "wallet/lite_wallet_controller.h" // lite send/new-address routing
|
||||
#include "wallet/lite_wallet_state_mapper.h" // LiteWalletAppRefreshModel for lite chat harvest
|
||||
#include "wallet/lite_diagnostics.h" // liteLog — chat note-buffer coordinator diagnostics
|
||||
#include "config/version.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
@@ -629,6 +630,11 @@ void App::onDisconnected(const std::string& reason)
|
||||
consecutive_core_failures_ = 0;
|
||||
send_progress_active_ = false;
|
||||
send_submissions_in_flight_ = 0;
|
||||
// Release the chat note-buffer's single-split guard on a plain disconnect (which doesn't run
|
||||
// resetChatSession) so a re-split isn't suppressed until the 1200s watchdog after reconnect.
|
||||
chat_split_outstanding_ = false;
|
||||
chat_split_submitted_at_ = 0.0;
|
||||
inflight_op_ = LiteInflightOp{}; // the in-flight chat send's callback was just fired above (Failed)
|
||||
network_refresh_.resetJobs();
|
||||
rescan_status_poll_in_progress_ = false;
|
||||
opid_poll_in_progress_ = false;
|
||||
@@ -2756,6 +2762,25 @@ void App::provisionChatIdentityFromSecret(std::string secret)
|
||||
// local — peers only ever exchange public keys).
|
||||
void App::resetChatSession()
|
||||
{
|
||||
// Lite send channel — reset UNCONDITIONALLY (independent of the chat feature flag; user sends set
|
||||
// inflight_op_/lite_send_callback_ even with chat off). Clear the note-buffer coordinator so wallet A's
|
||||
// queued sends can't drain under wallet B, and DON'T orphan a user Send callback across a controller
|
||||
// rebuild: rebuildLiteWallet(force) destroys the controller holding the in-flight send, so its result
|
||||
// slot dies with it and the callback would hang the Send dialog forever. Fire the orphaned callback on a
|
||||
// moved-out copy AFTER the channel state is reset, so a re-entrant sendTransaction stays clean.
|
||||
auto orphanedSend = std::move(lite_send_callback_);
|
||||
lite_send_callback_ = nullptr;
|
||||
chat_send_queue_.clear();
|
||||
inflight_op_ = LiteInflightOp{};
|
||||
chat_verified_note_budget_ = 0;
|
||||
chat_pipeline_note_count_ = 0;
|
||||
chat_verified_shielded_zat_ = 0;
|
||||
chat_note_model_seen_ = false;
|
||||
chat_split_outstanding_ = false;
|
||||
chat_split_submitted_at_ = 0.0;
|
||||
chat_note_scan_in_flight_ = false; // a stale in-flight note scan is dropped by its scanGen guard
|
||||
if (orphanedSend) orphanedSend(false, "Wallet/server changed — send aborted");
|
||||
|
||||
if (!chat::hushChatFeatureEnabledAtBuild()) return; // constexpr — folds away in OFF builds
|
||||
// Drop identity + decrypted plaintext in RAM, lock the seed-encrypted DB, re-arm provisioning.
|
||||
chat_service_.clearIdentity();
|
||||
@@ -2955,18 +2980,19 @@ bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::st
|
||||
const auto sessionGen = chat_session_generation_;
|
||||
|
||||
if (lite_wallet_) {
|
||||
const bool ok = broadcastChatMemosLite(memos);
|
||||
// The lite backend has no per-send completion callback here; resolve optimistically on a
|
||||
// successful queue (its own broadcast log surfaces a later failure).
|
||||
if (ok && sessionGen == chat_session_generation_)
|
||||
chat_service_.resolveOutgoing(echoLocalId, chat::ChatDelivery::Sent);
|
||||
return ok;
|
||||
// Lite chat is owned by the note-buffer coordinator (enqueue + drain + resolve from the real
|
||||
// broadcast result). Route any caller here through the queue rather than resolving optimistically
|
||||
// (the old code marked a merely-queued send "Sent", masking real failures). Kept for safety —
|
||||
// the send/Retry paths call enqueueChatSend directly and don't hit this branch.
|
||||
(void)sessionGen;
|
||||
enqueueChatSend(LiteOpKind::ChatSend, memos, echoLocalId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
ui::Notifications::instance().error(TR("chat_toast_not_connected"));
|
||||
return false;
|
||||
}
|
||||
// Coordinator-owned primitive (pumpChatNoteBuffer): the pump already gates on connected/unlocked and a
|
||||
// verified-note budget, so these refusals are belt-and-suspenders. Return false SILENTLY (no per-frame
|
||||
// toast) — the pump leaves the message queued ("Sending") and retries when the guard clears.
|
||||
if (!state_.connected || !rpc_ || !worker_) return false;
|
||||
|
||||
// Chat moves 0 value, and dragonxd REJECTS a 0-value tx whose fee exceeds the default miners fee
|
||||
// (0.0001) — so pin chat to exactly kChatMinFeeDrgx, independent of the user's default-fee setting
|
||||
@@ -2976,10 +3002,7 @@ bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::st
|
||||
// Pay from a z-address that can actually cover the fee. The memo still advertises the identity
|
||||
// reply address, so the peer replies to the right place regardless of which note paid.
|
||||
const std::string from = chatPayFromZaddr(fee);
|
||||
if (from.empty()) {
|
||||
ui::Notifications::instance().error(TR("chat_toast_need_funds"));
|
||||
return false;
|
||||
}
|
||||
if (from.empty()) return false; // no fundable note yet — pump keeps it queued, retries after a scan
|
||||
|
||||
// Full-node: each memo needs the daemon's "utf8:" prefix (raw JSON/hex is otherwise rejected).
|
||||
const auto outputs = chat::chatSendOutputs(memos, /*utf8Prefix=*/true);
|
||||
@@ -2994,14 +3017,20 @@ bool App::broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::st
|
||||
|
||||
// markFeeGapRetry=true is deliberate: it suppresses the fee-gap auto-retry, which rebuilds a
|
||||
// SINGLE-recipient tx from the scalar to/amount/memo and would drop the second (payload) output.
|
||||
// The callback flips the echo to Sent/Failed once the async op resolves — real delivery status.
|
||||
// This is now the note-buffer coordinator's full-node submit primitive (called only from the pump).
|
||||
// The terminal callback (fired once by the opid poller) releases the send channel and routes the
|
||||
// result through onChatBroadcastResult, so a transient "Insufficient shielded funds" requeues instead
|
||||
// of hard-failing. The sessionGen guard drops a stale wallet's echo AND protects the inflight_op_
|
||||
// clear from a callback landing after a wallet switch (which would otherwise clear a NEW op's token).
|
||||
submitZSendMany(from, memos.recipientZaddr, 0.0, fee, /*memo*/"", recipients,
|
||||
"HushChat / broadcast", /*markFeeGapRetry*/ true,
|
||||
[this, echoLocalId, sessionGen](bool ok, const std::string& /*result*/) {
|
||||
if (sessionGen != chat_session_generation_) return; // wallet locked/switched — stale
|
||||
chat_service_.resolveOutgoing(echoLocalId,
|
||||
ok ? chat::ChatDelivery::Sent : chat::ChatDelivery::Failed);
|
||||
});
|
||||
[this, echoLocalId, sessionGen](bool ok, const std::string& result) {
|
||||
if (sessionGen != chat_session_generation_) return; // stale wallet — don't touch new session
|
||||
inflight_op_ = LiteInflightOp{}; // release the channel for this session
|
||||
LiteInflightOp op; op.echoLocalId = echoLocalId; op.sessionGen = sessionGen;
|
||||
onChatBroadcastResult(op, ok, result);
|
||||
},
|
||||
/*background*/ true); // status is shown on the chat message, not the global send UI
|
||||
return true; // submitted (async build/broadcast; the callback resolves the final status)
|
||||
}
|
||||
|
||||
@@ -3021,11 +3050,9 @@ bool App::broadcastChatMemosLite(const chat::OutgoingChatMemos& memos)
|
||||
recipient.memo = out.memo;
|
||||
req.recipients.push_back(std::move(recipient));
|
||||
}
|
||||
if (!lite_wallet_->sendTransaction(req)) {
|
||||
ui::Notifications::instance().error(TR("chat_toast_lite_busy"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// No busy toast here: the coordinator (pumpChatNoteBuffer) owns this call, retries next frame when the
|
||||
// single broadcast channel frees, and surfaces status on the message itself.
|
||||
return lite_wallet_->sendTransaction(req);
|
||||
}
|
||||
|
||||
void App::sendChatMessage(const std::string& conversationId, const std::string& text)
|
||||
@@ -3073,10 +3100,9 @@ void App::sendChatMessage(const std::string& conversationId, const std::string&
|
||||
echo.payload_position = 0;
|
||||
echo.delivery = chat::ChatDelivery::Sending; // shows immediately; the broadcast callback resolves it
|
||||
chat_service_.recordOutgoingPending(echo); // in-memory now; persisted with the final status
|
||||
// A synchronous refusal (not connected / no funds) resolves to Failed right away so the Retry
|
||||
// affordance appears; otherwise the async callback flips it to Sent/Failed.
|
||||
if (!broadcastChatMemos(memos, echo.txid))
|
||||
chat_service_.resolveOutgoing(echo.txid, chat::ChatDelivery::Failed);
|
||||
// Both variants: hand to the note-buffer coordinator, which drains the queue against verified notes
|
||||
// (building/refilling the buffer as needed) and resolves the echo Sent/Failed from the REAL result.
|
||||
enqueueChatSend(LiteOpKind::ChatSend, memos, echo.txid);
|
||||
}
|
||||
|
||||
void App::startChatConversation(const std::string& peerZaddr, const std::string& text)
|
||||
@@ -3116,10 +3142,369 @@ void App::sendContactRequestForCid(const std::string& cid, const std::string& pe
|
||||
echo.payload_position = 0;
|
||||
echo.delivery = chat::ChatDelivery::Sending;
|
||||
chat_service_.recordOutgoingPending(echo);
|
||||
if (broadcastChatMemos(memos, echo.txid))
|
||||
ui::Notifications::instance().success(TR("chat_toast_request_queued"));
|
||||
else
|
||||
chat_service_.resolveOutgoing(echo.txid, chat::ChatDelivery::Failed);
|
||||
// Both variants: enqueue through the coordinator (resolves the echo from the real result).
|
||||
enqueueChatSend(LiteOpKind::ContactRequest, memos, echo.txid);
|
||||
ui::Notifications::instance().success(TR("chat_toast_request_queued"));
|
||||
}
|
||||
|
||||
// ── Chat note-buffer coordinator (lite only) ─────────────────────────────────────────────────────
|
||||
// Design in app.h. Keeps ~kChatBufferTarget verified self-notes so a burst of messages each spends a
|
||||
// separate note; refilled from change + background self-splits. Shares the controller's single broadcast
|
||||
// channel with user sends (inflight_op_ tracks ownership). Pumped from App::update().
|
||||
namespace {
|
||||
constexpr int kChatBufferTarget = 10; // verified self-notes we aim to keep
|
||||
constexpr int kChatRefillTrigger = 4; // (re)build only when the maturing pipeline drops below this
|
||||
// A note is spendable when its witness count > ANCHOR_OFFSET, i.e. block depth >= ANCHOR_OFFSET+1 = 5
|
||||
// (backend data.rs:543 needs anchor_offset+1 witnesses; ANCHOR_OFFSET=4 in lib.rs:25). Depth 4 is NOT yet
|
||||
// spendable — counting it there wastes a proving round-trip on a guaranteed "insufficient verified funds".
|
||||
constexpr int kChatConfsRequired = 5;
|
||||
constexpr double kChatNoteSizeDrgx = 0.001; // each buffer note (change after a send stays >= fee)
|
||||
constexpr double kChatSplitWatchdogSecs = 1200.0; // 20 min: clear a stuck split flag if it never mines (expiry/reorg)
|
||||
constexpr int kMaxChatSendRetries = 20; // transient-funds requeues before giving up
|
||||
inline std::uint64_t chatDrgxToZat(double drgx) {
|
||||
return static_cast<std::uint64_t>(std::llround(drgx * 100000000.0));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Confirmations a self-note needs before it's spendable: lite requires ANCHOR_OFFSET+1 witnesses (=5,
|
||||
// data.rs:543); the full node's chat z_sendmany uses minconf=1 (app_network.cpp z_sendmany call).
|
||||
int App::chatConfsRequired() const { return lite_wallet_ ? kChatConfsRequired : 1; }
|
||||
|
||||
// Status-bar summary of the chat note buffer (shown while the Chat tab is active). Empty unless chat has
|
||||
// an identity. Surfaces the buffer state so the user can see it filling / draining, and it doubles as a
|
||||
// diagnostic: "…" = no note data yet; "0/10 ready" = counted but no verified notes; "N/10 ready" = armed.
|
||||
std::string App::chatBufferStatusText() {
|
||||
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return "";
|
||||
const int queued = static_cast<int>(chat_send_queue_.size());
|
||||
const int verified = std::max(0, std::min(chat_verified_note_budget_, kChatBufferTarget));
|
||||
const int target = static_cast<int>(kChatBufferTarget);
|
||||
char buf[128];
|
||||
if (queued > 0) {
|
||||
snprintf(buf, sizeof(buf), TR(queued == 1 ? "chat_buffer_sending_one" : "chat_buffer_sending"), queued);
|
||||
return buf;
|
||||
}
|
||||
if (chat_split_outstanding_) {
|
||||
snprintf(buf, sizeof(buf), TR("chat_buffer_preparing"), verified, target);
|
||||
return buf;
|
||||
}
|
||||
if (!chat_note_model_seen_)
|
||||
return TR("chat_buffer_loading");
|
||||
snprintf(buf, sizeof(buf), TR("chat_buffer_ready"), verified, target);
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool App::isTransientVerifiedFundsError(const std::string& error) {
|
||||
// Both variants' "only unconfirmed change is short" phrasing, treated as transient (retry after the
|
||||
// buffer matures): lite = "Insufficient verified funds" (lightwallet.rs:2402); full node =
|
||||
// "Insufficient shielded funds" (asyncrpcoperation_sendmany, parsed at parseInsufficientShielded).
|
||||
return error.find("Insufficient verified funds") != std::string::npos
|
||||
|| error.find("Insufficient shielded funds") != std::string::npos;
|
||||
}
|
||||
|
||||
// Verified notes = ANY spendable shielded note (>= a fee) with >= kChatConfsRequired confirmations — the
|
||||
// ones a chat send can spend RIGHT NOW. We count notes at ALL addresses, not just the reply address,
|
||||
// because a chat send pays via chatPayFromZaddr (any funded z-addr) — gating on reply-address-only notes
|
||||
// would queue a sendable message whenever funds sit elsewhere. NB: the model's per-note `spendable` flag
|
||||
// means "unspent", NOT verified (lite_result_parsers.cpp recomputes it), so we derive it from depth here.
|
||||
int App::verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model) {
|
||||
if (!model.hasSpendableOutputs) return 0;
|
||||
// Use the cached tip (refreshChatNoteBudget updates it), not this model's — a notes-carrying model may
|
||||
// not carry sync status that cycle, which would otherwise zero the whole count.
|
||||
const std::int64_t tip = chat_last_chain_height_;
|
||||
if (tip <= 0) return 0;
|
||||
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
int n = 0;
|
||||
for (const auto& o : model.spendableOutputs) {
|
||||
if (o.kind != wallet::LiteSpendableOutputKind::UnspentNote) continue; // shielded notes only
|
||||
if (o.spent || o.unconfirmedSpent || o.pending) continue;
|
||||
if (o.valueZatoshis < minVal) continue; // skip dust below a fee
|
||||
if (!o.createdInBlock.has_value()) continue; // unknown depth -> not verified
|
||||
if (tip - *o.createdInBlock + 1 >= chatConfsRequired()) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Notes that are verified OR still maturing (any unspent shielded note of usable size) — i.e. notes that
|
||||
// will become spendable WITHOUT another split. Drives the refill trigger so we don't over-split while
|
||||
// change is confirming or when the wallet already holds enough notes anywhere.
|
||||
int App::pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model) {
|
||||
if (!model.hasSpendableOutputs) return 0;
|
||||
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
int n = 0;
|
||||
for (const auto& o : model.spendableOutputs) {
|
||||
if (o.kind != wallet::LiteSpendableOutputKind::UnspentNote) continue;
|
||||
if (o.spent || o.unconfirmedSpent) continue; // already consumed
|
||||
if (o.valueZatoshis < minVal) continue;
|
||||
++n; // verified or maturing (any addr)
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Recompute the cached note estimates from a fresh refresh model (called from update() when one arrives).
|
||||
void App::refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model) {
|
||||
if (!lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild()) return;
|
||||
// Cache the chain tip from whatever height source THIS model carries (sync status or chain info) — a
|
||||
// model with notes may lack sync that cycle, so verifiedSelfNoteCount reads the cache below.
|
||||
if (model.hasSyncStatus && model.sync.chainHeight > 0)
|
||||
chat_last_chain_height_ = static_cast<std::int64_t>(model.sync.chainHeight);
|
||||
else if (model.hasChainInfo && model.chain.latestBlockHeight.has_value() &&
|
||||
*model.chain.latestBlockHeight > 0)
|
||||
chat_last_chain_height_ = static_cast<std::int64_t>(*model.chain.latestBlockHeight);
|
||||
if (model.hasSpendableOutputs) {
|
||||
chat_note_model_seen_ = true;
|
||||
chat_verified_note_budget_ = verifiedSelfNoteCount(model);
|
||||
chat_pipeline_note_count_ = pipelineSelfNoteCount(model);
|
||||
}
|
||||
if (model.hasBalance)
|
||||
chat_verified_shielded_zat_ = model.balance.verifiedShieldedZatoshis;
|
||||
// Release the single-split guard once the outstanding split's outputs are mined+visible (pipeline
|
||||
// recovered) — or once a watchdog window elapses, so a split that never mines can't wedge refill.
|
||||
if (chat_split_outstanding_ &&
|
||||
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
|
||||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
|
||||
chat_split_outstanding_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Full-node twin of refreshChatNoteBudget: the shared balance poll discards per-note data, so run our own
|
||||
// rate-limited z_listunspent worker scan (mirrors fastScanChatMemos) to count verified/maturing spendable
|
||||
// notes. z_listunspent returns only the wallet's OWN notes, and a chat send pays from any of them
|
||||
// (chatPayFromZaddr), so we count ALL addresses — gating on the reply address only would queue a sendable
|
||||
// message when funds sit elsewhere. minconf=0 exposes maturing change so the pipeline count doesn't over-
|
||||
// split. On an older daemon that rejects z_listunspent it leaves chat_note_model_seen_ false → the pump
|
||||
// won't drain a phantom budget or split (graceful degradation to naive one-at-a-time sends).
|
||||
void App::refreshChatNoteBudgetNode() {
|
||||
if (lite_wallet_ || !chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
|
||||
if (!state_.connected || !rpc_ || !worker_ || state_.isLocked()) return;
|
||||
if (chat_note_scan_in_flight_) return; // one scan at a time
|
||||
const double now = ImGui::GetTime();
|
||||
if (now - chat_note_scan_last_ < 4.0) return; // rate limit; self-throttles via the in-flight flag too
|
||||
|
||||
chat_note_scan_last_ = now;
|
||||
chat_note_scan_in_flight_ = true;
|
||||
const int scanGen = chat_session_generation_;
|
||||
const int confsNeeded = chatConfsRequired(); // == 1 for full node
|
||||
const std::uint64_t minVal = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
worker_->post([this, scanGen, confsNeeded, minVal]() -> rpc::RPCWorker::MainCb {
|
||||
int verified = 0, pipeline = 0;
|
||||
std::uint64_t verifiedZat = 0;
|
||||
bool ok = false;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("HushChat / note-buffer scan");
|
||||
nlohmann::json notes = rpc_->call("z_listunspent", nlohmann::json::array({0})); // 0 = include maturing
|
||||
if (notes.is_array()) {
|
||||
ok = true;
|
||||
for (const auto& nz : notes) {
|
||||
if (!nz.is_object()) continue;
|
||||
if (nz.value("locked", false)) continue; // tied up by an in-flight send
|
||||
const std::uint64_t amtZat = chatDrgxToZat(nz.value("amount", 0.0));
|
||||
if (amtZat < minVal) continue; // skip dust below a fee
|
||||
// rawconfirmations is the TRUE depth; `confirmations` is dPoW-clamped to 1 and understates it.
|
||||
const int confs = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
|
||||
? nz["rawconfirmations"].get<int>()
|
||||
: nz.value("confirmations", 0);
|
||||
++pipeline; // unspent self-note (verified or maturing)
|
||||
if (confs >= confsNeeded) { ++verified; verifiedZat += amtZat; }
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {}
|
||||
return [this, scanGen, ok, verified, pipeline, verifiedZat]() {
|
||||
if (scanGen != chat_session_generation_) return; // wallet switched — drop (reset cleared the flag)
|
||||
chat_note_scan_in_flight_ = false;
|
||||
if (!ok) return; // z_listunspent unavailable — leave caches as-is
|
||||
chat_note_model_seen_ = true;
|
||||
chat_verified_note_budget_ = verified;
|
||||
chat_pipeline_note_count_ = pipeline;
|
||||
chat_verified_shielded_zat_ = verifiedZat;
|
||||
if (chat_split_outstanding_ &&
|
||||
(chat_pipeline_note_count_ >= kChatRefillTrigger ||
|
||||
ImGui::GetTime() - chat_split_submitted_at_ > kChatSplitWatchdogSecs)) {
|
||||
chat_split_outstanding_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId) {
|
||||
QueuedChatOp op;
|
||||
op.kind = kind;
|
||||
op.memos = memos; // kept so a transient-funds retry re-broadcasts, never recomposes
|
||||
op.echoLocalId = echoLocalId;
|
||||
op.sessionGen = chat_session_generation_;
|
||||
chat_send_queue_.push_back(std::move(op));
|
||||
wallet::liteLog("chat: queued outgoing " +
|
||||
std::string(kind == LiteOpKind::ContactRequest ? "contact request" : "message") +
|
||||
" (queue depth " + std::to_string(chat_send_queue_.size()) +
|
||||
", verified notes ~" + std::to_string(chat_verified_note_budget_) + ")");
|
||||
// pumpChatNoteBuffer() (next update tick) submits it when the channel is free + a verified note exists.
|
||||
}
|
||||
|
||||
// Self-send that mints `noteCount` reply-address notes (funds stay in the wallet), building/refilling the
|
||||
// buffer. Returns false if the single broadcast channel is busy (the pump retries next frame).
|
||||
bool App::broadcastSelfSplitLite(int noteCount) {
|
||||
if (!lite_wallet_ || noteCount < 1) return false;
|
||||
const std::string myZ = chatReplyZaddr();
|
||||
if (myZ.empty()) return false;
|
||||
wallet::LiteSendRequest req;
|
||||
const std::uint64_t noteZat = chatDrgxToZat(kChatNoteSizeDrgx);
|
||||
for (int i = 0; i < noteCount; ++i) {
|
||||
wallet::LiteSendRecipient r;
|
||||
r.address = myZ;
|
||||
r.amountZatoshis = noteZat;
|
||||
req.recipients.push_back(std::move(r));
|
||||
}
|
||||
if (!lite_wallet_->sendTransaction(req)) return false; // channel busy — retry next frame
|
||||
wallet::liteLog("Chat buffer: splitting funds into " + std::to_string(noteCount) +
|
||||
" notes (self-send) so messages can be sent back-to-back");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Full-node twin of broadcastSelfSplitLite: a z_sendmany self-send with `noteCount` outputs to our reply
|
||||
// address (funds stay in the wallet), building/refilling the buffer. Caps outputs to stay well under the
|
||||
// per-tx size limit after Sietch decoy padding. markFeeGapRetry=true is load-bearing — the fee-gap retry
|
||||
// rebuilds a SINGLE-recipient tx and would collapse the multi-output split. The terminal callback releases
|
||||
// the send channel; chat_split_outstanding_ stays set until a scan sees the outputs.
|
||||
bool App::broadcastSelfSplitNode(int noteCount) {
|
||||
if (lite_wallet_ || noteCount < 1) return false;
|
||||
if (!state_.connected || !rpc_ || !worker_) return false;
|
||||
const std::string myZ = chatReplyZaddr();
|
||||
if (myZ.empty()) return false;
|
||||
const double fee = kChatMinFeeDrgx;
|
||||
// Pay from any funded z-address; the split's OUTPUTS still go to the reply address (building the buffer
|
||||
// there). The note count now includes notes at every address and is sized against the total verified
|
||||
// balance, so paying from wherever the funds actually sit is consistent — and it can't stall when funds
|
||||
// aren't at the reply address (which paying strictly from myZ would).
|
||||
const std::string from = chatPayFromZaddr(fee);
|
||||
if (from.empty()) return false; // no fundable note — pump retries after a scan
|
||||
const int n = std::min(noteCount, 40); // bounded so the split tx stays under max_tx_size after decoys
|
||||
nlohmann::json recipients = nlohmann::json::array();
|
||||
for (int i = 0; i < n; ++i) {
|
||||
nlohmann::json r;
|
||||
r["address"] = myZ;
|
||||
r["amount"] = util::formatAmountFixed(kChatNoteSizeDrgx);
|
||||
recipients.push_back(std::move(r));
|
||||
}
|
||||
const auto sessionGen = chat_session_generation_;
|
||||
submitZSendMany(from, myZ, 0.0, fee, /*memo*/"", recipients,
|
||||
"HushChat / note-buffer split", /*markFeeGapRetry*/ true,
|
||||
[this, sessionGen](bool ok, const std::string& /*result*/) {
|
||||
if (sessionGen != chat_session_generation_) return; // stale wallet
|
||||
inflight_op_ = LiteInflightOp{}; // release the channel
|
||||
// On success the outputs aren't mined yet — chat_split_outstanding_ clears on the
|
||||
// next scan (pipeline recovered) or the watchdog. On FAILURE (e.g. a stale-scan
|
||||
// insufficient-funds throw) clear it now so the pump can retry after the next scan
|
||||
// instead of waiting out the 1200s watchdog. (No notes were created, so it's safe.)
|
||||
if (!ok) chat_split_outstanding_ = false;
|
||||
},
|
||||
/*background*/ true); // autonomous buffer maintenance — no global send UI
|
||||
wallet::liteLog("Chat buffer: splitting funds into " + std::to_string(n) +
|
||||
" notes (self-send) so messages can be sent back-to-back");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool App::shouldSplitChatBuffer() {
|
||||
// Only for engaged chat users (has a conversation or a queued send) — never split for someone who
|
||||
// never chats. Connected, unlocked, channel free, no split already outstanding, pipeline genuinely
|
||||
// depleted, funded. NB: a wall-clock cooldown is deliberately NOT used — a split's outputs stay
|
||||
// invisible until mined (~1 block, far longer than any UI cooldown), so we gate on a single-split-
|
||||
// in-flight flag (cleared when the pipeline recovers or the watchdog fires) to prevent runaway splits.
|
||||
if (chat_service_.store().conversationIds().empty() && chat_send_queue_.empty()) return false;
|
||||
if (!state_.connected || state_.isLocked()) return false;
|
||||
// Channel busy = our own op inflight, OR a user send holds the shared channel. Lite user sends set
|
||||
// lite_send_callback_; full-node user sends don't touch inflight_op_, so they're detected via the
|
||||
// shared single-flight counter + any outstanding opid (a chat/user/sweep z_sendmany not yet resolved).
|
||||
if (inflight_op_.kind != LiteOpKind::None) return false;
|
||||
if (lite_wallet_ ? (bool)lite_send_callback_
|
||||
: (send_submissions_in_flight_ > 0 || !pending_opids_.empty())) return false;
|
||||
if (chat_split_outstanding_) return false;
|
||||
if (!chat_note_model_seen_) return false;
|
||||
if (chat_pipeline_note_count_ >= kChatRefillTrigger) return false; // change refill covers it
|
||||
if (chat_verified_shielded_zat_ < chatDrgxToZat(kChatNoteSizeDrgx + kChatMinFeeDrgx)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the just-completed chat/contact broadcast onto its echo. The in-flight op is the queue FRONT
|
||||
// (the pump submits it without popping) so we can requeue it in place on a transient-funds failure.
|
||||
void App::onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error) {
|
||||
if (op.sessionGen != chat_session_generation_) return; // wallet switched since submit — echo is gone
|
||||
const bool frontMatches = !chat_send_queue_.empty()
|
||||
&& chat_send_queue_.front().echoLocalId == op.echoLocalId;
|
||||
if (ok) {
|
||||
chat_service_.resolveOutgoing(op.echoLocalId, chat::ChatDelivery::Sent);
|
||||
if (frontMatches) chat_send_queue_.pop_front();
|
||||
wallet::liteLog("chat: message broadcast (Sent); " + std::to_string(chat_send_queue_.size()) +
|
||||
" still queued");
|
||||
return;
|
||||
}
|
||||
if (frontMatches && isTransientVerifiedFundsError(error)
|
||||
&& chat_send_queue_.front().retries < kMaxChatSendRetries) {
|
||||
// The optimistic budget was wrong (change not confirmed yet). Stop draining until the next refresh
|
||||
// restores the true verified-note count; keep the message queued ("Sending") and retry then.
|
||||
chat_verified_note_budget_ = 0;
|
||||
++chat_send_queue_.front().retries;
|
||||
wallet::liteLog("chat: send deferred — funds still confirming (retry " +
|
||||
std::to_string(chat_send_queue_.front().retries) + "); message stays 'Sending'");
|
||||
return; // leave at front
|
||||
}
|
||||
chat_service_.resolveOutgoing(op.echoLocalId, chat::ChatDelivery::Failed); // hard failure -> Retry affordance
|
||||
if (frontMatches) chat_send_queue_.pop_front();
|
||||
wallet::liteLog("chat: send FAILED: " + error);
|
||||
}
|
||||
|
||||
// Per-frame pump: drain the chat queue against verified notes (priority 1); otherwise build/refill the
|
||||
// buffer during idle (priority 2). One op per frame — the single broadcast channel serializes the rest.
|
||||
void App::pumpChatNoteBuffer() {
|
||||
if (!chat::hushChatFeatureEnabledAtBuild()) return;
|
||||
if (!chat_service_.hasIdentity() || !state_.connected) return;
|
||||
// A locked wallet can't spend: submitting would return a non-transient lock error and flap the queued
|
||||
// message to Failed. Hold everything (send + split) until unlock — messages stay "Sending".
|
||||
if (state_.isLocked()) return;
|
||||
// Channel busy — exactly one send must be outstanding. Lite serializes via inflight_op_ + the send
|
||||
// callback; the full node has no single-broadcast channel, so we also honour the shared single-flight
|
||||
// counter and any outstanding opid (a chat/user/sweep z_sendmany not yet terminally resolved). Without
|
||||
// the latter two a chat send could overlap a user Send-tab send (which never sets inflight_op_).
|
||||
if (inflight_op_.kind != LiteOpKind::None) return;
|
||||
if (lite_wallet_ ? (bool)lite_send_callback_
|
||||
: (send_submissions_in_flight_ > 0 || !pending_opids_.empty())) return;
|
||||
|
||||
// Priority 1: drain a queued chat send while we (optimistically) still have a verified note.
|
||||
if (!chat_send_queue_.empty()) {
|
||||
QueuedChatOp& front = chat_send_queue_.front();
|
||||
if (front.sessionGen != chat_session_generation_) { chat_send_queue_.pop_front(); return; }
|
||||
if (chat_verified_note_budget_ >= 1) {
|
||||
// Submit onto the wallet's send channel; NOT popped until the async result resolves (so a
|
||||
// transient-funds failure can requeue it in place). Full-node broadcastChatMemos registers a
|
||||
// terminal opid callback that clears inflight_op_ + routes onChatBroadcastResult.
|
||||
const bool submitted = lite_wallet_ ? broadcastChatMemosLite(front.memos)
|
||||
: broadcastChatMemos(front.memos, front.echoLocalId);
|
||||
if (submitted) {
|
||||
inflight_op_ = LiteInflightOp{ front.kind, front.echoLocalId, front.sessionGen, ImGui::GetTime() };
|
||||
--chat_verified_note_budget_; // this note is now (optimistically) spent
|
||||
}
|
||||
return; // one op per frame; the front waits until resolved
|
||||
}
|
||||
// No verified note available: leave it queued ("Sending"); fall through to maybe build the buffer.
|
||||
}
|
||||
|
||||
// Priority 2: build/refill the buffer during idle.
|
||||
if (shouldSplitChatBuffer()) {
|
||||
const int need = kChatBufferTarget - chat_pipeline_note_count_;
|
||||
const std::uint64_t noteZat = chatDrgxToZat(kChatNoteSizeDrgx);
|
||||
const std::uint64_t feeZat = chatDrgxToZat(kChatMinFeeDrgx);
|
||||
int affordable = 0;
|
||||
if (chat_verified_shielded_zat_ > feeZat && noteZat > 0)
|
||||
affordable = static_cast<int>((chat_verified_shielded_zat_ - feeZat) / noteZat);
|
||||
const int n = std::min(need, affordable);
|
||||
const bool submitted = n >= 1 && (lite_wallet_ ? broadcastSelfSplitLite(n)
|
||||
: broadcastSelfSplitNode(n));
|
||||
if (submitted) {
|
||||
inflight_op_ = LiteInflightOp{ LiteOpKind::Split, {}, chat_session_generation_, ImGui::GetTime() };
|
||||
// Block further splits until this one's outputs mine in (or the watchdog fires): the outputs are
|
||||
// invisible to the note count for ~1 block, so without this we'd fire a fresh split every frame.
|
||||
chat_split_outstanding_ = true;
|
||||
chat_split_submitted_at_ = ImGui::GetTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HushChat (lite variant): the full-node harvest works off z_viewtransaction, but the lite wallet's
|
||||
@@ -3133,6 +3518,7 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
||||
|
||||
std::unordered_map<std::string, chat::HushChatTransactionInput> byTxid;
|
||||
std::unordered_map<std::string, std::int64_t> txTimestamps;
|
||||
bool anyUnconfirmedChatTx = false; // did a chat-carrying tx arrive still-unconfirmed (0-conf mempool)?
|
||||
for (const auto& tx : model.transactions) {
|
||||
if (tx.kind != wallet::LiteWalletAppTransactionKind::Receive) continue; // only incoming carry chat
|
||||
if (tx.memo.empty() || !tx.position) continue;
|
||||
@@ -3140,6 +3526,7 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
||||
input.txid = tx.txid;
|
||||
input.outputs.push_back({static_cast<std::size_t>(*tx.position), tx.memo});
|
||||
txTimestamps[tx.txid] = tx.timestamp;
|
||||
if (tx.unconfirmed) anyUnconfirmedChatTx = true;
|
||||
}
|
||||
if (byTxid.empty()) return;
|
||||
|
||||
@@ -3151,6 +3538,10 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
||||
if (!metadata.empty()) {
|
||||
std::vector<std::string> newChatCids;
|
||||
chat_service_.ingest(metadata, txTimestamps, std::time(nullptr), &newChatCids);
|
||||
if (!newChatCids.empty())
|
||||
wallet::liteLog("chat: harvested " + std::to_string(newChatCids.size()) + " new message(s) " +
|
||||
(anyUnconfirmedChatTx ? "(0-conf / mempool — monitor working)"
|
||||
: "(confirmed only — not seen at mempool speed)"));
|
||||
// Mirror the full-node paths: a new incoming message un-hides a hidden conversation (you can't
|
||||
// un-receive), and a non-muted new message toasts when we're off the Chat tab.
|
||||
if (settings_) {
|
||||
@@ -3182,6 +3573,8 @@ void App::fastScanChatMemos()
|
||||
const int scanGen = chat_session_generation_; // guard: drop the result if the wallet switches/locks
|
||||
worker_->post([this, addr, scanGen]() -> rpc::RPCWorker::MainCb {
|
||||
std::vector<chat::HushChatTransactionMetadata> metadata;
|
||||
int rawMemoCount = 0; // received notes carrying a memo at the reply addr (0-conf visibility signal)
|
||||
std::string scanError;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan");
|
||||
nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool
|
||||
@@ -3194,6 +3587,7 @@ void App::fastScanChatMemos()
|
||||
const std::string txid = note.value("txid", std::string());
|
||||
const std::string memo = note.value("memoStr", std::string());
|
||||
if (txid.empty() || memo.empty()) continue;
|
||||
++rawMemoCount;
|
||||
std::size_t pos = fallbackPos;
|
||||
for (const char* key : {"position", "outputIndex", "outindex"})
|
||||
if (note.contains(key) && note[key].is_number_integer() && note[key].get<int>() >= 0) {
|
||||
@@ -3209,13 +3603,25 @@ void App::fastScanChatMemos()
|
||||
for (auto& m : extracted.metadata) metadata.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {}
|
||||
return [this, scanGen, metadata = std::move(metadata)]() mutable {
|
||||
} catch (const std::exception& e) { scanError = e.what(); }
|
||||
const int parsedCount = static_cast<int>(metadata.size());
|
||||
return [this, scanGen, metadata = std::move(metadata), rawMemoCount, parsedCount, scanError]() mutable {
|
||||
// The wallet was switched/locked between post and now — this metadata belongs to the previous
|
||||
// session. resetChatSession already reset the in-flight flag, so just drop; clearing it here
|
||||
// would clobber a new session's own in-flight scan (mirrors the broadcast/identity guards).
|
||||
if (scanGen != chat_session_generation_) return;
|
||||
chat_fast_scan_in_flight_ = false;
|
||||
// Diagnostic (console App/chat channel): log when the memo-note count at the reply address
|
||||
// CHANGES, so a stable mempool doesn't spam every 2.5s. Reveals whether inbound 0-conf messages
|
||||
// are reaching us at all, and how many parse as chat vs are unrelated memos.
|
||||
if (!scanError.empty()) {
|
||||
wallet::liteLog("chat[0conf]: reply-addr scan failed: " + scanError);
|
||||
} else if (rawMemoCount != chat_fast_scan_last_seen_) {
|
||||
chat_fast_scan_last_seen_ = rawMemoCount;
|
||||
if (rawMemoCount > 0)
|
||||
wallet::liteLog("chat[0conf]: reply-addr scan sees " + std::to_string(rawMemoCount) +
|
||||
" memo note(s), " + std::to_string(parsedCount) + " parse as chat");
|
||||
}
|
||||
if (metadata.empty()) return;
|
||||
// Skip HIDDEN conversations — the mempool fast path deliberately doesn't surface them; they
|
||||
// still come back through the normal confirmed harvest (which un-hides on a new message).
|
||||
@@ -3229,6 +3635,9 @@ void App::fastScanChatMemos()
|
||||
std::unordered_map<std::string, std::int64_t> noTimes; // mempool: no block time → ingest uses now
|
||||
std::vector<std::string> newChatCids;
|
||||
chat_service_.ingest(visible, noTimes, std::time(nullptr), &newChatCids);
|
||||
if (!newChatCids.empty())
|
||||
wallet::liteLog("chat[0conf]: harvested " + std::to_string(newChatCids.size()) +
|
||||
" new message(s) from the mempool");
|
||||
if (current_page_ != ui::NavPage::Chat &&
|
||||
std::any_of(newChatCids.begin(), newChatCids.end(),
|
||||
[this](const std::string& cid){ return !(settings_ && settings_->isChatMuted(cid)); }))
|
||||
@@ -4087,6 +4496,9 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
||||
return;
|
||||
}
|
||||
lite_send_callback_ = std::move(callback); // delivered from update()
|
||||
// Claim the shared broadcast channel so the chat note-buffer coordinator yields to this user send
|
||||
// and routes the one global broadcast result back to lite_send_callback_ (not a chat echo).
|
||||
inflight_op_ = LiteInflightOp{ LiteOpKind::UserSend, {}, chat_session_generation_, ImGui::GetTime() };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4149,12 +4561,13 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
||||
void App::submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||
const std::string& memo, const nlohmann::json& recipients,
|
||||
const char* traceLabel, bool markFeeGapRetry,
|
||||
std::function<void(bool, const std::string&)> callback)
|
||||
std::function<void(bool, const std::string&)> callback,
|
||||
bool background)
|
||||
{
|
||||
send_progress_active_ = true;
|
||||
if (!background) send_progress_active_ = true; // autonomous chat/split: status shown on the message
|
||||
++send_submissions_in_flight_;
|
||||
worker_->post([this, from, to, amount, fee, memo, recipients, callback, traceLabel,
|
||||
markFeeGapRetry]() -> rpc::RPCWorker::MainCb {
|
||||
markFeeGapRetry, background]() -> rpc::RPCWorker::MainCb {
|
||||
bool ok = false;
|
||||
std::string result_str;
|
||||
try {
|
||||
@@ -4169,7 +4582,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
|
||||
// below would never run, leaving a stuck "Sending…" spinner.
|
||||
result_str = "Send failed (unknown error)";
|
||||
}
|
||||
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry]() {
|
||||
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry, background]() {
|
||||
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
||||
if (ok) {
|
||||
// A send changes address balances — refresh on next cycle
|
||||
@@ -4192,7 +4605,7 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
|
||||
callback(true, result_str); // no opid to track — report as-is
|
||||
}
|
||||
} else {
|
||||
send_progress_active_ = false;
|
||||
if (!background) send_progress_active_ = false;
|
||||
if (callback) callback(false, result_str);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,6 +228,17 @@ bool ChatDatabase::ensureOpen()
|
||||
exec("PRAGMA journal_mode=WAL");
|
||||
exec("PRAGMA synchronous=NORMAL");
|
||||
|
||||
// C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with
|
||||
// umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD
|
||||
// ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows).
|
||||
{
|
||||
std::error_code perr;
|
||||
const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write;
|
||||
std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
}
|
||||
|
||||
if (!createSchema()) {
|
||||
close();
|
||||
return false;
|
||||
|
||||
@@ -16,7 +16,8 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
const std::string& conversationId,
|
||||
const char* type,
|
||||
const std::string& streamHeaderHex,
|
||||
const std::string& publicKeyHex)
|
||||
const std::string& publicKeyHex,
|
||||
std::int64_t sentAt)
|
||||
{
|
||||
nlohmann::json header;
|
||||
header["h"] = 1; // header number (>= 1)
|
||||
@@ -26,6 +27,7 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
header["t"] = type; // "Memo" or "Cont"
|
||||
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
||||
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
||||
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
|
||||
return header.dump();
|
||||
}
|
||||
|
||||
@@ -67,7 +69,8 @@ ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex);
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemo = ciphertextHex;
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||
@@ -95,7 +98,8 @@ ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex);
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemo = requestText;
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||
|
||||
@@ -124,6 +124,10 @@ HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
||||
if (!readRequiredString(object, "t", type, error)) return fail(error);
|
||||
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
||||
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
|
||||
// Optional sender compose time (Unix seconds). Absent on older senders — leave sent_at = 0 so the
|
||||
// receiver falls back to the tx/receive time. Read leniently; never fail the header on a bad value.
|
||||
if (auto it = object.find("ts"); it != object.end() && it->is_number_integer())
|
||||
header.sent_at = it->get<std::int64_t>();
|
||||
|
||||
if (header.header_number < 1) return fail("header number must be positive");
|
||||
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
|
||||
@@ -303,6 +307,7 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||
metadata.sender_public_key_hex = pair.header.public_key_hex;
|
||||
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
||||
metadata.payload_memo = pair.payload_memo;
|
||||
metadata.sent_at = pair.header.sent_at; // carry the sender's compose time (0 if absent)
|
||||
result.metadata.push_back(std::move(metadata));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ struct HushChatHeader {
|
||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||
std::string secretstream_header_hex;
|
||||
std::string public_key_hex;
|
||||
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
||||
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
|
||||
std::int64_t sent_at = 0;
|
||||
};
|
||||
|
||||
struct HushChatHeaderParseResult {
|
||||
@@ -80,6 +83,7 @@ struct HushChatTransactionMetadata {
|
||||
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
|
||||
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
||||
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
|
||||
};
|
||||
|
||||
struct HushChatTransactionExtractionResult {
|
||||
|
||||
@@ -45,8 +45,21 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
message.conversation_id = meta.conversation_id;
|
||||
message.peer_zaddr = meta.reply_zaddr;
|
||||
message.peer_public_key_hex = meta.sender_public_key_hex;
|
||||
// Reference time: the tx/receive time (block time if confirmed, else the receiver's wall clock for
|
||||
// a mempool receive).
|
||||
const auto timeIt = txTimestamps.find(meta.txid);
|
||||
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
const std::int64_t refTime = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
// Prefer the sender's stamped compose time (header "ts") — the true send time, shown identically on
|
||||
// both ends. But REJECT a value implausibly in the FUTURE vs the reference: a wrong/ahead peer clock
|
||||
// would otherwise pin their messages to the bottom of the thread forever. A compose time in the
|
||||
// PAST is fine — the note buffer can broadcast a queued message long after it was composed, and a
|
||||
// confirmed tx's block time is always >= the compose time.
|
||||
constexpr std::int64_t kSenderTsFutureToleranceSec = 3600; // 1 hour of clock skew tolerated
|
||||
if (meta.sent_at > 0 && (refTime <= 0 || meta.sent_at <= refTime + kSenderTsFutureToleranceSec)) {
|
||||
message.timestamp = meta.sent_at;
|
||||
} else {
|
||||
message.timestamp = refTime;
|
||||
}
|
||||
message.payload_position = meta.payload_position;
|
||||
|
||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||
|
||||
@@ -28,6 +28,16 @@ public:
|
||||
// Distinct conversation ids, in first-seen order.
|
||||
std::vector<std::string> conversationIds() const;
|
||||
|
||||
// The set of on-chain txids that carried a chat message (sent or received, messages + contact
|
||||
// requests) — used by the History tab to badge / filter chat transactions. O(messages), no copies.
|
||||
std::unordered_set<std::string> chatTxids() const {
|
||||
std::unordered_set<std::string> out;
|
||||
out.reserve(messages_.size());
|
||||
for (const auto& m : messages_)
|
||||
if (!m.txid.empty()) out.insert(m.txid);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::size_t size() const { return messages_.size(); }
|
||||
bool empty() const { return messages_.empty(); }
|
||||
void clear();
|
||||
|
||||
@@ -196,6 +196,8 @@ bool Settings::load(const std::string& path)
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
|
||||
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
|
||||
loadScalar(j, "animate_avatars", animate_avatars_);
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||
@@ -467,6 +469,8 @@ bool Settings::save(const std::string& path)
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["contacts_view_mode"] = contacts_view_mode_;
|
||||
j["contacts_avatar_shape"] = contacts_avatar_shape_;
|
||||
j["contacts_list_scale"] = contacts_list_scale_;
|
||||
j["animate_avatars"] = animate_avatars_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
|
||||
@@ -91,7 +91,8 @@ public:
|
||||
bool showValue = true; // show the converted/fiat value on the card
|
||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
||||
int sparklineInterval = 4; // 0=minute 1=hour 2=day 3=week 4=month (default month: a real curve
|
||||
// from the daily series, vs the young in-session minute buffer)
|
||||
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
|
||||
// hash (shown only when that wallet is active). New entries are tagged with the current
|
||||
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||
@@ -226,13 +227,19 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
|
||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
|
||||
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts customization (gear modal): avatar shape + card/list row scale.
|
||||
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
|
||||
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
|
||||
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
float getContactsListScale() const { return contacts_list_scale_; }
|
||||
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
|
||||
bool getAnimateAvatars() const { return animate_avatars_; }
|
||||
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
||||
@@ -553,8 +560,10 @@ private:
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
|
||||
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
|
||||
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
|
||||
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
|
||||
bool scanline_enabled_ = true;
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
|
||||
@@ -136,6 +136,14 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
||||
}
|
||||
}
|
||||
|
||||
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
|
||||
// later step failed (empty address, or z_getnewaddress threw), the caller discards this
|
||||
// result without wiping it, which would leave the seed resident. Success keeps it deliberately.
|
||||
if (!r.ok && !r.seedPhrase.empty()) {
|
||||
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
|
||||
r.seedPhrase.clear();
|
||||
}
|
||||
|
||||
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
|
||||
cli.disconnect();
|
||||
temp.stop(20000);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
|
||||
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
|
||||
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
@@ -208,19 +208,20 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
try {
|
||||
fs::create_directories(fs::path(outPath).parent_path());
|
||||
std::ofstream ofs(outPath);
|
||||
std::ofstream ofs(outPath, std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
|
||||
#ifndef _WIN32
|
||||
// 0600 permissions — only owner can read/write
|
||||
// Restrict to owner (0600) BEFORE writing any secret material (API token, wallet
|
||||
// address, worker name). The file is still empty here, so the config is never
|
||||
// world-readable — closing the window between creation and the previous post-write chmod.
|
||||
chmod(outPath.c_str(), 0600);
|
||||
#endif
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
last_error_ = std::string("Config write error: ") + e.what();
|
||||
|
||||
39
src/embedded/logo_dragonx_svg.h
Normal file
39
src/embedded/logo_dragonx_svg.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Embedded source of the DragonX mark (res/img/logos/logo_dragonx.svg). Kept as a string so the logo
|
||||
// is available in every build (dev + portable single-file) with no resource-pipeline or file dependency.
|
||||
// It is rasterized + recolored per theme at runtime (see util/svg_texture.*). Two fills: the crimson
|
||||
// body (.cls-2 #d82652) becomes the theme accent; the white detail (.cls-1 #fff) becomes the light tone.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Global `embedded` namespace to match the generated embedded resources (embedded::ui_toml_data, etc.).
|
||||
namespace embedded {
|
||||
|
||||
inline constexpr const char* kLogoDragonXSvg = R"SVG(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>)SVG";
|
||||
|
||||
} // namespace embedded
|
||||
@@ -25,9 +25,11 @@ namespace {
|
||||
|
||||
// Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree
|
||||
// that held a secret (B7). Operates on the underlying std::string buffers via get_ref.
|
||||
void scrubJsonSecrets(nlohmann::json& j) {
|
||||
// Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter).
|
||||
template <typename J>
|
||||
void scrubJsonSecrets(J& j) {
|
||||
if (j.is_string()) {
|
||||
auto& s = j.get_ref<std::string&>();
|
||||
auto& s = j.template get_ref<std::string&>();
|
||||
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
||||
} else if (j.is_object() || j.is_array()) {
|
||||
for (auto& el : j) scrubJsonSecrets(el);
|
||||
@@ -96,6 +98,10 @@ void RPCClient::setTraceSource(std::string source)
|
||||
// Callback for libcurl to write response data
|
||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
||||
size_t totalSize = size * nmemb;
|
||||
// Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded
|
||||
// response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion.
|
||||
static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024;
|
||||
if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0; // short count aborts the transfer
|
||||
userp->append((char*)contents, totalSize);
|
||||
return totalSize;
|
||||
}
|
||||
@@ -202,6 +208,10 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
|
||||
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
|
||||
// Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's
|
||||
// build defaults. Harmless on the localhost http:// case; essential for a remote https daemon.
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
|
||||
// Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
|
||||
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
|
||||
@@ -508,14 +518,22 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
||||
}
|
||||
|
||||
auto& result = oj["result"];
|
||||
std::string out;
|
||||
if (result.is_null()) {
|
||||
return "null";
|
||||
out = "null";
|
||||
} else if (result.is_string()) {
|
||||
// Return the raw string (not JSON-encoded) — caller wraps as needed
|
||||
return result.get<std::string>();
|
||||
out = result.get<std::string>();
|
||||
} else {
|
||||
return result.dump(4);
|
||||
out = result.dump(4);
|
||||
}
|
||||
// B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey,
|
||||
// whose response carries plaintext key material. Zero the raw buffer and the parsed tree so
|
||||
// the secret does not linger in freed heap (matching callSecret). The single returned copy is
|
||||
// the caller's to manage.
|
||||
scrubJsonSecrets(oj);
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
|
||||
|
||||
@@ -434,8 +434,15 @@ std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::
|
||||
result.market.market_cap = data.value("usd_market_cap", 0.0);
|
||||
|
||||
char buf[64];
|
||||
std::tm* tm = std::localtime(&fetchedAt);
|
||||
if (tm && std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm) > 0) {
|
||||
// Runs on the RPC worker thread — std::localtime shares a process-wide static tm, so use the
|
||||
// reentrant variant into a local tm (matches the rest of the codebase).
|
||||
std::tm tmv{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &fetchedAt);
|
||||
#else
|
||||
localtime_r(&fetchedAt, &tmv);
|
||||
#endif
|
||||
if (std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmv) > 0) {
|
||||
result.market.last_updated = buf;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -160,6 +160,10 @@ void ThemeEffects::loadFromTheme() {
|
||||
|
||||
// ---- Gradient Border Shift ----
|
||||
gradient_border_.enabled = eff("gradient-border-enabled").sizeOr(0.0f) > 0.5f;
|
||||
// Opt-in: also draw the shifting border on every glass panel (not just the
|
||||
// active nav button). Off by default so themes that only want the button
|
||||
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
|
||||
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
|
||||
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
||||
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
||||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
||||
@@ -512,11 +516,15 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
// ============================================================================
|
||||
|
||||
void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const {
|
||||
float rounding, float phaseOffset,
|
||||
float alphaMul) const {
|
||||
if (!enabled_ || !gradient_border_.enabled) return;
|
||||
|
||||
// Smooth sinusoidal oscillation between color A and color B
|
||||
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||
// Smooth sinusoidal oscillation between color A and color B.
|
||||
// phaseOffset shifts where in the cycle this element sits so a wall of
|
||||
// panels reads like veins at different depths rather than one pulse.
|
||||
float phase = std::sin((time_ * gradient_border_.speed + phaseOffset)
|
||||
* 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||
|
||||
// Extract RGBA from both colors and lerp
|
||||
RGB ca = unpackRGB(gradient_border_.colorA);
|
||||
@@ -525,7 +533,7 @@ void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 p
|
||||
int r = ca.r + (int)((cb.r - ca.r) * phase);
|
||||
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
||||
int b = ca.b + (int)((cb.b - ca.b) * phase);
|
||||
int a = scaledAlpha(gradient_border_.alpha, bgOpacity_);
|
||||
int a = scaledAlpha(gradient_border_.alpha * alphaMul, bgOpacity_);
|
||||
|
||||
// Draw the shifting border
|
||||
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
|
||||
@@ -896,6 +904,22 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const {
|
||||
if (!enabled_ || effects::isLowSpecMode()) return;
|
||||
|
||||
// Gradient border on panels — a slow color-shifting outline that hugs the
|
||||
// panel's rounded corners (drawn via AddRect, so it follows the rounding
|
||||
// exactly — no polygonal corners). Position-based phase offset makes each
|
||||
// panel drift like a vein at a different depth; softer than the active
|
||||
// nav button (alphaMul 0.6) so a screenful of panels stays calm.
|
||||
if (gradient_border_.enabled && gradient_border_.panels) {
|
||||
float w = pMax.x - pMin.x;
|
||||
float h = pMax.y - pMin.y;
|
||||
if (w > 80 && h > 40) { // skip small panels
|
||||
float posKey = (pMin.x * 0.0073f + pMin.y * 0.0137f);
|
||||
posKey = posKey - (int)posKey; // fractional 0..1
|
||||
if (posKey < 0) posKey += 1.0f;
|
||||
drawGradientBorderShift(dl, pMin, pMax, rounding, posKey, 0.6f);
|
||||
}
|
||||
}
|
||||
|
||||
// Edge trace on panels — use position-based phase offset so each
|
||||
// panel's tracer is at a different position around the border
|
||||
if (edge_trace_.enabled) {
|
||||
|
||||
@@ -69,9 +69,12 @@ public:
|
||||
void drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const;
|
||||
|
||||
/// Draw a border that shifts between two colors over time (gem-like)
|
||||
/// Draw a border that shifts between two colors over time (gem-like).
|
||||
/// phaseOffset (0..1) shifts this element's point in the color cycle so
|
||||
/// many panels don't pulse in unison; alphaMul scales the whole effect.
|
||||
void drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const;
|
||||
float rounding, float phaseOffset = 0.0f,
|
||||
float alphaMul = 1.0f) const;
|
||||
|
||||
/// Draw ember particles that rise from an element (fire theme)
|
||||
void drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const;
|
||||
@@ -186,6 +189,7 @@ private:
|
||||
|
||||
struct GradientBorderConfig {
|
||||
bool enabled = false;
|
||||
bool panels = false; ///< also draw on glass panels, not just the active nav button
|
||||
float speed = 0.15f; ///< full color shift cycles per second
|
||||
float thickness = 1.5f; ///< border line thickness in pixels
|
||||
float alpha = 0.6f; ///< peak alpha
|
||||
|
||||
@@ -57,6 +57,21 @@ inline ImU32 ReadableError() {
|
||||
return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF);
|
||||
}
|
||||
|
||||
// Middle-ellipsis truncation ("front...back", roughly equal halves) so `text` fits within
|
||||
// maxWidth pixels when drawn with `font` at `fontSize`. Returns `text` unchanged if it already
|
||||
// fits (or maxWidth is non-positive). Display-only — never mutate the underlying value with this.
|
||||
inline std::string TruncateToWidth(const std::string& text, ImFont* font, float fontSize, float maxWidth) {
|
||||
if (text.empty() || !font || maxWidth <= 0.0f) return text;
|
||||
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, text.c_str()).x <= maxWidth) return text;
|
||||
const int n = static_cast<int>(text.size());
|
||||
for (int f = n / 2; f >= 3; --f) {
|
||||
const int b = (f - 2 > 3) ? (f - 2) : 3; // keep the two halves roughly equal
|
||||
std::string t = text.substr(0, f) + "..." + text.substr(n - b);
|
||||
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, t.c_str()).x <= maxWidth) return t;
|
||||
}
|
||||
return n > 6 ? (text.substr(0, 3) + "..." + text.substr(n - 3)) : text;
|
||||
}
|
||||
|
||||
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
|
||||
inline const char* LoadingDots() {
|
||||
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
@@ -1347,10 +1362,17 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
||||
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
|
||||
WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f);
|
||||
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, labels[i]);
|
||||
// Center when the label fits; otherwise left-align with a small pad (so a long translation clips
|
||||
// on the right, not on BOTH sides). Clip to the cell so no label can bleed into a neighbouring
|
||||
// segment or past the rounded track — English fits, but de/es/fr/pt/ru labels can overrun.
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= cellW - 2.0f * lpad) ? (cellW - ts.x) * 0.5f : lpad;
|
||||
dl->PushClipRect(cMin, cMax, true);
|
||||
dl->AddText(font, font->LegacySize,
|
||||
ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (height - ts.y) * 0.5f),
|
||||
ImVec2(cMin.x + tx, cMin.y + (height - ts.y) * 0.5f),
|
||||
active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||
labels[i]);
|
||||
dl->PopClipRect();
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetCursorScreenPos(cMin);
|
||||
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;
|
||||
|
||||
193
src/ui/material/settings_controls.h
Normal file
193
src/ui/material/settings_controls.h
Normal file
@@ -0,0 +1,193 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Settings design-system controls — the polished chat-settings look (accent subsection headers,
|
||||
// labeled rows with right-aligned controls, iOS-style segmented controls) promoted to reusable
|
||||
// components, plus a tiered ActionButton (Primary/Secondary/Tertiary/Destructive) with optional
|
||||
// leading Material icon and a ButtonFlow that wraps rows of buttons instead of shrinking them.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "draw_helpers.h" // colors, type, layout, icons, WithAlpha, ScaleAlpha, DrawButtonGlassOverlay
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
|
||||
// Accent small-caps subsection header ("KEYS & BACKUP", "APPEARANCE"…) — the chat-settings section() look.
|
||||
inline void SettingsSubheader(const char* text) {
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(Primary(), 235));
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0.0f, 2.0f * dp));
|
||||
}
|
||||
|
||||
// A labeled settings row: label left, control right-aligned in a fixed column. Construct once per card
|
||||
// section with the content width (0 = auto), then call .label(text) before drawing each control (leaves
|
||||
// the cursor at the control origin and sets the next item width to the control column).
|
||||
struct SettingsRow {
|
||||
float leftX, rowW, ctrlW, rowGap;
|
||||
explicit SettingsRow(float contentWidth, float controlWidth = 250.0f) {
|
||||
const float dp = Layout::dpiScale();
|
||||
leftX = ImGui::GetCursorPosX();
|
||||
rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x;
|
||||
ctrlW = controlWidth * dp;
|
||||
rowGap = 5.0f * dp;
|
||||
}
|
||||
void label(const char* text) {
|
||||
ImGui::Dummy(ImVec2(0.0f, rowGap));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), leftX + std::max(0.0f, rowW - ctrlW)));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
}
|
||||
};
|
||||
|
||||
// iOS-style segmented control (rounded track + inset pill on the selection). Draws its own labeled row
|
||||
// and returns the (possibly changed) index.
|
||||
inline int SegmentedControl(SettingsRow& row, const char* label, const char* const* items, int count, int value) {
|
||||
const float dp = Layout::dpiScale();
|
||||
row.label(label);
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const float seg = row.ctrlW / static_cast<float>(count);
|
||||
const float round = 7.0f * dp;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(origin, ImVec2(origin.x + row.ctrlW, origin.y + h), WithAlpha(OnSurface(), 20), round);
|
||||
int result = value;
|
||||
ImGui::PushID(label);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ImGui::PushID(i);
|
||||
const ImVec2 mn(origin.x + i * seg, origin.y), mx(origin.x + (i + 1) * seg, origin.y + h);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
if (ImGui::InvisibleButton("##s", ImVec2(seg, h))) result = i;
|
||||
const bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const bool sel = (value == i);
|
||||
if (sel) {
|
||||
const float in = 2.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(mn.x + in, mn.y + in), ImVec2(mx.x - in, mx.y - in),
|
||||
WithAlpha(Primary(), 210), std::max(1.0f, round - in));
|
||||
} else if (hov) {
|
||||
dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 26), round);
|
||||
}
|
||||
const ImVec2 ts = ImGui::CalcTextSize(items[i]);
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= seg - 2.0f * lpad) ? (seg - ts.x) * 0.5f : lpad;
|
||||
ImGui::PushClipRect(mn, mx, true);
|
||||
dl->AddText(ImVec2(mn.x + tx, mn.y + (h - ts.y) * 0.5f),
|
||||
sel ? IM_COL32(255, 255, 255, 236) : OnSurfaceMedium(), items[i]);
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::Dummy(ImVec2(row.ctrlW, h)); // reserve the control's rect for layout flow
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Tiered action buttons ───────────────────────────────────────────────────
|
||||
// Primary = filled accent (the one main action of a group)
|
||||
// Secondary = glass (default — common actions)
|
||||
// Tertiary = ghost / low-emphasis (rarely used)
|
||||
// Destructive = error-tinted outline (delete / reset)
|
||||
enum class ActionTier { Primary, Secondary, Tertiary, Destructive };
|
||||
|
||||
// The width an ActionButton will occupy (for ButtonFlow / manual layout).
|
||||
inline float ActionButtonWidth(const char* label, const char* icon, float minWidth = 0.0f) {
|
||||
ImFont* lf = Type().button();
|
||||
ImFont* icf = Type().iconSmall();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float padX = 12.0f * dp, gap = 6.0f * dp;
|
||||
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
|
||||
const float iconW = (icon && icon[0] && icf) ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
|
||||
const float w = padX * 2.0f + iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
|
||||
return std::max(w, minWidth);
|
||||
}
|
||||
|
||||
// A tiered action button with an optional leading Material icon (ICON_MD_* or nullptr). Auto-sizes to
|
||||
// its content (>= minWidth). Respects BeginDisabled() (dims via the style alpha, not clickable).
|
||||
inline bool ActionButton(const char* id, const char* label, const char* icon, ActionTier tier, float minWidth = 0.0f) {
|
||||
ImFont* lf = Type().button();
|
||||
ImFont* icf = Type().iconSmall();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float gap = 6.0f * dp;
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const bool hasIcon = icon && icon[0] && icf;
|
||||
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
|
||||
const float iconW = hasIcon ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
|
||||
const float w = ActionButtonWidth(label, icon, minWidth);
|
||||
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
const bool pressed = ImGui::InvisibleButton(id, ImVec2(w, h));
|
||||
const bool hov = ImGui::IsItemHovered(), act = ImGui::IsItemActive();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMax(pos.x + w, pos.y + h);
|
||||
const float round = ImGui::GetStyle().FrameRounding;
|
||||
const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
|
||||
|
||||
ImU32 bg = 0, border = 0, fg = OnSurface();
|
||||
bool glass = false;
|
||||
switch (tier) {
|
||||
case ActionTier::Primary:
|
||||
bg = WithAlpha(Primary(), act ? 255 : (hov ? 245 : 220));
|
||||
fg = IM_COL32(255, 255, 255, 240);
|
||||
break;
|
||||
case ActionTier::Secondary:
|
||||
bg = WithAlpha(OnSurface(), hov ? 26 : 16);
|
||||
border = WithAlpha(OnSurface(), 40);
|
||||
glass = true;
|
||||
fg = OnSurface();
|
||||
break;
|
||||
case ActionTier::Tertiary:
|
||||
bg = hov ? WithAlpha(OnSurface(), 18) : 0;
|
||||
fg = OnSurfaceMedium();
|
||||
break;
|
||||
case ActionTier::Destructive:
|
||||
bg = hov ? WithAlpha(Error(), 32) : WithAlpha(Error(), 12);
|
||||
border = WithAlpha(Error(), 90);
|
||||
fg = Error();
|
||||
break;
|
||||
}
|
||||
if (bg) dl->AddRectFilled(pos, pMax, ScaleAlpha(bg, a), round);
|
||||
if (border) dl->AddRect(pos, pMax, ScaleAlpha(border, a), round, 0, 1.0f);
|
||||
if (glass) DrawButtonGlassOverlay(dl, pos, pMax, round, act, hov);
|
||||
fg = ScaleAlpha(fg, a);
|
||||
|
||||
const float contentW = iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
|
||||
float cx = pos.x + (w - contentW) * 0.5f;
|
||||
if (hasIcon) {
|
||||
dl->AddText(icf, icf->LegacySize, ImVec2(cx, pos.y + (h - icf->LegacySize) * 0.5f), fg, icon);
|
||||
cx += iconW + gap;
|
||||
}
|
||||
dl->AddText(lf, lf->LegacySize, ImVec2(cx, pos.y + (h - lf->LegacySize) * 0.5f), fg, label);
|
||||
return pressed;
|
||||
}
|
||||
|
||||
// Places ActionButtons left→right, wrapping to a new row when the next one won't fit (instead of the
|
||||
// old font-scale-to-fit). Call next(width) before each ActionButton.
|
||||
struct ButtonFlow {
|
||||
float availW, gap; float x = 0.0f; bool firstOnRow = true;
|
||||
explicit ButtonFlow(float availWidth, float gapPx = 8.0f) : availW(availWidth) {
|
||||
gap = gapPx * Layout::dpiScale();
|
||||
}
|
||||
void next(float w) {
|
||||
if (firstOnRow) { firstOnRow = false; x = w; return; }
|
||||
if (x + gap + w <= availW) { ImGui::SameLine(0, gap); x += gap + w; }
|
||||
else { x = w; } // natural newline wraps to the next row
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,7 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
float spacingSm,
|
||||
float spacingXs)
|
||||
{
|
||||
(void)spacingXs; // trailing button now insets by rowPadLeft (mirrors the left margin)
|
||||
AddressRowLayout layout;
|
||||
layout.contentStartX = rowX + rowPadLeft;
|
||||
layout.contentStartY = rowY + spacingMd;
|
||||
@@ -77,7 +78,9 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
|
||||
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
|
||||
const float rightEdge = rowX + rowWidth;
|
||||
const float favoriteX = rightEdge - layout.buttonSize - spacingXs;
|
||||
// Inset the trailing (favorite/star) button by the card's inner padding — the same margin the
|
||||
// left content uses (rowPadLeft) — so it mirrors the left edge instead of hugging the card edge.
|
||||
const float favoriteX = rightEdge - layout.buttonSize - rowPadLeft;
|
||||
const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
|
||||
|
||||
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "console_command_executor.h"
|
||||
#include "console_channel.h"
|
||||
#include "console_command_reference.h" // consoleCommandCategories / liteConsoleCommandCategories
|
||||
#include "console_input_model.h" // BuildConsoleRpcCall
|
||||
|
||||
#include "../../app.h"
|
||||
@@ -28,6 +29,10 @@ namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
// Classifies a diagnostics line into a console channel (defined below in an anonymous namespace); both
|
||||
// executors drain the same LiteDiagnostics ring, so declare it up front for the full-node drain.
|
||||
namespace { ConsoleChannel liteLogChannel(const std::string& line); }
|
||||
|
||||
// ============================================================================
|
||||
// FullNodeConsoleExecutor
|
||||
// ============================================================================
|
||||
@@ -172,6 +177,25 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add)
|
||||
} else {
|
||||
last_xmrig_output_size_ = 0; // reset so we get fresh output when it restarts
|
||||
}
|
||||
|
||||
// Chat diagnostics ring: chat code logs via wallet::liteLog on both variants (send lifecycle, the
|
||||
// 0-conf harvest, the note buffer). Surface those lines here as App messages so the full-node console
|
||||
// shows chat activity too. Same generation-cursor drain the lite console uses.
|
||||
{
|
||||
auto& diag = wallet::LiteDiagnostics::instance();
|
||||
const std::uint64_t gen = diag.generation();
|
||||
if (gen != diag_gen_) {
|
||||
const auto snap = diag.snapshot();
|
||||
std::size_t startIdx = 0;
|
||||
if (diag_gen_ != static_cast<std::uint64_t>(-1)) {
|
||||
const std::uint64_t added = gen - diag_gen_;
|
||||
startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast<std::size_t>(added);
|
||||
}
|
||||
for (std::size_t i = startIdx; i < snap.size(); ++i)
|
||||
add(snap[i], liteLogChannel(snap[i]));
|
||||
diag_gen_ = gen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FullNodeConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
|
||||
@@ -194,6 +218,11 @@ void FullNodeConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
|
||||
add(TR("console_tab_completion"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
const std::vector<ConsoleCommandCategory>* FullNodeConsoleExecutor::commandReference() const
|
||||
{
|
||||
return &consoleCommandCategories();
|
||||
}
|
||||
|
||||
ConsoleStatusLine FullNodeConsoleExecutor::toolbarStatus() const
|
||||
{
|
||||
ConsoleStatusLine s;
|
||||
@@ -258,7 +287,7 @@ bool LiteConsoleExecutor::pollResult(std::string& result, bool& isError)
|
||||
if (!lw) return false;
|
||||
wallet::LiteConsoleResult res;
|
||||
if (!lw->takeConsoleResult(res)) return false;
|
||||
result = res.response.empty() ? std::string("(no output)") : res.response;
|
||||
result = res.response.empty() ? std::string(TR("console_no_output")) : res.response;
|
||||
isError = !res.ok;
|
||||
return true;
|
||||
}
|
||||
@@ -286,6 +315,19 @@ void LiteConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
|
||||
add(TR("console_help_clear"), ConsoleChannel::None);
|
||||
add(TR("console_help_help"), ConsoleChannel::None);
|
||||
add(TR("lite_console_help_passthrough"), ConsoleChannel::Info);
|
||||
// The lite backend's own command verbs (literal command tokens, not RPC methods — mirrors the
|
||||
// backend's get_commands() registry). Listed for discoverability: the C++ tab intercepts `help`
|
||||
// before it can reach the backend's own HelpCommand, so its "type help" advice would dead-end.
|
||||
add(TR("lite_console_backend_commands"), ConsoleChannel::Info);
|
||||
add(" sync syncstatus balance addresses height info list notes encryptionstatus", ConsoleChannel::None);
|
||||
add(" send shield new seed import timport export", ConsoleChannel::None);
|
||||
add(" encrypt decrypt lock unlock rescan save sietch saplingtree coinsupply", ConsoleChannel::None);
|
||||
add(TR("console_click_commands"), ConsoleChannel::Info);
|
||||
}
|
||||
|
||||
const std::vector<ConsoleCommandCategory>* LiteConsoleExecutor::commandReference() const
|
||||
{
|
||||
return &liteConsoleCommandCategories();
|
||||
}
|
||||
|
||||
std::vector<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const
|
||||
@@ -294,20 +336,20 @@ std::vector<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const
|
||||
wallet::LiteWalletController* lw = app_->liteWallet();
|
||||
if (lw && lw->walletOpen()) {
|
||||
const SyncInfo& sync = app_->state().sync;
|
||||
char buf[96];
|
||||
char buf[128];
|
||||
if (sync.syncing && !sync.isSynced()) {
|
||||
double vp = sync.verification_progress;
|
||||
if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0;
|
||||
std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)",
|
||||
vp * 100.0, sync.blocks, sync.headers);
|
||||
std::snprintf(buf, sizeof(buf), "%s %.1f%% (block %d / %d)",
|
||||
TR("lite_net_syncing"), vp * 100.0, sync.blocks, sync.headers);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks);
|
||||
std::snprintf(buf, sizeof(buf), "%s (block %d)", TR("lite_net_synced"), sync.blocks);
|
||||
}
|
||||
out.push_back({std::string(buf), OnSurfaceMedium(), false});
|
||||
}
|
||||
const std::string& err = app_->liteOpenError();
|
||||
if (!err.empty() && (!lw || !lw->walletOpen()))
|
||||
out.push_back({std::string("Last error: ") + err, Error(), false});
|
||||
out.push_back({std::string(TR("console_last_error")) + " " + err, Error(), false});
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -315,10 +357,10 @@ ConsoleStatusLine LiteConsoleExecutor::toolbarStatus() const
|
||||
{
|
||||
ConsoleStatusLine s;
|
||||
wallet::LiteWalletController* lw = app_->liteWallet();
|
||||
if (!lw) { s.text = "No backend"; s.color = Error(); return s; }
|
||||
if (lw->walletOpen()) { s.text = "Connected"; s.color = Success(); }
|
||||
else if (lw->openInProgress()) { s.text = "Connecting"; s.color = Warning(); s.pulse = true; }
|
||||
else { s.text = "Disconnected"; s.color = Error(); }
|
||||
if (!lw) { s.text = TR("console_backend_unavailable"); s.color = Error(); return s; }
|
||||
if (lw->walletOpen()) { s.text = TR("lite_net_connected"); s.color = Success(); }
|
||||
else if (lw->openInProgress()) { s.text = TR("lite_net_connecting"); s.color = Warning(); s.pulse = true; }
|
||||
else { s.text = TR("lite_net_disconnected"); s.color = Error(); }
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
struct ConsoleCommandCategory; // console_command_reference.h — command-reference table
|
||||
|
||||
using ConsoleAddLineFn = std::function<void(const std::string&, ConsoleChannel)>;
|
||||
|
||||
struct ConsoleStatusLine {
|
||||
@@ -64,7 +66,13 @@ public:
|
||||
virtual void pollLogLines(const ConsoleAddLineFn& add) { (void)add; }
|
||||
|
||||
// UI-chrome capabilities.
|
||||
virtual bool hasRpcReference() const { return false; } // show the RPC command-reference popup
|
||||
// True when this backend speaks JSON-RPC to a daemon (the full node). Gates daemon-specific
|
||||
// console behavior (the 'stop' shutdown confirm, "not connected to daemon" wording) and the
|
||||
// command-reference modal's JSON string-arg quoting — the lite backend takes bare tokens.
|
||||
virtual bool hasRpcReference() const { return false; }
|
||||
// The command-reference table this backend offers (browsed by the console's reference modal),
|
||||
// or nullptr for none. Full node = the JSON-RPC reference; lite = its own backend verbs.
|
||||
virtual const std::vector<ConsoleCommandCategory>* commandReference() const { return nullptr; }
|
||||
// Which log-filter toggles the toolbar should show (default: none).
|
||||
virtual ConsoleLogFilterCaps logFilterCaps() const { return {}; }
|
||||
|
||||
@@ -88,6 +96,7 @@ public:
|
||||
bool pollResult(std::string& result, bool& isError) override;
|
||||
void pollLogLines(const ConsoleAddLineFn& add) override;
|
||||
bool hasRpcReference() const override { return true; }
|
||||
const std::vector<ConsoleCommandCategory>* commandReference() const override;
|
||||
// Full node: daemon/xmrig log, errors-only, RPC trace, and app messages.
|
||||
ConsoleLogFilterCaps logFilterCaps() const override { return {true, true, true, true}; }
|
||||
void printHelp(const ConsoleAddLineFn& add) override;
|
||||
@@ -104,6 +113,9 @@ private:
|
||||
bool last_rpc_connected_ = false;
|
||||
std::deque<std::pair<std::string, bool>> results_; // {text, isError}
|
||||
std::mutex results_mutex_;
|
||||
// Chat diagnostics ring cursor: chat code logs via wallet::liteLog on BOTH variants, so the full-node
|
||||
// console surfaces those lines (as App messages) too — mirrors LiteConsoleExecutor's diag drain.
|
||||
std::uint64_t diag_gen_ = static_cast<std::uint64_t>(-1);
|
||||
};
|
||||
|
||||
// ── Lite: diagnostics ring + backend console command ─────────────────────────
|
||||
@@ -119,6 +131,7 @@ public:
|
||||
// Lite: no daemon log / RPC trace — its diagnostics ring maps to the App + Error
|
||||
// channels, so offer errors-only + app-messages (plus the always-shown text filter).
|
||||
ConsoleLogFilterCaps logFilterCaps() const override { return {false, true, false, true}; }
|
||||
const std::vector<ConsoleCommandCategory>* commandReference() const override;
|
||||
void printHelp(const ConsoleAddLineFn& add) override;
|
||||
std::vector<ConsoleStatusLine> statusLines() const override;
|
||||
ConsoleStatusLine toolbarStatus() const override;
|
||||
|
||||
@@ -280,6 +280,105 @@ const ConsoleCommandEntry kUtilityCommands[] = {
|
||||
"reconsiderblock \"0000000000abc123\"", "undo invalidate accept block again re-enable block restore chain reconsider fix rollback fork", true},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Lite backend command set — the verbs the SDXL lite backend accepts (see its
|
||||
// commands.rs get_commands()). Unlike the full node these are NOT JSON-RPC: the
|
||||
// backend takes plain, space-separated tokens (no quoting), so the reference
|
||||
// modal inserts them bare. clear/help/quit are intentionally omitted — the C++
|
||||
// console tab intercepts those before they reach the backend.
|
||||
// ============================================================================
|
||||
|
||||
const ConsoleCommandEntry kLiteWalletCommands[] = {
|
||||
{"balance", "Show your DRGX balance", "",
|
||||
"Shows the DRGX balance held in this wallet across all its shielded and transparent addresses.",
|
||||
"balance", "money funds amount total holdings"},
|
||||
{"addresses", "List all addresses in the wallet", "",
|
||||
"Lists every address this wallet owns \xE2\x80\x94 shielded (zs1...) and transparent (R.../t...) \xE2\x80\x94 so you can pick one to receive to.",
|
||||
"addresses", "receive address list wallet mine deposit"},
|
||||
{"new", "Create a new address in this wallet", "type",
|
||||
"Creates a fresh receive address. Pass zs for a shielded (private) sapling address, or R (a capital R) for a transparent one \xE2\x80\x94 those exact tokens (case-sensitive).",
|
||||
"new zs", "create address receive generate new shielded transparent zs"},
|
||||
{"list", "List all transactions in the wallet", "",
|
||||
"Shows the wallet's transaction history \xE2\x80\x94 sends, receives and shields \xE2\x80\x94 with amounts, addresses and confirmations.",
|
||||
"list", "history transactions txs payments activity sent received"},
|
||||
{"notes", "List sapling notes and UTXOs", "[all]",
|
||||
"Lists the individual shielded notes and transparent UTXOs that make up your balance. Pass all to include spent ones.",
|
||||
"notes", "utxo notes unspent coins inputs sapling"},
|
||||
{"info", "Get the lightwalletd server's info", "",
|
||||
"Reports the lightwalletd server the wallet is connected to \xE2\x80\x94 its version, chain and block height.",
|
||||
"info", "server node lightwalletd version connection status"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteSyncCommands[] = {
|
||||
{"sync", "Download compact blocks and sync to the server", "",
|
||||
"Fetches new compact blocks from the lightwalletd server and scans them for transactions belonging to this wallet.",
|
||||
"sync", "update refresh scan blocks download catch up"},
|
||||
{"syncstatus", "Get the sync status of the wallet", "",
|
||||
"Reports how far the wallet has synced \xE2\x80\x94 whether a sync is in progress and the block it has reached.",
|
||||
"syncstatus", "progress status syncing scanning percent blocks"},
|
||||
{"height", "Get the latest block height the wallet is at", "",
|
||||
"Shows the block height the wallet has scanned up to. Compare with the network height to gauge sync.",
|
||||
"height", "block height number chain tip synced"},
|
||||
{"rescan", "Rescan the wallet from scratch", "",
|
||||
"Discards the scanned state and re-downloads/re-scans every block from the wallet's birthday. Slow, but fixes a stuck or incomplete balance.",
|
||||
"rescan", "rescan resync repair fix balance rebuild from scratch"},
|
||||
{"save", "Save the wallet file to disk", "",
|
||||
"Writes the current wallet state to disk. The wallet also saves automatically after a sync or send.",
|
||||
"save", "save persist write disk store"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteSendCommands[] = {
|
||||
{"send", "Send DRGX to an address", "[{\"address\":\"zs1...\",\"amount\":0}]",
|
||||
"Sends DRGX from the console using a JSON array of recipients (the Send tab is the easy way; this is the power-user form). amount is in puposhis (the base unit); memo is optional and delivered privately to shielded recipients.",
|
||||
"send [{\"address\":\"zs1exampleaddress\",\"amount\":100000000,\"memo\":\"thanks\"}]",
|
||||
"pay transfer send spend money transaction json", true},
|
||||
{"shield", "Shield transparent DRGX into a sapling address", "[address]",
|
||||
"Moves your transparent (public) DRGX into a shielded sapling address for privacy. With no address it shields to the wallet's own sapling address.",
|
||||
"shield", "shield private sapling transparent move protect", true},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteKeyCommands[] = {
|
||||
{"seed", "Display the wallet seed phrase", "",
|
||||
"Reveals the seed phrase that backs up this wallet. Anyone who sees it can spend your funds \xE2\x80\x94 keep it secret and offline.",
|
||||
"seed", "seed phrase mnemonic backup recovery words secret", true},
|
||||
{"export", "Export the private key for an address", "[address]",
|
||||
"Prints the private/spending key for a wallet address \xE2\x80\x94 anyone with it controls the funds. With no address it exports every key.",
|
||||
"export zs1exampleaddress", "export private key spending backup secret", true},
|
||||
{"import", "Import a spending or viewing key", "key",
|
||||
"Imports a shielded spending or viewing key (pass just the key). The wallet rescans from the sapling activation height to find the key's transactions.",
|
||||
"import somekey", "import restore key spending viewing add watch"},
|
||||
{"timport", "Import a transparent WIF private key", "wif",
|
||||
"Imports a transparent private key in WIF format (begins with U, 5, K or L) so the wallet can spend its funds.",
|
||||
"timport somewifkey", "import transparent wif private key taddr"},
|
||||
{"encrypt", "Encrypt the wallet with a password", "password",
|
||||
"Encrypts the wallet with a password and locks it immediately. You will need the password to send or reveal keys afterwards. If you forget it, only the seed phrase can recover the wallet.",
|
||||
"encrypt strongpassword", "encrypt password protect lock secure passphrase", true},
|
||||
{"decrypt", "Completely remove wallet encryption", "password",
|
||||
"Permanently removes the wallet's password encryption, leaving it unprotected on disk. Requires the current password.",
|
||||
"decrypt strongpassword", "decrypt remove encryption password unprotect", true},
|
||||
{"unlock", "Unlock the wallet for spending", "password",
|
||||
"Temporarily unlocks an encrypted wallet so it can send or reveal keys. Use lock to re-lock it.",
|
||||
"unlock strongpassword", "unlock password spend open temporarily"},
|
||||
{"lock", "Lock a temporarily-unlocked wallet", "",
|
||||
"Re-locks a wallet that was unlocked for spending, without removing its encryption.",
|
||||
"lock", "lock secure re-lock protect close"},
|
||||
{"encryptionstatus", "Check if the wallet is encrypted and locked", "",
|
||||
"Reports whether the wallet is encrypted and, if so, whether it is currently locked or unlocked.",
|
||||
"encryptionstatus", "encryption status locked unlocked encrypted state"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kLiteAdvancedCommands[] = {
|
||||
{"sietch", "Create a Sietch address", "[type]",
|
||||
"Creates a Sietch address, used for enhanced-privacy sends. Pass zs for a sapling Sietch address.",
|
||||
"sietch zs", "sietch privacy address decoy sapling advanced"},
|
||||
{"saplingtree", "Dump the latest Sapling commitment tree (debug)", "",
|
||||
"Prints the latest Sapling commitment tree state \xE2\x80\x94 a debugging aid, not needed for everyday use.",
|
||||
"saplingtree", "sapling tree commitment debug advanced merkle"},
|
||||
{"coinsupply", "Get the coin supply info", "",
|
||||
"Reports coin-supply figures for the chain as seen by the wallet's server.",
|
||||
"coinsupply", "supply coins total emission circulating amount"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<ConsoleCommandCategory>& consoleCommandCategories()
|
||||
@@ -296,5 +395,17 @@ const std::vector<ConsoleCommandCategory>& consoleCommandCategories()
|
||||
return categories;
|
||||
}
|
||||
|
||||
const std::vector<ConsoleCommandCategory>& liteConsoleCommandCategories()
|
||||
{
|
||||
static const std::vector<ConsoleCommandCategory> categories = {
|
||||
{"Wallet", kLiteWalletCommands, CountOf(kLiteWalletCommands)},
|
||||
{"Sync", kLiteSyncCommands, CountOf(kLiteSyncCommands)},
|
||||
{"Send", kLiteSendCommands, CountOf(kLiteSendCommands)},
|
||||
{"Keys & Security", kLiteKeyCommands, CountOf(kLiteKeyCommands)},
|
||||
{"Advanced", kLiteAdvancedCommands, CountOf(kLiteAdvancedCommands)},
|
||||
};
|
||||
return categories;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -23,7 +23,12 @@ struct ConsoleCommandCategory {
|
||||
int count;
|
||||
};
|
||||
|
||||
// The full-node daemon's JSON-RPC command reference (browsed by the console's command-reference modal).
|
||||
const std::vector<ConsoleCommandCategory>& consoleCommandCategories();
|
||||
|
||||
// The lite backend's own command set (the ~25 verbs the SDXL backend accepts) — the lite-variant
|
||||
// analog of the RPC reference, shown by the same modal when the executor is the lite one.
|
||||
const std::vector<ConsoleCommandCategory>& liteConsoleCommandCategories();
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -311,8 +311,12 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
computeVisibleLines(has_text_filter_, filter_lower_);
|
||||
|
||||
// Main console layout
|
||||
// NoScrollWithMouse: the inner ConsoleOutput owns wheel scrolling (via ApplySmoothScroll). Without
|
||||
// this, a wheel over the output would scroll BOTH the output (smooth-scroll) and this outer container
|
||||
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
|
||||
// Safe because the output panel is sized to fill the remaining height, so this outer never overflows.
|
||||
ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Optional status header (lite: sync + last error) above the toolbar.
|
||||
renderStatusHeader(exec);
|
||||
@@ -460,12 +464,18 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandsPopupModal()
|
||||
void ConsoleTab::renderCommandsPopupModal(ConsoleCommandExecutor* exec)
|
||||
{
|
||||
if (!show_commands_popup_) {
|
||||
return;
|
||||
}
|
||||
renderCommandsPopup();
|
||||
// Need a backend that offers a reference table. If the console hasn't built its executor yet
|
||||
// (popup can't have been opened normally) or the backend has none, just dismiss.
|
||||
if (!exec || !exec->commandReference()) {
|
||||
show_commands_popup_ = false;
|
||||
return;
|
||||
}
|
||||
renderCommandsPopup(*exec);
|
||||
}
|
||||
|
||||
void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
@@ -540,14 +550,16 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Commands reference button (full-node RPC reference only)
|
||||
if (exec.hasRpcReference()) {
|
||||
// Commands reference button — shown whenever the backend offers a reference table (full-node
|
||||
// JSON-RPC commands, or the lite backend's own verbs).
|
||||
if (exec.commandReference()) {
|
||||
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
command_search_[0] = '\0'; // fresh search each open (dismiss paths don't all reset it)
|
||||
show_commands_popup_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", TR("console_show_rpc_ref"));
|
||||
material::Tooltip("%s", exec.hasRpcReference() ? TR("console_show_rpc_ref")
|
||||
: TR("console_show_backend_ref"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
@@ -568,12 +580,14 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float btnSz = ImGui::GetFrameHeight();
|
||||
// Capture the flag BEFORE the button can flip it — the push/pop guards must use the SAME value,
|
||||
// or a click leaves the colour stack unbalanced (ImGui then draws a red error rect on the window).
|
||||
const bool dim = !s_line_accents_enabled;
|
||||
const char* icon = s_line_accents_enabled ? ICON_MD_FORMAT_COLOR_FILL : ICON_MD_FORMAT_COLOR_RESET;
|
||||
if (!s_line_accents_enabled)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (dim) ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (TactileButton(icon, ImVec2(btnSz, btnSz), Type().iconMed()))
|
||||
s_line_accents_enabled = !s_line_accents_enabled;
|
||||
if (!s_line_accents_enabled) ImGui::PopStyleColor();
|
||||
if (dim) ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents"));
|
||||
}
|
||||
|
||||
@@ -581,11 +595,11 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float btnSz = ImGui::GetFrameHeight();
|
||||
if (!s_line_text_color_enabled)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
const bool dim = !s_line_text_color_enabled; // capture before the click flips it (balanced push/pop)
|
||||
if (dim) ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
if (TactileButton(ICON_MD_FORMAT_COLOR_TEXT, ImVec2(btnSz, btnSz), Type().iconMed()))
|
||||
s_line_text_color_enabled = !s_line_text_color_enabled;
|
||||
if (!s_line_text_color_enabled) ImGui::PopStyleColor();
|
||||
if (dim) ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color"));
|
||||
}
|
||||
|
||||
@@ -605,7 +619,7 @@ void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
|
||||
ImVec2 cp = ImGui::GetCursorScreenPos();
|
||||
float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale();
|
||||
float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f;
|
||||
float dotX = cp.x + dotR + 2;
|
||||
float dotX = cp.x + dotR + 2.0f * Layout::dpiScale();
|
||||
|
||||
if (st.pulse) {
|
||||
float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size);
|
||||
@@ -615,7 +629,7 @@ void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
|
||||
dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, st.color);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(dotR * 2 + 6, 0));
|
||||
ImGui::Dummy(ImVec2(dotR * 2 + 6.0f * Layout::dpiScale(), 0));
|
||||
ImGui::SameLine();
|
||||
Type().textColored(TypeStyle::Caption, st.color, st.text.c_str());
|
||||
} else {
|
||||
@@ -1428,18 +1442,22 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
||||
exec.printHelp(add);
|
||||
} else if (first == "quit" || first == "exit") {
|
||||
addLine(TR("console_quit_note"), ConsoleChannel::Info);
|
||||
} else if (first == "stop") {
|
||||
} else if (first == "stop" && exec.hasRpcReference()) {
|
||||
// Full-node 'stop' shuts down the daemon (destructive) — gate behind a confirming second
|
||||
// 'stop'. Lite has no node: `stop` isn't a backend verb, so it falls through below and the
|
||||
// backend reports it as unknown — no misleading "shut down the node" warning or dead gate.
|
||||
if (!stop_confirm_pending_) {
|
||||
stop_confirm_pending_ = true;
|
||||
addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.",
|
||||
ConsoleChannel::Warning);
|
||||
addLine(TR("console_stop_confirm_node"), ConsoleChannel::Warning);
|
||||
} else {
|
||||
stop_confirm_pending_ = false;
|
||||
if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||
else exec.submit(cmd);
|
||||
}
|
||||
} else if (!exec.isReady()) {
|
||||
addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||
// Full node connects to a daemon; the lite backend opens a wallet — word the error per variant.
|
||||
addLine(exec.hasRpcReference() ? TR("console_not_connected") : TR("console_not_connected_lite"),
|
||||
ConsoleChannel::Error);
|
||||
} else {
|
||||
exec.submit(cmd);
|
||||
}
|
||||
@@ -1551,6 +1569,11 @@ const char* consoleCategoryLabel(const char* name)
|
||||
if (!std::strcmp(name, "Wallet")) return TR("console_cat_wallet");
|
||||
if (!std::strcmp(name, "Raw Transactions")) return TR("console_cat_raw_transactions");
|
||||
if (!std::strcmp(name, "Utility")) return TR("console_cat_utility");
|
||||
// Lite backend reference categories.
|
||||
if (!std::strcmp(name, "Sync")) return TR("console_cat_sync");
|
||||
if (!std::strcmp(name, "Send")) return TR("console_cat_send");
|
||||
if (!std::strcmp(name, "Keys & Security")) return TR("console_cat_keys");
|
||||
if (!std::strcmp(name, "Advanced")) return TR("console_cat_advanced");
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
@@ -1570,7 +1593,7 @@ void ConsoleTab::insertCommandToInput(const ConsoleCommandEntry& cmd)
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel)
|
||||
void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel, bool jsonArgs)
|
||||
{
|
||||
using namespace material;
|
||||
float dp = Layout::dpiScale();
|
||||
@@ -1621,8 +1644,11 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(labelW);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
// Lite backend args are bare freeform tokens, so its param types (string/number) don't
|
||||
// apply — show a neutral "value" hint there instead of a misleading "number".
|
||||
std::string typeHint = jsonArgs ? s.type : std::string(TR("console_ref_value"));
|
||||
std::string hint = s.optional
|
||||
? (s.type + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : s.type;
|
||||
? (typeHint + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : typeHint;
|
||||
ImGui::InputTextWithHint("##pv", hint.c_str(), cmd_param_bufs_[k], sizeof(cmd_param_bufs_[k]));
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -1652,7 +1678,9 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
built += " " + specs[k].raw;
|
||||
complete = false;
|
||||
} else {
|
||||
if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
|
||||
// JSON-RPC (full node) auto-quotes string args; the lite backend takes bare tokens, so
|
||||
// leave the value exactly as typed there (quoting would break its address/key parsing).
|
||||
if (jsonArgs && specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
|
||||
val.front() != '[' && val.front() != '{')
|
||||
val = "\"" + val + "\"";
|
||||
built += " " + val;
|
||||
@@ -1725,13 +1753,16 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleTab::renderCommandsPopup()
|
||||
void ConsoleTab::renderCommandsPopup(ConsoleCommandExecutor& exec)
|
||||
{
|
||||
using namespace material;
|
||||
float dp = Layout::dpiScale();
|
||||
|
||||
// Full node speaks JSON-RPC (quote string args); the lite backend takes bare tokens.
|
||||
const bool jsonArgs = exec.hasRpcReference();
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("console_rpc_reference");
|
||||
ov.title = jsonArgs ? TR("console_rpc_reference") : TR("console_backend_reference");
|
||||
ov.p_open = &show_commands_popup_;
|
||||
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
||||
ov.cardWidth = 960.0f; // wide enough for two panes
|
||||
@@ -1760,7 +1791,7 @@ void ConsoleTab::renderCommandsPopup()
|
||||
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
|
||||
const bool searching = !q.empty();
|
||||
|
||||
const auto& categories = consoleCommandCategories();
|
||||
const auto& categories = *exec.commandReference(); // non-null: guarded by renderCommandsPopupModal
|
||||
|
||||
// Flat display order of (cat,idx): ranked when searching, category order when browsing. Drives
|
||||
// keyboard nav + auto-selection; the browse view still renders grouped headers below.
|
||||
@@ -1902,7 +1933,7 @@ void ConsoleTab::renderCommandsPopup()
|
||||
if (cmd_sel_cat_ >= 0 && cmd_sel_cat_ < (int)categories.size() &&
|
||||
cmd_sel_idx_ >= 0 && cmd_sel_idx_ < categories[cmd_sel_cat_].count) {
|
||||
renderCommandDetail(categories[cmd_sel_cat_].commands[cmd_sel_idx_],
|
||||
consoleCategoryLabel(categories[cmd_sel_cat_].name));
|
||||
consoleCategoryLabel(categories[cmd_sel_cat_].name), jsonArgs);
|
||||
} else {
|
||||
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||
ImGui::SetCursorPosY(av.y * 0.4f);
|
||||
|
||||
@@ -44,10 +44,11 @@ public:
|
||||
void render(ConsoleCommandExecutor& exec);
|
||||
|
||||
/**
|
||||
* @brief Render the RPC Command Reference popup at top-level scope.
|
||||
* Must be called outside any child window so the modal blocks all input.
|
||||
* @brief Render the command-reference popup at top-level scope.
|
||||
* Must be called outside any child window so the modal blocks all input. `exec` supplies the
|
||||
* reference table (full-node RPC vs lite backend verbs); may be null (no console yet) -> no-op.
|
||||
*/
|
||||
void renderCommandsPopupModal();
|
||||
void renderCommandsPopupModal(ConsoleCommandExecutor* exec);
|
||||
|
||||
// Debug/UI-sweep hook: force the RPC command-reference popup open/closed so the full UI sweep
|
||||
// can capture it (the popup is otherwise opened only by a toolbar button).
|
||||
@@ -115,8 +116,9 @@ private:
|
||||
// Format a completed command result (JSON role -> channel) into console lines.
|
||||
void addFormattedResult(const std::string& result, bool is_error);
|
||||
void renderStatusHeader(ConsoleCommandExecutor& exec);
|
||||
void renderCommandsPopup();
|
||||
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel); // right pane
|
||||
void renderCommandsPopup(ConsoleCommandExecutor& exec);
|
||||
// right pane; jsonArgs=true (full-node RPC) auto-quotes string args, false (lite) inserts bare
|
||||
void renderCommandDetail(const ConsoleCommandEntry& cmd, const char* catLabel, bool jsonArgs);
|
||||
void insertCommandToInput(const ConsoleCommandEntry& cmd); // fill input + close the modal
|
||||
|
||||
// renderToolbar() draws the top bar; these are its sub-steps:
|
||||
@@ -186,10 +188,11 @@ private:
|
||||
// consumed by the renderer + hit-testing.
|
||||
mutable ConsoleLayout layout_;
|
||||
|
||||
// Commands popup (RPC command explorer)
|
||||
// Commands popup (command explorer) — indexes into the active executor's commandReference()
|
||||
// table (full-node RPC commands or the lite backend verbs), not always the full-node one.
|
||||
bool show_commands_popup_ = false;
|
||||
char command_search_[128] = {0}; // RPC-reference search filter (cleared when the modal opens)
|
||||
int cmd_sel_cat_ = -1; // detail-pane selection: category index into consoleCommandCategories()
|
||||
char command_search_[128] = {0}; // reference search filter (cleared when the modal opens)
|
||||
int cmd_sel_cat_ = -1; // detail-pane selection: category index into that table
|
||||
int cmd_sel_idx_ = -1; // detail-pane selection: command index within that category
|
||||
std::string pending_submit_; // command to run next frame (deferred so the modal needs no executor)
|
||||
const ConsoleCommandEntry* run_confirm_cmd_ = nullptr; // destructive "Insert & run" awaiting confirmation
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace ui {
|
||||
static int s_selected_index = -1;
|
||||
static bool s_show_add_dialog = false;
|
||||
static bool s_show_edit_dialog = false;
|
||||
static bool s_show_contacts_settings = false; // Contacts customization modal (gear button)
|
||||
static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens
|
||||
static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms
|
||||
static char s_edit_label[128] = "";
|
||||
@@ -81,6 +82,8 @@ struct AvatarTex {
|
||||
static std::unordered_map<std::string, AvatarTex> s_avatarTexCache;
|
||||
static int s_avatarLoadsThisFrame = 0;
|
||||
static bool s_animateAvatars = true; // mirrors the setting; refreshed each frame in RenderContactsTab
|
||||
static int s_contactsShape = 0; // avatar shape (mirrors getContactsAvatarShape: 0/1/2)
|
||||
static float s_contactsScale = 1.0f; // card/list row scale (mirrors getContactsListScale)
|
||||
static bool s_avatarAnimatedThisFrame = false; // set when a live animated frame is drawn -> keep redrawing
|
||||
static constexpr int kAvatarLoadsPerFrame = 2;
|
||||
static constexpr int kAvatarMaxFrames = 300; // cap frames per animated avatar (bounds VRAM/decode)
|
||||
@@ -134,22 +137,48 @@ static void invalidateAvatarTexture(const std::string& path) {
|
||||
s_avatarTexCache.erase(it);
|
||||
}
|
||||
|
||||
// Draw a contact's avatar into the circle at `c` (radius `r`): a custom image (circular-cropped), a
|
||||
// Material icon in a tinted circle, or — the default — the Z/T type badge.
|
||||
// Draw a contact's avatar: a custom image, a Material icon in a tinted shape, or the Z/T type badge.
|
||||
// `shape` picks the outline: 0 = circle (centered at `c`, radius `r`); 1 = rounded square (same box as
|
||||
// the circle, all corners rounded); 2 = a full-row-height tab spanning [tabMin, tabMax] with rounded
|
||||
// LEFT corners (radius tabRound) + a straight right edge. The inner glyph is sized to the shape, so it
|
||||
// stays proportional at any list scale.
|
||||
static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::AddressBookEntry& e,
|
||||
bool shielded, ImU32 typeCol, bool light, float dp,
|
||||
ImFont* letterFont, ImFont* iconFont, bool onScreen = true) {
|
||||
ImFont* letterFont, ImFont* iconFont, bool onScreen = true, int shape = 0,
|
||||
ImVec2 tabMin = ImVec2(0, 0), ImVec2 tabMax = ImVec2(0, 0), float tabRound = 0.0f) {
|
||||
const bool circle = (shape == 0);
|
||||
// Resolve the shape rectangle, its rounding, the content center + radius, and the glyph size.
|
||||
ImVec2 cc = c; float cr = r;
|
||||
ImVec2 sMin(c.x - r, c.y - r), sMax(c.x + r, c.y + r);
|
||||
float sRound = r * 0.42f;
|
||||
ImDrawFlags sFlags = ImDrawFlags_RoundCornersAll;
|
||||
if (shape == 2) {
|
||||
sMin = tabMin; sMax = tabMax; sRound = tabRound; sFlags = ImDrawFlags_RoundCornersLeft;
|
||||
cc = ImVec2((tabMin.x + tabMax.x) * 0.5f, (tabMin.y + tabMax.y) * 0.5f);
|
||||
cr = (tabMax.y - tabMin.y) * 0.5f;
|
||||
}
|
||||
const float glyphSz = cr * 0.92f; // ~46% of the shape diameter — matches the classic badge ratio
|
||||
auto fillShape = [&](ImU32 col) {
|
||||
if (circle) dl->AddCircleFilled(cc, cr, col);
|
||||
else dl->AddRectFilled(sMin, sMax, col, sRound, sFlags);
|
||||
};
|
||||
auto borderShape = [&](ImU32 col, float th) {
|
||||
if (circle) dl->AddCircle(cc, cr, col, 0, th);
|
||||
else dl->AddRect(sMin, sMax, col, sRound, sFlags, th);
|
||||
};
|
||||
const std::string& av = e.avatar;
|
||||
if (av.rfind("img:", 0) == 0) {
|
||||
const AvatarTex* t = getAvatarTexture(av.substr(4));
|
||||
ImTextureID tex = currentAvatarFrame(t, onScreen);
|
||||
if (tex) {
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1; // centre-crop to a square so the circle isn't stretched
|
||||
if (t->w > t->h) { float m = (t->w - t->h) * 0.5f / t->w; u0 = m; u1 = 1 - m; }
|
||||
else if (t->h > t->w) { float m = (t->h - t->w) * 0.5f / t->h; v0 = m; v1 = 1 - m; }
|
||||
dl->AddImageRounded(tex, ImVec2(c.x - r, c.y - r), ImVec2(c.x + r, c.y + r),
|
||||
ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE, r);
|
||||
dl->AddCircle(c, r, material::WithAlpha(material::OnSurface(), 45), 0, 1.0f * dp);
|
||||
const float aspect = (sMax.x - sMin.x) / std::max(1.0f, sMax.y - sMin.y); // crop to the shape's aspect
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1;
|
||||
const float tw = static_cast<float>(t->w), th2 = static_cast<float>(t->h);
|
||||
if (tw / th2 > aspect) { float keep = aspect * th2 / tw; float m = (1 - keep) * 0.5f; u0 = m; u1 = 1 - m; }
|
||||
else { float keep = (tw / aspect) / th2; float m = (1 - keep) * 0.5f; v0 = m; v1 = 1 - m; }
|
||||
dl->AddImageRounded(tex, sMin, sMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE,
|
||||
circle ? cr : sRound, circle ? ImDrawFlags_RoundCornersAll : sFlags);
|
||||
borderShape(material::WithAlpha(material::OnSurface(), 45), 1.0f * dp);
|
||||
return;
|
||||
}
|
||||
// fall through to the type badge if the image failed to load
|
||||
@@ -158,19 +187,19 @@ static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::Add
|
||||
const bool known = (iconName == material::project_icons::kPickaxeName) ||
|
||||
(material::project_icons::glyphForName(iconName) != nullptr);
|
||||
if (known) {
|
||||
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
|
||||
material::project_icons::drawByName(dl, iconName, c, typeCol, iconFont, iconFont->LegacySize);
|
||||
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
|
||||
material::project_icons::drawByName(dl, iconName, cc, typeCol, iconFont, glyphSz);
|
||||
return;
|
||||
}
|
||||
// fall through to the type badge if the icon name is unknown
|
||||
}
|
||||
// Default: Z/T type badge.
|
||||
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
|
||||
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
|
||||
const char* letter = shielded ? "Z" : "T";
|
||||
const ImVec2 ls = letterFont->CalcTextSizeA(letterFont->LegacySize, FLT_MAX, 0, letter);
|
||||
dl->AddText(letterFont, letterFont->LegacySize, ImVec2(c.x - ls.x * 0.5f, c.y - ls.y * 0.5f), typeCol, letter);
|
||||
const ImVec2 ls = letterFont->CalcTextSizeA(glyphSz, FLT_MAX, 0, letter);
|
||||
dl->AddText(letterFont, glyphSz, ImVec2(cc.x - ls.x * 0.5f, cc.y - ls.y * 0.5f), typeCol, letter);
|
||||
}
|
||||
|
||||
static bool isShieldedAddr(const std::string& a) {
|
||||
@@ -268,6 +297,8 @@ void RenderContactsTab(App* app)
|
||||
{
|
||||
s_avatarLoadsThisFrame = 0; // reset the per-frame avatar-decode budget (see getAvatarTexture)
|
||||
s_animateAvatars = !app->settings() || app->settings()->getAnimateAvatars();
|
||||
s_contactsShape = app->settings() ? app->settings()->getContactsAvatarShape() : 0;
|
||||
s_contactsScale = app->settings() ? app->settings()->getContactsListScale() : 1.0f;
|
||||
auto& S = schema::UI();
|
||||
// Reuse the existing address-book schema/column config for the table + add/edit form.
|
||||
auto addrTable = S.table("dialogs.address-book", "address-table");
|
||||
@@ -416,9 +447,12 @@ void RenderContactsTab(App* app)
|
||||
const bool sh = isShieldedAddr(s_edit_address);
|
||||
const ImU32 tu = contactTypeColor(sh, light);
|
||||
ImVec2 avC(pMin.x + pad + avR, pMin.y + ph * 0.5f);
|
||||
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF);
|
||||
const float pTabW = ph; // full preview-height square tab (shape 2)
|
||||
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF, true,
|
||||
s_contactsShape, pMin, ImVec2(pMin.x + pTabW, pMax.y), 10.0f * dp);
|
||||
|
||||
float tx = avC.x + avR + Layout::spacingMd();
|
||||
float tx = (s_contactsShape == 2) ? (pMin.x + pTabW + Layout::spacingMd())
|
||||
: (avC.x + avR + Layout::spacingMd());
|
||||
float textRight = pMax.x - pad;
|
||||
float ty = pMin.y + (ph - blockH) * 0.5f;
|
||||
const bool hasName = s_edit_label[0] != '\0';
|
||||
@@ -819,7 +853,8 @@ void RenderContactsTab(App* app)
|
||||
// Inline tab content lives in a scroll child (mirrors peers_tab / explorer_tab).
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##ContactsScroll", avail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse); // inner ##contactList owns the wheel (smooth scroll)
|
||||
|
||||
// Toolbar — icon + label actions; the primary "Add New" is accented so it stands out.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
@@ -885,8 +920,10 @@ void RenderContactsTab(App* app)
|
||||
const char* segIcons[3] = { ICON_MD_VIEW_AGENDA, ICON_MD_VIEW_LIST, ICON_MD_TABLE_ROWS };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const float segW = 44.0f * dp * 3.0f;
|
||||
const float segGap = 6.0f * dp;
|
||||
const float grpW = segW + segGap + segH; // view toggle + gap + a square settings gear
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - segW));
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - grpW));
|
||||
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||
segIcons, 3, viewMode, material::Type().iconSmall(),
|
||||
@@ -896,11 +933,21 @@ void RenderContactsTab(App* app)
|
||||
app->settings()->save();
|
||||
viewMode = segClk;
|
||||
}
|
||||
ImGui::Dummy(ImVec2(segW, segH));
|
||||
// Contacts-customization gear, immediately right of the view toggle.
|
||||
ImGui::SetCursorScreenPos(ImVec2(segOrigin.x + segW + segGap, segOrigin.y));
|
||||
material::IconButtonStyle gst;
|
||||
gst.tooltip = TR("contacts_settings_tip");
|
||||
gst.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
gst.restBg = material::WithAlpha(material::OnSurface(), 20);
|
||||
gst.bgRounding = 7.0f * dp;
|
||||
if (material::IconButton("##contactsSettings", ICON_MD_SETTINGS, material::Type().iconSmall(),
|
||||
ImVec2(segH, segH), gst))
|
||||
s_show_contacts_settings = true;
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(grpW, segH));
|
||||
}
|
||||
|
||||
// Search / filter
|
||||
ImGui::Spacing();
|
||||
// Search / filter (tight against the toolbar row above — no extra spacer)
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
|
||||
s_search, sizeof(s_search));
|
||||
@@ -958,9 +1005,23 @@ void RenderContactsTab(App* app)
|
||||
|
||||
if (viewMode == 2) {
|
||||
// ── TABLE mode (material-ized): no outer/grid borders, row backgrounds, interactive sort. ──
|
||||
// Frosted-glass panel behind the table so it blurs the backdrop like the card/list view (the
|
||||
// table's translucent row backgrounds let the blur show through). A BeginTable can't take
|
||||
// AlwaysUseWindowPadding, so inset the table itself inside the glass so its headers/cells
|
||||
// don't hug the container edges.
|
||||
ImDrawList* tpdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 tpMin = ImGui::GetCursorScreenPos();
|
||||
const float tpW = ImGui::GetContentRegionAvail().x;
|
||||
const float tIpad = 12.0f * dp, tVpad = 10.0f * dp;
|
||||
{
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(tpdl, tpMin, ImVec2(tpMin.x + tpW, tpMin.y + listH), g);
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(tpMin.x + tIpad, tpMin.y + tVpad));
|
||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||
ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, listH)))
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
|
||||
ImVec2(tpW - 2.0f * tIpad, listH - 2.0f * tVpad)))
|
||||
{
|
||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultSort, 1.5f);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch, 2.6f);
|
||||
@@ -1009,9 +1070,12 @@ void RenderContactsTab(App* app)
|
||||
s_selected_index = static_cast<int>(i); s_confirm_delete_idx = -1; openContextMenu = true;
|
||||
}
|
||||
bool tAvOnScreen = ImGui::IsRectVisible(tAvP, ImVec2(tAvP.x + tAvR * 2.0f, tAvP.y + tLineH));
|
||||
// The table is a compact inline view: a full-height "tab" (shape 2) can't render inside a
|
||||
// table cell, so it falls back to the rounded square there (table scale is fixed too).
|
||||
const int tShape = (s_contactsShape == 2) ? 1 : s_contactsShape;
|
||||
drawContactAvatar(ImGui::GetWindowDrawList(), ImVec2(tAvP.x + tAvR, tAvP.y + tLineH * 0.5f),
|
||||
tAvR, entry, shielded, typeColor(shielded), lightTheme, dp,
|
||||
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen);
|
||||
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen, tShape);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T");
|
||||
@@ -1033,6 +1097,10 @@ void RenderContactsTab(App* app)
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
// Land the cursor at the glass-panel bottom (tpMin.y + listH) so the count footer lines up
|
||||
// with the Cards/List views. The table is inset by tVpad and its outer_size is listH-2*tVpad,
|
||||
// so it would otherwise end tVpad higher and pull the footer up.
|
||||
ImGui::SetCursorScreenPos(ImVec2(tpMin.x, tpMin.y + listH));
|
||||
} else {
|
||||
// ── CARDS (0) / LIST (1) mode — tactile Material items, no grid lines. ──
|
||||
const bool asCard = (viewMode == 0);
|
||||
@@ -1043,8 +1111,22 @@ void RenderContactsTab(App* app)
|
||||
return toLower(entries[a].label).compare(toLower(entries[b].label)) < 0;
|
||||
});
|
||||
}
|
||||
// Frosted-glass list container so the contacts area blurs the backdrop (peers_tab pattern:
|
||||
// glass behind a NoBackground list child) instead of showing the raw texture through the pane.
|
||||
{
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
const float pW = ImGui::GetContentRegionAvail().x;
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(pdl, pMin, ImVec2(pMin.x + pW, pMin.y + listH), g);
|
||||
}
|
||||
// AlwaysUseWindowPadding: a borderless child ignores WindowPadding without it, so the rows
|
||||
// would hug the frosted container's edges.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f * dp, 10.0f * dp));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0,0,0,0));
|
||||
ImGui::BeginChild("##contactList", ImVec2(0, listH), false, ImGuiWindowFlags_NoBackground);
|
||||
ImGui::BeginChild("##contactList", ImVec2(0, listH), ImGuiChildFlags_AlwaysUseWindowPadding,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll(); // wheel-lerp scroll, matching the rest of the app
|
||||
if (visibleRows.empty()) {
|
||||
emptyState(ICON_MD_CONTACTS, book.empty() ? "address_book_empty" : "contacts_search_no_match");
|
||||
} else {
|
||||
@@ -1056,10 +1138,13 @@ void RenderContactsTab(App* app)
|
||||
ImFont* lblF = material::Type().subtitle2();
|
||||
ImFont* adrF = material::Type().caption();
|
||||
ImFont* icoF = material::Type().iconSmall();
|
||||
const float sc = s_contactsScale; // card/list scale (avatar + text + row height; not the table)
|
||||
const float pad = 12.0f * dp;
|
||||
const float avR = 15.0f * dp;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + (asCard ? 16.0f * dp : 12.0f * dp);
|
||||
const float round = 10.0f * dp;
|
||||
const float lblSz = lblF->LegacySize * sc; // scaled label + address text
|
||||
const float adrSz = adrF->LegacySize * sc;
|
||||
bool rowDeleteRequested = false; // per-row delete is deferred until after the loop
|
||||
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||
size_t i = visibleRows[vi];
|
||||
@@ -1103,18 +1188,23 @@ void RenderContactsTab(App* app)
|
||||
// Only animate when the row is actually on-screen, so a scrolled-off animated avatar
|
||||
// doesn't hold the app awake (this loop has no clipper).
|
||||
const bool rowOnScreen = ImGui::IsRectVisible(mn, mx);
|
||||
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen);
|
||||
// Shape 2 = a full-row-height left tab (a square the height of the row, rounded-left to
|
||||
// match the row corner); shapes 0/1 are inset badges. Text shifts to clear the tab.
|
||||
const float tabW = rowH;
|
||||
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen,
|
||||
s_contactsShape, mn, ImVec2(mn.x + tabW, mx.y), asCard ? round : 0.0f);
|
||||
// Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions.
|
||||
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
|
||||
const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;
|
||||
const float trailW = 3.0f * actHit + 6.0f * dp + globeW;
|
||||
const float cy = mn.y + rowH * 0.5f;
|
||||
const float tx = mn.x + pad + avR * 2.0f + pad;
|
||||
const float tx = (s_contactsShape == 2) ? (mn.x + tabW + pad)
|
||||
: (mn.x + pad + avR * 2.0f + pad);
|
||||
const float textMaxX = mx.x - pad - trailW;
|
||||
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
|
||||
const float blockH = lblSz + adrSz + 3.0f * dp;
|
||||
const float ty = cy - blockH * 0.5f;
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||
dl->PopClipRect();
|
||||
// Un-collapse to the full address on hover (clipped to the text column so it never
|
||||
// runs under the trailing actions); middle-truncated otherwise.
|
||||
@@ -1122,7 +1212,7 @@ void RenderContactsTab(App* app)
|
||||
? entry.address
|
||||
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
|
||||
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
|
||||
material::OnSurfaceMedium(), addr.c_str());
|
||||
dl->PopClipRect();
|
||||
// Trailing: the globe badge stays pinned far-right (global contacts); per-row copy/edit/
|
||||
@@ -1183,6 +1273,7 @@ void RenderContactsTab(App* app)
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar(); // ##contactList inner WindowPadding
|
||||
}
|
||||
|
||||
// Shared right-click context menu (opened by either view's row right-click; acts on the selection).
|
||||
@@ -1226,6 +1317,119 @@ void RenderContactsTab(App* app)
|
||||
ImGui::EndChild();
|
||||
|
||||
renderEntryDialog();
|
||||
|
||||
// ---- Contacts customization modal (opened by the toolbar gear) — house BlurFloat overlay ----
|
||||
if (s_show_contacts_settings && app->settings()) {
|
||||
auto* st = app->settings();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("contacts_settings_title");
|
||||
ov.p_open = &s_show_contacts_settings;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 460.0f; ov.idSuffix = "contactsettings";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
const float ctrlW = 210.0f * dp;
|
||||
auto rowStart = [&](const char* label) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
};
|
||||
// Draw one sample contact card (avatar + label + muted address) at a given shape/scale, using
|
||||
// the SAME drawContactAvatar + row geometry the real list uses — so the preview matches reality.
|
||||
auto drawSampleRow = [&](ImDrawList* pdl, ImVec2 mn, float w,
|
||||
const data::AddressBookEntry& e, int shape, float sc) {
|
||||
ImFont* lblF = material::Type().subtitle2();
|
||||
ImFont* adrF = material::Type().caption();
|
||||
ImFont* icoF = material::Type().iconSmall();
|
||||
const float pad = 12.0f * dp;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + 16.0f * dp;
|
||||
const float round = 10.0f * dp;
|
||||
const float lblSz = lblF->LegacySize * sc;
|
||||
const float adrSz = adrF->LegacySize * sc;
|
||||
const ImVec2 mx(mn.x + w, mn.y + rowH);
|
||||
pdl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 18), round);
|
||||
const bool shielded = isShieldedAddr(e.address);
|
||||
const ImU32 tu = contactTypeColor(shielded, lightTheme);
|
||||
const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f);
|
||||
const float tabW = rowH; // shape 2 = full-row-height square tab on the left
|
||||
drawContactAvatar(pdl, avC, avR, e, shielded, tu, lightTheme, dp, lblF, icoF, true,
|
||||
shape, mn, ImVec2(mn.x + tabW, mx.y), round);
|
||||
const float tx = (shape == 2) ? (mn.x + tabW + pad) : (mn.x + pad + avR * 2.0f + pad);
|
||||
const float blockH = lblSz + adrSz + 3.0f * dp;
|
||||
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
|
||||
const std::string addr = util::truncateMiddle(e.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
pdl->PushClipRect(ImVec2(tx, mn.y), ImVec2(mx.x - pad, mx.y), true);
|
||||
pdl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), e.label.c_str());
|
||||
pdl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp), material::OnSurfaceMedium(), addr.c_str());
|
||||
pdl->PopClipRect();
|
||||
};
|
||||
// ---- Live preview: two sample cards at the current shape + scale (updates as controls move). ----
|
||||
{
|
||||
const float sc = st->getContactsListScale();
|
||||
const int shape = st->getContactsAvatarShape();
|
||||
data::AddressBookEntry sA; sA.label = "Ava Reyes"; sA.address = "zs1preview0shielded0contact0demo0address0q9k";
|
||||
data::AddressBookEntry sB; sB.label = "Node Ops"; sB.address = "t1PreviewTransparentContactDemoAddr8xQ";
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
const float padIn = 10.0f * dp, gap = 8.0f * dp;
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + 16.0f * dp;
|
||||
const float panelH = 2.0f * rowH + gap + 2.0f * padIn;
|
||||
const ImVec2 pOrigin = ImGui::GetCursorScreenPos();
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(pdl, pOrigin, ImVec2(pOrigin.x + w, pOrigin.y + panelH), g);
|
||||
const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn);
|
||||
const float rowW = w - 2.0f * padIn;
|
||||
drawSampleRow(pdl, rmn, rowW, sA, shape, sc);
|
||||
drawSampleRow(pdl, ImVec2(rmn.x, rmn.y + rowH + gap), rowW, sB, shape, sc);
|
||||
ImGui::Dummy(ImVec2(w, panelH));
|
||||
ImGui::Dummy(ImVec2(0.0f, 10.0f * dp));
|
||||
}
|
||||
// Avatar shape — circle, rounded square, or a full-row-height left tab (segmented control,
|
||||
// matching the chat settings modal's segmented style; the live preview above reflects it).
|
||||
{
|
||||
rowStart(TR("contacts_avatar_shape"));
|
||||
const char* items[] = { TR("contacts_shape_circle"), TR("contacts_shape_square"),
|
||||
TR("contacts_shape_tab") };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
const int cur = st->getContactsAvatarShape();
|
||||
const int clk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, ctrlW, segH,
|
||||
items, 3, cur, material::Type().caption(),
|
||||
"##contactsShape", dp);
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow
|
||||
if (clk >= 0 && clk != cur) { st->setContactsAvatarShape(clk); st->save(); }
|
||||
}
|
||||
// Card/list row scale (does not affect the table view).
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0.0f, 4.0f * dp));
|
||||
rowStart(TR("contacts_list_scale"));
|
||||
float v = st->getContactsListScale();
|
||||
if (ImGui::SliderFloat("##contactsScale", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
st->setContactsListScale(v); st->save();
|
||||
}
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
{
|
||||
// Done: sized to its text (+ a little shoulder room) and centered, not full width.
|
||||
const char* doneLbl = TR("chat_settings_done");
|
||||
ImGui::PushFont(material::Type().button());
|
||||
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
|
||||
s_show_contacts_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ContactsSweepOpenEditDialog(int avatarMode)
|
||||
|
||||
@@ -211,12 +211,11 @@ void ExportAllKeysDialog::render(App* app)
|
||||
std::string filepath = configDir + "/" + filename;
|
||||
bool writeOk = false;
|
||||
if (exported > 0) {
|
||||
std::ofstream file(filepath);
|
||||
if (file.is_open()) {
|
||||
file << keys;
|
||||
file.close();
|
||||
writeOk = true;
|
||||
}
|
||||
// Write the plaintext private keys 0600 + atomically (never
|
||||
// world/group-readable, never a half-written file) — the same restricted
|
||||
// atomic-write idiom the PIN vault uses. A default ofstream would create
|
||||
// the key dump with umask-derived (often 0644) permissions.
|
||||
writeOk = util::Platform::writeFileAtomically(filepath, keys, /*restrictPermissions=*/true);
|
||||
}
|
||||
if (!keys.empty()) sodium_memzero(&keys[0], keys.size()); // don't leave every key in freed heap
|
||||
|
||||
|
||||
@@ -36,21 +36,30 @@ static bool s_exporting = false;
|
||||
// Helper to escape CSV field
|
||||
static std::string escapeCSV(const std::string& field)
|
||||
{
|
||||
if (field.find(',') != std::string::npos ||
|
||||
field.find('"') != std::string::npos ||
|
||||
field.find('\n') != std::string::npos) {
|
||||
// Neutralize spreadsheet formula injection: a field beginning with '=', '+', '-', '@',
|
||||
// tab, or CR is interpreted as a formula by Excel/LibreOffice, letting an attacker-supplied
|
||||
// memo/address execute on open. Prefix such fields with a single quote so they render as text.
|
||||
std::string safe = field;
|
||||
if (!safe.empty()) {
|
||||
const char c0 = safe.front();
|
||||
if (c0 == '=' || c0 == '+' || c0 == '-' || c0 == '@' || c0 == '\t' || c0 == '\r')
|
||||
safe.insert(safe.begin(), '\'');
|
||||
}
|
||||
if (safe.find(',') != std::string::npos ||
|
||||
safe.find('"') != std::string::npos ||
|
||||
safe.find('\n') != std::string::npos) {
|
||||
// Escape quotes and wrap in quotes
|
||||
std::string escaped;
|
||||
escaped.reserve(field.size() + 4);
|
||||
escaped.reserve(safe.size() + 4);
|
||||
escaped += '"';
|
||||
for (char c : field) {
|
||||
for (char c : safe) {
|
||||
if (c == '"') escaped += "\"\"";
|
||||
else escaped += c;
|
||||
}
|
||||
escaped += '"';
|
||||
return escaped;
|
||||
}
|
||||
return field;
|
||||
return safe;
|
||||
}
|
||||
|
||||
void ExportTransactionsDialog::show()
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -132,7 +133,7 @@ struct PfEditState {
|
||||
bool showValue = true;
|
||||
bool show24h = false;
|
||||
bool showSparkline = false;
|
||||
int sparkInterval = 0; // 0=min 1=hour 2=day 3=week 4=month
|
||||
int sparkInterval = 4; // 0=min 1=hour 2=day 3=week 4=month (default month)
|
||||
};
|
||||
static PfEditState s_pfEdit;
|
||||
|
||||
@@ -162,16 +163,16 @@ static bool pfProjectSeries(const std::vector<double>& hist, ImVec2 mn, ImVec2 m
|
||||
}
|
||||
|
||||
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
const std::vector<double>& hist, ImU32 col)
|
||||
const std::vector<double>& hist, ImU32 col, float dp)
|
||||
{
|
||||
std::vector<ImVec2> pts;
|
||||
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
||||
dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f);
|
||||
dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f * dp);
|
||||
}
|
||||
|
||||
// Sparkline with a soft fill under the line — for the card's dedicated bottom strip.
|
||||
static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
const std::vector<double>& hist, ImU32 lineCol)
|
||||
const std::vector<double>& hist, ImU32 lineCol, float dp)
|
||||
{
|
||||
std::vector<ImVec2> pts;
|
||||
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
||||
@@ -180,7 +181,7 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
dl->PathLineTo(ImVec2(pts[n - 1].x, mx.y));
|
||||
dl->PathLineTo(ImVec2(pts[0].x, mx.y));
|
||||
dl->PathFillConcave(WithAlpha(lineCol, 28));
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f);
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f * dp);
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +284,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
||||
s_pfEdit.showValue = true;
|
||||
s_pfEdit.show24h = false;
|
||||
s_pfEdit.showSparkline = false;
|
||||
s_pfEdit.sparkInterval = 0;
|
||||
s_pfEdit.sparkInterval = 4; // default month (a real curve from the daily series)
|
||||
}
|
||||
s_pfEdit.search[0] = '\0';
|
||||
s_pfEdit.typeFilter = 0;
|
||||
@@ -667,14 +668,18 @@ static void pfDrawAddressSection(App* app)
|
||||
}
|
||||
filtered.push_back(&a);
|
||||
}
|
||||
// Build the selected-address set once — the sort comparator and the per-row membership test below
|
||||
// both consulted it via a linear PortfolioEntryContains scan (O(n) each) every frame.
|
||||
std::unordered_set<std::string> selSet(s_pfEdit.addrs.begin(), s_pfEdit.addrs.end());
|
||||
std::sort(filtered.begin(), filtered.end(), [&](const AddressInfo* x, const AddressInfo* y) {
|
||||
bool sx = data::PortfolioEntryContains(s_pfEdit.addrs, x->address);
|
||||
bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address);
|
||||
bool sx = selSet.count(x->address) != 0;
|
||||
bool sy = selSet.count(y->address) != 0;
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
if (selAllClicked)
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address);
|
||||
for (const AddressInfo* a : filtered)
|
||||
if (data::PortfolioEntryAdd(s_pfEdit.addrs, a->address)) selSet.insert(a->address);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImDrawList* ldl = material::BeginFadeScrollChild("##pfAddrList", s_pfEdit.addrFade,
|
||||
ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp);
|
||||
@@ -684,7 +689,7 @@ static void pfDrawAddressSection(App* app)
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_addr_match"));
|
||||
for (const AddressInfo* ap : filtered) {
|
||||
const AddressInfo& a = *ap;
|
||||
bool inSet = data::PortfolioEntryContains(s_pfEdit.addrs, a.address);
|
||||
bool inSet = selSet.count(a.address) != 0;
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
|
||||
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
|
||||
@@ -733,8 +738,8 @@ static void pfDrawAddressSection(App* app)
|
||||
ImGui::PushID(a.address.c_str());
|
||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address);
|
||||
else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address);
|
||||
if (inSet) { data::PortfolioEntryRemove(s_pfEdit.addrs, a.address); selSet.erase(a.address); }
|
||||
else { data::PortfolioEntryAdd(s_pfEdit.addrs, a.address); selSet.insert(a.address); }
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -956,7 +961,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
|
||||
ImU32 accent = s_pfEdit.color ? (ImU32)s_pfEdit.color : 0;
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
DrawGlassPanel(pdl, pMin, pMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f);
|
||||
@@ -968,7 +973,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f);
|
||||
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol, dp);
|
||||
}
|
||||
}
|
||||
float rowY = pMin.y + ppad, nameX = pMin.x + ppad;
|
||||
@@ -1080,23 +1085,63 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
// 2=value-hero. No left accent strip — identity comes from the tinted icon + a subtle accent
|
||||
// border. Fixed right-aligned columns keep numbers aligned across rows; on-but-absent fields show a
|
||||
// muted em-dash so a row never looks broken. Pure drawing into dl.
|
||||
// TABLE-style (portfolio_style 0) right-edge column anchors — shared by pfDrawRow's row body and the
|
||||
// column-header strip so the header labels sit exactly over the values they name.
|
||||
// TABLE-style (style 0) column widths (logical px) — shared by pfTableCols (which turns them into
|
||||
// right-edge anchors) and pfDrawRow (which feeds valW/drgxW to fit() truncation), so truncated text
|
||||
// always lines up with the column edges. Change here and both stay in sync.
|
||||
static constexpr float kPfChgColW = 62.0f;
|
||||
static constexpr float kPfValColW = 108.0f;
|
||||
static constexpr float kPfDrgxColW = 118.0f;
|
||||
|
||||
struct PfTableCols { float labelR, drgxR, valR, chgR; };
|
||||
static PfTableCols pfTableCols(float right, float dp)
|
||||
{
|
||||
const float colGap = Layout::spacingMd();
|
||||
PfTableCols c;
|
||||
c.chgR = right;
|
||||
c.valR = c.chgR - kPfChgColW * dp - colGap;
|
||||
c.drgxR = c.valR - kPfValColW * dp - colGap;
|
||||
c.labelR = c.drgxR - kPfDrgxColW * dp - colGap;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Portfolio row height + inter-row gap by style — one source shared by the row drawing
|
||||
// (mktDrawPortfolio) and the scroll-region height budget (RenderMarketTab) so they can't drift and clip.
|
||||
// Row heights: Table 48dp (a dedicated trend column needs vertical room), Cards 68dp (two-zone),
|
||||
// Spotlight 92dp (a dominant bottom chart band under the hero value). Uniform per style.
|
||||
static float pfRowHeight(int style, float dp) { return (style == 0 ? 48.0f : style == 1 ? 68.0f : 92.0f) * dp; }
|
||||
// Fraction of the Table label+trend zone given to the label; the rest is the centre TREND column.
|
||||
// Shared by the row body and the header strip so the sparkline and its "TREND" label line up.
|
||||
static constexpr float kPfTableTrendFrac = 0.38f;
|
||||
static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp : Layout::spacingSm(); }
|
||||
|
||||
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||
const MarketInfo& market, int style, float dp,
|
||||
ImFont* sub1, ImFont* capFont, bool hov)
|
||||
ImFont* sub1, ImFont* capFont, bool hov, bool tableReserveSpark = false)
|
||||
{
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
const double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
const bool zeroBal = (bal <= 0.0); // empty groups render dimmed (content only, not the container)
|
||||
|
||||
// Card — a touch more fill than before so rows read as distinct cards, plus a subtle accent
|
||||
// border for identity (the left accent strip was removed).
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
|
||||
} else if (hov) {
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), 10.0f, 0, 1.2f);
|
||||
// ---- Per-style CONTAINER: Table (0) = borderless ledger row (hairline + hover wash); Cards (1) =
|
||||
// glass card with a faint identity tint; Spotlight (2) = a heavier glass tile. No accent line —
|
||||
// the group's colour already reads from its icon and the sparkline, so it adds nothing here. ----
|
||||
const float round = 10.0f * dp;
|
||||
if (style == 0) {
|
||||
if (hov) dl->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 14), 0.0f);
|
||||
dl->AddLine(ImVec2(rowMin.x, rowMax.y - 0.5f), ImVec2(rowMax.x, rowMax.y - 0.5f),
|
||||
WithAlpha(OnSurface(), 24), 1.0f);
|
||||
} else if (style == 1) {
|
||||
GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round); // faint identity wash
|
||||
if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), round, 0, 1.2f);
|
||||
} else {
|
||||
GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), round, 0, 1.2f);
|
||||
}
|
||||
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
@@ -1130,76 +1175,138 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
float right = rowMax.x - padX;
|
||||
float midY = (rowMin.y + rowMax.y) * 0.5f;
|
||||
|
||||
// Empty groups: dim the BODY content (text/pill/bar/sparkline) to ~half alpha so a $0 group reads as
|
||||
// muted, while the container (card/tick) stays full-strength. Done by scaling the alpha of the
|
||||
// vertices this body emits — one place instead of tinting every draw call.
|
||||
const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1;
|
||||
|
||||
if (style == 0) {
|
||||
// ---- Compact: icon + label (left); fixed right-aligned columns DRGX | value | 24h | spark.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
// ---- TABLE: icon + label (left); a dedicated centre TREND column (aligned + header-labelled);
|
||||
// right-aligned numeric columns DRGX | value | 24h pill. The trend column is present for the
|
||||
// whole list when any group opts in (tableReserveSpark); each row fills it when it has one.
|
||||
const PfTableCols cols = pfTableCols(right, dp);
|
||||
const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR;
|
||||
const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols)
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
const float sparkW = 92.0f*dp, chgW = 60.0f*dp, valW = 112.0f*dp, drgxW = 122.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float chgR = sparkL - colGap;
|
||||
float valR = chgR - chgW - colGap;
|
||||
float drgxR = valR - valW - colGap;
|
||||
float labelR = drgxR - drgxW - colGap;
|
||||
|
||||
if (e.showDrgx) rtext(capFont, drgxR, midY, OnSurfaceMedium(), fit(drgxStr, capFont, drgxW));
|
||||
if (wantValue) rtext(sub1, valR, midY, hasValue?OnSurface():OnSurfaceDisabled(), hasValue?fit(valStr,sub1,valW):kDash);
|
||||
if (wantChange) rtext(capFont, chgR, midY, hasChange?chgCol:OnSurfaceDisabled(), hasChange?chgStr:kDash);
|
||||
if (hasSpark) pfDrawSparkline(dl, ImVec2(sparkL, rowMin.y+padY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
|
||||
float lblMaxW = std::max(24.0f*dp, labelR - x);
|
||||
if (wantChange) {
|
||||
const std::string s = hasChange ? chgStr : kDash;
|
||||
const float pw = tw(capFont, s) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const ImVec2 pmn(chgR - pw, midY - ph*0.5f);
|
||||
if (hasChange) dl->AddRectFilled(pmn, ImVec2(chgR, midY + ph*0.5f), WithAlpha(chgCol, 32), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f),
|
||||
hasChange?chgCol:OnSurfaceDisabled(), s.c_str());
|
||||
}
|
||||
// Dedicated centre TREND column: when the list has any sparkline (tableReserveSpark), cap the
|
||||
// label to a fixed boundary so the column + its header label align across every row; each row
|
||||
// fills it only when that group opts in. Rows without any list-wide trend keep the full label.
|
||||
// Trend-column origin from the fixed LEFT (icon-independent) so it lines up with the "TREND"
|
||||
// header and across rows whether or not a group has an icon (x may be shifted past the icon).
|
||||
const float zoneW = std::max(24.0f*dp, labelR - left);
|
||||
const float trendX0 = left + zoneW * kPfTableTrendFrac;
|
||||
const float lblMaxW = tableReserveSpark ? std::max(24.0f*dp, trendX0 - colGap - x) : std::max(24.0f*dp, labelR - x);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str());
|
||||
if (hasSpark && tableReserveSpark && labelR - trendX0 > 40.0f * dp)
|
||||
pfDrawSparklineFilled(dl, ImVec2(trendX0, rowMin.y + padY), ImVec2(labelR, rowMax.y - padY), spark, sparkCol, dp);
|
||||
} else if (style == 1) {
|
||||
// ---- Detailed: 2x2 grid — label/value on top, DRGX/24h below — plus a tall sparkline strip.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
const float sparkW = 150.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float textR = hasSpark ? (sparkL - colGap) : right;
|
||||
float topY = rowMin.y + padY;
|
||||
float botY = rowMax.y - padY - capFont->LegacySize;
|
||||
// ---- CARDS: a balanced snapshot — a LEFT-aligned info stack (icon + label / value+delta chip /
|
||||
// DRGX) grouped on the left, with a full-height sparkline filling the rest of the width.
|
||||
const float zoneTop = rowMin.y + padY;
|
||||
const float zoneBot = rowMax.y - padY;
|
||||
// Icon centred in the info zone; the label/value/DRGX stack sits to its right.
|
||||
const float iconSz = capFont->LegacySize;
|
||||
float textX = left;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(left + iconSz*0.5f, (zoneTop + zoneBot)*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
textX = left + iconSz + Layout::spacingSm();
|
||||
}
|
||||
// value (top-right, emphasized)
|
||||
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
if (wantValue) dl->AddText(sub1, sub1->LegacySize, ImVec2(textR - tw(sub1,vTop), topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
|
||||
// label (top-left, fills up to the value column)
|
||||
float labelR = wantValue ? (textR - tw(sub1,vTop) - colGap) : textR;
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, topY), OnSurface(), fit(e.label,sub1,std::max(24.0f*dp, labelR - x)).c_str());
|
||||
// DRGX (bottom-left, muted) + 24h (bottom-right, colored)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
|
||||
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(textR - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
// tall sparkline strip spanning both lines
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, topY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
// Info zone driven by the ACTUAL content width so a short group hands the extra room to the chart;
|
||||
// bounded [28%, 52%] so the info never cramps and the chart always keeps a decent band.
|
||||
float infoR = right, cSparkL = right;
|
||||
if (hasSpark) {
|
||||
const std::string vTxt0 = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
const float chipW = (wantChange && hasChange) ? (Layout::spacingSm() + tw(capFont, chgStr) + 12.0f*dp) : 0.0f;
|
||||
const float valLineW = wantValue ? (tw(sub1, vTxt0) + chipW) : 0.0f;
|
||||
const float contentW = std::max(std::max(tw(capFont, e.label), valLineW), e.showDrgx ? tw(capFont, drgxStr) : 0.0f);
|
||||
const float span = right - left;
|
||||
const float infoW = std::min(std::max((textX - left) + contentW + colGap * 2.0f, span * 0.28f), span * 0.52f);
|
||||
cSparkL = left + infoW;
|
||||
infoR = cSparkL - colGap;
|
||||
}
|
||||
const float gap = Layout::spacingXs();
|
||||
const float stackH = capFont->LegacySize + gap + sub1->LegacySize + gap + capFont->LegacySize;
|
||||
float cy = zoneTop + std::max(0.0f, (zoneBot - zoneTop - stackH) * 0.5f);
|
||||
// label (muted)
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(e.label, capFont, std::max(24.0f*dp, infoR - textX)).c_str());
|
||||
cy += capFont->LegacySize + gap;
|
||||
// value (leads via a soft shadow) + delta chip trailing it
|
||||
if (wantValue) {
|
||||
const std::string vTxt = hasValue ? valStr : kDash;
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(textX, cy), hasValue?OnSurface():OnSurfaceDisabled(), vTxt.c_str());
|
||||
if (wantChange && hasChange) {
|
||||
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const float chipX = textX + tw(sub1, vTxt) + Layout::spacingSm();
|
||||
const float chipY = cy + (sub1->LegacySize - ph)*0.5f;
|
||||
if (chipX + pw <= infoR) {
|
||||
dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
cy += sub1->LegacySize + gap;
|
||||
// DRGX (muted)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(drgxStr, capFont, std::max(24.0f*dp, infoR - textX)).c_str());
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, zoneTop), ImVec2(right, zoneBot), spark, sparkCol, dp);
|
||||
} else {
|
||||
// ---- Value-hero: label; big neutral value; 24h; DRGX + sparkline fill the right.
|
||||
// ---- SPOTLIGHT: "big number over a chart" hero tile — a top zone (muted name + DRGX, then a
|
||||
// LARGE value coloured by 24h with a delta chip) over a dominant full-width sparkline band
|
||||
// filling the bottom. The value spans the full width now (no competing right strip).
|
||||
const float heroSz = sub1->LegacySize * 1.5f; // ImGui 1.92 rebakes at this size (crisp, not upscaled)
|
||||
const float topY = rowMin.y + padY;
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
const float sparkW = (right-left)*0.40f;
|
||||
float sparkL = right - sparkW;
|
||||
float topY = rowMin.y + padY;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+capFont->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
float rightZoneL = hasSpark ? sparkL : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, rightZoneL - x - colGap)).c_str());
|
||||
// hero value — neutral bold (accent stays in the icon/border, so it can't clash with the 24h)
|
||||
float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||
std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), hasValue?OnSurface():OnSurfaceDisabled(), hero.c_str());
|
||||
// 24h below the value (colored / dash)
|
||||
float botY = valY + sub1->LegacySize + Layout::spacingXs();
|
||||
if (wantChange) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), hasChange?chgCol:OnSurfaceDisabled(), (hasChange?chgStr:kDash).c_str());
|
||||
// DRGX — right-aligned on the value line; fills the right when there's no sparkline.
|
||||
if (e.showDrgx && hasValue) {
|
||||
float dr = hasSpark ? (sparkL - colGap) : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dr - tw(capFont,drgxStr), valY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), drgxStr.c_str());
|
||||
// name (top-left, muted) + DRGX (top-right, muted)
|
||||
const float drgxW2 = e.showDrgx ? tw(capFont, drgxStr) : 0.0f;
|
||||
if (e.showDrgx) rtext(capFont, right, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr);
|
||||
const float nameMaxW = std::max(24.0f*dp, (right - (e.showDrgx ? drgxW2 + colGap : 0.0f)) - x);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,nameMaxW).c_str());
|
||||
// hero value — coloured by 24h direction (neutral when no live change)
|
||||
const float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||
const std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
const ImU32 heroCol = !hasValue ? OnSurfaceDisabled() : (hasChange ? chgCol : OnSurface());
|
||||
DrawTextShadow(dl, sub1, heroSz, ImVec2(left, valY), heroCol, hero.c_str());
|
||||
const float heroW = sub1->CalcTextSizeA(heroSz, FLT_MAX, 0, hero.c_str()).x;
|
||||
// delta chip trailing the hero value
|
||||
if (wantChange && hasChange) {
|
||||
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const float chipX = left + heroW + Layout::spacingSm();
|
||||
const float chipY = valY + (heroSz - ph)*0.5f;
|
||||
if (chipX + pw <= right) {
|
||||
dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str());
|
||||
}
|
||||
}
|
||||
// full-width sparkline band filling the bottom of the tile, below the hero value
|
||||
if (hasSpark) {
|
||||
const float bandTop = valY + heroSz + Layout::spacingXs();
|
||||
if (rowMax.y - padY - bandTop > 12.0f * dp)
|
||||
pfDrawSparklineFilled(dl, ImVec2(left, bandTop), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp);
|
||||
}
|
||||
}
|
||||
|
||||
if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group
|
||||
for (int vi = zdVtx; vi < dl->VtxBuffer.Size; ++vi) {
|
||||
ImDrawVert& v = dl->VtxBuffer[vi];
|
||||
v.col = material::ScaleAlpha(v.col, 0.5f);
|
||||
}
|
||||
// filled sparkline (right ~40%)
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, valY - 2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1396,6 +1503,23 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
float tradeBtnX = cardMax.x - pad - tradeBtnW;
|
||||
float tradeBtnY = cardMin.y + Layout::spacingSm();
|
||||
|
||||
// Chart line/candle segmented control, immediately left of the trade button — only when the
|
||||
// selected range has per-exchange candles (Live / aggregate is line-only, nothing to toggle).
|
||||
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
|
||||
const char* styleIcons[2] = { ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART };
|
||||
const float csW = 2.0f * tradeBtnH;
|
||||
const ImVec2 csOrigin(tradeBtnX - csW - Layout::spacingSm(), tradeBtnY);
|
||||
const ImVec2 csSaved = ImGui::GetCursorScreenPos();
|
||||
const int csCur = s_mkt.chartStyle;
|
||||
const int csClk = SegmentedControl(dl, csOrigin, csW, tradeBtnH, styleIcons, 2, csCur,
|
||||
Type().iconSmall(), "##chartStyleSeg", dp);
|
||||
if (csClk >= 0 && csClk != csCur) {
|
||||
s_mkt.chartStyle = csClk;
|
||||
if (app->settings()) { app->settings()->setChartStyle(csClk); app->settings()->save(); }
|
||||
}
|
||||
ImGui::SetCursorScreenPos(csSaved);
|
||||
}
|
||||
|
||||
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
|
||||
bool tradeHov = material::IsRectHovered(tMin, tMax);
|
||||
|
||||
@@ -1518,30 +1642,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
bx += bw + Layout::spacingXs();
|
||||
}
|
||||
|
||||
// Line/candle toggle — only when the selected range has per-exchange candles (the aggregate /
|
||||
// Live view is line-only, so the toggle is hidden there).
|
||||
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
|
||||
bx += Layout::spacingSm();
|
||||
ImFont* icoF = material::Typography::instance().iconSmall();
|
||||
const bool isCandle = (s_mkt.chartStyle == 1);
|
||||
const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART;
|
||||
ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH);
|
||||
bool thov = material::IsRectHovered(tmn, tmx);
|
||||
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
||||
ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(tmn.x + (pillH - tiSz.x) * 0.5f, tmn.y + (pillH - tiSz.y) * 0.5f),
|
||||
thov ? OnSurface() : OnSurfaceMedium(), styleIcon);
|
||||
ImGui::SetCursorScreenPos(tmn);
|
||||
if (ImGui::InvisibleButton("##ChartStyle", ImVec2(pillH, pillH))) {
|
||||
s_mkt.chartStyle = isCandle ? 0 : 1;
|
||||
if (app->settings()) { app->settings()->setChartStyle(s_mkt.chartStyle); app->settings()->save(); }
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
material::Tooltip("%s", TR(isCandle ? "market_style_line" : "market_style_candle"));
|
||||
bx += pillH;
|
||||
}
|
||||
// (Line/candle chart-style toggle now lives top-right of the price hero, left of the trade button.)
|
||||
|
||||
// Refresh button (far right).
|
||||
float rEdge = chartMax.x - chartPad;
|
||||
@@ -1625,7 +1726,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
// Keep the axis label inside the card even on narrow windows (min-padding may be
|
||||
// smaller than the label width) so it never spills onto the tab background.
|
||||
float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6);
|
||||
float lblX = std::max(chartMin.x + 3.0f * mktDp, plotLeft - labelSz.x - 6.0f * mktDp);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lblX, gy - labelSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), buf);
|
||||
@@ -1688,8 +1789,8 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
tipX = std::max(plotLeft, tipX);
|
||||
float tipY = plotTop + 4.0f * mktDp;
|
||||
ImVec2 tMin(tipX, tipY), tMax(tipX + tw + pad * 2, tipY + th);
|
||||
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f);
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f * mktDp);
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
||||
ImU32 cCol = (hc.close >= hc.open) ? Success() : Error();
|
||||
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad), OnSurface(), when);
|
||||
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad + lh + gap), cCol, l2);
|
||||
@@ -1792,16 +1893,16 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
snprintf(buf, sizeof(buf), "%s", FormatPrice(s_mkt.history[idx]).c_str());
|
||||
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
||||
float tipX = px + 10;
|
||||
float tipY = py - tipSz.y - tipPad * 2 - 4;
|
||||
float tipX = px + 10.0f * mktDp;
|
||||
float tipY = py - tipSz.y - tipPad * 2 - 4.0f * mktDp;
|
||||
if (tipX + tipSz.x + tipPad * 2 > plotRight)
|
||||
tipX = px - tipSz.x - tipPad * 2 - 10;
|
||||
if (tipY < plotTop) tipY = py + 10;
|
||||
tipX = px - tipSz.x - tipPad * 2 - 10.0f * mktDp;
|
||||
if (tipY < plotTop) tipY = py + 10.0f * mktDp;
|
||||
|
||||
ImVec2 tipMin(tipX, tipY);
|
||||
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
|
||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
|
||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f * mktDp);
|
||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
||||
}
|
||||
@@ -1867,12 +1968,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
// "Manage…" button, right-aligned on the header row — opens the portfolio editor.
|
||||
// Portfolio-style segmented control (Table / Cards / Spotlight) + "Manage…" button, right-aligned on
|
||||
// the header row (style picker left of Manage). Manage opens the portfolio editor.
|
||||
{
|
||||
const char* ml = TR("portfolio_manage");
|
||||
float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2;
|
||||
const char* styleItems[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const float segW = 210.0f * mktDp;
|
||||
const float sGap = Layout::spacingSm();
|
||||
ImGui::SameLine();
|
||||
material::RightAlignX(mBtnW);
|
||||
RightAlignX(segW + sGap + mBtnW); // right-align the [style picker | Manage] group
|
||||
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
const int curStyle = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
const int segClk = SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||
styleItems, 3, curStyle, Type().caption(), "##pfStyleSeg", mktDp);
|
||||
if (segClk >= 0 && segClk != curStyle && app->settings()) {
|
||||
app->settings()->setPortfolioStyle(segClk);
|
||||
app->settings()->save();
|
||||
}
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(segW, segH)); // reserve + restore layout flow
|
||||
ImGui::SameLine(0.0f, sGap);
|
||||
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
|
||||
// Open the editor on the first group visible to this wallet (or the empty state if none) —
|
||||
// never a raw index 0 that might belong to a different wallet.
|
||||
@@ -1982,8 +2100,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float rowGap = Layout::spacingSm();
|
||||
// The Table's dedicated TREND column (+ its header label) is present for the whole list when any
|
||||
// visible group opts into a sparkline, so the column aligns and doesn't flicker per row.
|
||||
bool anySpark = false;
|
||||
for (int i : vis) {
|
||||
const auto& e = allEntries[i];
|
||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { anySpark = true; break; }
|
||||
}
|
||||
float rowH = pfRowHeight(style, mktDp);
|
||||
// Table rows abut like a ledger (thin gap); Cards/Spotlight breathe.
|
||||
float rowGap = pfRowGapFor(style, mktDp);
|
||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||
@@ -2002,6 +2128,31 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
} else {
|
||||
// TABLE column-header strip (style 0): a thin static header labelling the numeric columns —
|
||||
// a signature the card styles don't have. Uses the shared pfTableCols so it aligns with rows.
|
||||
if (style == 0) {
|
||||
const float headerH = capFont->LegacySize + 8.0f * mktDp;
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
const float hLeft = hp.x + Layout::spacingMd();
|
||||
const float hRight = hp.x + rowW - Layout::spacingMd();
|
||||
const PfTableCols hc = pfTableCols(hRight, mktDp);
|
||||
ImFont* ovF = Type().overline();
|
||||
const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f;
|
||||
const ImU32 hcol = OnSurfaceDisabled();
|
||||
auto hdr = [&](float xr, const char* s){
|
||||
const float w = ovF->CalcTextSizeA(ovF->LegacySize, FLT_MAX, 0, s).x;
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(xr - w, ty), hcol, s);
|
||||
};
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft, ty), hcol, TR("market_col_name"));
|
||||
if (anySpark) // TREND column header, left-aligned at the same x the row sparklines start
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft + (hc.labelR - hLeft) * kPfTableTrendFrac, ty), hcol, TR("market_col_trend"));
|
||||
hdr(hc.drgxR, DRAGONX_TICKER);
|
||||
hdr(hc.valR, TR("market_col_value"));
|
||||
hdr(hc.chgR, TR("market_24h"));
|
||||
rdl->AddLine(ImVec2(hLeft, hp.y + headerH - 1.0f), ImVec2(hRight, hp.y + headerH - 1.0f),
|
||||
WithAlpha(OnSurface(), 20), 1.0f);
|
||||
ImGui::Dummy(ImVec2(rowW, headerH));
|
||||
}
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
@@ -2011,7 +2162,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov);
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov, anySpark);
|
||||
if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||
if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap));
|
||||
}
|
||||
@@ -2047,6 +2198,10 @@ void RenderMarketTab(App* app)
|
||||
// Also kick the historical price-chart fetch (self-throttled) so the chart's hour/day/week/
|
||||
// month intervals populate promptly when the user opens the Market tab.
|
||||
app->refreshMarketChart();
|
||||
// ...and the SELECTED pair's per-exchange OHLC candles (self-throttled, in-flight-guarded). Without
|
||||
// this, the candle series only loaded on a pair-chip / refresh click, so the candlestick toggle was
|
||||
// missing (and "switch to candlesticks" silently did nothing) when the tab was just opened.
|
||||
app->refreshExchangeChart();
|
||||
const auto& registry = EffectiveRegistry(market);
|
||||
|
||||
// Load persisted exchange/pair on first frame
|
||||
@@ -2058,14 +2213,16 @@ void RenderMarketTab(App* app)
|
||||
|
||||
// Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the
|
||||
// Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the
|
||||
// portfolio editor modal is open.
|
||||
if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
// portfolio editor / market-settings modal is open.
|
||||
if (app->settings() && !s_pfEdit.open &&
|
||||
!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
||||
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
||||
if (prev || next) {
|
||||
int st = app->settings()->getPortfolioStyle();
|
||||
st = next ? (st + 1) % 3 : (st + 2) % 3;
|
||||
app->settings()->setPortfolioStyle(st);
|
||||
app->settings()->save(); // persist the choice (matches the gear-modal control)
|
||||
const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]);
|
||||
@@ -2123,15 +2280,16 @@ void RenderMarketTab(App* app)
|
||||
for (const auto& e : pfEntriesGeo)
|
||||
if (e.scope.empty() || (!pfActiveHash.empty() && e.scope == pfActiveHash)) pfVisN++;
|
||||
int pfStyle = app->settings()->getPortfolioStyle();
|
||||
float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float pfRowGap = Layout::spacingSm();
|
||||
float pfRowH = pfRowHeight(pfStyle, mktDp);
|
||||
float pfRowGap = pfRowGapFor(pfStyle, mktDp); // Table rows abut
|
||||
float pfHeaderH = (pfStyle == 0) ? (capFont->LegacySize + 8.0f * mktDp) : 0.0f; // Table column strip
|
||||
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
|
||||
// Height for up to pfMaxVisibleRows rows. ItemSpacing is zeroed inside the row child, so the
|
||||
// content is exactly rows + inter-row gaps; one extra gap of bottom breathing room keeps the
|
||||
// last visible row off the clip edge. More groups than this scroll internally.
|
||||
int pfVisRows = std::min(pfVisN, pfMaxVisibleRows);
|
||||
float pfGroupsH = (pfVisRows > 0)
|
||||
? (pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
? (pfHeaderH + pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
: 0.0f;
|
||||
float portfolioH = pfSummaryH + pfGroupsH;
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
|
||||
// --- Compute thread grid layout based on controls card width ---
|
||||
// Estimate controlsW first to compute cols correctly
|
||||
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
|
||||
// The Mine button is square (= card height, which scales with DPI), so the width we
|
||||
// reserve for it here must scale too — a RAW clamp under-reserves at >100% scaling, which
|
||||
// over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%).
|
||||
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap;
|
||||
float innerW = estControlsW - pad * 2;
|
||||
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
|
||||
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
|
||||
|
||||
@@ -308,11 +308,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Item text with internal padding
|
||||
// Item text with internal padding, middle-truncated so a long saved URL
|
||||
// can't run under the trailing X (delete) button.
|
||||
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
|
||||
float maxTextW = popupInnerW - xZoneW - textPadX * 2.0f;
|
||||
std::string urlDisp = material::TruncateToWidth(url, rowFont, rowFontSz, maxTextW);
|
||||
pdl->AddText(rowFont, rowFontSz,
|
||||
ImVec2(rowMin.x + textPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), url.c_str());
|
||||
isCurrent ? Primary() : OnSurface(), urlDisp.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
|
||||
@@ -467,11 +470,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Full address text with internal padding
|
||||
// Address text with internal padding, middle-truncated so a full z-address
|
||||
// (~78 chars) can't run under the trailing X (delete) button.
|
||||
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
|
||||
float wMaxTextW = wPopupInnerW - wXZoneW - wTextPadX * 2.0f;
|
||||
std::string addrDisp = material::TruncateToWidth(addr, wRowFont, wRowFontSz, wMaxTextW);
|
||||
pdl->AddText(wRowFont, wRowFontSz,
|
||||
ImVec2(rowMin.x + wTextPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
||||
isCurrent ? Primary() : OnSurface(), addrDisp.c_str());
|
||||
// Tooltip for long addresses
|
||||
if (rowHov && !inXZone)
|
||||
material::Tooltip("%s", addr.c_str());
|
||||
|
||||
@@ -98,7 +98,12 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Scrollable child to contain all content within available space
|
||||
ImVec2 peersAvail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##PeersScroll", peersAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
// NoScrollWithMouse: the inner ##PeersList owns wheel scrolling (via ApplySmoothScroll). Without
|
||||
// this, a wheel over the list would scroll BOTH the list (smooth-scroll) and this outer container
|
||||
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
|
||||
// Safe because the peer panel is sized to fill the remaining height, so this outer never overflows.
|
||||
ImGui::BeginChild("##PeersScroll", peersAvail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Responsive: scale factors per frame
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
@@ -141,6 +142,12 @@ void RenderTransactionsTab(App* app)
|
||||
const auto addrLabel = S.label("tabs.transactions", "address-label");
|
||||
const auto& state = app->state();
|
||||
|
||||
// Txids that carried a chat message (sent or received) — drives the "Message" badge and the Chat
|
||||
// filter below. Rebuilt each frame from the in-memory chat store (cheap: O(chat messages), which
|
||||
// is small); empty when chat is disabled or the wallet has no chat identity. Both variants populate
|
||||
// the chat store before this tab renders, so the same lookup serves full-node and lite.
|
||||
const std::unordered_set<std::string> chatTxids = app->chatService().store().chatTxids();
|
||||
|
||||
// Responsive scale factors (recomputed every frame)
|
||||
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
||||
const float hs = Layout::hScale(contentAvail.x);
|
||||
@@ -250,8 +257,9 @@ void RenderTransactionsTab(App* app)
|
||||
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper",
|
||||
minedCount, minedTotal, "+", 3, type_filter);
|
||||
|
||||
// Selected card accent
|
||||
if (type_filter > 0) {
|
||||
// Selected card accent (only the three summary cards map to a card position — the Chat filter
|
||||
// (4) has no card, so guard the idx_map lookup to 1..3).
|
||||
if (type_filter > 0 && type_filter <= 3) {
|
||||
int idx_map[] = {-1, 1, 0, 2};
|
||||
int idx = idx_map[type_filter];
|
||||
float xOff = idx * (cardW + cardGap);
|
||||
@@ -281,7 +289,8 @@ void RenderTransactionsTab(App* app)
|
||||
ImGui::SameLine(0, filterGap);
|
||||
float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
|
||||
ImGui::SetNextItemWidth(comboW);
|
||||
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") };
|
||||
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"),
|
||||
TR("mined_filter"), TR("chat_filter") };
|
||||
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
|
||||
|
||||
// Sort selector
|
||||
@@ -476,6 +485,7 @@ void RenderTransactionsTab(App* app)
|
||||
if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
|
||||
if (type_filter == 2 && dtx.display_type != "receive") continue;
|
||||
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
|
||||
if (type_filter == 4 && chatTxids.count(dtx.txid) == 0) continue; // chat-only
|
||||
}
|
||||
if (!search_str.empty()) {
|
||||
if (!containsIgnoreCase(dtx.address, search_str) &&
|
||||
@@ -657,6 +667,7 @@ void RenderTransactionsTab(App* app)
|
||||
|
||||
// Determine type info
|
||||
bool shieldedDisplay = tx.display_type == "shield";
|
||||
const bool isChatTx = chatTxids.count(tx.txid) > 0; // carried a chat message
|
||||
ImU32 iconCol;
|
||||
const char* typeStr;
|
||||
if (shieldedDisplay) {
|
||||
@@ -749,12 +760,17 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
// Position status badge in the middle-right area
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
const char* shieldedStr = TR("shielded_type");
|
||||
ImVec2 shieldSz = shieldedDisplay
|
||||
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr)
|
||||
// Optional "top" badge stacked above the status pill: "Message" for a chat tx or
|
||||
// "Shielded" for an autoshield. Chat txs are send/receive (not the "shield" type),
|
||||
// so the two are mutually exclusive; chat takes precedence.
|
||||
const bool showTopBadge = isChatTx || shieldedDisplay;
|
||||
const char* topBadgeStr = isChatTx ? TR("tx_chat_badge") : TR("shielded_type");
|
||||
const ImU32 topBadgeCol = isChatTx ? Secondary() : Primary();
|
||||
ImVec2 topSz = showTopBadge
|
||||
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, topBadgeStr)
|
||||
: ImVec2(0, 0);
|
||||
float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f;
|
||||
float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x;
|
||||
float topPillW = topSz.x + Layout::spacingSm() * 2.0f;
|
||||
float stackW = showTopBadge ? std::max(sSz.x, topPillW) : sSz.x;
|
||||
float statusX = amtX - stackW - Layout::spacingXxl();
|
||||
float minStatusX = cx + innerW * 0.25f; // don't overlap address
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
@@ -763,18 +779,17 @@ void RenderTransactionsTab(App* app)
|
||||
// row background on every skin (faint alpha-30 fills used to vanish on dark-red /
|
||||
// near-white / gradient skins). Fill 48, border 90 of the state color's RGB.
|
||||
const float pillRound = schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size;
|
||||
if (shieldedDisplay) {
|
||||
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
|
||||
ImU32 shieldCol = Primary();
|
||||
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
|
||||
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
|
||||
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(shieldPillMin, shieldPillMax,
|
||||
(shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(48) << 24), pillRound);
|
||||
dl->AddRect(shieldPillMin, shieldPillMax,
|
||||
(shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(90) << 24), pillRound, 0, 1.0f);
|
||||
if (showTopBadge) {
|
||||
float topX = statusX + (stackW - topSz.x) * 0.5f;
|
||||
ImVec2 topPillMin(topX - Layout::spacingSm(), cy - 1.0f);
|
||||
ImVec2 topPillMax(topX + topSz.x + Layout::spacingSm(),
|
||||
topPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(topPillMin, topPillMax,
|
||||
(topBadgeCol & 0x00FFFFFFu) | (static_cast<ImU32>(48) << 24), pillRound);
|
||||
dl->AddRect(topPillMin, topPillMax,
|
||||
(topBadgeCol & 0x00FFFFFFu) | (static_cast<ImU32>(90) << 24), pillRound, 0, 1.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(shieldX, cy), shieldCol, shieldedStr);
|
||||
ImVec2(topX, cy), topBadgeCol, topBadgeStr);
|
||||
}
|
||||
// Background pill
|
||||
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||
|
||||
@@ -31,6 +31,31 @@ static bool endsWith(const std::string& s, const std::string& suffix) {
|
||||
return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
}
|
||||
|
||||
// Reject archive member names that would escape the extraction root (zip-slip).
|
||||
// The snapshot legitimately carries sub-paths (blocks/, chainstate/), so — unlike the
|
||||
// updaters, which flatten to baseName() — we keep the relative path but refuse any entry
|
||||
// that is absolute, drive/UNC-rooted, or contains a ".." component.
|
||||
static bool isSafeArchivePath(const std::string& name) {
|
||||
if (name.empty()) return false;
|
||||
std::string n = name;
|
||||
std::replace(n.begin(), n.end(), '\\', '/'); // normalize Windows separators
|
||||
if (n.front() == '/') return false; // absolute POSIX path
|
||||
const char c0 = n[0];
|
||||
if (n.size() >= 2 && n[1] == ':' &&
|
||||
((c0 >= 'A' && c0 <= 'Z') || (c0 >= 'a' && c0 <= 'z')))
|
||||
return false; // Windows drive letter (C:...)
|
||||
size_t start = 0;
|
||||
while (start <= n.size()) {
|
||||
const size_t slash = n.find('/', start);
|
||||
const std::string comp =
|
||||
n.substr(start, slash == std::string::npos ? std::string::npos : slash - start);
|
||||
if (comp == "..") return false; // path traversal component
|
||||
if (slash == std::string::npos) break;
|
||||
start = slash + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static size_t writeFileCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
size_t total = size * nmemb;
|
||||
FILE* fp = static_cast<FILE*>(userp);
|
||||
@@ -383,6 +408,16 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir)
|
||||
|
||||
std::string filename = stat.m_filename;
|
||||
|
||||
// *** SECURITY: reject zip-slip / path-traversal entries before building any path ***
|
||||
// A legitimate snapshot from the project host never contains these; an entry that does
|
||||
// indicates a malicious/corrupt archive, so abort rather than silently skip.
|
||||
if (!isSafeArchivePath(filename)) {
|
||||
DEBUG_LOGF("[Bootstrap] Unsafe archive path rejected: %s\n", filename.c_str());
|
||||
setProgress(State::Failed, "Refusing to extract unsafe archive entry: " + filename);
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
|
||||
// *** CRITICAL: Skip wallet.dat ***
|
||||
if (filename == "wallet.dat" || endsWith(filename, "/wallet.dat")) {
|
||||
DEBUG_LOGF("[Bootstrap] Skipping wallet.dat (protected)\n");
|
||||
|
||||
@@ -19,10 +19,19 @@ namespace util {
|
||||
|
||||
namespace {
|
||||
|
||||
// Cap for in-memory text/JSON responses (release metadata, price/candle data). Far above any
|
||||
// legitimate body, but bounds memory if a hostile/MITM'd server streams an unbounded response.
|
||||
constexpr std::size_t kMaxMetadataBytes = 16u * 1024 * 1024;
|
||||
|
||||
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;
|
||||
auto* s = static_cast<std::string*>(userp);
|
||||
const size_t n = size * nmemb;
|
||||
// Hard cap: a chunked response omits Content-Length, so CURLOPT_MAXFILESIZE cannot catch it —
|
||||
// aborting here (short count) makes curl fail the transfer instead of exhausting memory.
|
||||
if (s->size() + n > kMaxMetadataBytes) return 0;
|
||||
s->append(static_cast<char*>(contents), n);
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||
@@ -52,6 +61,7 @@ std::string httpGetString(const std::string& url, const char* logTag)
|
||||
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_MAXFILESIZE_LARGE, static_cast<curl_off_t>(kMaxMetadataBytes)); // reject oversized metadata
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
const CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
@@ -266,6 +266,13 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_rename"] = "Rename contact";
|
||||
strings_["chat_rename_hint"] = "Contact name";
|
||||
strings_["chat_renamed"] = "Contact renamed";
|
||||
// Chat note-buffer status (status bar, while the Chat tab is active). %d are counts;
|
||||
// keep the format specifiers intact in translations.
|
||||
strings_["chat_buffer_sending_one"] = "Chat: sending %d message\xE2\x80\xA6";
|
||||
strings_["chat_buffer_sending"] = "Chat: sending %d messages\xE2\x80\xA6";
|
||||
strings_["chat_buffer_preparing"] = "Chat buffer: preparing %d/%d\xE2\x80\xA6";
|
||||
strings_["chat_buffer_loading"] = "Chat buffer: \xE2\x80\xA6";
|
||||
strings_["chat_buffer_ready"] = "Chat buffer: %d/%d ready";
|
||||
// Chat customization (gear modal + Settings → Chat & Contacts)
|
||||
strings_["chat_settings_title"] = "Chat settings";
|
||||
strings_["chat_settings_tip"] = "Chat customization";
|
||||
@@ -432,6 +439,13 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mig_done_detail"] = "The daemon is rescanning to show your funds — this can take a few minutes. Your previous wallet was saved as a .bak in the data folder.";
|
||||
strings_["mig_done_btn"] = "Done";
|
||||
strings_["contacts_search_placeholder"] = "Search contacts...";
|
||||
strings_["contacts_settings_title"] = "Contacts settings";
|
||||
strings_["contacts_settings_tip"] = "Contacts customization";
|
||||
strings_["contacts_avatar_shape"] = "Avatar shape";
|
||||
strings_["contacts_shape_circle"] = "Circle";
|
||||
strings_["contacts_shape_square"] = "Square";
|
||||
strings_["contacts_shape_tab"] = "Left tab";
|
||||
strings_["contacts_list_scale"] = "List scale";
|
||||
strings_["contacts_search_no_match"] = "No matching contacts";
|
||||
strings_["address_book_confirm_delete"] = "Confirm delete?";
|
||||
strings_["mining"] = "Mining";
|
||||
@@ -707,6 +721,47 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["tt_idle_delay"] = "How long to wait before starting mining";
|
||||
strings_["tt_wizard"] = "Re-run the initial setup wizard\nDaemon will be restarted";
|
||||
strings_["tt_download_bootstrap"] = "Download blockchain bootstrap to speed up sync\nExisting block data will be replaced";
|
||||
// --- Full-node Node & Security tooltips ---
|
||||
strings_["tt_rpc_toggle"] = "Show or hide the read-only RPC connection details (host, port, user, password) for the daemon";
|
||||
strings_["tt_daemon_refresh"] = "Re-read the installed and bundled dragonxd version, size, and date shown above";
|
||||
// --- Lite wallet Node & Security tooltips ---
|
||||
strings_["tt_lite_lifecycle_toggle"] = "Show or hide the create / open / restore controls for managing your lite wallet file";
|
||||
strings_["tt_lite_lifecycle_op"] = "Choose whether to create a new wallet, open an existing one, or restore one from a seed phrase";
|
||||
strings_["tt_lite_wallet_path"] = "Path or name of the wallet file to open or restore into";
|
||||
strings_["tt_lite_restore_seed"] = "The 24-word recovery seed phrase to restore this wallet from; hidden as you type";
|
||||
strings_["tt_lite_restore_birthday"] = "Block height the wallet was created at; scanning starts here. Use 0 or the earliest height if unsure";
|
||||
strings_["tt_lite_restore_account"] = "HD account index to restore; leave 0 unless you used multiple accounts under this seed";
|
||||
strings_["tt_lite_restore_overwrite"] = "Replace an existing wallet file with this restore. Warning: overwrites the current wallet data";
|
||||
strings_["tt_lite_lifecycle_pass"] = "Passphrase to unlock or set on the wallet during this create / open / restore operation";
|
||||
strings_["tt_lite_lifecycle_run"] = "Run the selected create / open / restore operation with the values above";
|
||||
strings_["tt_lite_show_seed"] = "Reveal this wallet's recovery seed phrase and birthday. Anyone with the seed can spend your funds";
|
||||
strings_["tt_lite_show_keys"] = "Reveal this wallet's private spending keys. Anyone with a key can spend the funds it controls";
|
||||
strings_["tt_lite_copy"] = "Copy the revealed secret to the clipboard";
|
||||
strings_["tt_lite_save_seed_file"] = "Write the seed and birthday to an owner-only file (lite-seed-backup.txt) in the config folder";
|
||||
strings_["tt_lite_hide_wipe"] = "Hide the revealed secret and securely wipe it from memory";
|
||||
strings_["tt_lite_import_key"] = "Paste a private spending or viewing key to import; its history appears after the next sync";
|
||||
strings_["tt_lite_import_key_btn"] = "Import the entered private key into this wallet; funds and history appear after the next sync";
|
||||
strings_["tt_lite_encrypt_pass"] = "Passphrase to encrypt the wallet with. If lost, the wallet cannot be unlocked or recovered";
|
||||
strings_["tt_lite_encrypt"] = "Encrypt the wallet with the passphrase above; it locks immediately and requires the passphrase to unlock";
|
||||
strings_["tt_lite_unlock_pass"] = "Enter your passphrase to unlock the encrypted wallet";
|
||||
strings_["tt_lite_unlock"] = "Unlock the encrypted wallet using the passphrase above";
|
||||
strings_["tt_lite_lock"] = "Lock the wallet now; a passphrase is required to unlock and any chat session is torn down";
|
||||
strings_["tt_lite_decrypt_pass"] = "Enter your passphrase to remove encryption from the wallet";
|
||||
strings_["tt_lite_remove_encrypt"] = "Remove encryption and store the wallet unprotected; no passphrase will be required to open it";
|
||||
// --- Chat & Contacts tooltips ---
|
||||
strings_["tt_chat_emoji_style"] = "Render emoji in monochrome outline or full color";
|
||||
strings_["tt_chat_bubble_style"] = "Message bubble shape: rounded, square, or minimal (flat, borderless)";
|
||||
strings_["tt_chat_bubble_accent"] = "Accent color for your outgoing message bubbles (or follow the current theme)";
|
||||
strings_["tt_chat_density"] = "Spacing between messages: Comfortable adds more padding; Compact fits more on screen";
|
||||
strings_["tt_chat_font_size"] = "Scale chat message text from 0.8x to 1.5x. Affects only the Chat tab, not the rest of the app";
|
||||
strings_["tt_chat_poll_rate"] = "How often to check for new and 0-conf messages (0.5-15 s). Faster is more responsive but uses more CPU";
|
||||
strings_["tt_chat_timestamp"] = "Timestamp format for this tab only: follow the app-wide clock, or force 24-hour or 12-hour";
|
||||
strings_["tt_chat_enter_sends"] = "When on, Enter sends the message and Shift+Enter adds a newline; when off, Enter adds a newline";
|
||||
// --- Debug Options tooltips ---
|
||||
strings_["tt_screenshot_sweep"] = "Cycle every theme across every tab, saving a screenshot of each into the config screenshots folder (overwrites the last sweep)";
|
||||
strings_["tt_screenshot_sweep_full"] = "Like the theme sweep but also captures every modal / dialog / flow using temporary offline demo wallet data";
|
||||
strings_["tt_screenshot_open_dir"] = "Open the screenshots folder (under the config directory) in your file manager";
|
||||
strings_["tt_seed_demo_chat"] = "Inject sample conversations into the Chat tab so a sweep captures its UI; in-memory only, gone on restart";
|
||||
strings_["download_bootstrap"] = "Download Bootstrap";
|
||||
strings_["download"] = "Download";
|
||||
strings_["retry"] = "Retry";
|
||||
@@ -997,6 +1052,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["sent_filter"] = "Sent";
|
||||
strings_["received_filter"] = "Received";
|
||||
strings_["mined_filter"] = "Mined";
|
||||
strings_["chat_filter"] = "Chat";
|
||||
strings_["sort_date_newest"] = "Newest first";
|
||||
strings_["sort_date_oldest"] = "Oldest first";
|
||||
strings_["sort_amount_high"] = "Largest amount";
|
||||
@@ -1012,6 +1068,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["address_upper"] = "ADDRESS";
|
||||
strings_["memo_upper"] = "MEMO";
|
||||
strings_["shielded_type"] = "Shielded";
|
||||
strings_["tx_chat_badge"] = "Message";
|
||||
strings_["recv_type"] = "Recv";
|
||||
strings_["sent_type"] = "Sent";
|
||||
strings_["immature_type"] = "Immature";
|
||||
@@ -1388,6 +1445,12 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_available_commands"] = "Available commands:";
|
||||
strings_["console_quit_note"] = "'quit'/'exit' aren't needed here — just close the window.";
|
||||
strings_["lite_console_help_passthrough"] = "Any other input runs as a lite-wallet console command.";
|
||||
strings_["lite_console_backend_commands"] = "Backend commands:";
|
||||
strings_["console_no_output"] = "(no output)";
|
||||
strings_["console_backend_unavailable"] = "No backend";
|
||||
strings_["console_last_error"] = "Last error:";
|
||||
strings_["console_not_connected_lite"] = "Error: no wallet open";
|
||||
strings_["console_stop_confirm_node"] = "'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.";
|
||||
strings_["console_capturing_output"] = "Capturing daemon output...";
|
||||
strings_["console_clear"] = "Clear";
|
||||
strings_["console_clear_console"] = "Clear Console";
|
||||
@@ -1427,6 +1490,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_no_daemon"] = "No daemon";
|
||||
strings_["console_not_connected"] = "Error: Not connected to daemon";
|
||||
strings_["console_rpc_reference"] = "RPC Command Reference";
|
||||
strings_["console_backend_reference"] = "Backend Command Reference";
|
||||
strings_["console_rpc_trace"] = "RPC";
|
||||
strings_["console_app"] = "App";
|
||||
strings_["console_show_app_output"] = "Show [app] wallet log lines";
|
||||
@@ -1435,6 +1499,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_show_daemon_output"] = "Show daemon output";
|
||||
strings_["console_show_errors_only"] = "Show errors only";
|
||||
strings_["console_show_rpc_ref"] = "Show RPC command reference";
|
||||
strings_["console_show_backend_ref"] = "Show backend command reference";
|
||||
strings_["console_show_rpc_trace"] = "Show app RPC calls";
|
||||
strings_["console_showing_lines"] = "Showing %zu of %zu lines";
|
||||
strings_["console_starting_node"] = "Starting node...";
|
||||
@@ -1460,10 +1525,15 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_cat_wallet"] = "Wallet";
|
||||
strings_["console_cat_raw_transactions"] = "Raw Transactions";
|
||||
strings_["console_cat_utility"] = "Utility";
|
||||
strings_["console_cat_sync"] = "Sync";
|
||||
strings_["console_cat_send"] = "Send";
|
||||
strings_["console_cat_keys"] = "Keys & Security";
|
||||
strings_["console_cat_advanced"] = "Advanced";
|
||||
strings_["console_ref_search_hint"] = "Search by name or task\xE2\x80\xA6";
|
||||
strings_["console_ref_parameters"] = "Parameters";
|
||||
strings_["console_ref_no_params"] = "Takes no parameters.";
|
||||
strings_["console_ref_optional"] = "optional";
|
||||
strings_["console_ref_value"] = "value";
|
||||
strings_["console_ref_example"] = "Example";
|
||||
strings_["console_ref_builds"] = "Builds";
|
||||
strings_["console_ref_destructive"] = "Consequential";
|
||||
@@ -1559,6 +1629,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["market_12h"] = "12h";
|
||||
strings_["market_18h"] = "18h";
|
||||
strings_["market_24h"] = "24h";
|
||||
strings_["market_col_name"] = "Name";
|
||||
strings_["market_col_value"] = "Value";
|
||||
strings_["market_col_trend"] = "Trend";
|
||||
strings_["market_24h_volume"] = "24H VOLUME";
|
||||
strings_["market_6h"] = "6h";
|
||||
strings_["market_iv_live"] = "Live";
|
||||
@@ -1575,6 +1648,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["market_chart_loading"] = "Loading price history";
|
||||
strings_["market_style_line"] = "Switch to line chart";
|
||||
strings_["market_style_candle"] = "Switch to candlesticks";
|
||||
strings_["market_style_line_label"] = "Line";
|
||||
strings_["market_style_candle_label"] = "Candlestick";
|
||||
strings_["market_settings_title"] = "Market settings";
|
||||
strings_["market_settings_tip"] = "Market options";
|
||||
strings_["market_opt_chart_style"] = "Chart style";
|
||||
strings_["market_no_price"] = "No price data";
|
||||
strings_["market_now"] = "Now";
|
||||
strings_["market_pct_shielded"] = "%.0f%% Shielded";
|
||||
@@ -1622,9 +1700,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["portfolio_clear_sel"] = "Clear";
|
||||
strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses.";
|
||||
strings_["portfolio_style_label"] = "Portfolio style";
|
||||
strings_["portfolio_style_compact"] = "Compact rows";
|
||||
strings_["portfolio_style_detailed"] = "Detailed rows";
|
||||
strings_["portfolio_style_featured"] = "Featured rows";
|
||||
strings_["portfolio_style_compact"] = "Table";
|
||||
strings_["portfolio_style_detailed"] = "Cards";
|
||||
strings_["portfolio_style_featured"] = "Spotlight";
|
||||
strings_["portfolio_edit"] = "Edit";
|
||||
strings_["portfolio_delete"] = "Delete";
|
||||
strings_["portfolio_save"] = "Save";
|
||||
@@ -1839,6 +1917,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["lite_net_show_hidden"] = "Show hidden servers";
|
||||
strings_["lite_net_hidden_section"] = "Hidden servers";
|
||||
strings_["lite_net_connected"] = "Connected";
|
||||
strings_["lite_net_connecting"] = "Connecting";
|
||||
strings_["lite_net_disconnected"] = "Not connected";
|
||||
strings_["lite_net_syncing"] = "Syncing";
|
||||
strings_["lite_net_synced"] = "Synced";
|
||||
|
||||
@@ -57,7 +57,15 @@ void Logger::write(const std::string& message)
|
||||
now.time_since_epoch()) % 1000;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
|
||||
// Reachable from worker/monitor threads — std::localtime shares a process-wide static tm, so use the
|
||||
// reentrant variant into a local tm (the logger's own mutex can't protect other localtime callers).
|
||||
std::tm tmv{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &time);
|
||||
#else
|
||||
localtime_r(&time, &tmv);
|
||||
#endif
|
||||
ss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
|
||||
ss << '.' << std::setfill('0') << std::setw(3) << ms.count();
|
||||
ss << " | " << message;
|
||||
|
||||
|
||||
@@ -30,14 +30,6 @@ const std::vector<KnownPool>& knownPools()
|
||||
"https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
|
||||
/*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true,
|
||||
},
|
||||
KnownPool{
|
||||
// The mining (stratum) host is us.dragonx.cc — pool.dragonx.cc is the
|
||||
// Cloudflare-proxied web/API host and does NOT accept stratum on :3333.
|
||||
// Stats still come from pool.dragonx.cc/api/pools (proxied HTTP is fine).
|
||||
"dragonx-cc-pplns", "pool.dragonx.cc", "us.dragonx.cc:3333", "rx/dragonx",
|
||||
"https://pool.dragonx.cc/api/pools", PoolStatsSchema::Miningcore,
|
||||
/*miningcorePoolId=*/"dragonx-pplns", /*feePercent=*/3.0, /*official=*/true,
|
||||
},
|
||||
};
|
||||
return pools;
|
||||
}
|
||||
|
||||
75
src/util/svg_texture.cpp
Normal file
75
src/util/svg_texture.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "svg_texture.h"
|
||||
#include "texture_loader.h" // CreateRawTexture
|
||||
#include "logger.h"
|
||||
|
||||
// nanosvg (memononen, zlib/public-domain, vendored in libs/nanosvg). Implementations compiled ONLY here.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
// Perceptual brightness of a nanosvg color (byte order R,G,B,A — matches ImU32/IM_COL32).
|
||||
static inline float svgLuma(unsigned int c) {
|
||||
const float r = (c & 0xFFu) / 255.0f;
|
||||
const float g = ((c >> 8) & 0xFFu) / 255.0f;
|
||||
const float b = ((c >> 16) & 0xFFu) / 255.0f;
|
||||
return 0.299f * r + 0.587f * g + 0.114f * b;
|
||||
}
|
||||
|
||||
bool LoadTextureFromSvg(const char* svgText, int pxSize, ImU32 bodyColor, ImU32 detailColor,
|
||||
ImTextureID* outTex, int* outW, int* outH) {
|
||||
if (!svgText || pxSize <= 0 || !outTex) return false;
|
||||
|
||||
// nsvgParse mutates its input buffer — parse a copy.
|
||||
std::string buf(svgText);
|
||||
NSVGimage* img = nsvgParse(&buf[0], "px", 96.0f);
|
||||
if (!img || img->width <= 0.0f || img->height <= 0.0f) {
|
||||
if (img) nsvgDelete(img);
|
||||
DEBUG_LOGF("LoadTextureFromSvg: parse failed / empty image\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recolor by brightness: light fills -> detail, darker fills -> body. Force opaque so a theme color
|
||||
// with partial alpha can't make the mark translucent. ImU32 and nanosvg color share byte order.
|
||||
const unsigned int body = (static_cast<unsigned int>(bodyColor) & 0x00FFFFFFu) | 0xFF000000u;
|
||||
const unsigned int detail = (static_cast<unsigned int>(detailColor) & 0x00FFFFFFu) | 0xFF000000u;
|
||||
for (NSVGshape* s = img->shapes; s; s = s->next) {
|
||||
if (s->fill.type == NSVG_PAINT_COLOR)
|
||||
s->fill.color = (svgLuma(s->fill.color) > 0.6f) ? detail : body;
|
||||
if (s->stroke.type == NSVG_PAINT_COLOR)
|
||||
s->stroke.color = (svgLuma(s->stroke.color) > 0.6f) ? detail : body;
|
||||
}
|
||||
|
||||
const float scale = static_cast<float>(pxSize) / img->width;
|
||||
const int w = pxSize;
|
||||
const int h = std::max(1, static_cast<int>(img->height * scale + 0.5f));
|
||||
|
||||
std::vector<unsigned char> rgba(static_cast<size_t>(w) * h * 4, 0);
|
||||
NSVGrasterizer* rast = nsvgCreateRasterizer();
|
||||
if (!rast) { nsvgDelete(img); return false; }
|
||||
nsvgRasterize(rast, img, 0.0f, 0.0f, scale, rgba.data(), w, h, w * 4);
|
||||
nsvgDeleteRasterizer(rast);
|
||||
nsvgDelete(img);
|
||||
|
||||
if (!CreateRawTexture(rgba.data(), w, h, /*repeat=*/false, outTex)) {
|
||||
DEBUG_LOGF("LoadTextureFromSvg: CreateRawTexture failed (%dx%d)\n", w, h);
|
||||
return false;
|
||||
}
|
||||
if (outW) *outW = w;
|
||||
if (outH) *outH = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
24
src/util/svg_texture.h
Normal file
24
src/util/svg_texture.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Rasterize a (two-tone) SVG to a themed RGBA GPU texture via nanosvg. Used for the DragonX logo /
|
||||
// custom emoji: the darker fill becomes `bodyColor` (the theme accent), the light fill `detailColor`.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
// Rasterize `svgText` (nul-terminated SVG markup) to a texture of width `pxSize` (height follows the
|
||||
// SVG aspect). Fills are recolored by brightness — light fills -> detailColor, darker fills -> bodyColor
|
||||
// (both forced opaque). Returns false on parse/rasterize/upload failure; on success *outTex is a texture
|
||||
// the caller owns (release via util::DestroyTexture). Safe to call off the initial frame (needs a live
|
||||
// GL/DX device, like the other texture loaders).
|
||||
bool LoadTextureFromSvg(const char* svgText, int pxSize, ImU32 bodyColor, ImU32 detailColor,
|
||||
ImTextureID* outTex, int* outW, int* outH);
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
@@ -314,6 +314,12 @@ void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRele
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (kXmrigRequireSignature) {
|
||||
// Signatures are required but no key is pinned in this build: fail closed rather than
|
||||
// silently downgrading to checksum-only (the checksum is same-origin as the archive).
|
||||
fs::remove(zipPath, ec);
|
||||
setProgress(State::Failed, "No signing key is pinned in this build — refusing to install.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,10 +81,56 @@ LiteConnectionSettings defaultLiteConnectionSettings()
|
||||
return settings;
|
||||
}
|
||||
|
||||
// Strict dotted-decimal check for the 127.0.0.0/8 loopback block: exactly four numeric octets
|
||||
// (each 0-255) with the first equal to 127. This must NOT be a prefix match — "127.0.0.1.evil.com"
|
||||
// and "127.evil.com" are attacker-controlled DNS names that a startsWith("127.") test would wrongly
|
||||
// treat as loopback, reopening the plaintext-downgrade hole.
|
||||
static bool isNumericIpv4Loopback(const std::string& h)
|
||||
{
|
||||
int parts = 0;
|
||||
size_t start = 0;
|
||||
while (true) {
|
||||
const size_t dot = h.find('.', start);
|
||||
const std::string seg = h.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
|
||||
if (seg.empty() || seg.size() > 3) return false;
|
||||
int v = 0;
|
||||
for (char c : seg) { if (c < '0' || c > '9') return false; v = v * 10 + (c - '0'); }
|
||||
if (v > 255) return false;
|
||||
if (parts == 0 && v != 127) return false; // 127.0.0.0/8 only
|
||||
++parts;
|
||||
if (dot == std::string::npos) break;
|
||||
start = dot + 1;
|
||||
}
|
||||
return parts == 4;
|
||||
}
|
||||
|
||||
static bool isLoopbackLiteHostSpec(const std::string& hostPort)
|
||||
{
|
||||
std::string h = hostPort;
|
||||
const size_t term = h.find_first_of("/?#"); // strip path/query/fragment
|
||||
if (term != std::string::npos) h = h.substr(0, term);
|
||||
const size_t at = h.rfind('@'); // strip userinfo (user:pass@host)
|
||||
if (at != std::string::npos) h = h.substr(at + 1);
|
||||
if (!h.empty() && h.front() == '[') { // [::1]:port (bracketed IPv6)
|
||||
const size_t close = h.find(']');
|
||||
h = (close == std::string::npos) ? h : h.substr(1, close - 1);
|
||||
} else { // host or host:port
|
||||
const size_t colon = h.find(':');
|
||||
if (colon != std::string::npos) h = h.substr(0, colon);
|
||||
}
|
||||
return h == "localhost" || h == "::1" || isNumericIpv4Loopback(h);
|
||||
}
|
||||
|
||||
bool isLiteServerUrlUsable(const std::string& serverUrl)
|
||||
{
|
||||
const std::string normalized = liteTrimCopy(serverUrl);
|
||||
return startsWith(normalized, "https://") || startsWith(normalized, "http://");
|
||||
if (startsWith(normalized, "https://")) return true;
|
||||
// SECURITY: plaintext http:// is a TLS downgrade for lightwalletd traffic (view keys,
|
||||
// transactions, addresses). Permit it only for loopback (a local dev lightwalletd);
|
||||
// reject remote plaintext servers instead of silently accepting the downgrade.
|
||||
if (startsWith(normalized, "http://"))
|
||||
return isLoopbackLiteHostSpec(normalized.substr(sizeof("http://") - 1));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isOfficialLiteServer(const std::string& serverUrl)
|
||||
|
||||
@@ -1066,7 +1066,16 @@ LiteEncryptionResult LiteWalletController::encryptWallet(std::string passphrase)
|
||||
}
|
||||
out = parseEncryptionOpResponse(bridge_->execute("encrypt", passphrase));
|
||||
secureWipeLiteSecret(passphrase);
|
||||
if (out.ok) bridge_->execute("save", ""); // persist the now-encrypted wallet
|
||||
if (out.ok) {
|
||||
// Persist the now-encrypted wallet. If the save fails, do NOT report success — the
|
||||
// on-disk wallet would still be unencrypted, contradicting what the user was told.
|
||||
const auto saved = bridge_->execute("save", "");
|
||||
if (!saved.ok) {
|
||||
out.ok = false;
|
||||
out.error = "wallet encrypted in memory but saving to disk failed" +
|
||||
(saved.error.empty() ? std::string() : (": " + saved.error));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1084,7 +1093,16 @@ LiteEncryptionResult LiteWalletController::decryptWallet(std::string passphrase)
|
||||
}
|
||||
out = parseEncryptionOpResponse(bridge_->execute("decrypt", passphrase));
|
||||
secureWipeLiteSecret(passphrase);
|
||||
if (out.ok) bridge_->execute("save", ""); // persist the now-unencrypted wallet
|
||||
if (out.ok) {
|
||||
// Persist the now-unencrypted wallet. If the save fails, do NOT report success — the
|
||||
// on-disk wallet would still be encrypted, contradicting what the user was told.
|
||||
const auto saved = bridge_->execute("save", "");
|
||||
if (!saved.ok) {
|
||||
out.ok = false;
|
||||
out.error = "wallet decrypted in memory but saving to disk failed" +
|
||||
(saved.error.empty() ? std::string() : (": " + saved.error));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -3434,9 +3434,11 @@ void testBalanceAddressListModel()
|
||||
EXPECT_NEAR(layout.contentStartX, 22.0, 0.0001);
|
||||
EXPECT_NEAR(layout.contentStartY, 26.0, 0.0001);
|
||||
EXPECT_NEAR(layout.buttonSize, 38.0, 0.0001);
|
||||
EXPECT_NEAR(layout.favoriteButton.x, 270.0, 0.0001);
|
||||
EXPECT_NEAR(layout.visibilityButton.x, 228.0, 0.0001);
|
||||
EXPECT_NEAR(layout.contentRight, 224.0, 0.0001);
|
||||
// Trailing (favorite) button is inset by rowPadLeft (12) so it mirrors the left margin
|
||||
// instead of hugging the card edge: 310 - 38 - 12 = 260.
|
||||
EXPECT_NEAR(layout.favoriteButton.x, 260.0, 0.0001);
|
||||
EXPECT_NEAR(layout.visibilityButton.x, 218.0, 0.0001);
|
||||
EXPECT_NEAR(layout.contentRight, 214.0, 0.0001);
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(2.0, 3.5), std::string("$7.00"));
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.001, 2.0), std::string("$0.002000"));
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string(""));
|
||||
@@ -5300,8 +5302,11 @@ void testXmrigAssetSelection()
|
||||
EXPECT_TRUE(linux >= 0);
|
||||
EXPECT_TRUE(win >= 0);
|
||||
EXPECT_TRUE(linux != win);
|
||||
EXPECT_TRUE(rel.assets[linux].name.find("linux-x64.zip") != std::string::npos);
|
||||
EXPECT_TRUE(rel.assets[win].name.find("win-x64.zip") != std::string::npos);
|
||||
// Guard the index reads: select*Asset returns -1 when nothing matches, and EXPECT_TRUE doesn't
|
||||
// abort this harness, so an unguarded rel.assets[-1] on a fixture/parser regression would SIGSEGV
|
||||
// the whole suite instead of reporting the failed EXPECT above.
|
||||
if (linux >= 0) EXPECT_TRUE(rel.assets[linux].name.find("linux-x64.zip") != std::string::npos);
|
||||
if (win >= 0) EXPECT_TRUE(rel.assets[win].name.find("win-x64.zip") != std::string::npos);
|
||||
// No macOS build in this fixture -> graceful "not found".
|
||||
EXPECT_EQ(selectXmrigAsset(rel, "macos-x86_64"), -1);
|
||||
EXPECT_EQ(selectXmrigAsset(rel, "macos-arm64"), -1);
|
||||
@@ -5607,9 +5612,11 @@ void testDaemonAssetSelection()
|
||||
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);
|
||||
// Guard the index reads (select*Asset returns -1 on no match; EXPECT_TRUE doesn't abort here) so a
|
||||
// fixture/parser regression reports the failed EXPECT above instead of an OOB rel.assets[-1] crash.
|
||||
if (lin >= 0) EXPECT_TRUE(rel.assets[lin].name.find("linux-amd64.zip") != std::string::npos);
|
||||
if (mac >= 0) EXPECT_TRUE(rel.assets[mac].name.find("macos.zip") != std::string::npos);
|
||||
if (win >= 0) 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);
|
||||
@@ -5769,23 +5776,20 @@ void testXmrigLiveInstall()
|
||||
void testPoolRegistryLookup()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(2));
|
||||
// pool.dragonx.cc was removed from the built-in defaults; pool.dragonx.is is the sole entry.
|
||||
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(1));
|
||||
|
||||
// The algo follows the pool. Note pool.dragonx.cc's stratum host is us.dragonx.cc
|
||||
// (the .cc domain is only the Cloudflare-proxied web/API host).
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/dragonx"));
|
||||
// The algo follows the pool: pool.dragonx.is resolves to rx/hush regardless of the caller default.
|
||||
EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush"));
|
||||
// Bare host (no port) still matches; scheme + path are tolerated.
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc", "rx/hush"), std::string("rx/dragonx"));
|
||||
EXPECT_EQ(resolvePoolAlgo("stratum+tcp://us.dragonx.cc:3333/x", "rx/hush"),
|
||||
std::string("rx/dragonx"));
|
||||
// Unknown host -> fallback algo.
|
||||
// The former us.dragonx.cc (pool.dragonx.cc) host is now unknown -> caller's fallback algo.
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/hush"));
|
||||
EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush"));
|
||||
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") != nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:3433") != nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") == nullptr); // removed built-in default
|
||||
EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr);
|
||||
// A mismatched explicit port must NOT match a known pool.
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:9999") == nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:9999") == nullptr);
|
||||
}
|
||||
|
||||
// Schema-aware pool hashrate parsing (the two pools speak different APIs).
|
||||
@@ -6193,12 +6197,17 @@ void testHushChatOutgoing()
|
||||
|
||||
ChatService bobSvc;
|
||||
bobSvc.setIdentity(bob);
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, 5), 1);
|
||||
// Realistic receive-time fallback (the header "ts" is a real std::time stamp; an artificially-tiny
|
||||
// fallback would trip the future-plausibility clamp in ingest and reject the real ts).
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, static_cast<std::int64_t>(std::time(nullptr))), 1);
|
||||
std::vector<ChatMessage> conv = bobSvc.store().conversation(cid);
|
||||
EXPECT_EQ((int)conv.size(), 1);
|
||||
EXPECT_EQ(conv[0].body, std::string("hello bob"));
|
||||
EXPECT_EQ(conv[0].peer_public_key_hex, ra.public_key_hex); // Bob learns Alice's key
|
||||
EXPECT_EQ(conv[0].peer_zaddr, aliceZ);
|
||||
// The header now carries the sender's compose time (header "ts"); ingest prefers it over the fallback
|
||||
// (5) so both ends show the same send time. A real std::time() stamp is well past year-2001.
|
||||
EXPECT_TRUE(conv[0].timestamp > 1000000000);
|
||||
|
||||
// Plaintext contact request Alice -> Bob.
|
||||
OutgoingChatMemos creq;
|
||||
@@ -6235,7 +6244,9 @@ void testHushChatOutgoing()
|
||||
echo.peer_zaddr = aliceZ;
|
||||
echo.peer_public_key_hex = ra.public_key_hex;
|
||||
echo.body = "reply!";
|
||||
echo.timestamp = 7;
|
||||
// A reply is sent AFTER the received message; the incoming "hello bob" now carries a real compose-time
|
||||
// "ts" (~now), so stamp the echo just after it (not the old artificial 7, which would sort before it).
|
||||
echo.timestamp = static_cast<std::int64_t>(std::time(nullptr)) + 10;
|
||||
echo.txid = "out:local1";
|
||||
echo.payload_position = 0;
|
||||
EXPECT_TRUE(bobSvc.recordOutgoing(echo));
|
||||
@@ -6243,6 +6254,27 @@ void testHushChatOutgoing()
|
||||
EXPECT_EQ((int)conv2.size(), 2);
|
||||
EXPECT_TRUE(conv2.back().direction == ChatDirection::Outgoing);
|
||||
EXPECT_EQ(conv2.back().body, std::string("reply!"));
|
||||
|
||||
// Future-clock clamp: a header "ts" implausibly ahead of the receive time is rejected (falls back to
|
||||
// the receive time), so a wrong/ahead-clocked peer can't pin their messages to the bottom of a thread.
|
||||
{
|
||||
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
|
||||
const std::int64_t futureTs = nowSec + 10 * 24 * 3600; // 10 days ahead — well past the 1h tolerance
|
||||
nlohmann::json h;
|
||||
h["h"] = 1; h["v"] = 0; h["z"] = aliceZ; h["cid"] = "conv-clamp";
|
||||
h["t"] = "Cont"; h["e"] = ""; h["p"] = ra.public_key_hex; h["ts"] = futureTs;
|
||||
HushChatTransactionInput ctx;
|
||||
ctx.txid = "txclamp";
|
||||
ctx.outputs.push_back({0, h.dump()}); // header at the lower position
|
||||
ctx.outputs.push_back({1, "please add"}); // contact-request plaintext payload (must not start with '{')
|
||||
auto exc = extractHushChatTransactionMetadata(ctx, true);
|
||||
EXPECT_EQ((int)exc.metadata.size(), 1);
|
||||
EXPECT_EQ(exc.metadata[0].sent_at, futureTs); // the future ts parses through
|
||||
bobSvc.ingest(exc.metadata, {}, nowSec); // realistic receive time
|
||||
auto cc = bobSvc.store().conversation("conv-clamp");
|
||||
EXPECT_EQ((int)cc.size(), 1);
|
||||
EXPECT_EQ(cc[0].timestamp, nowSec); // clamped to receive time, NOT the future ts
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: the transport encoding (chatSendOutputs) — header first, "utf8:" for full-node / raw for
|
||||
@@ -6357,8 +6389,11 @@ void testAddressBookScope()
|
||||
fs::create_directories(tmp);
|
||||
setenv("HOME", tmp.string().c_str(), 1);
|
||||
|
||||
// A pre-scoping addressbook.json (no "scope" field) migrates to global on load.
|
||||
fs::path cfg = tmp / ".config" / "ObsidianDragon";
|
||||
// A pre-scoping addressbook.json (no "scope" field) migrates to global on load. Write to the
|
||||
// SAME per-variant config dir AddressBook::load() reads from (Lite -> ObsidianDragonLite/),
|
||||
// resolved under the temp HOME set above. Hardcoding ".config/ObsidianDragon" made the lite-build
|
||||
// ctest write where load() never looks -> 0 entries -> the entries()[0] below segfaulted.
|
||||
fs::path cfg = dragonx::util::Platform::getConfigDir();
|
||||
fs::create_directories(cfg);
|
||||
std::ofstream(cfg / "addressbook.json")
|
||||
<< R"({"entries":[{"label":"Legacy","address":"zs1legacy","notes":""}]})";
|
||||
@@ -6366,7 +6401,9 @@ void testAddressBookScope()
|
||||
AddressBook book;
|
||||
EXPECT_TRUE(book.load());
|
||||
EXPECT_EQ(book.size(), (size_t)1);
|
||||
EXPECT_TRUE(book.entries()[0].isGlobal()); // migrated -> global
|
||||
// Guard the [0] access — EXPECT_EQ doesn't abort this harness, so an empty book here must not
|
||||
// SIGSEGV the whole suite (it would take every later test down with it).
|
||||
EXPECT_TRUE(!book.entries().empty() && book.entries()[0].isGlobal()); // migrated -> global
|
||||
}
|
||||
|
||||
// Same address may be a contact in two DIFFERENT wallets, but not twice in one; and a global
|
||||
|
||||
12
third_party/silentdragonxlite/lib/rust-toolchain.toml
vendored
Normal file
12
third_party/silentdragonxlite/lib/rust-toolchain.toml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Rust toolchain pin for the vendored SilentDragonXLite (SDXL) backend.
|
||||
#
|
||||
# The pinned librustzcash + transitive crates (notably traitobject 0.1.0) rely on
|
||||
# pre-1.70 trait-coherence rules and fail to compile on newer rustc with
|
||||
# error[E0119]: conflicting implementations of trait `Trait` for type `(dyn Send + Sync)`
|
||||
# so the backend must be built with 1.63 (the toolchain scripts/build-lite-backend-artifact.sh
|
||||
# and CLAUDE.md target). rustup auto-selects this when cargo runs in this tree, so no
|
||||
# RUSTUP_TOOLCHAIN / `cargo +1.63.0` is needed.
|
||||
#
|
||||
# Install it once with: rustup toolchain install 1.63.0
|
||||
[toolchain]
|
||||
channel = "1.63.0"
|
||||
Reference in New Issue
Block a user