Compare commits
181 Commits
6f0f95f4fb
...
fix/balanc
| Author | SHA1 | Date | |
|---|---|---|---|
| 0942691eb3 | |||
| a2f84be2d4 | |||
| 7e8b99a82b | |||
| 29274c2f48 | |||
| 870793433b | |||
| 08cfeb0e08 | |||
| 5daf2d83b6 | |||
| 6d26ccd0ed | |||
| a7514becbc | |||
| 558cfcbe56 | |||
| d0bd55b9c1 | |||
| b37d3d97b6 | |||
| 8778398d31 | |||
| d5c30237d2 | |||
| a598217975 | |||
| 9205addf55 | |||
| ce8c7696d4 | |||
| 99d1e73676 | |||
| 755cf22ad0 | |||
| e24ca015d1 | |||
| 0de44569b7 | |||
| 06afbee4f8 | |||
| 6ee81a5abe | |||
| ea26c0cbbb | |||
| 13b225d8f5 | |||
| 08aed34bbe | |||
| 5edbe8a276 | |||
| 001e85ac1a | |||
| 393f3d147e | |||
| ac49f44f84 | |||
| 3216debc7d | |||
| 8ffcd9cc8c | |||
| bc183257b7 | |||
| b2e037bb67 | |||
| 975650f11b | |||
| 384d64ea5d | |||
| 3b7423f3a1 | |||
| d136916e80 | |||
| f88304fed2 | |||
| 5296dd7ae5 | |||
| a1d3964e34 | |||
| 5b6ba5094b | |||
| c3e81a5fa6 | |||
| d603a54618 | |||
| c61c211dfe | |||
| 16244d84a0 | |||
| 32be868dbc | |||
| 8bb3198562 | |||
| 4b3f0fa92b | |||
| c7c3440a7b | |||
| e779ded2e8 | |||
| 940dd21464 | |||
| 207f9074db | |||
| 05b00b158b | |||
| f9ddab059e | |||
| de1ae736de | |||
| 03c1b63e03 | |||
| 8c12b27c0a | |||
| c7d163f44a | |||
| 7e4822c021 | |||
| f9b622cb25 | |||
| 9204fa148a | |||
| da0e9f5915 | |||
| d188a08db7 | |||
| ff5f5ddf23 | |||
| 56f9802fb9 | |||
| efb271cb9a | |||
| eb69e491b9 | |||
| 2675b8ab93 | |||
| b3444e0a89 | |||
| 45b652f514 | |||
| 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 | |||
| 2c909d35ea | |||
| 3b5db02e09 | |||
| 8d8cd337cf | |||
| acde8c0833 | |||
| a78b44246e | |||
| 63ed31d2ec | |||
| 64b27db2ff | |||
| 03fe6e077c | |||
| 125ffa8863 | |||
| 3465acc57b | |||
| 30bd0d99ff | |||
| 29fbe46cce | |||
| fc414eeed4 | |||
| fce873936e | |||
| 7351d8a06d | |||
| 76093fe82d | |||
| ef247c95ff | |||
| c291e8a587 | |||
| 06e55d6394 | |||
| 2072f70a60 | |||
| ee9ea15233 | |||
| 11de117331 | |||
| fac0245297 | |||
| 9cb91415d9 | |||
| 24c661f743 | |||
| fecc9015fb | |||
| bce69362eb | |||
| 7f46d9e2d5 | |||
| 100ed01469 | |||
| 055685a4c4 | |||
| 46f93e380b | |||
| 6f6ad89c9f | |||
| d9911a9bc9 | |||
| faf77de9ce | |||
| 7f414b3dcf | |||
| 994ddea6cd | |||
| 41e4e73e63 | |||
| 88091bd77c | |||
| 02839dc415 | |||
| 17b70a1388 | |||
| 42d59709f9 | |||
| 30bc80b2f2 | |||
| 3808b3ee82 | |||
| bb5124c1ae | |||
| e6c78da062 | |||
| 5874142186 | |||
| 928335dd6b | |||
| 63dcda0ad1 | |||
| 122db9d903 | |||
| fc509a9340 | |||
| f546d3e2b1 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
|
|||||||
!prebuilt-binaries/dragonxd-win/.gitkeep
|
!prebuilt-binaries/dragonxd-win/.gitkeep
|
||||||
prebuilt-binaries/dragonxd-mac/*
|
prebuilt-binaries/dragonxd-mac/*
|
||||||
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
||||||
prebuilt-binaries/xmrig-hac/*
|
prebuilt-binaries/drg-xmrig/*
|
||||||
!prebuilt-binaries/xmrig-hac/.gitkeep
|
!prebuilt-binaries/drg-xmrig/.gitkeep
|
||||||
|
|
||||||
|
|
||||||
# External sources / toolchains (created by scripts/setup.sh)
|
# External sources / toolchains (created by scripts/setup.sh)
|
||||||
@@ -33,7 +33,11 @@ imgui.ini
|
|||||||
*.bak*
|
*.bak*
|
||||||
*.params
|
*.params
|
||||||
asmap.dat
|
asmap.dat
|
||||||
/external/xmrig-hac
|
# Wallet files hold PRIVATE KEYS — never commit them
|
||||||
|
wallet.dat
|
||||||
|
wallet-*.dat
|
||||||
|
wallet.dat.*
|
||||||
|
/external/drg-xmrig
|
||||||
/memory
|
/memory
|
||||||
/todo.md
|
/todo.md
|
||||||
/.github/
|
/.github/
|
||||||
@@ -54,3 +58,7 @@ third_party/silentdragonxlite/lib/vendor/
|
|||||||
|
|
||||||
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
|
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
|
||||||
res/ObsidianDragon.manifest
|
res/ObsidianDragon.manifest
|
||||||
|
|
||||||
|
# Cross-built mingw FreeType (color emoji) — regenerated by scripts/build-freetype-mingw.sh
|
||||||
|
third_party/freetype-mingw/
|
||||||
|
third_party/.freetype-mingw-build/
|
||||||
|
|||||||
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable user-facing changes to ObsidianDragon are documented here. The format loosely
|
||||||
|
follows [Keep a Changelog](https://keepachangelog.com/); the project uses Conventional Commits.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### ⚠️ Breaking changes
|
||||||
|
|
||||||
|
- **Remote RPC over plain HTTP is now refused by default.** If your wallet is configured to
|
||||||
|
reach a **remote** `rpchost`/`rpcconnect` **without TLS**, it will no longer connect — it
|
||||||
|
previously sent your `rpcuser`/`rpcpassword` in cleartext (capturable by anyone on the
|
||||||
|
network path) after only a dismissible warning. To reconnect, either:
|
||||||
|
- add **`rpctls=1`** to `DRAGONX.conf` (preferred, if your daemon supports TLS), or
|
||||||
|
- add **`rpcallowplaintext=1`** to `DRAGONX.conf` to explicitly accept the plaintext link.
|
||||||
|
|
||||||
|
Local and embedded daemons (`127.0.0.0/8`, `localhost`, `::1`) are unaffected.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Refuse remote plaintext RPC credential transmission by default (see Breaking changes above).
|
||||||
|
- Tightened localhost detection: a hostname that merely *starts* with `127.` (e.g.
|
||||||
|
`127.evil.com`) is no longer mistaken for a loopback address, so it can no longer bypass the
|
||||||
|
plaintext-RPC protection.
|
||||||
|
- Sapling parameters are now integrity-checked (SHA-256) against pinned canonical digests
|
||||||
|
before use, instead of only checking that the files exist. A truncated or corrupt parameter
|
||||||
|
file is caught up front rather than surfacing later as a confusing shielded-operation failure.
|
||||||
|
(Cached via a `size:mtime` marker so it doesn't re-hash ~48 MB on every launch.)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Daemon crashes are no longer occasionally missed: a race between the UI thread and the
|
||||||
|
process monitor could consume the daemon's exit status, hiding a crash and defeating the
|
||||||
|
automatic-restart cap. The monitor is now the sole reaper.
|
||||||
|
- A daemon that fails to launch (missing execute permission, wrong architecture, corrupt
|
||||||
|
binary) now reports a precise error immediately instead of briefly showing "running" and
|
||||||
|
then a generic "exited unexpectedly (exit code 127)".
|
||||||
|
- A quick stop→start no longer triggers a restart storm: the wallet now waits briefly for a
|
||||||
|
previous daemon to release the data-directory lock and shows a clear, non-crash message
|
||||||
|
instead of exhausting the crash-restart budget.
|
||||||
|
- Failures while writing the daemon binaries or Sapling parameters (disk full, permission
|
||||||
|
denied) are now surfaced clearly up front instead of failing opaquely when the daemon later
|
||||||
|
can't start.
|
||||||
|
- Directory-creation failures on startup (read-only home, permission denied) now produce a
|
||||||
|
clear "Cannot create <dir>" message instead of a confusing downstream "config missing" /
|
||||||
|
"binary not found" error (or, in one path, an uncaught exception).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A "Taking longer than expected" notice now appears if the daemon is reachable but hasn't
|
||||||
|
finished initializing after ~45 s (configurable via `ui.toml`), with guidance to restart the
|
||||||
|
daemon or open the Console — instead of an indefinite silent spinner. It clears itself
|
||||||
|
automatically once the daemon connects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Engineering detail and the finding-by-finding rationale for this batch live in
|
||||||
|
`docs/daemon-startup-hardening.md`.
|
||||||
130
CMakeLists.txt
130
CMakeLists.txt
@@ -15,7 +15,7 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 2.0.0
|
VERSION 2.0.1
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
@@ -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_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_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")
|
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
|
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
|
||||||
litelib_wallet_exists
|
litelib_wallet_exists
|
||||||
litelib_initialize_new
|
litelib_initialize_new
|
||||||
@@ -126,36 +125,24 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
|||||||
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
|
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}")
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
endif()
|
endif()
|
||||||
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
|
# Note (F15-1): the former signature-metadata gate was removed. It trusted a
|
||||||
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
|
# "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
|
||||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
|
# self-attested with no cryptographic check (the "verified" SHA was just the artifact's
|
||||||
endif()
|
# own SHA). The trust root is now build-from-source: that script builds the backend from
|
||||||
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
|
# the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
|
||||||
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
|
# is the one built from reviewed source. The required-symbol inventory check above stays.
|
||||||
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
|
|
||||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
|
|
||||||
endif()
|
|
||||||
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
|
|
||||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
|
|
||||||
endif()
|
|
||||||
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
|
|
||||||
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
|
|
||||||
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
|
|
||||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
|
|
||||||
endif()
|
|
||||||
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
|
|
||||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
|
|
||||||
endif()
|
|
||||||
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
|
|
||||||
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
|
|
||||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
||||||
set_target_properties(dragonx_lite_backend PROPERTIES
|
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||||
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
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(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||||
if(NOT IS_DIRECTORY "${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}")
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||||
@@ -226,7 +213,7 @@ include(FetchContent)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
json
|
json
|
||||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||||
GIT_TAG v3.11.3
|
GIT_TAG 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 # v3.11.3 — pinned to immutable commit (L-08); tags are mutable
|
||||||
GIT_SHALLOW TRUE
|
GIT_SHALLOW TRUE
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(json)
|
FetchContent_MakeAvailable(json)
|
||||||
@@ -235,7 +222,7 @@ FetchContent_MakeAvailable(json)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
tomlplusplus
|
tomlplusplus
|
||||||
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
||||||
GIT_TAG v3.4.0
|
GIT_TAG 30172438cee64926dc41fdd9c11fb3ba5b2ba9de # v3.4.0 — pinned to immutable commit (L-08); tags are mutable
|
||||||
GIT_SHALLOW TRUE
|
GIT_SHALLOW TRUE
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(tomlplusplus)
|
FetchContent_MakeAvailable(tomlplusplus)
|
||||||
@@ -302,8 +289,17 @@ message(STATUS "Fetching libwebp (decode-only, static)...")
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
libwebp
|
libwebp
|
||||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||||
GIT_TAG v1.4.0
|
GIT_TAG 845d5476a866141ba35ac133f856fa62f0b7445f # v1.4.0 — pinned to immutable commit (L-08); tags are mutable
|
||||||
GIT_SHALLOW TRUE
|
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_LINK_STATIC ON CACHE BOOL "" FORCE)
|
||||||
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
||||||
@@ -406,6 +402,35 @@ else()
|
|||||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/backends/imgui_impl_opengl3.h)
|
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/backends/imgui_impl_opengl3.h)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Optional FreeType font loader — enables color-emoji rendering (COLR/CPAL Twemoji) when the chat
|
||||||
|
# "color emoji" setting is on; otherwise the wallet falls back to the monochrome emoji subset.
|
||||||
|
# - Native Linux/macOS: use the system FreeType via find_package.
|
||||||
|
# - Windows (mingw cross): the toolchain ships no FreeType, so build.sh --win-release cross-builds a
|
||||||
|
# static one (scripts/build-freetype-mingw.sh) and passes -DDRAGONX_MINGW_FREETYPE_PREFIX here.
|
||||||
|
# - Other cross builds (osxcross) without FreeType: silently fall back to monochrome.
|
||||||
|
set(DRAGONX_FREETYPE OFF)
|
||||||
|
set(DRAGONX_FREETYPE_LIB "")
|
||||||
|
set(DRAGONX_FREETYPE_INC "")
|
||||||
|
if(DEFINED DRAGONX_MINGW_FREETYPE_PREFIX AND EXISTS "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||||
|
set(DRAGONX_FREETYPE ON)
|
||||||
|
set(DRAGONX_FREETYPE_LIB "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||||
|
set(DRAGONX_FREETYPE_INC "${DRAGONX_MINGW_FREETYPE_PREFIX}/include/freetype2")
|
||||||
|
message(STATUS "FreeType (mingw cross-built) found — chat color emoji enabled")
|
||||||
|
elseif(NOT CMAKE_CROSSCOMPILING)
|
||||||
|
find_package(Freetype QUIET)
|
||||||
|
if(FREETYPE_FOUND)
|
||||||
|
set(DRAGONX_FREETYPE ON)
|
||||||
|
set(DRAGONX_FREETYPE_LIB Freetype::Freetype) # imported target carries include dirs
|
||||||
|
message(STATUS "FreeType ${FREETYPE_VERSION_STRING} found — chat color emoji enabled")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(DRAGONX_FREETYPE)
|
||||||
|
list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/misc/freetype/imgui_freetype.cpp)
|
||||||
|
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/misc/freetype/imgui_freetype.h)
|
||||||
|
else()
|
||||||
|
message(STATUS "FreeType not found — chat color emoji falls back to monochrome")
|
||||||
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# QR Code library (bundled)
|
# QR Code library (bundled)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -519,6 +544,7 @@ set(APP_SOURCES
|
|||||||
src/util/async_task_manager.cpp
|
src/util/async_task_manager.cpp
|
||||||
src/util/amount_format.cpp
|
src/util/amount_format.cpp
|
||||||
src/util/address_validation.cpp
|
src/util/address_validation.cpp
|
||||||
|
src/util/seed_phrase.cpp
|
||||||
src/util/base64.cpp
|
src/util/base64.cpp
|
||||||
src/util/single_instance.cpp
|
src/util/single_instance.cpp
|
||||||
src/util/i18n.cpp
|
src/util/i18n.cpp
|
||||||
@@ -526,6 +552,7 @@ set(APP_SOURCES
|
|||||||
src/util/platform.cpp
|
src/util/platform.cpp
|
||||||
src/util/payment_uri.cpp
|
src/util/payment_uri.cpp
|
||||||
src/util/texture_loader.cpp
|
src/util/texture_loader.cpp
|
||||||
|
src/util/svg_texture.cpp
|
||||||
src/util/noise_texture.cpp
|
src/util/noise_texture.cpp
|
||||||
src/daemon/embedded_daemon.cpp
|
src/daemon/embedded_daemon.cpp
|
||||||
src/daemon/seed_wallet_creator.cpp
|
src/daemon/seed_wallet_creator.cpp
|
||||||
@@ -730,7 +757,9 @@ ${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
|||||||
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
|
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
|
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/NotoEmoji-Subset.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/TwemojiMozilla-Color.ttf"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(ObsidianDragon
|
add_executable(ObsidianDragon
|
||||||
@@ -863,6 +892,15 @@ else()
|
|||||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Color-emoji font loader (FreeType) — linked + flagged only when found (see DRAGONX_FREETYPE above).
|
||||||
|
if(DRAGONX_FREETYPE)
|
||||||
|
target_link_libraries(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_LIB})
|
||||||
|
if(DRAGONX_FREETYPE_INC)
|
||||||
|
target_include_directories(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_INC})
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAVE_FREETYPE)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(HushChatFixtureCheck
|
add_executable(HushChatFixtureCheck
|
||||||
tools/hushchat_fixture_check.cpp
|
tools/hushchat_fixture_check.cpp
|
||||||
src/chat/chat_protocol.cpp
|
src/chat/chat_protocol.cpp
|
||||||
@@ -1037,6 +1075,32 @@ install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
|||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# dragonx-wallet-rebuild — offline recovery helper for a BDB-inconsistent wallet.dat.
|
||||||
|
# Bundled next to the daemon; the app spawns it out-of-process. It is the ONLY thing that links
|
||||||
|
# Berkeley DB, so the AGPLv3 BDB never contaminates the GPLv3 GUI (same boundary as the daemon).
|
||||||
|
# Release builds should point DRAGONX_BDB_ROOT at the vendored static libdb (external/dragonx/depends);
|
||||||
|
# a dev build falls back to the system Berkeley DB. Skipped (with a note) if no BDB is found.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
find_path(BDB_INCLUDE_DIR db.h HINTS ${DRAGONX_BDB_ROOT}/include /usr/include /usr/local/include)
|
||||||
|
find_library(BDB_LIBRARY NAMES db-6.2 db-6.0 db-5.3 db libdb
|
||||||
|
HINTS ${DRAGONX_BDB_ROOT}/lib /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu)
|
||||||
|
if(BDB_INCLUDE_DIR AND BDB_LIBRARY)
|
||||||
|
add_executable(dragonx-wallet-rebuild tools/wallet_rebuild/main.cpp)
|
||||||
|
target_include_directories(dragonx-wallet-rebuild PRIVATE ${CMAKE_SOURCE_DIR}/src ${BDB_INCLUDE_DIR})
|
||||||
|
target_link_libraries(dragonx-wallet-rebuild PRIVATE ${BDB_LIBRARY})
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(dragonx-wallet-rebuild PRIVATE ws2_32) # static libdb-6.2 pulls in winsock
|
||||||
|
else()
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
target_link_libraries(dragonx-wallet-rebuild PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) # static libdb needs pthread/dl
|
||||||
|
endif()
|
||||||
|
set_target_properties(dragonx-wallet-rebuild PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
message(STATUS "wallet-rebuild helper: ON (Berkeley DB ${BDB_LIBRARY})")
|
||||||
|
else()
|
||||||
|
message(STATUS "wallet-rebuild helper: OFF (no Berkeley DB found; set DRAGONX_BDB_ROOT for release builds)")
|
||||||
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Tests
|
# Tests
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1085,6 +1149,7 @@ if(BUILD_TESTING)
|
|||||||
src/util/payment_uri.cpp
|
src/util/payment_uri.cpp
|
||||||
src/util/amount_format.cpp
|
src/util/amount_format.cpp
|
||||||
src/util/address_validation.cpp
|
src/util/address_validation.cpp
|
||||||
|
src/util/seed_phrase.cpp
|
||||||
src/util/i18n.cpp
|
src/util/i18n.cpp
|
||||||
src/util/text_format.cpp
|
src/util/text_format.cpp
|
||||||
src/data/wallet_state.cpp
|
src/data/wallet_state.cpp
|
||||||
@@ -1092,6 +1157,7 @@ if(BUILD_TESTING)
|
|||||||
src/data/address_book.cpp
|
src/data/address_book.cpp
|
||||||
src/data/wallet_index.cpp
|
src/data/wallet_index.cpp
|
||||||
src/daemon/lifecycle_adapters.cpp
|
src/daemon/lifecycle_adapters.cpp
|
||||||
|
src/daemon/embedded_daemon.cpp
|
||||||
src/rpc/connection.cpp
|
src/rpc/connection.cpp
|
||||||
src/config/settings.cpp
|
src/config/settings.cpp
|
||||||
src/resources/embedded_resources.cpp
|
src/resources/embedded_resources.cpp
|
||||||
@@ -1163,5 +1229,5 @@ message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
|
|||||||
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
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 "")
|
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-win/
|
||||||
- prebuilt-binaries/dragonxd-mac/
|
- prebuilt-binaries/dragonxd-mac/
|
||||||
|
|
||||||
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
|
||||||
- prebuilt-binaries/xmrig-hac/
|
- prebuilt-binaries/drg-xmrig/
|
||||||
|
|
||||||
|
|
||||||
## Build Steps
|
## Build Steps
|
||||||
|
|||||||
198
build.sh
198
build.sh
@@ -131,7 +131,7 @@ fi
|
|||||||
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||||
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||||
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
_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)
|
_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_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)
|
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
@@ -195,6 +195,24 @@ should_bundle_full_node_assets() {
|
|||||||
! $DO_LITE
|
! $DO_LITE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The offline wallet-rebuild helper is the ONLY thing that repairs a genuinely BDB-inconsistent
|
||||||
|
# wallet.dat — plain "Restore" just re-triggers the daemon's salvage cascade. A full-node release must
|
||||||
|
# NEVER ship without it (the in-app "Repair automatically" option silently disappears otherwise), so
|
||||||
|
# treat a missing helper as a HARD build failure instead of degrading recovery to Restore-only.
|
||||||
|
# $1 = built helper path (e.g. bin/dragonx-wallet-rebuild[.exe]); $2 = the BDB depends dir for the hint.
|
||||||
|
require_wallet_rebuild_helper() {
|
||||||
|
local helper="$1" depends="$2"
|
||||||
|
should_bundle_full_node_assets || return 0 # lite builds have no BDB wallet.dat to rebuild
|
||||||
|
if [[ ! -f "$helper" ]]; then
|
||||||
|
err "wallet-rebuild helper was NOT built: $helper"
|
||||||
|
err " → the recovery 'Repair automatically' option would be MISSING from this release."
|
||||||
|
err " Cause: the vendored Berkeley DB depends are absent, so CMake skipped the dragonx-wallet-rebuild target."
|
||||||
|
err " Fix: provide ${depends}/{lib/libdb-6.2.a,include/db.h} (same static libdb the daemon links), then rebuild."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
info " wallet-rebuild helper present: $helper"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Helper: find resource files ──────────────────────────────────────────────
|
# ── Helper: find resource files ──────────────────────────────────────────────
|
||||||
find_sapling_params() {
|
find_sapling_params() {
|
||||||
local dirs=(
|
local dirs=(
|
||||||
@@ -286,6 +304,9 @@ bundle_linux_daemon() {
|
|||||||
# asmap.dat
|
# asmap.dat
|
||||||
find_asmap && cp "$ASMAP_DAT" "$dest/asmap.dat" && info " Bundled asmap.dat"
|
find_asmap && cp "$ASMAP_DAT" "$dest/asmap.dat" && info " Bundled asmap.dat"
|
||||||
|
|
||||||
|
# (The dragonx-wallet-rebuild recovery helper is built into bin/ by CMake and packaged explicitly
|
||||||
|
# by each release path — required via require_wallet_rebuild_helper — so it is not copied here.)
|
||||||
|
|
||||||
return $found
|
return $found
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +357,24 @@ build_release_linux() {
|
|||||||
mkdir -p "$bd" && cd "$bd"
|
mkdir -p "$bd" && cd "$bd"
|
||||||
|
|
||||||
# ── Compile ──────────────────────────────────────────────────────────────
|
# ── Compile ──────────────────────────────────────────────────────────────
|
||||||
|
# Point the wallet-rebuild helper at the vendored static Berkeley DB (same libdb the daemon links)
|
||||||
|
# so its output is a v6.2 btree the bundled dragonxd reads. Pass the paths EXPLICITLY (bypasses
|
||||||
|
# find_library + its cache); the helper target is simply not built if the depends tree is absent.
|
||||||
|
local lin_bdb="$SCRIPT_DIR/external/dragonx/depends/x86_64-unknown-linux-gnu"
|
||||||
|
local BDB_ARGS=()
|
||||||
|
if [[ -f "$lin_bdb/lib/libdb-6.2.a" && -f "$lin_bdb/include/db.h" ]]; then
|
||||||
|
BDB_ARGS=( -DBDB_INCLUDE_DIR="$lin_bdb/include" -DBDB_LIBRARY="$lin_bdb/lib/libdb-6.2.a" )
|
||||||
|
elif should_bundle_full_node_assets; then
|
||||||
|
err "Vendored Berkeley DB depends missing at $lin_bdb — the wallet-rebuild recovery helper cannot be built."
|
||||||
|
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
info "Configuring ..."
|
info "Configuring ..."
|
||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||||
|
"${BDB_ARGS[@]}" \
|
||||||
"${CMAKE_LITE_ARGS[@]}"
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
@@ -348,8 +382,12 @@ build_release_linux() {
|
|||||||
|
|
||||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
||||||
|
|
||||||
|
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
|
||||||
|
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$lin_bdb"
|
||||||
|
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip "bin/${APP_BASENAME}"
|
strip "bin/${APP_BASENAME}"
|
||||||
|
[[ -f "bin/dragonx-wallet-rebuild" ]] && strip "bin/dragonx-wallet-rebuild"
|
||||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||||
|
|
||||||
if should_bundle_full_node_assets; then
|
if should_bundle_full_node_assets; then
|
||||||
@@ -384,9 +422,12 @@ build_release_linux() {
|
|||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
|
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
|
||||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
||||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||||
|
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
|
||||||
|
cp bin/dragonx-wallet-rebuild "$dist_dir/" && chmod +x "$dist_dir/dragonx-wallet-rebuild"
|
||||||
|
info " Bundled dragonx-wallet-rebuild"
|
||||||
fi
|
fi
|
||||||
# Bundle xmrig for mining support
|
# 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"
|
[[ -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
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
@@ -417,9 +458,11 @@ build_release_linux() {
|
|||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
||||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
||||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||||
|
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
|
||||||
|
cp bin/dragonx-wallet-rebuild "$APPDIR/usr/bin/" && chmod +x "$APPDIR/usr/bin/dragonx-wallet-rebuild"
|
||||||
fi
|
fi
|
||||||
# Bundle xmrig for mining support
|
# 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"; }
|
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||||
|
|
||||||
# Desktop entry
|
# Desktop entry
|
||||||
@@ -478,18 +521,28 @@ APPRUN
|
|||||||
done
|
done
|
||||||
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
|
[[ -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=""
|
local APPIMAGETOOL=""
|
||||||
if command -v appimagetool &>/dev/null; then
|
if command -v appimagetool &>/dev/null; then
|
||||||
APPIMAGETOOL="appimagetool"
|
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||||
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
|
|
||||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
|
||||||
else
|
else
|
||||||
info "Downloading appimagetool ..."
|
local at="$bd/appimagetool-x86_64.AppImage"
|
||||||
wget -q -O "$bd/appimagetool-x86_64.AppImage" \
|
# Re-verify any cached copy too; a stale unverified download must not be trusted.
|
||||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||||
chmod +x "$bd/appimagetool-x86_64.AppImage"
|
info "Downloading appimagetool 1.9.0 (pinned) ..."
|
||||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
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
|
fi
|
||||||
|
|
||||||
local ARCH
|
local ARCH
|
||||||
@@ -628,8 +681,32 @@ HDR
|
|||||||
info "Lite mode: skipping embedded daemon binaries"
|
info "Lite mode: skipping embedded daemon binaries"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
# ── Wallet-rebuild recovery helper ───────────────────────────────
|
||||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
# Built in-tree (not a prebuilt like the daemon), so compile it standalone HERE — before the
|
||||||
|
# main app compiles embedded_resources.cpp — and INCBIN it, so a bare, self-extracting
|
||||||
|
# ObsidianDragon.exe carries the recovery tool exactly like it does the daemon.
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
|
local WBDB="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
|
||||||
|
if [[ -f "$WBDB/lib/libdb-6.2.a" && -f "$WBDB/include/db.h" ]]; then
|
||||||
|
info "Compiling + embedding wallet-rebuild helper ..."
|
||||||
|
x86_64-w64-mingw32-g++ -std=c++17 -O2 -static -static-libgcc -static-libstdc++ \
|
||||||
|
-I"$SCRIPT_DIR/src" -I"$WBDB/include" \
|
||||||
|
"$SCRIPT_DIR/tools/wallet_rebuild/main.cpp" \
|
||||||
|
"$WBDB/lib/libdb-6.2.a" -lws2_32 \
|
||||||
|
-o "$RES/dragonx-wallet-rebuild.exe" \
|
||||||
|
|| { err "wallet-rebuild helper failed to compile for embedding"; exit 1; }
|
||||||
|
x86_64-w64-mingw32-strip "$RES/dragonx-wallet-rebuild.exe" 2>/dev/null || true
|
||||||
|
echo -e "\n#define HAS_EMBEDDED_WALLET_REBUILD 1" >> "$GEN/embedded_data.h"
|
||||||
|
echo "INCBIN(dragonx_wallet_rebuild_exe, \"$RES/dragonx-wallet-rebuild.exe\");" >> "$GEN/embedded_data.h"
|
||||||
|
info " Embedded dragonx-wallet-rebuild.exe ($(du -h "$RES/dragonx-wallet-rebuild.exe" | cut -f1))"
|
||||||
|
else
|
||||||
|
err "Vendored mingw Berkeley DB missing at $WBDB — cannot embed the wallet-rebuild recovery helper."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 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
|
# 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
|
# 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
|
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
|
||||||
@@ -725,12 +802,40 @@ HDR
|
|||||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
|
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── FreeType for Windows (color-emoji rendering) ───────────────────────
|
||||||
|
# The mingw toolchain ships no FreeType; cross-build a minimal static one (COLR/CPAL, no external
|
||||||
|
# deps). Failure is non-fatal — the wallet just falls back to monochrome emoji.
|
||||||
|
local FT_MINGW_PREFIX="$SCRIPT_DIR/third_party/freetype-mingw"
|
||||||
|
if [[ ! -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||||
|
info "Cross-building FreeType for Windows (color emoji) ..."
|
||||||
|
"$SCRIPT_DIR/scripts/build-freetype-mingw.sh" "$FT_MINGW_PREFIX" \
|
||||||
|
|| warn "FreeType cross-build failed — Windows build will use monochrome emoji"
|
||||||
|
fi
|
||||||
|
local FT_CMAKE_ARG=()
|
||||||
|
if [[ -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||||
|
FT_CMAKE_ARG=(-DDRAGONX_MINGW_FREETYPE_PREFIX="$FT_MINGW_PREFIX")
|
||||||
|
fi
|
||||||
|
|
||||||
# ── CMake + build ────────────────────────────────────────────────────────
|
# ── CMake + build ────────────────────────────────────────────────────────
|
||||||
|
# The wallet-rebuild helper links the vendored mingw static Berkeley DB (the mingw toolchain's
|
||||||
|
# find_library is sysroot-only, so pass the depends paths EXPLICITLY to bypass the search). Only
|
||||||
|
# enabled if the depends tree is present; guarded with -DBDB_* left empty otherwise.
|
||||||
|
local win_bdb="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
|
||||||
|
local BDB_ARGS=()
|
||||||
|
if [[ -f "$win_bdb/lib/libdb-6.2.a" && -f "$win_bdb/include/db.h" ]]; then
|
||||||
|
BDB_ARGS=( -DBDB_INCLUDE_DIR="$win_bdb/include" -DBDB_LIBRARY="$win_bdb/lib/libdb-6.2.a" )
|
||||||
|
elif should_bundle_full_node_assets; then
|
||||||
|
err "Vendored Berkeley DB depends missing at $win_bdb — the wallet-rebuild recovery helper cannot be built."
|
||||||
|
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
info "Configuring (cross-compile) ..."
|
info "Configuring (cross-compile) ..."
|
||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
|
"${BDB_ARGS[@]}" \
|
||||||
|
"${FT_CMAKE_ARG[@]}" \
|
||||||
"${CMAKE_LITE_ARGS[@]}"
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
@@ -739,6 +844,9 @@ HDR
|
|||||||
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||||
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
||||||
|
|
||||||
|
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
|
||||||
|
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild.exe" "$win_bdb"
|
||||||
|
|
||||||
# ── Package: release/windows/ ────────────────────────────────────────────
|
# ── Package: release/windows/ ────────────────────────────────────────────
|
||||||
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
@@ -754,6 +862,9 @@ HDR
|
|||||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||||
done
|
done
|
||||||
|
# dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) — required.
|
||||||
|
cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/" && info " Bundled dragonx-wallet-rebuild.exe"
|
||||||
|
[[ -f "$dist_dir/dragonx-wallet-rebuild.exe" ]] || { err "Failed to bundle dragonx-wallet-rebuild.exe"; exit 1; }
|
||||||
|
|
||||||
# Bundle Sapling params + asmap for the zip distribution
|
# Bundle Sapling params + asmap for the zip distribution
|
||||||
# (The single-file exe has these embedded via INCBIN, but the zip
|
# (The single-file exe has these embedded via INCBIN, but the zip
|
||||||
@@ -766,7 +877,7 @@ HDR
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Bundle xmrig for mining support
|
# 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"
|
[[ -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
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
@@ -876,8 +987,26 @@ build_release_mac() {
|
|||||||
fi
|
fi
|
||||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||||
else
|
else
|
||||||
# Native macOS: build universal binary (arm64 + x86_64)
|
# Native macOS: build universal (arm64 + x86_64) by default. Override with
|
||||||
MAC_ARCH="universal"
|
# 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"
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -965,7 +1094,7 @@ TOOLCHAIN
|
|||||||
need_sodium=true
|
need_sodium=true
|
||||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
# 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 ..."
|
info "Existing libsodium is not universal — rebuilding ..."
|
||||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||||
need_sodium=true
|
need_sodium=true
|
||||||
@@ -976,13 +1105,13 @@ TOOLCHAIN
|
|||||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Configuring (native universal arm64+x86_64) ..."
|
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
|
||||||
"${CMAKE_LITE_ARGS[@]}"
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -991,6 +1120,11 @@ TOOLCHAIN
|
|||||||
|
|
||||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
|
||||||
|
|
||||||
|
# A full-node release MUST include the recovery helper. macOS needs a static libdb-6.2 (Homebrew
|
||||||
|
# berkeley-db for a native build, or a vendored external/dragonx/depends/<triple>) — otherwise CMake
|
||||||
|
# skips the target and this fails loudly rather than shipping a mac release with no recovery option.
|
||||||
|
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$SCRIPT_DIR/external/dragonx/depends/aarch64-apple-darwin"
|
||||||
|
|
||||||
# Strip — use osxcross strip for cross-builds
|
# Strip — use osxcross strip for cross-builds
|
||||||
if $IS_CROSS; then
|
if $IS_CROSS; then
|
||||||
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
||||||
@@ -1012,8 +1146,12 @@ TOOLCHAIN
|
|||||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||||
|
|
||||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
|
||||||
mkdir -p "$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 APP="$out/${APP_BASENAME}.app"
|
||||||
local CONTENTS="$APP/Contents"
|
local CONTENTS="$APP/Contents"
|
||||||
@@ -1059,12 +1197,14 @@ TOOLCHAIN
|
|||||||
else
|
else
|
||||||
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
||||||
fi
|
fi
|
||||||
|
# Offline wallet-rebuild recovery helper — required (asserted after build); next to the daemon.
|
||||||
|
cp "bin/dragonx-wallet-rebuild" "$MACOS/" && chmod +x "$MACOS/dragonx-wallet-rebuild" && info " Bundled dragonx-wallet-rebuild"
|
||||||
else
|
else
|
||||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
|
||||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||||
if [[ -f "$XMRIG_MAC" ]]; then
|
if [[ -f "$XMRIG_MAC" ]]; then
|
||||||
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||||
chmod +x "$MACOS/xmrig"
|
chmod +x "$MACOS/xmrig"
|
||||||
@@ -1213,8 +1353,10 @@ PLIST
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||||
local DMG_BASENAME="DragonX_Wallet"
|
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
|
||||||
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
# 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"
|
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||||
|
|
||||||
if command -v create-dmg &>/dev/null; then
|
if command -v create-dmg &>/dev/null; then
|
||||||
@@ -1296,3 +1438,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/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
|
||||||
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
|
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
|
||||||
fi
|
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()
|
||||||
549
docs/daemon-startup-hardening.md
Normal file
549
docs/daemon-startup-hardening.md
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
# Daemon Startup Hardening — Implementation Plan
|
||||||
|
|
||||||
|
Eight verified edge-case defects in how ObsidianDragon brings up (and watches) the
|
||||||
|
`dragonxd` daemon at launch. Each entry is a buildable fix: the defect (with exact
|
||||||
|
line references), the chosen approach, the call sites, a representative change, and how
|
||||||
|
to verify it.
|
||||||
|
|
||||||
|
- **Scope:** full-node startup path (`--lite` excludes the embedded daemon entirely).
|
||||||
|
- **Source:** line references are exact against branch `dev` @ `45b652f`.
|
||||||
|
- **Provenance:** findings verified by direct source read; each fix designed by an
|
||||||
|
independent agent grounded in the cited files, with a sequencing pass for ordering,
|
||||||
|
shared helpers, and merge conflicts.
|
||||||
|
|
||||||
|
**Severity:** 2 High, 6 Medium · **Effort:** ≈ 25–35 engineering-hours · **7 landing steps.**
|
||||||
|
|
||||||
|
Status legend: ☐ not started · ◐ in progress · ☑ landed & verified
|
||||||
|
|
||||||
|
**Status: all 8 landed & verified** (build-clean, `ctest` green after each) across four commits on
|
||||||
|
`dev` — lifecycle cluster (F1/F2/F4), filesystem+params cluster (F7/F6/F5), F3, and F8. Six new
|
||||||
|
pure-helper unit tests added.
|
||||||
|
|
||||||
|
**Wrap-up done:** release notes added (`CHANGELOG.md`, F8 breaking change front and center); i18n
|
||||||
|
back-fill applied additively to `res/lang/*.json` (42 keys — all 6 for es/de/fr/pt/ru; 6 zh/ja/ko
|
||||||
|
entries whose glyphs aren't in the current `NotoSansCJK-Subset.ttf` were left on English fallback
|
||||||
|
rather than render as tofu).
|
||||||
|
|
||||||
|
**Still owed before release:** a **CJK subset-font rebuild** (`scripts/build_cjk_subset.py`, needs
|
||||||
|
the Noto CJK source font) to cover the 6 deferred zh/ja/ko strings. *(F1 and F2 now have headless
|
||||||
|
integration-test coverage — see the progress log — so their GUI repros are optional, not blocking.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended rollout sequence
|
||||||
|
|
||||||
|
A real dependency order, not a checklist. The daemon-lifecycle cluster lands first
|
||||||
|
because it makes the `State::Error` / `crash_count_` contract trustworthy — which the
|
||||||
|
connect-stall panel and the lock gate both build on. The filesystem cluster lands
|
||||||
|
around a single shared helper. The connectivity-breaking security flip lands last.
|
||||||
|
|
||||||
|
| Step | Finding(s) | Site | Why here | Status |
|
||||||
|
|------|-----------|------|----------|--------|
|
||||||
|
| 1 | **F1** | `embedded_daemon.cpp` · `isRunning()` | Smallest/highest-severity; establishes the reliable Error/crash-count transition steps 3 & 6 depend on. | ☑ |
|
||||||
|
| 2 | **F2** | `embedded_daemon.cpp` · `startProcess()` | Same file family, different function; test the F1+F2 pair together with `kill -SEGV` / bad-binary repros. | ☑ |
|
||||||
|
| 3 | **F4** | `embedded_daemon.cpp` · `start()` | After F1/F2 so crash-count semantics are settled; its bail deliberately stays out of the crash path. | ☑ |
|
||||||
|
| 4 | **F7** | `util/platform` · `connection.cpp` | Structural owner of the fs-error idiom + `ConnectionConfig` that F5/F6/F8 reuse. | ☑ |
|
||||||
|
| 5 | **F6 + F5** | `app.cpp` · `verifySaplingParams()` | Same `startEmbeddedDaemon` / `verifySaplingParams` block; land together. | ☑ |
|
||||||
|
| 6 | **F3** | `app.cpp` · `renderLoadingOverlay()` | After F1 — panel is guarded off during `State::Error` (owned by the crash-count hint). | ☑ |
|
||||||
|
| 7 | **F8** | `connection.cpp` · `tryConnect()` | Largest; only connectivity-breaking default flip — land last, with release notes. | ☑ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F1 — Double-`waitpid` race can swallow a daemon crash
|
||||||
|
|
||||||
|
**Severity:** High · **Effort:** S (~1–2h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
`EmbeddedDaemon::isRunning()` (`embedded_daemon.cpp:1136`, POSIX branch) calls
|
||||||
|
`waitpid(WNOHANG)` — from the **UI thread, nearly every frame** — racing
|
||||||
|
`monitorProcess()`'s own reap at `:1244`. `waitpid` is one-shot: if the UI thread wins,
|
||||||
|
the monitor never decodes the exit, so `crash_count_` never increments, `State::Error`
|
||||||
|
never fires, and the 3-strike auto-restart cap (`app_network.cpp:479`) is defeated. The
|
||||||
|
sibling `XmrigManager::isRunning()` (`xmrig_manager.cpp:512`) already fixed exactly this
|
||||||
|
with an atomic read.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
Make `isRunning()` read the existing `std::atomic<State> state_` (member at
|
||||||
|
`embedded_daemon.h:253`) instead of calling `waitpid`, leaving `monitorProcess()` as the
|
||||||
|
sole reaper. Predicate is `Running || Stopping` — `Stopping` must stay "alive" because
|
||||||
|
`stop()`'s graceful/SIGTERM wait loops poll `isRunning()` before the process has exited.
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/daemon/embedded_daemon.cpp` — `isRunning()`, POSIX branch (~1136)
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
bool EmbeddedDaemon::isRunning() const // POSIX branch
|
||||||
|
{
|
||||||
|
// Read the atomic state_ instead of waitpid() — monitorProcess() is the
|
||||||
|
// sole reaper. Previously both threads reaped; if the UI thread won, the
|
||||||
|
// monitor never saw the exit (crash_count_ / exit code / Error all lost).
|
||||||
|
if (process_pid_ <= 0) return false;
|
||||||
|
|
||||||
|
State s = state_.load(std::memory_order_relaxed);
|
||||||
|
// Stopping stays "alive": stop()'s wait loops poll isRunning() while
|
||||||
|
// state_ == Stopping, before the process has actually terminated.
|
||||||
|
return (s == State::Running || s == State::Stopping);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Manual: `kill -SEGV` the daemon 10–20×; the monitor must report the exit and increment `crash_count_` every time (previously intermittent).
|
||||||
|
- Regression: a normal Settings-driven stop still escalates SIGTERM→SIGKILL (the `Stopping` predicate).
|
||||||
|
- Not unit-testable (real fork/exec/waitpid) — consistent with the no-process-spawn harness.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
Mirrors `XmrigManager::isRunning()`. Flags a separate latent hazard (out of scope):
|
||||||
|
`stop()`'s final blocking `waitpid` (`:1220`) can still race a mid-sleep monitor
|
||||||
|
iteration — file as its own ticket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F2 — exec-after-fork silent failure: "Running" for a daemon that never started
|
||||||
|
|
||||||
|
**Severity:** High · **Effort:** S (~2–3h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
In `startProcess()` (`embedded_daemon.cpp:957–1061`, POSIX) the parent runs
|
||||||
|
`process_pid_ = pid; return true;` **unconditionally** after `fork()` — with no
|
||||||
|
exec-status handshake. On a non-executable / wrong-arch / corrupt binary the child's
|
||||||
|
`execv` fails and it `_exit(127)`s, but `start()` has already set `State::Running`
|
||||||
|
(`:565`). The real cause never reaches `last_error_`; it surfaces later, generically,
|
||||||
|
as "exited unexpectedly (exit code 127)".
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
Add a **close-on-exec self-pipe** handshake — `pipe() + fcntl(FD_CLOEXEC)`, deliberately
|
||||||
|
**not** `pipe2()` (macOS lacks it; the POSIX branch is shared). The child writes `errno`
|
||||||
|
only on `execv` failure; a successful exec closes the write end for free. Parent reads:
|
||||||
|
EOF ⇒ success; 4 bytes ⇒ reap the zombie, set a precise `last_error_` ("not executable
|
||||||
|
or wrong architecture"), and return `false` so `start()` never reports Running. EINTR-safe
|
||||||
|
on both ends. Also comments the unchecked parent-side `setpgid` at `:1053`.
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/daemon/embedded_daemon.cpp` — `startProcess()` parent read path
|
||||||
|
- `src/daemon/embedded_daemon.cpp` — child `execv`-failure write (~1043)
|
||||||
|
- `src/daemon/embedded_daemon.cpp` — `setpgid` best-effort comment (~1053)
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
// Self-pipe exec handshake (pipe()+FD_CLOEXEC; NOT pipe2 — macOS lacks it).
|
||||||
|
int execpipe[2]; pipe(execpipe);
|
||||||
|
fcntl(execpipe[0], F_SETFD, FD_CLOEXEC);
|
||||||
|
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) { // child
|
||||||
|
close(execpipe[0]);
|
||||||
|
/* setpgid / chdir / dup2 / argv … */
|
||||||
|
execv(binary_path.c_str(), argv.data());
|
||||||
|
int e = errno; // execv failed
|
||||||
|
while (write(execpipe[1], &e, sizeof e) < 0 && errno == EINTR) {}
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(execpipe[1]); // parent: must close or read() never EOFs
|
||||||
|
int child_errno = 0, total = 0;
|
||||||
|
for (;;) { // EOF ⇒ exec ok; 4 bytes ⇒ exec failed
|
||||||
|
ssize_t n = read(execpipe[0], (char*)&child_errno + total, sizeof(int) - total);
|
||||||
|
if (n == 0) break;
|
||||||
|
if (n < 0) { if (errno == EINTR) continue; break; }
|
||||||
|
if ((total += n) >= (int)sizeof(int)) break;
|
||||||
|
}
|
||||||
|
close(execpipe[0]);
|
||||||
|
if (total >= (int)sizeof(int)) { // exec never happened
|
||||||
|
waitpid(pid, nullptr, 0); // reap the zombie
|
||||||
|
last_error_ = "dragonxd could not be executed: " +
|
||||||
|
std::string(strerror(child_errno)) +
|
||||||
|
" — not executable or wrong architecture";
|
||||||
|
return false; // start() no longer reports Running
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Point at a `chmod -x` / wrong-arch file → `start()` returns false immediately, precise message, no leftover zombie.
|
||||||
|
- Success path: real binary still starts with no perceptible added latency.
|
||||||
|
- Optional pure `formatExecFailureError(errno)` helper for a `test_phase4.cpp` unit test.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
F1 (same function family; sequence F1→F2). **Highest-risk mistake:** forgetting
|
||||||
|
`FD_CLOEXEC` makes every successful start hang the parent read forever.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F4 — Stale datadir-lock start → restart storm that wedges the UI
|
||||||
|
|
||||||
|
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
`start()` (`embedded_daemon.cpp:466`) gates only on the RPC port (`:482`), never on
|
||||||
|
`isDaemonProcessRunning()` (`:1292`). A graceful shutdown frees the port but keeps the
|
||||||
|
datadir `.lock` for up to ~90s. A rapid stop→start spawns a daemon that dies "Cannot
|
||||||
|
obtain a lock on data directory" — routed to the generic crash path. With a ~4s retry
|
||||||
|
cadence, **three lock races in ~12s exhaust the 3-strike budget** and wedge the UI long
|
||||||
|
before the lock actually clears.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
Fail-fast with a **short bounded local wait (~300ms), not a 90s block**. After the port
|
||||||
|
bail, consult `isDaemonProcessRunning()` — gated by `!skip_port_check_` and exempt when
|
||||||
|
`override_datadir_` is set, so the isolated migrate-to-seed daemon still works. A pure
|
||||||
|
`evaluateDatadirLockGate()` returns a **distinct non-crash Error** that never increments
|
||||||
|
`crash_count_`. The connect loop's own retry then absorbs the transient.
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/daemon/embedded_daemon.h` — decision struct, helper decl, poll constants
|
||||||
|
- `src/daemon/embedded_daemon.cpp` — `start()` gate + `evaluateDatadirLockGate()`
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
static StartLockGateDecision evaluateDatadirLockGate(
|
||||||
|
bool skipPortCheck, bool isolatedOverride, bool stillRunningAfterWait) {
|
||||||
|
if (skipPortCheck || isolatedOverride) return {true, ""}; // migrate-to-seed exempt
|
||||||
|
if (!stillRunningAfterWait) return {true, ""};
|
||||||
|
return {false, "A previous dragonxd is still shutting down and holding the "
|
||||||
|
"data directory lock. Retrying shortly…"};
|
||||||
|
}
|
||||||
|
|
||||||
|
// start() — after the isPortInUse() bail, before setState(Starting):
|
||||||
|
if (!skip_port_check_ && override_datadir_.empty()) {
|
||||||
|
bool stillLocked = false; // ~300ms bounded wait, NOT ~90s
|
||||||
|
for (int i = 0; i < kDatadirLockWaitMaxPolls; ++i) {
|
||||||
|
if (!isDaemonProcessRunning()) { stillLocked = false; break; }
|
||||||
|
stillLocked = true;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(kDatadirLockWaitPollMs));
|
||||||
|
}
|
||||||
|
auto gate = evaluateDatadirLockGate(false, false, stillLocked);
|
||||||
|
if (!gate.proceed) { setState(State::Error, gate.errorMessage); return false; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Unit: `evaluateDatadirLockGate()` across the skip / isolated / still-running matrix.
|
||||||
|
- Manual: rapid restart into a lingering lock → distinct message, no crash-cap wedge.
|
||||||
|
- Migrate-to-seed second daemon still starts (isolated exemption).
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
F1/F2 (must not touch `crash_count_`; wording must not collide with the monitor's
|
||||||
|
"exited unexpectedly"). Same TU, different function.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F5 — Extraction / copy write-failures never surfaced up front
|
||||||
|
|
||||||
|
**Severity:** Medium · **Effort:** S (~2–3h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
`startEmbeddedDaemon()` discards `extractEmbeddedResources()`'s `bool` return
|
||||||
|
(`app.cpp:4152`) and the second copy-fallback loop drops `copy_file`'s `error_code`
|
||||||
|
entirely (`:4236`). Only Sapling params **existence** is re-checked — never the daemon
|
||||||
|
binary/CLI/tx/asmap. A disk-full or truncated `dragonxd` write falls straight through to
|
||||||
|
spawn and fails opaquely. The innermost write already returns `false`
|
||||||
|
(`embedded_resources.cpp:307`) — the signal is simply thrown away.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
Minimal, surgical wiring — no new abstraction. Capture the extraction return and, on
|
||||||
|
failure, set `daemon_status_ = TR("sb_daemon_extract_failed")` and `return false` before
|
||||||
|
spawning. In the second copy loop, check `ec` after each `copy_file`, track `copyFailed`,
|
||||||
|
and abort with a dir-parameterized `sb_daemon_files_failed`. An **absent source** stays
|
||||||
|
fine (optional files); only an actual `error_code` counts. Written so F6/F7 slot in later
|
||||||
|
without re-touching this control flow.
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/app.cpp` — `startEmbeddedDaemon()` extraction check (~4152)
|
||||||
|
- `src/app.cpp` — second copy-fallback loop (~4210–4242)
|
||||||
|
- `src/util/i18n.cpp` + `res/lang/*.json` — 2 additive keys
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
// stop discarding the extraction result (~4152)
|
||||||
|
if (!resources::extractEmbeddedResources()) {
|
||||||
|
daemon_status_ = TR("sb_daemon_extract_failed"); // disk full / permission denied
|
||||||
|
return false; // abort before spawning
|
||||||
|
}
|
||||||
|
|
||||||
|
// second copy-fallback loop — was dropping ec entirely (~4236)
|
||||||
|
bool copyFailed = false;
|
||||||
|
for (const char* name : { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" }) {
|
||||||
|
fs::path dst = fs::path(daemon_dir) / name;
|
||||||
|
if (fs::exists(dst)) continue; // already present — skip
|
||||||
|
for (const auto& dir : searchDirs) {
|
||||||
|
fs::path src = fs::path(dir) / name;
|
||||||
|
if (!fs::exists(src)) continue; // absent source is OK, not a failure
|
||||||
|
fs::copy_file(src, dst, ec);
|
||||||
|
if (ec) { copyFailed = true; ec.clear(); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (copyFailed) {
|
||||||
|
char buf[512];
|
||||||
|
snprintf(buf, sizeof buf, TR("sb_daemon_files_failed"), daemon_dir.c_str());
|
||||||
|
daemon_status_ = buf;
|
||||||
|
return false; // don't fall through to spawn
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Unit: `extractEmbeddedResources()` returns false without embedded resources.
|
||||||
|
- Extract the copy loop into a testable helper; force one dst write to fail (dst is an existing directory).
|
||||||
|
- Manual: near-full tmpfs / read-only dir → clear status, daemon controller never constructed.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
Shares the `daemon_status_` surfacing convention with F6; its early-return pattern is the
|
||||||
|
template F7 matches. Open item: remove truncated dst files so a retry re-copies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F6 — Sapling params validated by existence/size only, never hashed
|
||||||
|
|
||||||
|
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
> **As-built note.** `verifySaplingParams()` now delegates to a public, injectable
|
||||||
|
> `verifySaplingParamsIn(dir, digests)` so the integrity + marker-cache logic is unit-testable
|
||||||
|
> with synthetic small files (the real 48 MB params aren't in the repo). i18n keys for F5 were
|
||||||
|
> added to `i18n.cpp` (English source of truth); the `res/lang/*.json` back-fill via
|
||||||
|
> `scripts/add_missing_translations.py` is deferred to a single run at the end of the batch,
|
||||||
|
> per the cross-cutting note. Non-English locales fall back to English until then.
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
`verifySaplingParams()` (`connection.cpp:123`) only calls `fs::exists()`;
|
||||||
|
`resourceNeedsUpdate()` (`embedded_resources.cpp:250`) is size-only. On Linux (no
|
||||||
|
embedded resources) a **truncated-but-present** param passes and is handed to the daemon,
|
||||||
|
which then fails to build shielded proofs mid-operation — far from the real cause.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
Add a pinned `{ filename → size, sha256 }` table (one source of truth, cross-referenced
|
||||||
|
to `scripts/build-lite-backend-artifact.sh`) and hash-check each param after the
|
||||||
|
existence check, reusing the existing `util::sha256Hex` (no second implementation). Since
|
||||||
|
these are ~48 MB, **cache the result** via a `.sapling_verified` marker keyed on
|
||||||
|
`size:mtime` — re-hash only when the stat line changes, so startup isn't slowed.
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/rpc/connection.h` — `verifySaplingParams` decl
|
||||||
|
- `src/rpc/connection.cpp` — digest table, marker helpers, rewrite
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
// connection.cpp — pinned known-good digests
|
||||||
|
// (source of truth: scripts/build-lite-backend-artifact.sh ensure_sapling_params)
|
||||||
|
constexpr SaplingParamDigest kSaplingParamDigests[] = {
|
||||||
|
{ "sapling-spend.params", 47958396, "8e48ffd2…efc13" },
|
||||||
|
{ "sapling-output.params", 3592860, "2f0ebbcb…fb0e4" },
|
||||||
|
};
|
||||||
|
|
||||||
|
bool Connection::verifySaplingParams() {
|
||||||
|
// existence check (unchanged) …
|
||||||
|
// cache: skip re-hashing a ~48 MB file unless size:mtime changed
|
||||||
|
if (readMarkerMatches(marker, statLines)) return true;
|
||||||
|
for (auto& d : kSaplingParamDigests)
|
||||||
|
if (util::sha256Hex(bytes) != d.sha256) return false; // reuse existing helper
|
||||||
|
writeMarker(marker, statLines);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Unit: good params pass; truncated / wrong-bytes rejected; marker cache short-circuits re-hash unless size/mtime changed. Real temp-file fixtures (matches existing `sha256Hex` tests).
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
F7 (reuse fs-error idiom; shares the `startEmbeddedDaemon`/`verifySaplingParams` block).
|
||||||
|
Third caller of the existing `util::sha256Hex`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F7 — Directory-create errors universally ignored on the daemon-env path
|
||||||
|
|
||||||
|
**Severity:** Medium · **Effort:** S (~3–4h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
> **As-built notes.** Two deviations from the original design, both confirmed against the code:
|
||||||
|
> (1) `embedded_resources.cpp:270` already checks its `error_code` and returns `false` on failure — it was **not** a bug, so it is left untouched.
|
||||||
|
> (2) Of the four `autoDetectConfig` callers, only the primary connect path (`app_network.cpp:243`) was wired to check `dir_error`; the other three degrade gracefully on their own — `app.cpp:4306` and `app_wizard.cpp:912` are stop paths that already gate on empty creds, and `settings_page.cpp:434` is read-only display. `dir_error` is set by `autoDetectConfig`, so they can be wired later if desired.
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
Five startup directory-create sites either drop the `error_code` or use the throwing
|
||||||
|
overload with no `catch`: `main.cpp:730`, `connection.cpp:216` (can throw **uncaught**
|
||||||
|
through its callers), `embedded_resources.cpp:270`, `app.cpp:4172`/`4218`. A read-only
|
||||||
|
home or permission-denied yields a confusing "conf missing" / "binary not found"
|
||||||
|
downstream — or an uncaught `filesystem_error` — instead of a clear cause.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
One shared, non-throwing `Platform::ensureDirectory(dir, outError)` in
|
||||||
|
`util/platform.{h,cpp}` that produces a single consistent message. Replace all five
|
||||||
|
sites; `autoDetectConfig()` moves off the throwing overload and sets a new
|
||||||
|
`ConnectionConfig::dir_error` that its four callers check and bail on. This is the
|
||||||
|
**structural owner** of the fs-error idiom that F5 and F6 reuse.
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/util/platform.h` / `.cpp` — `ensureDirectory()`
|
||||||
|
- `src/rpc/connection.h` / `.cpp` — `dir_error` + `autoDetectConfig`
|
||||||
|
- `main.cpp`, `app.cpp`, `app_network.cpp`, `app_wizard.cpp`, `settings_page.cpp`, `embedded_resources.cpp` — 5 sites + 4 callers
|
||||||
|
- `tests/test_phase4.cpp` — `TestPlatformEnsureDirectory`
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
// util/platform.cpp — one shared, non-throwing helper
|
||||||
|
bool Platform::ensureDirectory(const std::string& dir, std::string* outError) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::is_directory(dir, ec)) return true;
|
||||||
|
ec.clear();
|
||||||
|
std::filesystem::create_directories(dir, ec);
|
||||||
|
if (ec) {
|
||||||
|
if (outError)
|
||||||
|
*outError = "Cannot create " + dir + ": " + ec.message() +
|
||||||
|
". Check permissions / free space.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Replaces 5 ad-hoc sites; autoDetectConfig() now sets ConnectionConfig::dir_error,
|
||||||
|
// and its 4 callers bail on it.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Unit `TestPlatformEnsureDirectory`: existing dir → true; fresh nested → created; POSIX unwritable → false + message.
|
||||||
|
- All four `autoDetectConfig` callers tolerate `dir_error`. Pre-App-init site (main.cpp) reports via stderr / MessageBox.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
**Owns** `Platform::ensureDirectory` (used by F5, F6) and the `ConnectionConfig`
|
||||||
|
extension (coordinated with F8). Land before F5/F6/F8.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F8 — Plaintext-remote RPC credential transmission is warn-only
|
||||||
|
|
||||||
|
**Severity:** Medium · **Effort:** M (~6–9h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
> **⚠️ RELEASE NOTES REQUIRED — breaking default flip.** A wallet configured to talk to a
|
||||||
|
> **remote** `rpchost` over **plain HTTP** (no `rpctls=1`) will now be **refused** at connect
|
||||||
|
> time instead of warned. Affected users must add **`rpcallowplaintext=1`** to `DRAGONX.conf`
|
||||||
|
> (or switch to `rpctls=1`) to reconnect. Local/embedded daemons (`127.0.0.0/8`, `localhost`,
|
||||||
|
> `::1`) are unaffected. Call this out prominently in the release notes.
|
||||||
|
>
|
||||||
|
> **As-built note.** Shipped the security-complete core: `isLocalHost` tightened to exact
|
||||||
|
> loopback (`isExactIPv4Loopback` — `127.evil.com` no longer passes), refuse-by-default in
|
||||||
|
> `tryConnect`, and the `rpcallowplaintext` conf-key opt-in. The **Settings toggle UI was
|
||||||
|
> deferred** — the RPC section of `settings_page.cpp` is read-only display and a security
|
||||||
|
> toggle there is riskier surface; the conf-key opt-in fully covers recovery, and the refusal
|
||||||
|
> status/notification tells the user exactly what to add. The toggle can be added later
|
||||||
|
> (persist a `Settings` flag and OR it into `allowsPlaintextRemote`).
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
A remote `rpchost` without `rpctls=1` sends Basic-auth `rpcuser:rpcpassword` over
|
||||||
|
cleartext HTTP. `tryConnect()` (`app_network.cpp:314`) only shows a **dismissible
|
||||||
|
warning** then proceeds — a local-network MITM sees the credentials. Compounding it,
|
||||||
|
`isLocalHost()`'s naive `rfind("127.",0)==0` misclassifies `127.evil.com` as local,
|
||||||
|
suppressing even the warning.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
Change the policy to **refuse-by-default with an explicit, persisted opt-in** — a
|
||||||
|
`rpcallowplaintext=1` conf key (for hand-editors) and a Settings toggle. Block the
|
||||||
|
connect and show a **blocking modal** explaining the risk and how to enable TLS or opt
|
||||||
|
in; localhost is unaffected. Tighten `isLocalHost()` to exact `127.x.y.z` / `::1` /
|
||||||
|
`localhost` via `isExactIPv4Loopback()`. **Back-compat:** default off ⇒ existing remote
|
||||||
|
users hit a hard stop until they opt in — **ship with prominent release notes.**
|
||||||
|
|
||||||
|
### Files touched
|
||||||
|
- `src/rpc/connection.h` / `.cpp` — `isLocalHost`, `allow_plaintext_remote`, `parseConfFile`
|
||||||
|
- `src/config/settings.h` / `.cpp` — persisted opt-in
|
||||||
|
- `src/app_network.cpp`, `src/app.h` — refuse + modal dispatch
|
||||||
|
- `src/ui/windows/plaintext_remote_rpc_dialog.h` — new blocking modal
|
||||||
|
- `src/ui/pages/settings_page.cpp` — toggle UI
|
||||||
|
|
||||||
|
### Core change
|
||||||
|
```cpp
|
||||||
|
// Tightened loopback test — "127.evil.com" is NOT local
|
||||||
|
bool Connection::isLocalHost(const std::string& host) {
|
||||||
|
std::string h = stripBrackets(lowercase(host));
|
||||||
|
return h == "localhost" || h == "::1" || isExactIPv4Loopback(h); // exact 127.x.y.z
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refuse-by-default with an explicit, persisted opt-in
|
||||||
|
const bool plaintextRemote = rpc::Connection::usesPlaintextRemote(config);
|
||||||
|
const bool plaintextAllowed = config.allow_plaintext_remote // rpcallowplaintext=1
|
||||||
|
|| settings_.getAllowPlaintextRemoteRpc(); // Settings toggle
|
||||||
|
if (plaintextRemote && !plaintextAllowed) {
|
||||||
|
connection_status_ = TR("sb_plaintext_remote_blocked");
|
||||||
|
showPlaintextRemoteRpcDialog(config.host + ":" + config.port); // blocking modal
|
||||||
|
return; // no creds sent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Unit: `isLocalHost` — `127.evil.com` false, `127.0.0.1`/`::1`/`localhost` true; `allowsPlaintextRemote` honors conf key + settings flag.
|
||||||
|
- Manual: remote plaintext blocked; modal fires; opt-in persists across restart.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
F7 (second extender of `ConnectionConfig`/`parseConfFile`; land after so the struct grows
|
||||||
|
once). Wire `renderPlaintextRemoteRpcDialog` into the app modal-dispatch list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared helpers & coordination points
|
||||||
|
|
||||||
|
| Helper | Purpose | Used by |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| `Platform::ensureDirectory()` | Single non-throwing directory-create with one consistent message; replaces five ad-hoc sites. Owned by F7. | F7, F5, F6 |
|
||||||
|
| `ConnectionConfig` extension | Coordination point, not a function: F7 adds `dir_error`, F8 adds `allow_plaintext_remote`. Land F7→F8 so it grows once per step. | F7, F8 |
|
||||||
|
| `util::sha256Hex` *(existing)* | Already-compiled, curl-free SHA-256. F6 becomes its third caller — no second hash routine. | F6 |
|
||||||
|
| `connectHasStalled()` *(new, pure)* | Stall predicate split out of the ImGui/App code for unit testing, per the `*_updater_core.cpp` precedent. | F3 |
|
||||||
|
| `evaluateDatadirLockGate()` *(new, pure)* | Lock-gate decision as `{proceed, message}` from three booleans — unit-testable without real process/fs I/O. | F4 |
|
||||||
|
|
||||||
|
## F3 — Unbounded connect spinner (deferred to step 6)
|
||||||
|
|
||||||
|
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||||
|
|
||||||
|
> **As-built note.** `renderLoadingOverlay()` is a pure draw-list overlay with **no interactive
|
||||||
|
> widgets** (the existing crash case at ~5289 already communicates via guidance *text*, relying on
|
||||||
|
> the sidebar staying reachable). So rather than inject `ActionButton`s — which would fight the
|
||||||
|
> non-interactive overlay — the stall notice follows that same idiom: a "Taking longer than
|
||||||
|
> expected" title + a reassuring body (with elapsed seconds) + a full-node-gated hint ("Open
|
||||||
|
> Settings → Restart Daemon, or check the Console"). This let me drop the planned
|
||||||
|
> `WalletState::connect_stalled` flag too: the stalled state is computed locally in the overlay
|
||||||
|
> from `connect_stall_since_`, so the only new member is `App::connect_stall_since_`.
|
||||||
|
|
||||||
|
The connect loop retries forever while `!state_.connected` (`app.cpp:1239`);
|
||||||
|
`loading_timer_` only animates the spinner. Stamp `connect_stall_since_` when
|
||||||
|
"reachable but not ready" is first seen; a pure `connectHasStalled()` helper (new
|
||||||
|
`util/connect_stall.h`, default 45s from `ui.toml`) flips `state_.connect_stalled` at
|
||||||
|
threshold, and `renderLoadingOverlay()` shows a "Taking longer than expected" panel with
|
||||||
|
Retry / Restart daemon / Open console (full-node gated). The background retry keeps
|
||||||
|
firing — recovery clears the panel automatically. Guarded off while the daemon is in
|
||||||
|
`State::Error` (owned by F1's crash-count hint). Full detail lives in the sequencing/
|
||||||
|
design record; see the shared-helper table above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-cutting notes
|
||||||
|
|
||||||
|
- **One TU, three functions.** `embedded_daemon.cpp` is edited by F1 (`isRunning`),
|
||||||
|
F2 (`startProcess`) and F4 (`start`) — no literal hunk overlap, but land in order to
|
||||||
|
keep "monitorProcess is the sole reaper" coherent.
|
||||||
|
- **Connection struct grows twice.** `connection.h/.cpp` is touched by F6, F7 and F8;
|
||||||
|
F7 and F8 both extend `ConnectionConfig` and `parseConfFile` — highest collision risk.
|
||||||
|
Sequence F7→F6→F8.
|
||||||
|
- **Testability split.** The three new pure predicates all get `tests/test_phase4.cpp`
|
||||||
|
coverage. F1/F2's fork/exec/waitpid changes are **not** unit-testable — they rely on
|
||||||
|
manual `kill` / non-executable-binary repros, consistent with the no-process-spawn harness.
|
||||||
|
- **i18n is additive-only.** Add each finding's English keys to `strings_`, then run
|
||||||
|
`scripts/add_missing_translations.py` **once at the very end**
|
||||||
|
(`json.dump indent=4, sort_keys=True, ensure_ascii=False`) — never bulk-regenerate a
|
||||||
|
`res/lang/*.json`.
|
||||||
|
- **F8 is a breaking default flip.** Refuse-plaintext-by-default stops existing
|
||||||
|
remote-RPC users cold until they opt in. Lands last, gated behind a persisted opt-in,
|
||||||
|
with release notes calling out the new `rpcallowplaintext` key and the Settings toggle.
|
||||||
|
- **Latent hazard, out of scope.** F1 surfaces (but doesn't fix) a second
|
||||||
|
double-`waitpid` window between `stop()`'s final blocking reap (`:1220`) and a
|
||||||
|
mid-sleep monitor iteration — file it as its own ticket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
- **F1/F2 integration tests** — ☑ added `testExecFailureReported` (F2) and `testDaemonCrashDetected` (F1) to `test_phase4.cpp`, driving the **real** `EmbeddedDaemon` fork/exec/waitpid code headlessly (POSIX; required linking `embedded_daemon.cpp` into the test target — its deps were already there). The F1 test hammers `isRunning()` from the test thread while the child exits, so it's a genuine regression test for the reap race. **The F2 test caught a real bug:** `start()`'s failure branch overwrote `startProcess()`'s precise `last_error_` ("…not executable or wrong architecture") with a generic "Failed to start dragonxd process" (because `setState(Error, …)` stores its message into `last_error_`), so the precise reason never reached `getLastError()`/the UI — **fixed** to preserve the detail (now also surfaced via the state callback / crash panel). Build-clean; `ctest` 1/1.
|
||||||
|
|
||||||
|
- **F1** — ☑ landed: `isRunning()` (POSIX) now reads the atomic `state_` (predicate `Running || Stopping`) instead of calling `waitpid`, leaving `monitorProcess()` the sole reaper. Clean build (all targets link); `ctest` 1/1 passing. Not unit-testable — needs the manual `kill -SEGV` repro before release.
|
||||||
|
- **F2** — ☑ landed: `startProcess()` (POSIX) now creates a `FD_CLOEXEC` self-pipe before `fork()`; the child writes `errno` to it on `execv` failure, the parent reads EOF-vs-errno and, on failure, reaps the zombie + sets a precise `last_error_` ("not executable or wrong architecture") + returns `false` (so `start()` no longer reports `Running` for a daemon that never started). Parent-side `setpgid` is now best-effort with a `DEBUG_LOGF` on failure. Clean build; `ctest` 1/1 passing. Not unit-testable — needs the manual non-executable / wrong-arch-binary repro before release.
|
||||||
|
- **F8** — ☑ landed: `isLocalHost()` tightened to exact loopback via `isExactIPv4Loopback` (a `127.`-prefixed *hostname* like `127.evil.com` is no longer misclassified as local). `tryConnect()` now **refuses** a plaintext connection to a remote host instead of warn-and-proceeding — a local-network MITM can no longer capture `rpcuser:rpcpassword` — unless the user opts in with `rpcallowplaintext=1` in `DRAGONX.conf` (new `ConnectionConfig::allow_plaintext_remote` + `allowsPlaintextRemote()` policy). The refusal surfaces via status line + a one-time notification. New `testIsLocalHost` (12 assertions) + `testAllowsPlaintextRemote` (5). Clean build; `ctest` 1/1 passing. **Breaking — needs release notes; Settings-toggle UI deferred (see as-built note).**
|
||||||
|
- **F3** — ☑ landed: the connect loop now stamps `connect_stall_since_ = ImGui::GetTime()` the moment the daemon first goes "reachable but not ready" (warmup branch + `applyDaemonInitStatus`), and clears it in `onConnected` / `onDisconnected` / warmup-complete — all in `app_network.cpp`. The pure `util::connectHasStalled(stallSince, now, threshold)` helper (new `util/connect_stall.h`, default 45 s from `ui.toml`) drives a draw-list "Taking longer than expected" notice in `renderLoadingOverlay()` (title + elapsed-seconds body + full-node hint), guarded off while the daemon is in `State::Error`. Background retry continues, so the notice self-clears on connect. New `testConnectHasStalled` unit test (7 assertions). Clean build; `ctest` 1/1 passing. (Draw-list text, not buttons — see as-built note above.)
|
||||||
|
- **F6** — ☑ landed: `verifySaplingParams()` now hash-verifies each Sapling param against its pinned canonical SHA-256 (from `build-lite-backend-artifact.sh`), replacing the existence-only check, so a truncated/corrupt-but-present param is rejected instead of failing later on a shielded op. A `<params_dir>/.sapling_verified` marker keyed on `size:mtime` skips re-hashing ~48 MB on every startup. Logic extracted to the injectable `verifySaplingParamsIn(dir, digests)`; new `testVerifySaplingParams` unit test (valid / marker fast-path / wrong-hash / truncated / missing). Clean build; `ctest` 1/1 passing.
|
||||||
|
- **F5** — ☑ landed: `startEmbeddedDaemon()` now checks `extractEmbeddedResources()`'s return (abort with `sb_daemon_extract_failed` on failure) and the previously-dropped `copy_file` `error_code` in the daemon-binary fallback loop (abort with `sb_daemon_files_failed` incl. the dir), so a disk-full / truncated `dragonxd` write is surfaced up front instead of failing opaquely at spawn. An absent source file stays non-fatal. Two i18n keys added to `i18n.cpp`. Clean build; `ctest` 1/1 passing.
|
||||||
|
- **F7** — ☑ landed: new non-throwing `Platform::ensureDirectory(dir, outError)` in `util/platform.{h,cpp}` with one consistent message. Replaces the unchecked/throwing directory-create sites at `main.cpp:730` (pre-init: now logs + `MessageBoxA` on Windows + `return 1`), `connection.cpp:216` (autoDetectConfig now uses the ec overload — **no more uncaught `filesystem_error`** — and sets the new `ConnectionConfig::dir_error`), and both `app.cpp` daemon-dir sites (surface via `daemon_status_` + `return false`). Primary connect path (`app_network.cpp:243`) checks `dir_error` and bails to the status line instead of mislabelling it "waiting for config". `embedded_resources.cpp:270` left as-is (already correct). New `testPlatformEnsureDirectory` unit test (existing-dir / fresh-nested / empty / parent-is-file). Clean build; `ctest` 1/1 passing.
|
||||||
|
- **F4** — ☑ landed: `start()` now gates on a lingering datadir lock after the port bail. When `!skip_port_check_ && override_datadir_.empty()`, it polls `isDaemonProcessRunning()` with a bounded ~300 ms wait (3 × 100 ms, breaks early), then a pure header-inline `evaluateDatadirLockGate()` decides: if a sibling `dragonxd` is still alive it bails with a distinct **non-crash** `State::Error` ("…holding the data directory lock. Retrying shortly…") that never touches `crash_count_`, so the 3-strike cap can't trip; the connect loop's retry resumes once the lock clears. Isolated migrate-to-seed starts are exempt. New `testDatadirLockGate` unit test (5 assertions, proceed/bail/2× exempt) added to `test_phase4.cpp`. Clean build; `ctest` 1/1 passing.
|
||||||
167
docs/wallet-hardening.md
Normal file
167
docs/wallet-hardening.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Wallet Loading & Management — Hardening Plan
|
||||||
|
|
||||||
|
Prioritized, grouped remediation for the wallet loading/management audit (33 verified findings +
|
||||||
|
diagnosability QoL). Companion to the findings artifact. Line references are against `dev`.
|
||||||
|
|
||||||
|
- **Provenance:** 7 parallel subsystem finders, each finding adversarially verified against the
|
||||||
|
code; the 3 highest-impact confirmed findings re-checked by hand. 32 confirmed, 1 refuted
|
||||||
|
(W1-5), 1 raised (W5-3 Low→Med).
|
||||||
|
- **Severity:** 8 High · 12 Medium · 13 Low.
|
||||||
|
|
||||||
|
Status legend: ☐ not started · ◐ in progress · ☑ landed & verified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap (ordered by risk; shared fixes grouped)
|
||||||
|
|
||||||
|
| Phase | Findings | Theme | Status |
|
||||||
|
|-------|----------|-------|--------|
|
||||||
|
| **P0-A** | W7-1, W2-1, W4-1, W4-3, W2-3, W4-5, W5-3 ✓ | Secret hardening (console redaction + delete-export + memzero + lite encrypt-at-create) | ☑ 7/7 |
|
||||||
|
| **P0-B** | W2-2/W4-2, W2-4 | Encryption integrity (never silently unencrypted) | ☑ |
|
||||||
|
| **P1-A** | W3-1, W3-2, W3-4, W3-3 ✓ | Migrate-to-seed correctness (fund-adjacent) | ☑ 4/4 (W3-3 pending a live-mainnet run) |
|
||||||
|
| **P1-B** | W1-1, W1-2, W1-3, W1-4 ✓ + startup guard | Missing/wrong wallet-file safety | ☑ |
|
||||||
|
| **P2** | W5-1, W5-2, W6-1, W6-3, W6-2 ✓ | Stale state & lite save-failure surfacing | ☑ 5/5 |
|
||||||
|
| **F** | W7-2, W7-3, W7-4 ✓ · QoL: copy-diag + open-log + node-error-banner + staleness-badge + alert-history ✓ | Diagnostics foundation + QoL bundle | ☑ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0-A — Secret hardening
|
||||||
|
|
||||||
|
Shared fix: a `SecureString` RAII buffer (zeroes on destruction) retrofitted onto the un-scrubbed
|
||||||
|
key/passphrase paths, plus console redaction and deleting the plaintext export.
|
||||||
|
|
||||||
|
- **W7-1 (High)** `console_tab.cpp:1419` — RPC console echoes/stores/clipboards raw secrets. Fix: an
|
||||||
|
allowlist of secret-bearing first-tokens (`walletpassphrase`, `walletpassphrasechange`,
|
||||||
|
`encryptwallet`, `importprivkey`, `importwallet`, `z_importkey`, `z_importviewingkey`,
|
||||||
|
`signrawtransaction`, `magicrecoverkey`, lite equivalents); echo `> walletpassphrase ****` and
|
||||||
|
keep the raw text out of `command_history_`. Extract a pure `redactConsoleCommand(cmd)` helper for
|
||||||
|
unit testing. **← implementing first (self-contained + testable).**
|
||||||
|
- **W2-1 (High)** `wallet_security_workflow.cpp:66` — delete the `obsidiandecryptexport<ts>` plaintext
|
||||||
|
key dump after `z_importwallet` succeeds (overwrite-then-unlink).
|
||||||
|
- **W4-3 (High)** `app_network.cpp:4481` — `sodium_memzero` the concatenated all-keys string in
|
||||||
|
`exportAllKeys`; write the backup 0600. (Also unify with `ExportAllKeysDialog` — QoL.)
|
||||||
|
- **W4-1 (High)** `app_network.cpp:3801` — zero the key copies in `importPrivateKey`/`sweepPrivateKey`
|
||||||
|
(local + worker-lambda copies).
|
||||||
|
- **W2-3 (Med)** `app_security.cpp:1481` — zero the passphrase threaded through the decrypt lambda chain.
|
||||||
|
- **W4-5 (Med)** `app.cpp:3577` — the seed-backup `.txt` is a permanent predictable cleartext seed;
|
||||||
|
at minimum warn + offer to delete, ideally discourage file save in favor of the on-screen phrase.
|
||||||
|
- **W5-3 (Med)** `lite_wallet_lifecycle_service.cpp:322` — remove the dead `passphrase` field from the
|
||||||
|
lite create/open/restore requests (unused; a secret copied for nothing).
|
||||||
|
|
||||||
|
## P0-B — Encryption integrity
|
||||||
|
|
||||||
|
- **W2-2 / W4-2 (High)** `wallet_security_controller.h:89` — the wizard's deferred encryption is
|
||||||
|
in-memory only and silently lost if the daemon doesn't connect or the app quits/crashes first, so a
|
||||||
|
wallet the user believes is encrypted stays plaintext. Fix: persist a lightweight
|
||||||
|
`encryption_requested_but_incomplete` settings flag (NEVER the passphrase) when
|
||||||
|
`beginDeferredEncryption` is called; surface a persistent warning banner while it's set; clear it
|
||||||
|
only on confirmed `encryptwallet` success; on next connect, if set, re-prompt for the passphrase to
|
||||||
|
complete it.
|
||||||
|
- **W2-4 (Med)** `app_security.cpp:480` — `lockWallet` only sets `locked` on RPC success; log the
|
||||||
|
failure and notify (currently a silent no-op that can leave the wallet unlocked).
|
||||||
|
|
||||||
|
## P1-A — Migrate-to-seed correctness (fund-adjacent; verify carefully)
|
||||||
|
|
||||||
|
- **W3-1 (High)** `app_network.cpp:4327` — adopt hardcodes `datadir + "/wallet.dat"`; use
|
||||||
|
`settings_->getActiveWalletFile()` so migrating a non-default active wallet swaps the right file.
|
||||||
|
- **W3-2 (High)** `seed_wallet_creator.cpp:57` — `remove_all(<config>/seed-migrate)` unconditionally
|
||||||
|
at Phase-1 start; refuse to wipe if a temp `DRAGONX/wallet.dat` already exists (a prior un-adopted
|
||||||
|
swept wallet) and surface it, so swept funds in the temp wallet can't be destroyed by re-entry.
|
||||||
|
- **W3-4 (Med)** `app_network.cpp:1124` — block wallet switching while a migration is *pending*
|
||||||
|
(`getSeedMigrationPending()`), not only while the dialog is open.
|
||||||
|
- **W3-3 (Med)** `app_network.cpp:4231` — persist the sweep opid so an app-close mid-Sweeping can
|
||||||
|
resume/re-poll it instead of silently dropping the txid.
|
||||||
|
|
||||||
|
## P1-B — Missing/wrong wallet-file safety
|
||||||
|
|
||||||
|
- **W1-1 (High)** `app_network.cpp:1109` — `fs::exists()`-check the target wallet file in
|
||||||
|
`switchToWallet()` and before the first daemon launch at startup; if missing, block with an explicit
|
||||||
|
"Wallet file not found — moved or deleted?" dialog (browse / create-new) instead of letting the
|
||||||
|
daemon fabricate an empty wallet.
|
||||||
|
- **W1-3 (Med)** `app_network.cpp:1095` — defer the `syncedHere=true` stamp to the first successful
|
||||||
|
address/balance readback (idHash non-empty), not bare `onConnected()`.
|
||||||
|
- **W1-2 (Med)** `app_network.cpp:198` — split `DB_CORRUPT`-specific strings from the generic "Error
|
||||||
|
loading wallet" fallback; give `DB_TOO_NEW` its own message/action (not a salvage offer).
|
||||||
|
- **W1-4 (Low)** `wallets_dialog.h:393` — re-`fs::exists()` the in-datadir row before switching (match
|
||||||
|
the out-of-datadir path).
|
||||||
|
|
||||||
|
## P2 — State & lite persistence
|
||||||
|
|
||||||
|
- **W6-2 (Med)** `network_refresh_service.cpp:1183` — record a per-field last-success timestamp / a
|
||||||
|
"refresh failed" flag so the UI can show a staleness badge instead of last-good-as-current.
|
||||||
|
- **W5-1 / W5-2 (Med)** `lite_wallet_controller.cpp:78,603` — `liteLog()` the failed save and bubble a
|
||||||
|
one-shot UI warning (both call sites currently discard the bool).
|
||||||
|
- **W6-1 (Med)** `wallet_state.h:313` — reset `mining`/`pool_mining` in `clear()` (or comment why not).
|
||||||
|
- **W6-3 (Low)** `address_book.cpp:46` — per-entry try/catch: skip + count malformed entries instead
|
||||||
|
of discarding the whole list.
|
||||||
|
|
||||||
|
## F — Diagnostics foundation + QoL
|
||||||
|
|
||||||
|
Land W7-2 first — it unblocks the rest.
|
||||||
|
|
||||||
|
- **W7-2 (Med)** `logger.cpp:31` — call `Logger::instance().init(<config>/dragonx-debug.log)` early in
|
||||||
|
`main()` on all platforms; add an "Open log folder" action.
|
||||||
|
- **W7-3 (Med)** `main.cpp:144` — add a `sigaction`-based crash handler writing `dragonx-crash.log` on
|
||||||
|
POSIX (mirror the Windows SEH path).
|
||||||
|
- **W7-4 (Low)** `logger.cpp:39` — size-cap/rotate the log on `init()`.
|
||||||
|
- **QoL** — "Copy diagnostics for support" bundle; persistent alert history; daemon/RPC error banner;
|
||||||
|
refresh-staleness badge; multi-wallet diagnostic panel; refresh-diagnostics panel; structured
|
||||||
|
switch/migration audit logging; restore-from-seed entry point (W4-4, effort L).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
- **Adversarial review of the 3 diagnostics UI features** — ran a 5-dimension finder → per-finding verify workflow over the node-banner + staleness-badge + alert-history commits (the hand-laid ImGui I couldn't visually verify). 4 confirmed, 1 refuted (banner title never overlaps its button — button is absolutely positioned + title is short), and the dedicated ImGui-stack-balance finder found **no** Push/Pop imbalance. Fixes landed:
|
||||||
|
- **(Med) Alert popup grew off the right edge** — pivot `(0,1)` pinned the panel's *left* edge at the bell (which sits near the window's right edge), so a 320px panel overflowed rightward (an explicit `SetNextWindowPos` pivot skips ImGui's on-screen clamp). Fixed to anchor the bottom-*right* corner at the bell (pivot `(1,1)`, at `bellMax.x`) so it grows left over the canvas.
|
||||||
|
- **(Low) Staleness badge could flash red on reconnect** — `WalletState::clear()` reset everything *except* the four `last_*_update` stamps, so after a reconnect the pre-outage timestamp survived and the badge briefly showed "Updated Nm ago" (red) on the same frame the node banner cleared — the exact contradiction the design forbids. Fixed by zeroing the four stamps in `clear()` (all readers treat 0 as "never"; verified `app_network.cpp:1473` guards on `!= 0`).
|
||||||
|
- **(Low) Banner min-height floor wasn't DPI-scaled** — `std::max(minH, baseH*vScale())` compared a raw-px floor against a scaled value; now `minH * dpiScale()`.
|
||||||
|
- **(Low) New i18n keys weren't in `res/lang/`** — back-filled all 16 diagnostics/QoL keys (this session's node_banner_*/data_stale_*/alerts_*/settings_*/tt_*) into all 8 language files, additively (128 insertions, 0 deletions). zh/ja/ko reworded around 2 glyphs missing from the CJK subset (提醒→通知; ko tooltip avoids 닐) and hard-asserted tofu-free against the subset font.
|
||||||
|
- **Foundation QoL / Persistent alert history** — ☑ landed. Toasts fade in 1–4s; there was no way to review what scrolled past. `Notifications` now retains every pushed alert in a capped (100) ring buffer with a wall-clock epoch (`AlertRecord`) — separate from the 5-item live-toast deque — plus a monotonic `total_pushed_` counter. A bell in the status-bar right cluster (`ICON_MD_NOTIFICATIONS`) opens an upward popup listing recent alerts newest-first with a severity icon/colour (reusing the toast palette), the message, and a relative age (`formatTimeAgoShort`), with a Clear-all action. An **unread dot** on the bell (coloured by the most-severe unseen alert) marks alerts that arrived since the panel was last opened — driven by `totalPushed()` deltas so it survives capping/clearing. Thread-safety: every push is on the UI thread (RPC results run as main-thread `MainCb`s), matching the class's existing lock-free model — documented as a no-raw-worker-thread invariant. Build-clean; `ctest` 1/1 (adds `testNotificationHistory`: retention, order, cap, monotonic counter, clear). **This closes the QoL bundle and the Foundation tier.**
|
||||||
|
- **W6-2 / Refresh-staleness badge** — ☑ landed. The Total Balance card now shows a small pill on its status line ("Updated 2m ago", amber → red past 3 min) **only when connected but the balance stopped refreshing** — a busy daemon can fail `z_gettotalbalance` without dropping the whole connection (only *both* core RPCs failing 3× triggers a disconnect), leaving stale numbers on screen while the node-status banner stays hidden. No refresh-path changes were needed: `WalletState::last_balance_update` is already stamped only on a successful fetch (`network_refresh_service.cpp:1187`), so the badge just reads it and computes age against the same `std::time` clock (`util::formatTimeAgoShort`). Decision is a pure, unit-tested helper (`ui/staleness_badge.h::evaluateStalenessBadge`, thresholds 45s/180s) gated on `connected` so it never contradicts the banner; hover shows a "may be out of date — check your node connection" tooltip. Build-clean; `ctest` 1/1 (adds `testStalenessBadge`). **This closes P2 (5/5).**
|
||||||
|
- **Foundation QoL / Persistent node-status banner** — ☑ landed. A persistent horizontal strip now sits at the top of the content column whenever the wallet can't reach its node — distinct from the transient toasts, so an offline wallet is never silently mistaken for a working one. The show/severity/action decision is a pure function (`ui/node_status_banner.h` → `evaluateNodeStatusBanner`, unit-tested) fed a state snapshot by `App::renderNodeStatusBanner()`. Three cases: **full-node offline** (amber, "Reconnect" → `tryConnect`), **embedded daemon crashed & auto-restart gave up** (red, "Restart node" → `restartDaemon`), **lite wallet failed to open** (red, message-only). Suppressed during the wizard / wallet-switch / daemon-restart / screenshot-sweep / shutdown, and while an expected startup phase (warmup/init/connect-in-progress) already owns the screen. Height in `res/themes/ui.toml` (`banners.node-status`); colours from the material semantic palette; detail text ellipsis-clipped so it can't shove the action button off-screen. Build-clean; `ctest` 1/1 (added `testNodeStatusBanner`). **Remaining QoL:** persistent alert history, and the W6-2 refresh-staleness badge.
|
||||||
|
- **Foundation QoL / "Copy diagnostics" + "Open log folder"** — ☑ landed: Settings (logging section) now has two actions. **Open log folder** opens the config dir (`Platform::openFolder`) so users can actually find `dragonx-debug.log`/`dragonx-crash.log`. **Copy diagnostics** copies a plaintext support snapshot to the clipboard via the new `App::buildDiagnosticsReport()` — version, build variant, platform, connection status, active wallet path + existence + size, encryption/lock state, sync heights, daemon status/running/crash-count/lastError (full-node), and the log paths. No secrets. Build-clean; `ctest` 1/1. **Remaining QoL:** persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge.
|
||||||
|
- **Foundation / W7-2 · W7-3 · W7-4 (diagnostics infrastructure)** — ☑ landed (answers the original "easier to diagnose" ask — the logging/crash foundation now actually works):
|
||||||
|
- **W7-2 (Med, keystone):** the app-level `Logger` file sink was never initialized, so `LOG`/`LOGF`/`VERBOSE_LOGF` went nowhere and `dragonx-debug.log` didn't exist on Linux/macOS at all. `main()` now calls `Logger::init(<config>/dragonx-debug.log)` on all platforms. Also fixed a **latent deadlock** this exposed: `init()` wrote its banner via `write()`, which re-locks the non-recursive `mutex_` it already holds — now written directly. On Windows the raw stdout/stderr `freopen` was moved to a separate `dragonx-stdout.log` so the two writers don't contend. New `testLoggerFileSink` (also a deadlock guard — it would hang if that regressed).
|
||||||
|
- **W7-3 (Med):** no crash handler existed on Linux/macOS. Added an **async-signal-safe** `sigaction` handler (SIGSEGV/ABRT/BUS/FPE/ILL) that writes a signal id + `backtrace_symbols_fd` backtrace to `dragonx-crash.log`, then re-raises the default disposition for a core dump — the POSIX counterpart of the Windows SEH filter.
|
||||||
|
- **W7-4 (Low):** `Logger::init` now rotates the log to a single `.1` backup when it exceeds 10 MB, so a long/verbose session can't grow it unbounded.
|
||||||
|
Build-clean; `ctest` 1/1. **Remaining Foundation:** the QoL bundle (mostly UI) — "copy diagnostics for support", an "open log folder" action, persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge.
|
||||||
|
- **P2 / W5-1 · W5-2 · W6-1 · W6-3 (localized batch)** — ☑ landed:
|
||||||
|
- **W5-1 (Med):** `persistAfterBroadcast` (lite send/shield save) returned false on a persistent save failure but both callers discarded it and it never logged — completely silent. It now `liteLog`s the failure (the note re-derives on next sync, so it's a robustness gap, not fund loss).
|
||||||
|
- **W5-2 (Med):** the post-**sync** and post-**rescan** `save` results (in the detached scan threads) were ignored; both now `liteLog` on failure (`LiteDiagnostics::log` is mutex-guarded, safe from those threads).
|
||||||
|
- **W6-1 (Med):** `WalletState::clear()` didn't reset `mining`/`pool_mining`, so a wallet switch could briefly show the previous wallet's hashrate/blocks. Now reset in `clear()` (the daemon restarts on switch, so mining genuinely stops).
|
||||||
|
- **W6-3 (Low):** `AddressBook::load()` did `entries_.clear()` then threw on the first non-object element — discarding **every** contact. Now it guards `is_object()` + per-entry try/catch, skipping and counting malformed entries.
|
||||||
|
Build-clean; `ctest` 1/1. **Remaining P2:** W6-2 (surface refresh staleness — the timestamps exist in `WalletState`; this needs the UI "updated Xs ago" badge, which overlaps the diagnostics/QoL Foundation bundle).
|
||||||
|
- **P1-B / W1-3 + startup wallet-existence guard** — ☑ landed:
|
||||||
|
- **W1-3 (Med):** `syncedHere` was stamped in the `markOpened` block at bare connect (idHash still empty), letting a freshly-restored wallet skip its needed rescan. It's now stamped only once the identity is verified (idHash non-empty), so it takes effect at the post-address-refresh index update (`updateWalletIndexForActiveWallet` after addresses load), while `lastOpenedEpoch` still records at open.
|
||||||
|
- **Startup guard (the W1-1 launch counterpart):** `App::init` now `exists()`-checks the recorded active wallet before the daemon is configured; a **non-default** active wallet that was moved/deleted between sessions falls back to the default `wallet.dat` with a warning, instead of the daemon silently auto-creating an empty wallet under the missing name. Runs before the PIN-vault init so the vault is scoped to the wallet actually opened.
|
||||||
|
Build-clean; `ctest` 1/1.
|
||||||
|
- **P1-A / W3-3 (sweep opid persistence)** — ☑ **implemented + two rounds of adversarial review** (the "live mainnet run" the migration code mandates is the remaining gate — see below). The deferral's core fear (re-tracking a stale opid hangs forever) was **refuted by the code**: the opid poller (`app.cpp:1122`) + `parseOperationStatusPoll` classify a tracked opid absent from a *successful* `z_getoperationstatus` as stale, remove it, and fire the callback `ok=false` — a thrown RPC aborts the poll so there's never a *false* stale. So re-tracking yields at worst one clean failure, never a hang.
|
||||||
|
- **What landed:** a persisted `seed_migration_sweep_opid` setting; the opid is adopted **atomically** with clearing any prior txid in the *same* `settings.save()` **only once the submit succeeds** (torn-write safe; txid always outranks opid on resume). Resume routing is a pure, unit-tested helper (`data/seed_migration_resume.h::decideSeedMigrationResume`): txid → Confirming; opid **and connected** → re-track (`Sweeping`); otherwise → the dismissable Sweep gate. The shared `makeSweepCompletionCallback(resumed)`: success → Confirming; resumed-stale → Sweep gate (re-fetch balance, honest "may have already completed" copy); fresh-fail → Error.
|
||||||
|
- **Round 1 (design review, 4 skeptics)** confirmed both safety facts (no fund loss — adopt gate + never-deleted `.bak` untouched; no hang) and caught 3 real resume-UX traps, all fixed: a missing **connectivity gate** (would trap the user in the buttonless `Sweeping` spinner while offline), a **missing balance re-fetch** on the stale fallback (permanent "Checking balance…"), and honest messaging since a daemon restart makes even a *successful* sweep read "stale".
|
||||||
|
- **Round 2 (implementation review, 3 reviewers)** caught one regression — clearing the old txid at sweep *entry* would forget an already-mined first sweep if a remainder re-sweep's submit failed; fixed by the atomic-on-success swap above. All other fixes verified present + correct.
|
||||||
|
- **⚑ Remaining gate — live mainnet run (user):** per CLAUDE.md this fund-moving path must be exercised once on mainnet before it ships. The self-verifiable parts (build, unit test, both review rounds) are green; a real interrupted-sweep resume on mainnet is the human gate I cannot perform.
|
||||||
|
- **P1-B / W1-1 (+ W1-4) · W1-2 (wallet-file safety)** — ☑ landed:
|
||||||
|
- **W1-1 (High):** `switchToWallet` never checked the target wallet file exists, so a moved/deleted file "opened" as a fresh empty wallet (dragonxd auto-creates for a missing `-wallet=`), looking exactly like fund loss. It now `std::filesystem::exists`-checks `datadir + "/" + walletFile` before switching and blocks with a "not found (moved or deleted?)" warning. Placed before the daemon-stop prompt, and — since the check runs no matter how `switchToWallet` is invoked — it also **closes W1-4** (the stale-switcher-row TOCTOU).
|
||||||
|
- **W1-2 (Med):** `walletOutputLooksCorrupt` matched the generic "Error loading wallet" string, so a `DB_TOO_NEW` (newer-version) wallet was offered a `-salvagewallet` repair that can't fix it. Now the generic match is excluded when the output also contains "newer version".
|
||||||
|
Build-clean; `ctest` 1/1. **Remaining P1-B:** W1-3 (defer the `syncedHere` stamp to a verified readback) + the startup-path existence check (`app.cpp` hands `getActiveWalletFile()` to the daemon with no `exists()` check — same silent-empty-wallet risk as W1-1 but at launch).
|
||||||
|
- **P1-A / W3-1 · W3-2 · W3-4 (migrate-to-seed correctness)** — ☑ landed (fund-adjacent — reviewed carefully):
|
||||||
|
- **W3-1 (High):** `beginAdoptSeedWallet` hardcoded `datadir + "/wallet.dat"` as the file to swap. With a non-default active wallet (e.g. `wallet-2.dat`), that installed the swept seed wallet into an unloaded `wallet.dat` and left the daemon reloading the emptied legacy — funds only recoverable via the seed phrase. Now swaps `datadir + "/" + getActiveWalletFile()` (captured on the main thread; switching is blocked during migration so it can't race).
|
||||||
|
- **W3-2 (High):** `SeedWalletCreator::create` did `remove_all(<config>/seed-migrate)` unconditionally at the start. A prior migration that swept funds into the temp wallet but was abandoned/crashed before adopting would have that fund-bearing wallet destroyed. It now refuses (with a clear message) when `DRAGONX/wallet.dat` already exists — a completed migration removes the dir on adopt, so a leftover means an unfinished one.
|
||||||
|
- **W3-4 (Med):** `switchToWallet` only blocked switching while the migration *dialog* was open; closing it via "Later" mid-migration dropped the guard. Now also blocks while `getSeedMigrationPending()`.
|
||||||
|
Build-clean; `ctest` 1/1. **Remaining P1-A:** W3-3 (persist the sweep opid so an app-close mid-sweep can resume/re-poll instead of silently dropping the txid).
|
||||||
|
|
||||||
|
- **P0-B / W2-2 (deferred encryption silently lost) + W2-4 (auto-lock silent-fail)** — ☑ landed:
|
||||||
|
- **W2-2:** the wizard's deferred encryption was stored only in memory, so a quit/crash or a failed daemon connect before it applied left the wallet unencrypted with **no record it was ever requested** — the user believing it was encrypted. Now a persisted `encryption_pending` settings flag is set the moment encryption is requested (**never the passphrase** — only the fact). `refreshWalletEncryptionState()` reconciles it on every connect: wallet observed **encrypted** → clear the flag; wallet **not** encrypted while the flag is set and no deferred encryption is pending/in-flight → a once-per-session **"your wallet is NOT encrypted — open Settings to finish"** warning (the flag stays set, so it recurs each launch until resolved). We deliberately don't persist the passphrase to auto-complete — surfacing it is the secure choice.
|
||||||
|
- **W2-4:** `lockWallet()`'s continuation only handled success — a failed `walletlock` silently left the wallet **unlocked** (an unfulfilled auto-lock). It now logs and warns once (reset on the next successful lock), so a failing auto-lock is visible instead of leaving the wallet exposed.
|
||||||
|
Touches `settings.{h,cpp}`, `app_wizard.cpp`, `app_security.cpp`, `app.h`. Not unit-testable at this layer (RPC/connect-driven state machine); build-clean, `ctest` 1/1.
|
||||||
|
|
||||||
|
- **P0-A / W5-3 (lite create-time passphrase)** — ☑ landed (chose option **(b) wire it up**). The lite create/open/restore passphrase was collected but never consumed by the backend — a "passphrase" field that did nothing. It now has a real meaning for all three operations, in `LiteWalletController`: **create/restore** → `encryptWallet(passphrase)` (the backend encrypts + locks + saves the brand-new wallet); **open** → `unlockWallet(passphrase)`, but only when `encryptionStatus()` reports the existing wallet is actually encrypted+locked (skips a spurious unlock otherwise). Encrypt/unlock take their own copy and wipe it; a post-create encrypt failure is `liteLog`'d (the wallet still exists — the create isn't failed). Six existing lite-controller tests carried an incidental `hunter2` create passphrase from the dead-field era; removed (they test non-encryption flows and want an unencrypted wallet), and added `testLiteWalletControllerCreateEncryptsWithPassphrase` to prove the new behavior. Build-clean; `ctest` 1/1. *(Follow-up UX polish: `settings_page` could show the passphrase field's meaning per operation — "encrypt" for create/restore vs "unlock" for open.)*
|
||||||
|
- **P0-A / W4-5 (seed-backup file)** — ☑ landed (proportionate): the seed "Save" already wrote 0600 + zeroed the in-memory buffer, but the success message was a bare "Saved to <path>". It now reads "**Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this copy**: <path>", so the plaintext-on-disk risk is called out. `i18n.cpp` (English source; `res/lang` back-fill of this changed key is deferred to the batch i18n pass). A stronger fix (pre-save confirmation, or dropping the file-save in favor of on-screen + Copy) is a follow-up UX decision.
|
||||||
|
- **P0-A / W4-1 · W4-3 · W2-3 (memzero cluster)** — ☑ landed, using the file's established `sodium_memzero` pattern (matching the existing lambda-capture scrub at app_network.cpp:2885 and JSON scrub at :4025) rather than a new type, since this is fund-moving code:
|
||||||
|
- **W4-1** `importPrivateKey`/`sweepPrivateKey`: the spending/viewing key is now scrubbed on all paths — the calling-frame copy (after the worker post), the worker-lambda's captured copy (lambda made `mutable`, zeroed after the request is sent), and the JSON request `params` copy.
|
||||||
|
- **W4-3** `exportAllKeys`/`backupWallet`: the concatenated all-keys buffer is zeroed after the consumer uses it, and the backup file is now written via `Platform::writeFileAtomically(..., restrictPermissions=true)` (atomic + 0600) instead of a umask-default `ofstream`.
|
||||||
|
- **W2-3** decrypt-wallet passphrase: `std::move`-captured into the worker lambda (so no plaintext copy is left in the calling frame) and `sodium_memzero`'d right after `unlockWallet` (its only use).
|
||||||
|
Not unit-testable (the scrubbing has no observable RPC effect — the key value sent to the daemon is unchanged; only post-use memory zeroing is added). Build-clean; `ctest` 1/1 (no regression). **Remaining in P0-A:** W5-3 (remove the dead lite `passphrase` field), W4-5 (predictable plaintext seed-backup file).
|
||||||
|
- **P0-A / W2-1** — ☑ landed: the decrypt-wallet flow now scrubs (best-effort in-place zero-overwrite) and removes the plaintext key export (`obsidiandecryptexport…`) as soon as the `z_importwallet` attempt resolves — success or failure — so a full cleartext dump of every private key is no longer left on disk forever. Recovery remains the encrypted backup (`wallet.dat.encrypted.bak`). `app_security.cpp` (after the import call). Not unit-testable (fs I/O in a deep lambda); build-clean, `ctest` 1/1 (no regression).
|
||||||
|
- **P0-A / W7-1** — ☑ landed: `RedactConsoleCommand`/`ConsoleCommandCarriesSecret` in `console_tab_helpers` redact secret-bearing commands (an allowlist of 13 first-tokens: `walletpassphrase`, `encryptwallet`, `z_importkey`, …) to `> walletpassphrase ****` before they hit the console echo AND the recall history; the real command still executes unredacted. Wired into `submitConsoleCommand` (`console_tab.cpp`). New `testConsoleSecretRedaction` (11 assertions). Clean build; `ctest` 1/1. (Output-secret commands like `z_exportkey` — result redaction — remain a follow-up.)
|
||||||
@@ -67,7 +67,8 @@
|
|||||||
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
||||||
|
|
||||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||||
//#define IMGUI_USE_WCHAR32
|
//---- Enabled so chat can render emoji (U+1F300+, above the BMP) — see Typography::loadFont emoji merge (Q12).
|
||||||
|
#define IMGUI_USE_WCHAR32
|
||||||
|
|
||||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||||
|
|||||||
744
libs/imgui/misc/freetype/imgui_freetype.cpp
Normal file
744
libs/imgui/misc/freetype/imgui_freetype.cpp
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||||
|
// (code)
|
||||||
|
|
||||||
|
// Get the latest version at https://github.com/ocornut/imgui/tree/master/misc/freetype
|
||||||
|
// Original code by @vuhdo (Aleksei Skriabin) in 2017, with improvements by @mikesart.
|
||||||
|
// Maintained since 2019 by @ocornut.
|
||||||
|
|
||||||
|
// CHANGELOG
|
||||||
|
// (minor and older changes stripped away, please see git history for details)
|
||||||
|
// 2025/06/11: refactored for the new ImFontLoader architecture, and ImGuiBackendFlags_RendererHasTextures support.
|
||||||
|
// 2024/10/17: added plutosvg support for SVG Fonts (seems faster/better than lunasvg). Enable by using '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG'. (#7927)
|
||||||
|
// 2023/11/13: added support for ImFontConfig::RasterizationDensity field for scaling render density without scaling metrics.
|
||||||
|
// 2023/08/01: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG'. (#6591)
|
||||||
|
// 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly.
|
||||||
|
// 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns nullptr.
|
||||||
|
// 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs.
|
||||||
|
// 2021/03/02: set 'atlas->TexPixelsUseColors = true' to help some backends with deciding of a preferred texture format.
|
||||||
|
// 2021/01/28: added support for color-layered glyphs via ImGuiFreeTypeBuilderFlags_LoadColor (require Freetype 2.10+).
|
||||||
|
// 2021/01/26: simplified integration by using '#define IMGUI_ENABLE_FREETYPE'. renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. removed ImGuiFreeType::BuildFontAtlas().
|
||||||
|
// 2020/06/04: fix for rare case where FT_Get_Char_Index() succeed but FT_Load_Glyph() fails.
|
||||||
|
// 2019/02/09: added RasterizerFlags::Monochrome flag to disable font anti-aliasing (combine with ::MonoHinting for best results!)
|
||||||
|
// 2019/01/15: added support for imgui allocators + added FreeType only override function SetAllocatorFunctions().
|
||||||
|
// 2019/01/10: re-factored to match big update in STB builder. fixed texture height waste. fixed redundant glyphs when merging. support for glyph padding.
|
||||||
|
// 2018/06/08: added support for ImFontConfig::GlyphMinAdvanceX, GlyphMaxAdvanceX.
|
||||||
|
// 2018/02/04: moved to main imgui repository (away from http://www.github.com/ocornut/imgui_club)
|
||||||
|
// 2018/01/22: fix for addition of ImFontAtlas::TexUvscale member.
|
||||||
|
// 2017/10/22: minor inconsequential change to match change in master (removed an unnecessary statement).
|
||||||
|
// 2017/09/26: fixes for imgui internal changes.
|
||||||
|
// 2017/08/26: cleanup, optimizations, support for ImFontConfig::RasterizerFlags, ImFontConfig::RasterizerMultiply.
|
||||||
|
// 2017/08/16: imported from https://github.com/Vuhdo/imgui_freetype into http://www.github.com/ocornut/imgui_club, updated for latest changes in ImFontAtlas, minor tweaks.
|
||||||
|
|
||||||
|
// About Gamma Correct Blending:
|
||||||
|
// - FreeType assumes blending in linear space rather than gamma space.
|
||||||
|
// - See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Render_Glyph
|
||||||
|
// - For correct results you need to be using sRGB and convert to linear space in the pixel shader output.
|
||||||
|
// - The default dear imgui styles will be impacted by this change (alpha values will need tweaking).
|
||||||
|
|
||||||
|
// FIXME: cfg.OversampleH, OversampleV are not supported, but generally not necessary with this rasterizer because Hinting makes everything look better.
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#ifndef IMGUI_DISABLE
|
||||||
|
#include "imgui_freetype.h"
|
||||||
|
#include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*,
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H // <freetype/freetype.h>
|
||||||
|
#include FT_MODULE_H // <freetype/ftmodapi.h>
|
||||||
|
#include FT_GLYPH_H // <freetype/ftglyph.h>
|
||||||
|
#include FT_SIZES_H // <freetype/ftsizes.h>
|
||||||
|
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
|
||||||
|
|
||||||
|
// Handle LunaSVG and PlutoSVG
|
||||||
|
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||||
|
#error "Cannot enable both IMGUI_ENABLE_FREETYPE_LUNASVG and IMGUI_ENABLE_FREETYPE_PLUTOSVG"
|
||||||
|
#endif
|
||||||
|
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
#include FT_OTSVG_H // <freetype/otsvg.h>
|
||||||
|
#include FT_BBOX_H // <freetype/ftbbox.h>
|
||||||
|
#include <lunasvg.h>
|
||||||
|
#endif
|
||||||
|
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||||
|
#include <plutosvg.h>
|
||||||
|
#endif
|
||||||
|
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||||
|
#if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||||
|
#error IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#pragma warning (push)
|
||||||
|
#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff)
|
||||||
|
#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||||
|
#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used
|
||||||
|
#ifndef __clang__
|
||||||
|
#pragma GCC diagnostic ignored "-Wsubobject-linkage" // warning: 'xxxx' has a field 'xxxx' whose type uses the anonymous namespace
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Data
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Default memory allocators
|
||||||
|
static void* ImGuiFreeTypeDefaultAllocFunc(size_t size, void* user_data) { IM_UNUSED(user_data); return IM_ALLOC(size); }
|
||||||
|
static void ImGuiFreeTypeDefaultFreeFunc(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_FREE(ptr); }
|
||||||
|
|
||||||
|
// Current memory allocators
|
||||||
|
static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFreeTypeDefaultAllocFunc;
|
||||||
|
static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc;
|
||||||
|
static void* GImGuiFreeTypeAllocatorUserData = nullptr;
|
||||||
|
|
||||||
|
// Lunasvg support
|
||||||
|
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* state);
|
||||||
|
static void ImGuiLunasvgPortFree(FT_Pointer* state);
|
||||||
|
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state);
|
||||||
|
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Code
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point
|
||||||
|
#define FT_SCALEFACTOR 64.0f
|
||||||
|
|
||||||
|
// Glyph metrics:
|
||||||
|
// --------------
|
||||||
|
//
|
||||||
|
// xmin xmax
|
||||||
|
// | |
|
||||||
|
// |<-------- width -------->|
|
||||||
|
// | |
|
||||||
|
// | +-------------------------+----------------- ymax
|
||||||
|
// | | ggggggggg ggggg | ^ ^
|
||||||
|
// | | g:::::::::ggg::::g | | |
|
||||||
|
// | | g:::::::::::::::::g | | |
|
||||||
|
// | | g::::::ggggg::::::gg | | |
|
||||||
|
// | | g:::::g g:::::g | | |
|
||||||
|
// offsetX -|-------->| g:::::g g:::::g | offsetY |
|
||||||
|
// | | g:::::g g:::::g | | |
|
||||||
|
// | | g::::::g g:::::g | | |
|
||||||
|
// | | g:::::::ggggg:::::g | | |
|
||||||
|
// | | g::::::::::::::::g | | height
|
||||||
|
// | | gg::::::::::::::g | | |
|
||||||
|
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
|
||||||
|
// / | | g:::::g | |
|
||||||
|
// origin | | gggggg g:::::g | |
|
||||||
|
// | | g:::::gg gg:::::g | |
|
||||||
|
// | | g::::::ggg:::::::g | |
|
||||||
|
// | | gg:::::::::::::g | |
|
||||||
|
// | | ggg::::::ggg | |
|
||||||
|
// | | gggggg | v
|
||||||
|
// | +-------------------------+----------------- ymin
|
||||||
|
// | |
|
||||||
|
// |------------- advanceX ----------->|
|
||||||
|
|
||||||
|
// Stored in ImFontAtlas::FontLoaderData. ALLOCATED BY US.
|
||||||
|
struct ImGui_ImplFreeType_Data
|
||||||
|
{
|
||||||
|
FT_Library Library;
|
||||||
|
FT_MemoryRec_ MemoryManager;
|
||||||
|
ImGui_ImplFreeType_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stored in ImFontConfig::FontLoaderData. ALLOCATED BY US.
|
||||||
|
struct ImGui_ImplFreeType_FontSrcData
|
||||||
|
{
|
||||||
|
// Initialize from an external data buffer. Doesn't copy data, and you must ensure it stays valid up to this object lifetime.
|
||||||
|
bool InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_user_flags);
|
||||||
|
void CloseFont();
|
||||||
|
ImGui_ImplFreeType_FontSrcData() { memset((void*)this, 0, sizeof(*this)); }
|
||||||
|
~ImGui_ImplFreeType_FontSrcData() { CloseFont(); }
|
||||||
|
|
||||||
|
// Members
|
||||||
|
FT_Face FtFace;
|
||||||
|
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
|
||||||
|
FT_Int32 LoadFlags;
|
||||||
|
ImFontBaked* BakedLastActivated;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
|
||||||
|
struct ImGui_ImplFreeType_FontSrcBakedData
|
||||||
|
{
|
||||||
|
FT_Size FtSize; // This represent a FT_Face with a given size.
|
||||||
|
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
|
||||||
|
{
|
||||||
|
FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace);
|
||||||
|
if (error != 0)
|
||||||
|
return false;
|
||||||
|
error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE);
|
||||||
|
if (error != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Convert to FreeType flags (NB: Bold and Oblique are processed separately)
|
||||||
|
UserFlags = (ImGuiFreeTypeLoaderFlags)(src->FontLoaderFlags | extra_font_loader_flags);
|
||||||
|
|
||||||
|
LoadFlags = 0;
|
||||||
|
if ((UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) == 0)
|
||||||
|
LoadFlags |= FT_LOAD_NO_BITMAP;
|
||||||
|
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoHinting)
|
||||||
|
LoadFlags |= FT_LOAD_NO_HINTING;
|
||||||
|
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoAutoHint)
|
||||||
|
LoadFlags |= FT_LOAD_NO_AUTOHINT;
|
||||||
|
if (UserFlags & ImGuiFreeTypeLoaderFlags_ForceAutoHint)
|
||||||
|
LoadFlags |= FT_LOAD_FORCE_AUTOHINT;
|
||||||
|
if (UserFlags & ImGuiFreeTypeLoaderFlags_LightHinting)
|
||||||
|
LoadFlags |= FT_LOAD_TARGET_LIGHT;
|
||||||
|
else if (UserFlags & ImGuiFreeTypeLoaderFlags_MonoHinting)
|
||||||
|
LoadFlags |= FT_LOAD_TARGET_MONO;
|
||||||
|
else
|
||||||
|
LoadFlags |= FT_LOAD_TARGET_NORMAL;
|
||||||
|
|
||||||
|
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
|
||||||
|
LoadFlags |= FT_LOAD_COLOR;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplFreeType_FontSrcData::CloseFont()
|
||||||
|
{
|
||||||
|
if (FtFace)
|
||||||
|
{
|
||||||
|
FT_Done_Face(FtFace);
|
||||||
|
FtFace = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint)
|
||||||
|
{
|
||||||
|
uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint);
|
||||||
|
if (glyph_index == 0)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// If this crash for you: FreeType 2.11.0 has a crash bug on some bitmap/colored fonts.
|
||||||
|
// - https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076
|
||||||
|
// - https://github.com/ocornut/imgui/issues/4567
|
||||||
|
// - https://github.com/ocornut/imgui/issues/4566
|
||||||
|
// You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version.
|
||||||
|
FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags);
|
||||||
|
if (error)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// Need an outline for this to work
|
||||||
|
FT_GlyphSlot slot = src_data->FtFace->glyph;
|
||||||
|
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||||
|
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG);
|
||||||
|
#else
|
||||||
|
#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||||
|
IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG in imconfig.h and install required libraries in order to use this font");
|
||||||
|
#endif
|
||||||
|
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP);
|
||||||
|
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
|
||||||
|
// Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting)
|
||||||
|
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold)
|
||||||
|
FT_GlyphSlot_Embolden(slot);
|
||||||
|
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique)
|
||||||
|
{
|
||||||
|
FT_GlyphSlot_Oblique(slot);
|
||||||
|
//FT_BBox bbox;
|
||||||
|
//FT_Outline_Get_BBox(&slot->outline, &bbox);
|
||||||
|
//slot->metrics.width = bbox.xMax - bbox.xMin;
|
||||||
|
//slot->metrics.height = bbox.yMax - bbox.yMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &slot->metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGui_ImplFreeType_BlitGlyph(const FT_Bitmap* ft_bitmap, uint32_t* dst, uint32_t dst_pitch)
|
||||||
|
{
|
||||||
|
IM_ASSERT(ft_bitmap != nullptr);
|
||||||
|
const uint32_t w = ft_bitmap->width;
|
||||||
|
const uint32_t h = ft_bitmap->rows;
|
||||||
|
const uint8_t* src = ft_bitmap->buffer;
|
||||||
|
const uint32_t src_pitch = ft_bitmap->pitch;
|
||||||
|
|
||||||
|
switch (ft_bitmap->pixel_mode)
|
||||||
|
{
|
||||||
|
case FT_PIXEL_MODE_GRAY: // Grayscale image, 1 byte per pixel.
|
||||||
|
{
|
||||||
|
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||||
|
for (uint32_t x = 0; x < w; x++)
|
||||||
|
dst[x] = IM_COL32(255, 255, 255, src[x]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FT_PIXEL_MODE_MONO: // Monochrome image, 1 bit per pixel. The bits in each byte are ordered from MSB to LSB.
|
||||||
|
{
|
||||||
|
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||||
|
{
|
||||||
|
uint8_t bits = 0;
|
||||||
|
const uint8_t* bits_ptr = src;
|
||||||
|
for (uint32_t x = 0; x < w; x++, bits <<= 1)
|
||||||
|
{
|
||||||
|
if ((x & 7) == 0)
|
||||||
|
bits = *bits_ptr++;
|
||||||
|
dst[x] = IM_COL32(255, 255, 255, (bits & 0x80) ? 255 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FT_PIXEL_MODE_BGRA:
|
||||||
|
{
|
||||||
|
// FIXME: Converting pre-multiplied alpha to straight. Doesn't smell good.
|
||||||
|
#define DE_MULTIPLY(color, alpha) ImMin((ImU32)(255.0f * (float)color / (float)(alpha + FLT_MIN) + 0.5f), 255u)
|
||||||
|
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||||
|
for (uint32_t x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
uint8_t r = src[x * 4 + 2], g = src[x * 4 + 1], b = src[x * 4], a = src[x * 4 + 3];
|
||||||
|
dst[x] = IM_COL32(DE_MULTIPLY(r, a), DE_MULTIPLY(g, a), DE_MULTIPLY(b, a), a);
|
||||||
|
}
|
||||||
|
#undef DE_MULTIPLY
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
IM_ASSERT(0 && "FreeTypeFont::BlitGlyph(): Unknown bitmap pixel mode!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeType memory allocation callbacks
|
||||||
|
static void* FreeType_Alloc(FT_Memory /*memory*/, long size)
|
||||||
|
{
|
||||||
|
return GImGuiFreeTypeAllocFunc((size_t)size, GImGuiFreeTypeAllocatorUserData);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FreeType_Free(FT_Memory /*memory*/, void* block)
|
||||||
|
{
|
||||||
|
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void* FreeType_Realloc(FT_Memory /*memory*/, long cur_size, long new_size, void* block)
|
||||||
|
{
|
||||||
|
// Implement realloc() as we don't ask user to provide it.
|
||||||
|
if (block == nullptr)
|
||||||
|
return GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||||
|
|
||||||
|
if (new_size == 0)
|
||||||
|
{
|
||||||
|
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_size > cur_size)
|
||||||
|
{
|
||||||
|
void* new_block = GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||||
|
memcpy(new_block, block, (size_t)cur_size);
|
||||||
|
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||||
|
return new_block;
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas)
|
||||||
|
{
|
||||||
|
IM_ASSERT(atlas->FontLoaderData == nullptr);
|
||||||
|
ImGui_ImplFreeType_Data* bd = IM_NEW(ImGui_ImplFreeType_Data)();
|
||||||
|
|
||||||
|
// FreeType memory management: https://www.freetype.org/freetype2/docs/design/design-4.html
|
||||||
|
bd->MemoryManager.user = nullptr;
|
||||||
|
bd->MemoryManager.alloc = &FreeType_Alloc;
|
||||||
|
bd->MemoryManager.free = &FreeType_Free;
|
||||||
|
bd->MemoryManager.realloc = &FreeType_Realloc;
|
||||||
|
|
||||||
|
// https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library
|
||||||
|
FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library);
|
||||||
|
if (error != 0)
|
||||||
|
{
|
||||||
|
IM_DELETE(bd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator.
|
||||||
|
FT_Add_Default_Modules(bd->Library);
|
||||||
|
|
||||||
|
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
// Install svg hooks for FreeType
|
||||||
|
// https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks
|
||||||
|
// https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts
|
||||||
|
SVG_RendererHooks hooks = { ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot };
|
||||||
|
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", &hooks);
|
||||||
|
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||||
|
// With plutosvg, use provided hooks
|
||||||
|
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks());
|
||||||
|
#endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||||
|
|
||||||
|
// Store our data
|
||||||
|
atlas->FontLoaderData = (void*)bd;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas)
|
||||||
|
{
|
||||||
|
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||||
|
IM_ASSERT(bd != nullptr);
|
||||||
|
FT_Done_Library(bd->Library);
|
||||||
|
IM_DELETE(bd);
|
||||||
|
atlas->FontLoaderData = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
|
||||||
|
{
|
||||||
|
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||||
|
ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData);
|
||||||
|
IM_ASSERT(src->FontLoaderData == nullptr);
|
||||||
|
src->FontLoaderData = bd_font_data;
|
||||||
|
|
||||||
|
if (!bd_font_data->InitFont(bd->Library, src, (ImGuiFreeTypeLoaderFlags)atlas->FontLoaderFlags))
|
||||||
|
{
|
||||||
|
IM_DELETE(bd_font_data);
|
||||||
|
src->FontLoaderData = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGui_ImplFreeType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
|
||||||
|
{
|
||||||
|
IM_UNUSED(atlas);
|
||||||
|
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||||
|
IM_DELETE(bd_font_data);
|
||||||
|
src->FontLoaderData = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||||
|
{
|
||||||
|
IM_UNUSED(atlas);
|
||||||
|
float size = baked->Size;
|
||||||
|
if (src->MergeMode && src->SizePixels != 0.0f)
|
||||||
|
size *= (src->SizePixels / baked->OwnerFont->Sources[0]->SizePixels);
|
||||||
|
size *= src->ExtraSizeScale;
|
||||||
|
|
||||||
|
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||||
|
bd_font_data->BakedLastActivated = baked;
|
||||||
|
|
||||||
|
// We use one FT_Size per (source + baked) combination.
|
||||||
|
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||||
|
IM_ASSERT(bd_baked_data != nullptr);
|
||||||
|
IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData();
|
||||||
|
|
||||||
|
FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize);
|
||||||
|
FT_Activate_Size(bd_baked_data->FtSize);
|
||||||
|
|
||||||
|
// Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height'
|
||||||
|
// is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me.
|
||||||
|
// FT_Set_Pixel_Sizes() doesn't seem to get us the same result."
|
||||||
|
// (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL)
|
||||||
|
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||||
|
FT_Size_RequestRec req;
|
||||||
|
req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM;
|
||||||
|
req.width = 0;
|
||||||
|
req.height = (uint32_t)(size * 64 * rasterizer_density);
|
||||||
|
req.horiResolution = 0;
|
||||||
|
req.vertResolution = 0;
|
||||||
|
FT_Request_Size(bd_font_data->FtFace, &req);
|
||||||
|
|
||||||
|
// Output
|
||||||
|
if (src->MergeMode == false)
|
||||||
|
{
|
||||||
|
// Read metrics
|
||||||
|
FT_Size_Metrics metrics = bd_baked_data->FtSize->metrics;
|
||||||
|
const float scale = 1.0f / (rasterizer_density * src->ExtraSizeScale);
|
||||||
|
baked->Ascent = (float)FT_CEIL(metrics.ascender) * scale; // The pixel extents above the baseline in pixels (typically positive).
|
||||||
|
baked->Descent = (float)FT_CEIL(metrics.descender) * scale; // The extents below the baseline in pixels (typically negative).
|
||||||
|
//LineSpacing = (float)FT_CEIL(metrics.height) * scale; // The baseline-to-baseline distance. Note that it usually is larger than the sum of the ascender and descender taken as absolute values. There is also no guarantee that no glyphs extend above or below subsequent baselines when using this distance. Think of it as a value the designer of the font finds appropriate.
|
||||||
|
//LineGap = (float)FT_CEIL(metrics.height - metrics.ascender + metrics.descender) * scale; // The spacing in pixels between one row's descent and the next row's ascent.
|
||||||
|
//MaxAdvanceWidth = (float)FT_CEIL(metrics.max_advance) * scale; // This field gives the maximum horizontal cursor advance for all glyphs in the font.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||||
|
{
|
||||||
|
IM_UNUSED(atlas);
|
||||||
|
IM_UNUSED(baked);
|
||||||
|
IM_UNUSED(src);
|
||||||
|
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||||
|
IM_ASSERT(bd_baked_data != nullptr);
|
||||||
|
FT_Done_Size(bd_baked_data->FtSize);
|
||||||
|
bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE()
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
|
||||||
|
{
|
||||||
|
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||||
|
uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||||
|
if (glyph_index == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (bd_font_data->BakedLastActivated != baked) // <-- could use id
|
||||||
|
{
|
||||||
|
// Activate current size
|
||||||
|
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||||
|
FT_Activate_Size(bd_baked_data->FtSize);
|
||||||
|
bd_font_data->BakedLastActivated = baked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FT_Glyph_Metrics* metrics = ImGui_ImplFreeType_LoadGlyph(bd_font_data, codepoint);
|
||||||
|
if (metrics == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
FT_Face face = bd_font_data->FtFace;
|
||||||
|
FT_GlyphSlot slot = face->glyph;
|
||||||
|
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||||
|
|
||||||
|
// Load metrics only mode
|
||||||
|
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
|
||||||
|
if (out_advance_x != NULL)
|
||||||
|
{
|
||||||
|
IM_ASSERT(out_glyph == NULL);
|
||||||
|
*out_advance_x = advance_x;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render glyph into a bitmap (currently held by FreeType)
|
||||||
|
FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL;
|
||||||
|
FT_Error error = FT_Render_Glyph(slot, render_mode);
|
||||||
|
const FT_Bitmap* ft_bitmap = &slot->bitmap;
|
||||||
|
if (error != 0 || ft_bitmap == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const int w = (int)ft_bitmap->width;
|
||||||
|
const int h = (int)ft_bitmap->rows;
|
||||||
|
const bool is_visible = (w != 0 && h != 0);
|
||||||
|
|
||||||
|
// Prepare glyph
|
||||||
|
out_glyph->Codepoint = codepoint;
|
||||||
|
out_glyph->AdvanceX = advance_x;
|
||||||
|
|
||||||
|
// Pack and retrieve position inside texture atlas
|
||||||
|
if (is_visible)
|
||||||
|
{
|
||||||
|
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
|
||||||
|
if (pack_id == ImFontAtlasRectId_Invalid)
|
||||||
|
{
|
||||||
|
// Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
|
||||||
|
IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
|
||||||
|
|
||||||
|
// Render pixels to our temporary buffer
|
||||||
|
atlas->Builder->TempBuffer.resize(w * h * 4);
|
||||||
|
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
|
||||||
|
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
|
||||||
|
|
||||||
|
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
|
||||||
|
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
|
||||||
|
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); // Snap scaled offset.
|
||||||
|
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
|
||||||
|
float recip_h = 1.0f / rasterizer_density;
|
||||||
|
float recip_v = 1.0f / rasterizer_density;
|
||||||
|
|
||||||
|
// Register glyph
|
||||||
|
float glyph_off_x = (float)face->glyph->bitmap_left;
|
||||||
|
float glyph_off_y = (float)-face->glyph->bitmap_top;
|
||||||
|
out_glyph->X0 = glyph_off_x * recip_h + font_off_x;
|
||||||
|
out_glyph->Y0 = glyph_off_y * recip_v + font_off_y;
|
||||||
|
out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x;
|
||||||
|
out_glyph->Y1 = (glyph_off_y + h) * recip_v + font_off_y;
|
||||||
|
out_glyph->Visible = true;
|
||||||
|
out_glyph->Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
|
||||||
|
out_glyph->PackId = pack_id;
|
||||||
|
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
|
||||||
|
{
|
||||||
|
IM_UNUSED(atlas);
|
||||||
|
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||||
|
int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||||
|
return glyph_index != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImFontLoader* ImGuiFreeType::GetFontLoader()
|
||||||
|
{
|
||||||
|
static ImFontLoader loader;
|
||||||
|
loader.Name = "FreeType";
|
||||||
|
loader.LoaderInit = ImGui_ImplFreeType_LoaderInit;
|
||||||
|
loader.LoaderShutdown = ImGui_ImplFreeType_LoaderShutdown;
|
||||||
|
loader.FontSrcInit = ImGui_ImplFreeType_FontSrcInit;
|
||||||
|
loader.FontSrcDestroy = ImGui_ImplFreeType_FontSrcDestroy;
|
||||||
|
loader.FontSrcContainsGlyph = ImGui_ImplFreetype_FontSrcContainsGlyph;
|
||||||
|
loader.FontBakedInit = ImGui_ImplFreeType_FontBakedInit;
|
||||||
|
loader.FontBakedDestroy = ImGui_ImplFreeType_FontBakedDestroy;
|
||||||
|
loader.FontBakedLoadGlyph = ImGui_ImplFreeType_FontBakedLoadGlyph;
|
||||||
|
loader.FontBakedSrcLoaderDataSize = sizeof(ImGui_ImplFreeType_FontSrcBakedData);
|
||||||
|
return &loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data)
|
||||||
|
{
|
||||||
|
GImGuiFreeTypeAllocFunc = alloc_func;
|
||||||
|
GImGuiFreeTypeFreeFunc = free_func;
|
||||||
|
GImGuiFreeTypeAllocatorUserData = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImGuiFreeType::DebugEditFontLoaderFlags(unsigned int* p_font_loader_flags)
|
||||||
|
{
|
||||||
|
bool edited = false;
|
||||||
|
edited |= ImGui::CheckboxFlags("NoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoHinting);
|
||||||
|
edited |= ImGui::CheckboxFlags("NoAutoHint", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoAutoHint);
|
||||||
|
edited |= ImGui::CheckboxFlags("ForceAutoHint",p_font_loader_flags, ImGuiFreeTypeLoaderFlags_ForceAutoHint);
|
||||||
|
edited |= ImGui::CheckboxFlags("LightHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LightHinting);
|
||||||
|
edited |= ImGui::CheckboxFlags("MonoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_MonoHinting);
|
||||||
|
edited |= ImGui::CheckboxFlags("Bold", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bold);
|
||||||
|
edited |= ImGui::CheckboxFlags("Oblique", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Oblique);
|
||||||
|
edited |= ImGui::CheckboxFlags("Monochrome", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Monochrome);
|
||||||
|
edited |= ImGui::CheckboxFlags("LoadColor", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LoadColor);
|
||||||
|
edited |= ImGui::CheckboxFlags("Bitmap", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bitmap);
|
||||||
|
return edited;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c
|
||||||
|
// The original code from the demo is licensed under CeCILL-C Free Software License Agreement (https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT)
|
||||||
|
struct LunasvgPortState
|
||||||
|
{
|
||||||
|
FT_Error err = FT_Err_Ok;
|
||||||
|
lunasvg::Matrix matrix;
|
||||||
|
std::unique_ptr<lunasvg::Document> svg = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state)
|
||||||
|
{
|
||||||
|
*_state = IM_NEW(LunasvgPortState)();
|
||||||
|
return FT_Err_Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGuiLunasvgPortFree(FT_Pointer* _state)
|
||||||
|
{
|
||||||
|
IM_DELETE(*(LunasvgPortState**)_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state)
|
||||||
|
{
|
||||||
|
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||||
|
|
||||||
|
// If there was an error while loading the svg in ImGuiLunasvgPortPresetSlot(), the renderer hook still get called, so just returns the error.
|
||||||
|
if (state->err != FT_Err_Ok)
|
||||||
|
return state->err;
|
||||||
|
|
||||||
|
// rows is height, pitch (or stride) equals to width * sizeof(int32)
|
||||||
|
lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch);
|
||||||
|
#if LUNASVG_VERSION_MAJOR >= 3
|
||||||
|
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||||
|
#else
|
||||||
|
state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value
|
||||||
|
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||||
|
#endif
|
||||||
|
state->err = FT_Err_Ok;
|
||||||
|
return state->err;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state)
|
||||||
|
{
|
||||||
|
FT_SVG_Document document = (FT_SVG_Document)slot->other;
|
||||||
|
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||||
|
FT_Size_Metrics& metrics = document->metrics;
|
||||||
|
|
||||||
|
// This function is called twice, once in the FT_Load_Glyph() and another right before ImGuiLunasvgPortRender().
|
||||||
|
// If it's the latter, don't do anything because it's // already done in the former.
|
||||||
|
if (cache)
|
||||||
|
return state->err;
|
||||||
|
|
||||||
|
state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length);
|
||||||
|
if (state->svg == nullptr)
|
||||||
|
{
|
||||||
|
state->err = FT_Err_Invalid_SVG_Document;
|
||||||
|
return state->err;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if LUNASVG_VERSION_MAJOR >= 3
|
||||||
|
lunasvg::Box box = state->svg->boundingBox();
|
||||||
|
#else
|
||||||
|
lunasvg::Box box = state->svg->box();
|
||||||
|
#endif
|
||||||
|
double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h);
|
||||||
|
double xx = (double)document->transform.xx / (1 << 16);
|
||||||
|
double xy = -(double)document->transform.xy / (1 << 16);
|
||||||
|
double yx = -(double)document->transform.yx / (1 << 16);
|
||||||
|
double yy = (double)document->transform.yy / (1 << 16);
|
||||||
|
double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem;
|
||||||
|
double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem;
|
||||||
|
|
||||||
|
#if LUNASVG_VERSION_MAJOR >= 3
|
||||||
|
// Scale, transform and pre-translate the matrix for the rendering step
|
||||||
|
state->matrix = lunasvg::Matrix::translated(-box.x, -box.y);
|
||||||
|
state->matrix.multiply(lunasvg::Matrix(xx, xy, yx, yy, x0, y0));
|
||||||
|
state->matrix.scale(scale, scale);
|
||||||
|
|
||||||
|
// Apply updated transformation to the bounding box
|
||||||
|
box.transform(state->matrix);
|
||||||
|
#else
|
||||||
|
// Scale and transform, we don't translate the svg yet
|
||||||
|
state->matrix.identity();
|
||||||
|
state->matrix.scale(scale, scale);
|
||||||
|
state->matrix.transform(xx, xy, yx, yy, x0, y0);
|
||||||
|
state->svg->setMatrix(state->matrix);
|
||||||
|
|
||||||
|
// Pre-translate the matrix for the rendering step
|
||||||
|
state->matrix.translate(-box.x, -box.y);
|
||||||
|
|
||||||
|
// Get the box again after the transformation
|
||||||
|
box = state->svg->box();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Calculate the bitmap size
|
||||||
|
slot->bitmap_left = FT_Int(box.x);
|
||||||
|
slot->bitmap_top = FT_Int(-box.y);
|
||||||
|
slot->bitmap.rows = (unsigned int)(ImCeil((float)box.h));
|
||||||
|
slot->bitmap.width = (unsigned int)(ImCeil((float)box.w));
|
||||||
|
slot->bitmap.pitch = slot->bitmap.width * 4;
|
||||||
|
slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA;
|
||||||
|
|
||||||
|
// Compute all the bearings and set them correctly. The outline is scaled already, we just need to use the bounding box.
|
||||||
|
double metrics_width = box.w;
|
||||||
|
double metrics_height = box.h;
|
||||||
|
double horiBearingX = box.x;
|
||||||
|
double horiBearingY = -box.y;
|
||||||
|
double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0;
|
||||||
|
double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0;
|
||||||
|
slot->metrics.width = FT_Pos(IM_ROUND(metrics_width * 64.0)); // Using IM_ROUND() assume width and height are positive
|
||||||
|
slot->metrics.height = FT_Pos(IM_ROUND(metrics_height * 64.0));
|
||||||
|
slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64);
|
||||||
|
slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64);
|
||||||
|
slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64);
|
||||||
|
slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64);
|
||||||
|
|
||||||
|
if (slot->metrics.vertAdvance == 0)
|
||||||
|
slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0);
|
||||||
|
|
||||||
|
state->err = FT_Err_Ok;
|
||||||
|
return state->err;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // #ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#pragma warning (pop)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // #ifndef IMGUI_DISABLE
|
||||||
83
libs/imgui/misc/freetype/imgui_freetype.h
Normal file
83
libs/imgui/misc/freetype/imgui_freetype.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||||
|
// (headers)
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "imgui.h" // IMGUI_API
|
||||||
|
#ifndef IMGUI_DISABLE
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
// - Add '#define IMGUI_ENABLE_FREETYPE' in your imconfig to automatically enable support
|
||||||
|
// for imgui_freetype in imgui. It is equivalent to selecting the default loader with:
|
||||||
|
// io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())
|
||||||
|
|
||||||
|
// Optional support for OpenType SVG fonts:
|
||||||
|
// - Add '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG' to use plutosvg (not provided). See #7927.
|
||||||
|
// - Add '#define IMGUI_ENABLE_FREETYPE_LUNASVG' to use lunasvg (not provided). See #6591.
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
struct ImFontAtlas;
|
||||||
|
struct ImFontLoader;
|
||||||
|
|
||||||
|
// Hinting greatly impacts visuals (and glyph sizes).
|
||||||
|
// - By default, hinting is enabled and the font's native hinter is preferred over the auto-hinter.
|
||||||
|
// - When disabled, FreeType generates blurrier glyphs, more or less matches the stb_truetype.h
|
||||||
|
// - The Default hinting mode usually looks good, but may distort glyphs in an unusual way.
|
||||||
|
// - The Light hinting mode generates fuzzier glyphs but better matches Microsoft's rasterizer.
|
||||||
|
// You can set those flags globally in ImFontAtlas::FontLoaderFlags
|
||||||
|
// You can set those flags on a per font basis in ImFontConfig::FontLoaderFlags
|
||||||
|
typedef unsigned int ImGuiFreeTypeLoaderFlags;
|
||||||
|
enum ImGuiFreeTypeLoaderFlags_
|
||||||
|
{
|
||||||
|
ImGuiFreeTypeLoaderFlags_NoHinting = 1 << 0, // Disable hinting. This generally generates 'blurrier' bitmap glyphs when the glyph are rendered in any of the anti-aliased modes.
|
||||||
|
ImGuiFreeTypeLoaderFlags_NoAutoHint = 1 << 1, // Disable auto-hinter.
|
||||||
|
ImGuiFreeTypeLoaderFlags_ForceAutoHint = 1 << 2, // Indicates that the auto-hinter is preferred over the font's native hinter.
|
||||||
|
ImGuiFreeTypeLoaderFlags_LightHinting = 1 << 3, // A lighter hinting algorithm for gray-level modes. Many generated glyphs are fuzzier but better resemble their original shape. This is achieved by snapping glyphs to the pixel grid only vertically (Y-axis), as is done by Microsoft's ClearType and Adobe's proprietary font renderer. This preserves inter-glyph spacing in horizontal text.
|
||||||
|
ImGuiFreeTypeLoaderFlags_MonoHinting = 1 << 4, // Strong hinting algorithm that should only be used for monochrome output.
|
||||||
|
ImGuiFreeTypeLoaderFlags_Bold = 1 << 5, // Styling: Should we artificially embolden the font?
|
||||||
|
ImGuiFreeTypeLoaderFlags_Oblique = 1 << 6, // Styling: Should we slant the font, emulating italic style?
|
||||||
|
ImGuiFreeTypeLoaderFlags_Monochrome = 1 << 7, // Disable anti-aliasing. Combine this with MonoHinting for best results!
|
||||||
|
ImGuiFreeTypeLoaderFlags_LoadColor = 1 << 8, // Enable FreeType color-layered glyphs
|
||||||
|
ImGuiFreeTypeLoaderFlags_Bitmap = 1 << 9, // Enable FreeType bitmap glyphs
|
||||||
|
|
||||||
|
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||||
|
ImGuiFreeTypeBuilderFlags_NoHinting = ImGuiFreeTypeLoaderFlags_NoHinting,
|
||||||
|
ImGuiFreeTypeBuilderFlags_NoAutoHint = ImGuiFreeTypeLoaderFlags_NoAutoHint,
|
||||||
|
ImGuiFreeTypeBuilderFlags_ForceAutoHint = ImGuiFreeTypeLoaderFlags_ForceAutoHint,
|
||||||
|
ImGuiFreeTypeBuilderFlags_LightHinting = ImGuiFreeTypeLoaderFlags_LightHinting,
|
||||||
|
ImGuiFreeTypeBuilderFlags_MonoHinting = ImGuiFreeTypeLoaderFlags_MonoHinting,
|
||||||
|
ImGuiFreeTypeBuilderFlags_Bold = ImGuiFreeTypeLoaderFlags_Bold,
|
||||||
|
ImGuiFreeTypeBuilderFlags_Oblique = ImGuiFreeTypeLoaderFlags_Oblique,
|
||||||
|
ImGuiFreeTypeBuilderFlags_Monochrome = ImGuiFreeTypeLoaderFlags_Monochrome,
|
||||||
|
ImGuiFreeTypeBuilderFlags_LoadColor = ImGuiFreeTypeLoaderFlags_LoadColor,
|
||||||
|
ImGuiFreeTypeBuilderFlags_Bitmap = ImGuiFreeTypeLoaderFlags_Bitmap,
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obsolete names (will be removed)
|
||||||
|
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||||
|
typedef ImGuiFreeTypeLoaderFlags_ ImGuiFreeTypeBuilderFlags_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ImGuiFreeType
|
||||||
|
{
|
||||||
|
// This is automatically assigned when using '#define IMGUI_ENABLE_FREETYPE'.
|
||||||
|
// If you need to dynamically select between multiple builders:
|
||||||
|
// - you can manually assign this builder with 'atlas->SetFontLoader(ImGuiFreeType::GetFontLoader())'
|
||||||
|
// - prefer deep-copying this into your own ImFontLoader instance if you use hot-reloading that messes up static data.
|
||||||
|
IMGUI_API const ImFontLoader* GetFontLoader();
|
||||||
|
|
||||||
|
// Override allocators. By default ImGuiFreeType will use IM_ALLOC()/IM_FREE()
|
||||||
|
// However, as FreeType does lots of allocations we provide a way for the user to redirect it to a separate memory heap if desired.
|
||||||
|
IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = nullptr);
|
||||||
|
|
||||||
|
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
|
||||||
|
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
|
||||||
|
|
||||||
|
// Obsolete names (will be removed)
|
||||||
|
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||||
|
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
|
||||||
|
//static inline bool BuildFontAtlas(ImFontAtlas* atlas, unsigned int flags = 0) { atlas->FontBuilderIO = GetBuilderForFreeType(); atlas->FontLoaderFlags = flags; return atlas->Build(); } // Prefer using '#define IMGUI_ENABLE_FREETYPE'
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // #ifndef IMGUI_DISABLE
|
||||||
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
BIN
res/fonts/NotoEmoji-Subset.ttf
Normal file
BIN
res/fonts/NotoEmoji-Subset.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
res/fonts/TwemojiMozilla-Color.ttf
Normal file
BIN
res/fonts/TwemojiMozilla-Color.ttf
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 |
278
res/lang/de.json
278
res/lang/de.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "ERWEITERT",
|
"advanced": "ERWEITERT",
|
||||||
"advanced_effects": "Erweiterte Effekte...",
|
"advanced_effects": "Erweiterte Effekte...",
|
||||||
"ago": "her",
|
"ago": "her",
|
||||||
|
"alerts_clear": "Meldungsverlauf löschen",
|
||||||
|
"alerts_history_tooltip": "Letzte Meldungen",
|
||||||
|
"alerts_none": "Noch keine Meldungen",
|
||||||
|
"alerts_recent": "LETZTE MELDUNGEN",
|
||||||
"all_filter": "Alle",
|
"all_filter": "Alle",
|
||||||
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
|
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
|
||||||
"amount": "Betrag",
|
"amount": "Betrag",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender hat den Miner blockiert",
|
"av_title": "Windows Defender hat den Miner blockiert",
|
||||||
"available": "Verfügbar",
|
"available": "Verfügbar",
|
||||||
"backup_backing_up": "Sicherung läuft...",
|
"backup_backing_up": "Sicherung läuft...",
|
||||||
|
"backup_col_backup": "SICHERUNG",
|
||||||
|
"backup_col_export": "EXPORTIEREN",
|
||||||
|
"backup_col_import": "IMPORTIEREN & WIEDERHERSTELLEN",
|
||||||
"backup_create": "Sicherung erstellen",
|
"backup_create": "Sicherung erstellen",
|
||||||
"backup_created": "Wallet-Sicherung erstellt",
|
"backup_created": "Wallet-Sicherung erstellt",
|
||||||
"backup_data": "SICHERUNG & DATEN",
|
"backup_data": "SICHERUNG & DATEN",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "Guthaben",
|
"balance": "Guthaben",
|
||||||
"balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...",
|
"balance_history_collecting": "Guthabenverlauf — Daten werden gesammelt...",
|
||||||
"balance_layout": "Guthaben-Layout",
|
"balance_layout": "Guthaben-Layout",
|
||||||
|
"balance_layout_switched": "Layout: %s",
|
||||||
|
"balance_mining_rate": "Schürfe %s",
|
||||||
"balance_shielded_fmt": "Abgeschirmt: %.8f",
|
"balance_shielded_fmt": "Abgeschirmt: %.8f",
|
||||||
|
"balance_syncing_pct": "Synchronisiere %.1f%%",
|
||||||
"balance_transparent_fmt": "Transparent: %.8f",
|
"balance_transparent_fmt": "Transparent: %.8f",
|
||||||
"ban": "Sperren",
|
"ban": "Sperren",
|
||||||
"banned_peers": "Gesperrte Peers",
|
"banned_peers": "Gesperrte Peers",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "Prüfsummen werden überprüft...",
|
"bootstrap_verifying": "Prüfsummen werden überprüft...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat ist geschützt)",
|
"bootstrap_wallet_protected": "(wallet.dat ist geschützt)",
|
||||||
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
|
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
|
||||||
|
"byte_count_fmt": "%zu / %zu Byte",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"change_pass_confirm": "Neue bestätigen:",
|
"change_pass_confirm": "Neue bestätigen:",
|
||||||
"change_pass_current": "Aktuelle Passphrase:",
|
"change_pass_current": "Aktuelle Passphrase:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "Passphrase ändern",
|
"change_pass_title": "Passphrase ändern",
|
||||||
"characters": "Zeichen",
|
"characters": "Zeichen",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"chat_accent_amber": "Bernstein",
|
||||||
|
"chat_accent_blue": "Blau",
|
||||||
|
"chat_accent_green": "Grün",
|
||||||
|
"chat_accent_pink": "Rosa",
|
||||||
|
"chat_accent_purple": "Lila",
|
||||||
|
"chat_accent_theme": "Design",
|
||||||
|
"chat_add_contact": "Kontakt hinzufügen",
|
||||||
|
"chat_awaiting_key": "Warten auf Antwort",
|
||||||
|
"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_cancel": "Abbrechen",
|
||||||
|
"chat_contact_added": "Kontakt hinzugefügt – benenne ihn in Kontakte um",
|
||||||
"chat_contact_request": "kontaktanfrage",
|
"chat_contact_request": "kontaktanfrage",
|
||||||
|
"chat_copy_address_tip": "Zum Kopieren der Adresse klicken",
|
||||||
|
"chat_density_comfortable": "Komfortabel",
|
||||||
|
"chat_density_compact": "Kompakt",
|
||||||
|
"chat_emoji_color": "Farbig",
|
||||||
|
"chat_emoji_mono": "Monochrom",
|
||||||
|
"chat_emoji_search": "Emoji suchen",
|
||||||
"chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.",
|
"chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.",
|
||||||
|
"chat_empty_start": "Starte eine mit \"Neue Unterhaltung\".",
|
||||||
|
"chat_empty_title": "Noch keine Unterhaltungen",
|
||||||
|
"chat_export": "Chat exportieren…",
|
||||||
|
"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",
|
||||||
|
"chat_jump_latest": "Neueste",
|
||||||
|
"chat_len_over": "Nachricht zu lang",
|
||||||
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
|
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
|
||||||
"chat_new_button": "Neue Unterhaltung",
|
"chat_mute": "Stummschalten",
|
||||||
|
"chat_new_button": "Neuer Chat",
|
||||||
"chat_new_message": "Nachricht",
|
"chat_new_message": "Nachricht",
|
||||||
|
"chat_new_message_toast": "Neue verschlüsselte Chat-Nachricht",
|
||||||
"chat_new_send": "Anfrage senden",
|
"chat_new_send": "Anfrage senden",
|
||||||
"chat_new_title": "Neue Unterhaltung",
|
"chat_new_title": "Neuer Chat",
|
||||||
"chat_new_zaddr": "z-Adresse des Empfängers",
|
"chat_new_zaddr": "z-Adresse des Empfängers",
|
||||||
|
"chat_no_matches": "Keine Unterhaltungen entsprechen deiner Suche.",
|
||||||
|
"chat_no_z_contacts": "Noch keine Kontakte mit geschützter Adresse",
|
||||||
|
"chat_opt_bubble_accent": "Blasenfarbe",
|
||||||
|
"chat_opt_bubble_style": "Blasenstil",
|
||||||
|
"chat_opt_density": "Nachrichtendichte",
|
||||||
|
"chat_opt_emoji": "Emoji-Stil",
|
||||||
|
"chat_opt_enter_sends": "Eingabetaste sendet",
|
||||||
|
"chat_opt_font_size": "Textgröße",
|
||||||
|
"chat_opt_global_clock": "Globales Uhrzeitformat",
|
||||||
|
"chat_opt_poll": "Abrufrate",
|
||||||
|
"chat_opt_timestamp": "Zeitstempel",
|
||||||
|
"chat_pick_contact": "Aus Kontakten wählen…",
|
||||||
|
"chat_rename": "Kontakt umbenennen",
|
||||||
|
"chat_rename_hint": "Kontaktname",
|
||||||
|
"chat_renamed": "Kontakt umbenannt",
|
||||||
|
"chat_retry": "Wiederholen",
|
||||||
|
"chat_search": "Unterhaltungen durchsuchen",
|
||||||
|
"chat_sec_appearance": "DARSTELLUNG",
|
||||||
|
"chat_sec_messaging": "NACHRICHTEN",
|
||||||
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",
|
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",
|
||||||
"chat_send": "Senden",
|
"chat_send": "Senden",
|
||||||
"chat_send_failed": "nicht gesendet",
|
"chat_send_failed": "nicht gesendet",
|
||||||
|
"chat_sending": "senden…",
|
||||||
|
"chat_settings_done": "Fertig",
|
||||||
|
"chat_settings_section": "CHAT & KONTAKTE",
|
||||||
|
"chat_settings_tip": "Chat anpassen",
|
||||||
|
"chat_settings_title": "Chat-Einstellungen",
|
||||||
|
"chat_show_hidden": "Ausgeblendete anzeigen",
|
||||||
|
"chat_time_now": "jetzt",
|
||||||
"chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).",
|
"chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).",
|
||||||
"chat_toast_lite_busy": "Es wird bereits gesendet, oder es ist keine Wallet geöffnet.",
|
"chat_toast_lite_busy": "Es wird bereits gesendet, oder es ist keine Wallet geöffnet.",
|
||||||
|
"chat_toast_need_funds": "Ein kleines geschütztes Guthaben ist nötig, um Chats zu senden (zur Deckung der Gebühr).",
|
||||||
"chat_toast_no_zaddr": "Keine z-Adresse verfügbar, um den Chat zu senden.",
|
"chat_toast_no_zaddr": "Keine z-Adresse verfügbar, um den Chat zu senden.",
|
||||||
"chat_toast_not_connected": "Nicht verbunden – Nachricht nicht gesendet.",
|
"chat_toast_not_connected": "Nicht verbunden – Nachricht nicht gesendet.",
|
||||||
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
|
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
|
||||||
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
|
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
|
||||||
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
|
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
|
||||||
|
"chat_today": "Heute",
|
||||||
|
"chat_ts_12h": "12-Stunden",
|
||||||
|
"chat_ts_24h": "24-Stunden",
|
||||||
|
"chat_ts_global": "Global folgen",
|
||||||
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_unhide": "Einblenden",
|
||||||
|
"chat_unmute": "Stummschaltung aufheben",
|
||||||
|
"chat_verify_key": "Identitätsschlüssel – zum Verifizieren vergleichen",
|
||||||
"chat_waiting_reply": "Warte auf die Antwort dieses Kontakts – sobald er antwortet, kannst du ihm schreiben.",
|
"chat_waiting_reply": "Warte auf die Antwort dieses Kontakts – sobald er antwortet, kannst du ihm schreiben.",
|
||||||
|
"chat_yesterday": "Gestern",
|
||||||
"chat_you": "Du",
|
"chat_you": "Du",
|
||||||
"choose_icon": "Symbol wählen",
|
"choose_icon": "Symbol wählen",
|
||||||
"clear": "Leeren",
|
"clear": "Leeren",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "Klicken zum Kopieren der Adresse",
|
"click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||||
"click_copy_uri": "Klicken zum Kopieren der URI",
|
"click_copy_uri": "Klicken zum Kopieren der URI",
|
||||||
"click_to_copy": "Klicken zum Kopieren",
|
"click_to_copy": "Klicken zum Kopieren",
|
||||||
|
"clock_format": "Uhrzeitformat",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"conf_count": "%d Best.",
|
"conf_count": "%d Best.",
|
||||||
"confirm_and_send": "Bestätigen & Senden",
|
"confirm_and_send": "Bestätigen & Senden",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "App",
|
"console_app": "App",
|
||||||
"console_auto_scroll": "Automatisch scrollen",
|
"console_auto_scroll": "Automatisch scrollen",
|
||||||
"console_available_commands": "Verfügbare Befehle:",
|
"console_available_commands": "Verfügbare Befehle:",
|
||||||
|
"console_backend_reference": "Backend-Befehlsreferenz",
|
||||||
|
"console_backend_unavailable": "Kein Backend",
|
||||||
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||||
|
"console_cat_advanced": "Erweitert",
|
||||||
"console_cat_blockchain": "Blockchain",
|
"console_cat_blockchain": "Blockchain",
|
||||||
"console_cat_control": "Steuerung",
|
"console_cat_control": "Steuerung",
|
||||||
|
"console_cat_keys": "Schlüssel & Sicherheit",
|
||||||
"console_cat_mining": "Mining",
|
"console_cat_mining": "Mining",
|
||||||
"console_cat_network": "Netzwerk",
|
"console_cat_network": "Netzwerk",
|
||||||
"console_cat_raw_transactions": "Rohtransaktionen",
|
"console_cat_raw_transactions": "Rohtransaktionen",
|
||||||
|
"console_cat_send": "Senden",
|
||||||
|
"console_cat_sync": "Synchronisierung",
|
||||||
"console_cat_utility": "Dienstprogramme",
|
"console_cat_utility": "Dienstprogramme",
|
||||||
"console_cat_wallet": "Wallet",
|
"console_cat_wallet": "Wallet",
|
||||||
"console_clear": "Leeren",
|
"console_clear": "Leeren",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - Diese Hilfe anzeigen",
|
"console_help_help": " help - Diese Hilfe anzeigen",
|
||||||
"console_help_setgenerate": " setgenerate - Mining steuern",
|
"console_help_setgenerate": " setgenerate - Mining steuern",
|
||||||
"console_help_stop": " stop - Daemon stoppen",
|
"console_help_stop": " stop - Daemon stoppen",
|
||||||
|
"console_last_error": "Letzter Fehler:",
|
||||||
"console_line_count": "%zu Zeilen",
|
"console_line_count": "%zu Zeilen",
|
||||||
"console_matches": "Treffer",
|
"console_matches": "Treffer",
|
||||||
"console_new_lines": "%d neue Zeilen",
|
"console_new_lines": "%d neue Zeilen",
|
||||||
"console_no_daemon": "Kein Daemon",
|
"console_no_daemon": "Kein Daemon",
|
||||||
|
"console_no_output": "(keine Ausgabe)",
|
||||||
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
|
"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_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.",
|
||||||
"console_ref_builds": "Ergibt",
|
"console_ref_builds": "Ergibt",
|
||||||
"console_ref_cancel": "Abbrechen",
|
"console_ref_cancel": "Abbrechen",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
|
"console_ref_run_confirm": "%s jetzt ausführen? Dies ist ein folgenreicher Befehl.",
|
||||||
"console_ref_search_hint": "Nach Name oder Aufgabe suchen…",
|
"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_select_hint": "Wählen Sie einen Befehl, um zu sehen, was er tut.",
|
||||||
|
"console_ref_value": "Wert",
|
||||||
"console_rpc_reference": "RPC-Befehlsreferenz",
|
"console_rpc_reference": "RPC-Befehlsreferenz",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "Konsolen-Scanline",
|
"console_scanline": "Konsolen-Scanline",
|
||||||
"console_search_commands": "Befehle suchen...",
|
"console_search_commands": "Befehle suchen...",
|
||||||
"console_select_all": "Alles auswählen",
|
"console_select_all": "Alles auswählen",
|
||||||
"console_show_app_output": "[App]-Wallet-Protokollzeilen anzeigen",
|
"console_show_app_output": "[App]-Wallet-Protokollzeilen anzeigen",
|
||||||
|
"console_show_backend_ref": "Backend-Befehlsreferenz anzeigen",
|
||||||
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
||||||
"console_show_errors_only": "Nur Fehler anzeigen",
|
"console_show_errors_only": "Nur Fehler anzeigen",
|
||||||
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "Gestoppt",
|
"console_status_stopped": "Gestoppt",
|
||||||
"console_status_stopping": "Stoppt",
|
"console_status_stopping": "Stoppt",
|
||||||
"console_status_unknown": "Unbekannt",
|
"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_tab_completion": "Tab zur Vervollständigung",
|
||||||
"console_text_colors": "Textfarben",
|
"console_text_colors": "Textfarben",
|
||||||
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
|
"console_toggle_accents": "Farbakzente der Zeilen umschalten",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "Ein: Dieser Kontakt bleibt sichtbar, egal welche Wallet Sie laden. Aus: Er gehört nur zur aktuellen Wallet.",
|
"contact_global_tt": "Ein: Dieser Kontakt bleibt sichtbar, egal welche Wallet Sie laden. Aus: Er gehört nur zur aktuellen Wallet.",
|
||||||
"contact_preview_addr": "Adresse erscheint hier",
|
"contact_preview_addr": "Adresse erscheint hier",
|
||||||
"contact_preview_name": "Kontaktname",
|
"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": "Kontakte",
|
||||||
|
"contacts_avatar_shape": "Avatarform",
|
||||||
|
"contacts_list_scale": "Listengröße",
|
||||||
"contacts_search_no_match": "Keine passenden Kontakte",
|
"contacts_search_no_match": "Keine passenden Kontakte",
|
||||||
"contacts_search_placeholder": "Kontakte durchsuchen...",
|
"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!",
|
"copied": "Kopiert!",
|
||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"copy_address": "Vollständige Adresse kopieren",
|
"copy_address": "Vollständige Adresse kopieren",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "Gebündelt",
|
"daemon_bundled": "Gebündelt",
|
||||||
"daemon_install_bundled": "Gebündelten installieren",
|
"daemon_install_bundled": "Gebündelten installieren",
|
||||||
"daemon_installed": "Installiert",
|
"daemon_installed": "Installiert",
|
||||||
|
"daemon_maintenance_label": "WARTUNG",
|
||||||
"daemon_none_bundled": "keiner in diesem Build",
|
"daemon_none_bundled": "keiner in diesem Build",
|
||||||
"daemon_not_installed": "nicht installiert",
|
"daemon_not_installed": "nicht installiert",
|
||||||
"daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.",
|
"daemon_status_differ": "Installierte Binärdatei unterscheidet sich von der gebündelten Version.",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "Neueste:",
|
"daemon_update_latest": "Neueste:",
|
||||||
"daemon_update_loading": "Releases werden geladen…",
|
"daemon_update_loading": "Releases werden geladen…",
|
||||||
"daemon_update_now": "Jetzt aktualisieren",
|
"daemon_update_now": "Jetzt aktualisieren",
|
||||||
|
"daemon_update_prompt_title": "Node-Daemon aktualisieren?",
|
||||||
"daemon_update_reinstall": "Neu installieren",
|
"daemon_update_reinstall": "Neu installieren",
|
||||||
"daemon_update_restart_note": "Starten Sie den Daemon neu, um die neue Version auszuführen.",
|
"daemon_update_restart_note": "Starten Sie den Daemon neu, um die neue Version auszuführen.",
|
||||||
"daemon_update_restart_now": "Daemon jetzt neu starten",
|
"daemon_update_restart_now": "Daemon jetzt neu starten",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "Der Download wird vor der Installation anhand der veröffentlichten SHA-256-Prüfsumme des Releases und einer fest hinterlegten ed25519-Signatur verifiziert.",
|
"daemon_update_verify_note": "Der Download wird vor der Installation anhand der veröffentlichten SHA-256-Prüfsumme des Releases und einer fest hinterlegten ed25519-Signatur verifiziert.",
|
||||||
"daemon_update_verifying": "Wird verifiziert…",
|
"daemon_update_verifying": "Wird verifiziert…",
|
||||||
"daemon_update_version": "Version:",
|
"daemon_update_version": "Version:",
|
||||||
|
"daemon_updates_label": "AKTUALISIERUNGEN",
|
||||||
"daemon_version": "Daemon",
|
"daemon_version": "Daemon",
|
||||||
"dark": "Dunkel",
|
"dark": "Dunkel",
|
||||||
|
"data_stale_prefix": "Aktualisiert",
|
||||||
|
"data_stale_tooltip": "Der Kontostand ist möglicherweise veraltet – die Wallet hat kürzlich keine Aktualisierung erhalten. Überprüfe deine Node-Verbindung.",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"date_label": "Datum:",
|
"date_label": "Datum:",
|
||||||
"debug_logging": "FEHLERPROTOKOLLIERUNG",
|
"debug_logging": "FEHLERPROTOKOLLIERUNG",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "Bootstrap herunterladen",
|
"download_bootstrap": "Bootstrap herunterladen",
|
||||||
"dragonx_green": "DragonX (Grün)",
|
"dragonx_green": "DragonX (Grün)",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
|
"empty_wallet_keys_suffix": "Schlüssel",
|
||||||
|
"empty_wallet_open_manager": "Wallet-Verwaltung öffnen",
|
||||||
|
"empty_wallet_restore": "Mein Wallet wiederherstellen",
|
||||||
|
"empty_wallet_salvage_body": "Dieses Wallet ist leer, weil eine frühere automatische Reparatur Ihr ursprüngliches Wallet als Sicherung beiseitegelegt hat. Ihre Coins befinden sich fast sicher in dieser Sicherung und sind nicht verloren. Stellen Sie sie wieder her, um Ihr Guthaben erneut zu laden — nichts wird gelöscht; die aktuelle Datei wird zuerst beiseitegelegt.",
|
||||||
|
"empty_wallet_salvage_headline": "Ihre Coins sind sicher in einer Sicherungsdatei.",
|
||||||
|
"empty_wallet_salvage_title": "Ihr Wallet wurde möglicherweise repariert",
|
||||||
|
"empty_wallet_warning_body": "Dieses Wallet hat keine Adressen und kein Guthaben, aber eine andere Wallet-Datei in Ihrem DragonX-Ordner enthält Schlüssel. Ihre Coins befinden sich höchstwahrscheinlich dort und sind nicht verloren. Öffnen Sie die Wallet-Verwaltung, um zu dem Wallet mit Ihrem Guthaben zu wechseln.",
|
||||||
|
"empty_wallet_warning_dismiss": "Für dieses Wallet nicht mehr warnen",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "Beendet diese Warnung nur für die aktuelle Wallet-Datei. Wenn Sie später zu einem anderen leeren Wallet wechseln, kann die Warnung erneut erscheinen.",
|
||||||
|
"empty_wallet_warning_headline": "Möglicherweise haben Sie das falsche Wallet geöffnet.",
|
||||||
|
"empty_wallet_warning_title": "Dieses Wallet ist leer",
|
||||||
"enc_confirm": "Bestätigen:",
|
"enc_confirm": "Bestätigen:",
|
||||||
"enc_desc": "Die Verschlüsselung Ihrer Wallet schützt Ihre privaten Schlüssel mit einer Passphrase. Nach der Verschlüsselung wird der Daemon neu gestartet.",
|
"enc_desc": "Die Verschlüsselung Ihrer Wallet schützt Ihre privaten Schlüssel mit einer Passphrase. Nach der Verschlüsselung wird der Daemon neu gestartet.",
|
||||||
"enc_encrypting": "Wallet wird verschlüsselt...",
|
"enc_encrypting": "Wallet wird verschlüsselt...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "Hell",
|
"light": "Hell",
|
||||||
"lite_account_label": "Konto",
|
"lite_account_label": "Konto",
|
||||||
"lite_action": "Aktion",
|
"lite_action": "Aktion",
|
||||||
|
"lite_backend_unavailable": "Lite-Wallet-Backend nicht verfügbar",
|
||||||
"lite_backup_keys": "Sicherung & Schlüssel",
|
"lite_backup_keys": "Sicherung & Schlüssel",
|
||||||
"lite_birthday_backup": "Geburtstag: %llu (auch diesen sichern)",
|
"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_hint": "Blockhöhe, ab der gescannt werden soll. Bei 0 belassen, falls unbekannt (langsamerer vollständiger Scan).",
|
||||||
"lite_birthday_label": "Geburtsblock",
|
"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_console_help_passthrough": "Jede andere Eingabe wird als Lite-Wallet-Konsolenbefehl ausgeführt.",
|
||||||
"lite_copy": "Kopieren",
|
"lite_copy": "Kopieren",
|
||||||
|
"lite_could_not_start": "Der Vorgang konnte nicht gestartet werden",
|
||||||
"lite_could_not_write": "Konnte nicht schreiben ",
|
"lite_could_not_write": "Konnte nicht schreiben ",
|
||||||
"lite_encrypt_wallet": "Wallet verschlüsseln",
|
"lite_encrypt_wallet": "Wallet verschlüsseln",
|
||||||
"lite_encryption_removed": "Verschlüsselung entfernt",
|
"lite_encryption_removed": "Verschlüsselung entfernt",
|
||||||
|
"lite_enter_all_seed_words": "Alle 24 Seed-Wörter zur Wiederherstellung eingeben (%d erhalten)",
|
||||||
|
"lite_enter_wallet_path": "Wallet-Pfad eingeben",
|
||||||
"lite_hide_wipe": "Ausblenden & löschen",
|
"lite_hide_wipe": "Ausblenden & löschen",
|
||||||
"lite_import": "Importieren",
|
"lite_import": "Importieren",
|
||||||
"lite_import_key_label": "Schlüssel importieren",
|
"lite_import_key_label": "Schlüssel importieren",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://ihr-lite-server",
|
"lite_net_add_url_hint": "https://ihr-lite-server",
|
||||||
"lite_net_checking": "wird geprüft…",
|
"lite_net_checking": "wird geprüft…",
|
||||||
"lite_net_connected": "Verbunden",
|
"lite_net_connected": "Verbunden",
|
||||||
|
"lite_net_connecting": "Verbinde…",
|
||||||
"lite_net_custom": "Benutzerdefiniert",
|
"lite_net_custom": "Benutzerdefiniert",
|
||||||
"lite_net_disconnected": "Nicht verbunden",
|
"lite_net_disconnected": "Nicht verbunden",
|
||||||
"lite_net_hidden_section": "Ausgeblendete Server",
|
"lite_net_hidden_section": "Ausgeblendete Server",
|
||||||
@@ -638,6 +765,9 @@
|
|||||||
"lite_working": "In Arbeit…",
|
"lite_working": "In Arbeit…",
|
||||||
"loading": "Laden...",
|
"loading": "Laden...",
|
||||||
"loading_addresses": "Adressen werden geladen...",
|
"loading_addresses": "Adressen werden geladen...",
|
||||||
|
"loading_stall_body": "Der Daemon initialisiert seit %.0f s. Das kann nach einem Update oder beim ersten Start normal sein (Laden des Blockindex oder erneutes Scannen) – die Verbindung wird automatisch hergestellt, sobald er bereit ist.",
|
||||||
|
"loading_stall_hint": "Hängt es noch? Öffne die Einstellungen und nutze „Daemon neu starten“ oder sieh in der Konsole nach Details.",
|
||||||
|
"loading_stall_title": "Dauert länger als erwartet",
|
||||||
"loading_transactions": "Transaktionen werden geladen",
|
"loading_transactions": "Transaktionen werden geladen",
|
||||||
"local_hashrate": "Lokale Hashrate",
|
"local_hashrate": "Lokale Hashrate",
|
||||||
"low_spec_mode": "Energiesparmodus",
|
"low_spec_mode": "Energiesparmodus",
|
||||||
@@ -654,6 +784,9 @@
|
|||||||
"market_cap": "Marktkapitalisierung",
|
"market_cap": "Marktkapitalisierung",
|
||||||
"market_cap_short": "Kap.",
|
"market_cap_short": "Kap.",
|
||||||
"market_chart_loading": "Preisverlauf wird geladen",
|
"market_chart_loading": "Preisverlauf wird geladen",
|
||||||
|
"market_col_name": "Name",
|
||||||
|
"market_col_trend": "Trend",
|
||||||
|
"market_col_value": "Wert",
|
||||||
"market_iv_1d": "1T",
|
"market_iv_1d": "1T",
|
||||||
"market_iv_1h": "1S",
|
"market_iv_1h": "1S",
|
||||||
"market_iv_1m": "1M",
|
"market_iv_1m": "1M",
|
||||||
@@ -662,13 +795,18 @@
|
|||||||
"market_no_history": "Kein Preisverlauf verfügbar",
|
"market_no_history": "Kein Preisverlauf verfügbar",
|
||||||
"market_no_price": "Keine Preisdaten",
|
"market_no_price": "Keine Preisdaten",
|
||||||
"market_now": "Jetzt",
|
"market_now": "Jetzt",
|
||||||
|
"market_opt_chart_style": "Diagrammstil",
|
||||||
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
||||||
"market_portfolio": "PORTFOLIO",
|
"market_portfolio": "PORTFOLIO",
|
||||||
"market_price_loading": "Preisdaten werden geladen...",
|
"market_price_loading": "Preisdaten werden geladen...",
|
||||||
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||||
"market_refresh_price": "Preisdaten aktualisieren",
|
"market_refresh_price": "Preisdaten aktualisieren",
|
||||||
|
"market_settings_tip": "Marktoptionen",
|
||||||
|
"market_settings_title": "Markteinstellungen",
|
||||||
"market_style_candle": "Zu Kerzenchart wechseln",
|
"market_style_candle": "Zu Kerzenchart wechseln",
|
||||||
|
"market_style_candle_label": "Kerzen",
|
||||||
"market_style_line": "Zum Liniendiagramm wechseln",
|
"market_style_line": "Zum Liniendiagramm wechseln",
|
||||||
|
"market_style_line_label": "Linie",
|
||||||
"market_trade_on": "Handeln auf %s",
|
"market_trade_on": "Handeln auf %s",
|
||||||
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
|
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
|
||||||
"market_vol_short": "Vol.",
|
"market_vol_short": "Vol.",
|
||||||
@@ -762,6 +900,7 @@
|
|||||||
"mining_difficulty_copied": "Schwierigkeit kopiert",
|
"mining_difficulty_copied": "Schwierigkeit kopiert",
|
||||||
"mining_est_block": "Gesch. Block",
|
"mining_est_block": "Gesch. Block",
|
||||||
"mining_est_daily": "Gesch. täglich",
|
"mining_est_daily": "Gesch. täglich",
|
||||||
|
"mining_est_daily_pool_sub": "grobe Solo-Äquivalenz, vor Pool-Gebühr",
|
||||||
"mining_filter_all": "Alle",
|
"mining_filter_all": "Alle",
|
||||||
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
|
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
|
||||||
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
|
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
|
||||||
@@ -790,10 +929,12 @@
|
|||||||
"mining_open_in_explorer": "Im Explorer öffnen",
|
"mining_open_in_explorer": "Im Explorer öffnen",
|
||||||
"mining_payout_address": "Auszahlungsadresse",
|
"mining_payout_address": "Auszahlungsadresse",
|
||||||
"mining_payout_foreign": "⚠ Diese Auszahlungsadresse befindet sich nicht in Ihrer aktuellen Wallet — geschürfte Belohnungen würden an eine andere Wallet gehen. Aktualisieren Sie sie, wenn Sie die Wallet gewechselt haben.",
|
"mining_payout_foreign": "⚠ Diese Auszahlungsadresse befindet sich nicht in Ihrer aktuellen Wallet — geschürfte Belohnungen würden an eine andere Wallet gehen. Aktualisieren Sie sie, wenn Sie die Wallet gewechselt haben.",
|
||||||
|
"mining_payout_invalid": "Keine gültige DragonX-Adresse — vor dem Start korrigieren, sonst gehen die Mining-Belohnungen verloren.",
|
||||||
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
|
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
|
||||||
"mining_pool": "Pool",
|
"mining_pool": "Pool",
|
||||||
"mining_pool_fee": "Gebühr",
|
"mining_pool_fee": "Gebühr",
|
||||||
"mining_pool_hashrate": "Pool-Hashrate",
|
"mining_pool_hashrate": "Pool-Hashrate",
|
||||||
|
"mining_pool_needs_payout_tooltip": "Zuerst eine Auszahlungsadresse eingeben (Z-Adresse erzeugen)",
|
||||||
"mining_pool_url": "Pool-URL",
|
"mining_pool_url": "Pool-URL",
|
||||||
"mining_pools_header": "POOLS",
|
"mining_pools_header": "POOLS",
|
||||||
"mining_recent_blocks": "LETZTE BLÖCKE",
|
"mining_recent_blocks": "LETZTE BLÖCKE",
|
||||||
@@ -823,6 +964,9 @@
|
|||||||
"mining_syncing_tooltip": "Blockchain synchronisiert...",
|
"mining_syncing_tooltip": "Blockchain synchronisiert...",
|
||||||
"mining_tag": " · Mining",
|
"mining_tag": " · Mining",
|
||||||
"mining_threads": "Mining-Threads",
|
"mining_threads": "Mining-Threads",
|
||||||
|
"mining_threads_input_tooltip": "Genaue Thread-Anzahl eingeben (Enter zum Übernehmen)",
|
||||||
|
"mining_threads_minus_tooltip": "Weniger Threads",
|
||||||
|
"mining_threads_plus_tooltip": "Mehr Threads",
|
||||||
"mining_to_save": "zum Speichern",
|
"mining_to_save": "zum Speichern",
|
||||||
"mining_today": "Heute",
|
"mining_today": "Heute",
|
||||||
"mining_uptime": "Laufzeit",
|
"mining_uptime": "Laufzeit",
|
||||||
@@ -849,6 +993,11 @@
|
|||||||
"no_transactions": "Keine Transaktionen gefunden",
|
"no_transactions": "Keine Transaktionen gefunden",
|
||||||
"no_transactions_yet": "Noch keine Transaktionen",
|
"no_transactions_yet": "Noch keine Transaktionen",
|
||||||
"node": "KNOTEN",
|
"node": "KNOTEN",
|
||||||
|
"node_banner_crashed_title": "Der Node wurde unerwartet beendet",
|
||||||
|
"node_banner_lite_open_failed": "Wallet konnte nicht geöffnet werden",
|
||||||
|
"node_banner_offline_title": "Nicht mit dem DragonX-Node verbunden",
|
||||||
|
"node_banner_reconnect": "Erneut verbinden",
|
||||||
|
"node_banner_restart": "Node neu starten",
|
||||||
"node_security": "KNOTEN & SICHERHEIT",
|
"node_security": "KNOTEN & SICHERHEIT",
|
||||||
"noise": "Rauschen",
|
"noise": "Rauschen",
|
||||||
"not_connected": "Nicht mit Daemon verbunden...",
|
"not_connected": "Nicht mit Daemon verbunden...",
|
||||||
@@ -972,11 +1121,12 @@
|
|||||||
"portfolio_spark_min": "Minute",
|
"portfolio_spark_min": "Minute",
|
||||||
"portfolio_spark_month": "Monat",
|
"portfolio_spark_month": "Monat",
|
||||||
"portfolio_spark_week": "Woche",
|
"portfolio_spark_week": "Woche",
|
||||||
"portfolio_style_compact": "Kompakte Zeilen",
|
"portfolio_style_compact": "Tabelle",
|
||||||
"portfolio_style_detailed": "Detaillierte Zeilen",
|
"portfolio_style_detailed": "Karten",
|
||||||
"portfolio_style_featured": "Hervorgehobene Zeilen",
|
"portfolio_style_featured": "Hervorgehoben",
|
||||||
"portfolio_style_label": "Portfolio-Stil",
|
"portfolio_style_label": "Portfolio-Stil",
|
||||||
"portfolio_untitled": "Ohne Titel",
|
"portfolio_untitled": "Ohne Titel",
|
||||||
|
"portfolio_wallet_loading": "Warte, bis die Wallet fertig geladen ist, um eine Gruppe hinzuzufügen.",
|
||||||
"price_chart": "Preisdiagramm",
|
"price_chart": "Preisdiagramm",
|
||||||
"privacy_great": "Großartige Privatsphäre!",
|
"privacy_great": "Großartige Privatsphäre!",
|
||||||
"privacy_low": "Geringe Privatsphäre — Gelder abschirmen",
|
"privacy_low": "Geringe Privatsphäre — Gelder abschirmen",
|
||||||
@@ -986,6 +1136,8 @@
|
|||||||
"qr_failed": "QR-Code konnte nicht generiert werden",
|
"qr_failed": "QR-Code konnte nicht generiert werden",
|
||||||
"qr_title": "QR-Code",
|
"qr_title": "QR-Code",
|
||||||
"qr_unavailable": "QR nicht verfügbar",
|
"qr_unavailable": "QR nicht verfügbar",
|
||||||
|
"quick_receive": "Schnell empfangen",
|
||||||
|
"quick_send": "Schnell senden",
|
||||||
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
||||||
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
||||||
"ram_system_gb": "System: %.1f / %.0f GB",
|
"ram_system_gb": "System: %.1f / %.0f GB",
|
||||||
@@ -1035,6 +1187,7 @@
|
|||||||
"rpc_connection": "RPC-Verbindung...",
|
"rpc_connection": "RPC-Verbindung...",
|
||||||
"rpc_host": "RPC-Host",
|
"rpc_host": "RPC-Host",
|
||||||
"rpc_pass": "Passwort",
|
"rpc_pass": "Passwort",
|
||||||
|
"rpc_plaintext_remote_warning": "Die Remote-RPC-Verbindung verwendet unverschlüsseltes HTTP. Füge rpctls=1 zur DRAGONX.conf hinzu, falls dein Daemon TLS unterstützt.",
|
||||||
"rpc_port": "Port",
|
"rpc_port": "Port",
|
||||||
"rpc_user": "Benutzername",
|
"rpc_user": "Benutzername",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
@@ -1049,6 +1202,8 @@
|
|||||||
"sb_connecting_external": "Verbindung zu externem Daemon...",
|
"sb_connecting_external": "Verbindung zu externem Daemon...",
|
||||||
"sb_connecting_generic": "Verbindung zum Daemon...",
|
"sb_connecting_generic": "Verbindung zum Daemon...",
|
||||||
"sb_daemon_crashed": "Daemon ist %d mal abgestürzt",
|
"sb_daemon_crashed": "Daemon ist %d mal abgestürzt",
|
||||||
|
"sb_daemon_extract_failed": "Daemon-Dateien konnten nicht geschrieben werden – prüfe freien Speicherplatz und Berechtigungen.",
|
||||||
|
"sb_daemon_files_failed": "Daemon-Dateien konnten nicht nach %s geschrieben werden – prüfe freien Speicherplatz und Berechtigungen.",
|
||||||
"sb_daemon_not_found": "Daemon nicht gefunden",
|
"sb_daemon_not_found": "Daemon nicht gefunden",
|
||||||
"sb_daemon_start_failed": "dragonxd konnte nicht gestartet werden",
|
"sb_daemon_start_failed": "dragonxd konnte nicht gestartet werden",
|
||||||
"sb_dragonxd_running": "dragonxd läuft",
|
"sb_dragonxd_running": "dragonxd läuft",
|
||||||
@@ -1064,6 +1219,7 @@
|
|||||||
"sb_net_mhs": "Netz: %.2f MH/s",
|
"sb_net_mhs": "Netz: %.2f MH/s",
|
||||||
"sb_no_conf": "DRAGONX.conf nicht gefunden",
|
"sb_no_conf": "DRAGONX.conf nicht gefunden",
|
||||||
"sb_peers": "Peers: %zu",
|
"sb_peers": "Peers: %zu",
|
||||||
|
"sb_plaintext_remote_blocked": "RPC-Anmeldedaten werden nicht im Klartext an einen entfernten Host gesendet. Füge rpcallowplaintext=1 zu DRAGONX.conf hinzu, um dies zu erlauben, oder aktiviere TLS mit rpctls=1.",
|
||||||
"sb_rescanning": "Neuscan",
|
"sb_rescanning": "Neuscan",
|
||||||
"sb_rescanning_pct": "Neuscan %.0f%%",
|
"sb_rescanning_pct": "Neuscan %.0f%%",
|
||||||
"sb_restarting_daemon": "Daemon wird neu gestartet...",
|
"sb_restarting_daemon": "Daemon wird neu gestartet...",
|
||||||
@@ -1077,6 +1233,7 @@
|
|||||||
"sb_waiting_daemon_err": "Warten auf dragonxd — %s",
|
"sb_waiting_daemon_err": "Warten auf dragonxd — %s",
|
||||||
"sb_warming_up": "Aufwärmen...",
|
"sb_warming_up": "Aufwärmen...",
|
||||||
"sb_witness_cache": "Zeugen werden neu aufgebaut",
|
"sb_witness_cache": "Zeugen werden neu aufgebaut",
|
||||||
|
"scale_effects": "SKALIERUNG & EFFEKTE",
|
||||||
"screenshot_open_dir": "Speicherort öffnen",
|
"screenshot_open_dir": "Speicherort öffnen",
|
||||||
"screenshot_sweep": "Screenshot-Durchlauf ausführen",
|
"screenshot_sweep": "Screenshot-Durchlauf ausführen",
|
||||||
"screenshot_sweep_desc": "Durchläuft jedes Design über jeden Tab und speichert von jedem einen Screenshot in tab-spezifischen Unterordnern im Screenshots-Ordner des Konfigurationsverzeichnisses (überschreibt den vorherigen Durchlauf). Läuft einige Sekunden.",
|
"screenshot_sweep_desc": "Durchläuft jedes Design über jeden Tab und speichert von jedem einen Screenshot in tab-spezifischen Unterordnern im Screenshots-Ordner des Konfigurationsverzeichnisses (überschreibt den vorherigen Durchlauf). Läuft einige Sekunden.",
|
||||||
@@ -1141,6 +1298,7 @@
|
|||||||
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
|
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
|
||||||
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
|
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
|
||||||
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
|
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
|
||||||
|
"send_tooltip_view_only": "Nur-Lese-Adresse — kein Spending Key, Senden nicht möglich",
|
||||||
"send_total": "Gesamt",
|
"send_total": "Gesamt",
|
||||||
"send_transaction": "Transaktion senden",
|
"send_transaction": "Transaktion senden",
|
||||||
"send_tx_failed": "Transaktion fehlgeschlagen",
|
"send_tx_failed": "Transaktion fehlgeschlagen",
|
||||||
@@ -1160,16 +1318,16 @@
|
|||||||
"sent_filter": "Gesendet",
|
"sent_filter": "Gesendet",
|
||||||
"sent_type": "Gesendet",
|
"sent_type": "Gesendet",
|
||||||
"sent_upper": "GESENDET",
|
"sent_upper": "GESENDET",
|
||||||
"set_label": "Label setzen...",
|
"set_label": "Label setzen",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.",
|
"settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.",
|
||||||
"settings_acrylic_level": "Acrylstufe:",
|
"settings_acrylic_level": "Acrylstufe:",
|
||||||
"settings_address_book": "Adressbuch...",
|
"settings_address_book": "Adressbuch…",
|
||||||
"settings_auto_detected": "Automatisch erkannt aus DRAGONX.conf",
|
"settings_auto_detected": "Automatisch erkannt aus DRAGONX.conf",
|
||||||
"settings_auto_lock": "AUTO-SPERRE",
|
"settings_auto_lock": "AUTO-SPERRE",
|
||||||
"settings_auto_shield_desc": "Transparente Guthaben automatisch an geschirmte Adressen verschieben",
|
"settings_auto_shield_desc": "Transparente Guthaben automatisch an geschirmte Adressen verschieben",
|
||||||
"settings_auto_shield_funds": "Transparente Guthaben automatisch abschirmen",
|
"settings_auto_shield_funds": "Transparente Guthaben automatisch abschirmen",
|
||||||
"settings_backup": "Sicherung...",
|
"settings_backup": "Sicherung…",
|
||||||
"settings_block_explorer_urls": "Block-Explorer-URLs",
|
"settings_block_explorer_urls": "Block-Explorer-URLs",
|
||||||
"settings_builtin": "Integriert",
|
"settings_builtin": "Integriert",
|
||||||
"settings_change_passphrase": "Passphrase ändern",
|
"settings_change_passphrase": "Passphrase ändern",
|
||||||
@@ -1180,60 +1338,71 @@
|
|||||||
"settings_configure_explorer": "Externe Block-Explorer-Links konfigurieren",
|
"settings_configure_explorer": "Externe Block-Explorer-Links konfigurieren",
|
||||||
"settings_configure_rpc": "Verbindung zum dragonxd-Daemon konfigurieren",
|
"settings_configure_rpc": "Verbindung zum dragonxd-Daemon konfigurieren",
|
||||||
"settings_connection": "Verbindung",
|
"settings_connection": "Verbindung",
|
||||||
|
"settings_copy_diagnostics": "Diagnose kopieren",
|
||||||
"settings_copyright": "Copyright 2024-2026 DragonX-Entwickler | GPLv3-Lizenz",
|
"settings_copyright": "Copyright 2024-2026 DragonX-Entwickler | GPLv3-Lizenz",
|
||||||
"settings_custom": "Benutzerdefiniert",
|
"settings_custom": "Benutzerdefiniert",
|
||||||
"settings_data_dir": "Datenverzeichnis:",
|
"settings_data_dir": "Datenverzeichnis",
|
||||||
"settings_debug_changed": "Debug-Kategorien geändert — Daemon neu starten zum Anwenden",
|
"settings_debug_changed": "Debug-Kategorien geändert — Daemon neu starten zum Anwenden",
|
||||||
"settings_debug_restart_note": "Änderungen werden nach einem Neustart des Daemons wirksam.",
|
"settings_debug_restart_note": "Änderungen werden nach einem Neustart des Daemons wirksam.",
|
||||||
"settings_debug_select": "Kategorien auswählen, um Daemon-Fehlerprotokollierung zu aktivieren (-debug= Flags).",
|
"settings_debug_select": "Kategorien auswählen, um Daemon-Fehlerprotokollierung zu aktivieren (-debug= Flags).",
|
||||||
|
"settings_diagnostics_copied": "Diagnose in die Zwischenablage kopiert",
|
||||||
"settings_encrypt_first_pin": "Verschlüsseln Sie zuerst die Wallet, um PIN zu aktivieren",
|
"settings_encrypt_first_pin": "Verschlüsseln Sie zuerst die Wallet, um PIN zu aktivieren",
|
||||||
"settings_encrypt_wallet": "Wallet verschlüsseln",
|
"settings_encrypt_wallet": "Wallet verschlüsseln",
|
||||||
"settings_explorer_hint": "URLs sollten einen abschließenden Schrägstrich enthalten. Die txid/Adresse wird angehängt.",
|
"settings_explorer_hint": "URLs sollten einen abschließenden Schrägstrich enthalten. Die txid/Adresse wird angehängt.",
|
||||||
"settings_export_all": "Alle exportieren...",
|
"settings_export_all": "Alle exportieren…",
|
||||||
"settings_export_csv": "CSV exportieren...",
|
"settings_export_csv": "CSV exportieren…",
|
||||||
"settings_export_key": "Schlüssel exportieren...",
|
"settings_export_key": "Schlüssel exportieren…",
|
||||||
"settings_gradient_bg": "Hintergrund-Verlauf",
|
"settings_gradient_bg": "Hintergrund-Verlauf",
|
||||||
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
|
"settings_gradient_desc": "Strukturierte Hintergründe durch sanfte Verläufe ersetzen",
|
||||||
"settings_idle_after": "nach",
|
"settings_idle_after": "nach",
|
||||||
"settings_import_key": "Privaten Schlüssel importieren...",
|
"settings_import_key": "Privaten Schlüssel importieren…",
|
||||||
"settings_import_viewkey": "Anzeigeschlüssel importieren...",
|
"settings_import_viewkey": "Anzeigeschlüssel importieren…",
|
||||||
"settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
|
"settings_language_note": "Hinweis: Manche Texte erfordern einen Neustart zur Aktualisierung",
|
||||||
"settings_lock_now": "Jetzt sperren",
|
"settings_lock_now": "Jetzt sperren",
|
||||||
"settings_locked": "Gesperrt",
|
"settings_locked": "Gesperrt",
|
||||||
"settings_merge_to_address": "An Adresse zusammenführen...",
|
"settings_merge_to_address": "An Adresse zusammenführen…",
|
||||||
"settings_noise_opacity": "Rauschdichte:",
|
"settings_noise_opacity": "Rauschdichte:",
|
||||||
|
"settings_not_connected": "Nicht mit dem Daemon verbunden",
|
||||||
"settings_not_encrypted": "Nicht verschlüsselt",
|
"settings_not_encrypted": "Nicht verschlüsselt",
|
||||||
"settings_not_found": "Nicht gefunden",
|
"settings_not_found": "Nicht gefunden",
|
||||||
"settings_open_app_dir": "App-Ordner öffnen",
|
"settings_open_app_dir": "App-Ordner öffnen",
|
||||||
"settings_open_data_dir": "Datenordner öffnen",
|
"settings_open_data_dir": "Datenordner öffnen",
|
||||||
|
"settings_open_log_folder": "Log-Ordner öffnen",
|
||||||
"settings_other": "Sonstiges",
|
"settings_other": "Sonstiges",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "Datenschutz",
|
"settings_privacy": "Datenschutz",
|
||||||
"settings_quick_unlock_pin": "Schnell-Entsperr-PIN",
|
"settings_quick_unlock_pin": "Schnell-Entsperr-PIN",
|
||||||
"settings_reduce_transparency": "Transparenz reduzieren",
|
"settings_reduce_transparency": "Transparenz reduzieren",
|
||||||
|
"settings_reloaded": "Einstellungen von der Festplatte neu geladen",
|
||||||
"settings_remove_encryption": "Verschlüsselung entfernen",
|
"settings_remove_encryption": "Verschlüsselung entfernen",
|
||||||
"settings_remove_pin": "PIN entfernen",
|
"settings_remove_pin": "PIN entfernen",
|
||||||
"settings_request_payment": "Zahlung anfordern...",
|
"settings_request_payment": "Zahlung anfordern…",
|
||||||
"settings_rescan_desc": "Blockchain nach fehlenden Transaktionen neu scannen",
|
"settings_rescan_desc": "Blockchain nach fehlenden Transaktionen neu scannen",
|
||||||
"settings_restart_daemon": "Daemon neu starten",
|
"settings_restart_daemon": "Daemon neu starten",
|
||||||
"settings_rpc_connection": "RPC-Verbindung",
|
"settings_rpc_connection": "RPC-Verbindung",
|
||||||
|
"settings_rpc_error_prefix": "RPC-Fehler: ",
|
||||||
"settings_rpc_note": "Hinweis: Verbindungseinstellungen werden automatisch aus DRAGONX.conf erkannt",
|
"settings_rpc_note": "Hinweis: Verbindungseinstellungen werden automatisch aus DRAGONX.conf erkannt",
|
||||||
|
"settings_rpc_ok": "RPC-Verbindung OK",
|
||||||
"settings_save_shielded_desc": "Speichert z-addr Transaktionen in einer lokalen Datei zur Ansicht",
|
"settings_save_shielded_desc": "Speichert z-addr Transaktionen in einer lokalen Datei zur Ansicht",
|
||||||
"settings_save_shielded_local": "Geschirmten Transaktionsverlauf lokal speichern",
|
"settings_save_shielded_local": "Geschirmten Transaktionsverlauf lokal speichern",
|
||||||
|
"settings_saved": "Einstellungen gespeichert",
|
||||||
"settings_set_pin": "PIN festlegen",
|
"settings_set_pin": "PIN festlegen",
|
||||||
"settings_shield_mining": "Mining abschirmen...",
|
"settings_shield_mining": "Mining abschirmen…",
|
||||||
"settings_solid_colors_desc": "Feste Farben anstelle von Unschärfe-Effekten verwenden (Barrierefreiheit)",
|
"settings_solid_colors_desc": "Feste Farben anstelle von Unschärfe-Effekten verwenden (Barrierefreiheit)",
|
||||||
|
"settings_theme_refreshed": "Themenliste aktualisiert",
|
||||||
"settings_tor_desc": "Alle Verbindungen für erhöhte Privatsphäre über Tor leiten",
|
"settings_tor_desc": "Alle Verbindungen für erhöhte Privatsphäre über Tor leiten",
|
||||||
"settings_unlocked": "Entsperrt",
|
"settings_unlocked": "Entsperrt",
|
||||||
"settings_use_tor_network": "Tor für Netzwerkverbindungen verwenden",
|
"settings_use_tor_network": "Tor für Netzwerkverbindungen verwenden",
|
||||||
"settings_validate_address": "Adresse überprüfen...",
|
"settings_validate_address": "Adresse überprüfen…",
|
||||||
"settings_visual_effects": "Visuelle Effekte",
|
"settings_visual_effects": "Visuelle Effekte",
|
||||||
"settings_wallet_file_size": "Wallet-Dateigröße: %s",
|
"settings_wallet_file_size": "Wallet-Dateigröße: %s",
|
||||||
"settings_wallet_info": "Wallet-Informationen",
|
"settings_wallet_info": "Wallet-Informationen",
|
||||||
"settings_wallet_location": "Wallet-Speicherort: %s",
|
"settings_wallet_location": "Wallet-Speicherort: %s",
|
||||||
"settings_wallet_maintenance": "Wallet-Wartung",
|
"settings_wallet_maintenance": "Wallet-Wartung",
|
||||||
"settings_wallet_not_found": "Wallet-Datei nicht gefunden",
|
"settings_wallet_not_found": "Wallet-Datei nicht gefunden",
|
||||||
"settings_wallet_size_label": "Wallet-Größe:",
|
"settings_wallet_size_label": "Wallet-Größe",
|
||||||
|
"settings_ztx_cleared": "Z-Transaktionsverlauf gelöscht",
|
||||||
|
"settings_ztx_not_found": "Keine Verlaufsdatei gefunden",
|
||||||
"setup_wizard": "Einrichtungsassistent",
|
"setup_wizard": "Einrichtungsassistent",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"shield_check_status": "Status prüfen",
|
"shield_check_status": "Status prüfen",
|
||||||
@@ -1292,6 +1461,23 @@
|
|||||||
"sweep_to": "Gefegt an:",
|
"sweep_to": "Gefegt an:",
|
||||||
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
|
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
|
||||||
"sweep_tx": "Transaktion:",
|
"sweep_tx": "Transaktion:",
|
||||||
|
"switch_corrupt_body": "Diese Wallet scheint beschädigt zu sein – der Knoten konnte sie nicht öffnen. Aus einem Backup wiederherstellen, neu erstellen oder eine Reparatur versuchen.",
|
||||||
|
"switch_corrupt_repair": "Reparatur versuchen (Salvage)",
|
||||||
|
"switch_progress_background": "Im Hintergrund fortsetzen",
|
||||||
|
"switch_progress_default_wallet": "Standard-Wallet",
|
||||||
|
"switch_progress_elapsed": "Vergangen",
|
||||||
|
"switch_progress_external_wallet": "Externe Wallet",
|
||||||
|
"switch_progress_failed_title": "Wallet-Wechsel fehlgeschlagen",
|
||||||
|
"switch_progress_from_label": "von",
|
||||||
|
"switch_progress_hint": "Ein sauberes Herunterfahren kann bis zu einer Minute dauern.",
|
||||||
|
"switch_progress_reconnecting": "Neu verbinden",
|
||||||
|
"switch_progress_starting": "Knoten wird mit der neuen Wallet gestartet",
|
||||||
|
"switch_progress_stopping": "Aktueller Knoten wird gestoppt",
|
||||||
|
"switch_progress_title": "Wallet wird gewechselt",
|
||||||
|
"switch_stopnode_body": "Beim Wallet-Wechsel wird der Knoten mit der ausgewählten Wallet neu gestartet. Der laufende Knoten wird gestoppt und mit der neuen Wallet neu gestartet – wenn Sie ihn absichtlich laufen ließen, startet er automatisch wieder.",
|
||||||
|
"switch_stopnode_confirm": "Knoten stoppen & wechseln",
|
||||||
|
"switch_stopnode_title": "Laufenden Knoten stoppen?",
|
||||||
|
"switch_stopnode_warn": "Es läuft bereits ein Knoten, den diese Wallet nicht gestartet hat.",
|
||||||
"syncing": "Synchronisiere...",
|
"syncing": "Synchronisiere...",
|
||||||
"t_address": "T-Adresse",
|
"t_address": "T-Adresse",
|
||||||
"t_addresses": "T-Adressen",
|
"t_addresses": "T-Adressen",
|
||||||
@@ -1299,6 +1485,7 @@
|
|||||||
"theme": "Design",
|
"theme": "Design",
|
||||||
"theme_effects": "Design-Effekte",
|
"theme_effects": "Design-Effekte",
|
||||||
"theme_language": "THEMA & SPRACHE",
|
"theme_language": "THEMA & SPRACHE",
|
||||||
|
"tile_click_to_open": "Zum Öffnen klicken",
|
||||||
"time_days_ago": "vor %d Tagen",
|
"time_days_ago": "vor %d Tagen",
|
||||||
"time_hours_ago": "vor %d Stunden",
|
"time_hours_ago": "vor %d Stunden",
|
||||||
"time_minutes_ago": "vor %d Minuten",
|
"time_minutes_ago": "vor %d Minuten",
|
||||||
@@ -1313,7 +1500,9 @@
|
|||||||
"to_upper": "AN",
|
"to_upper": "AN",
|
||||||
"tools": "WERKZEUGE",
|
"tools": "WERKZEUGE",
|
||||||
"tools_actions": "Werkzeuge & Aktionen...",
|
"tools_actions": "Werkzeuge & Aktionen...",
|
||||||
|
"tools_actions_hdr": "WERKZEUGE & AKTIONEN",
|
||||||
"total": "Gesamt",
|
"total": "Gesamt",
|
||||||
|
"total_balance_label": "Gesamtguthaben",
|
||||||
"transaction_id": "TRANSAKTIONS-ID",
|
"transaction_id": "TRANSAKTIONS-ID",
|
||||||
"transaction_sent": "Transaktion erfolgreich gesendet",
|
"transaction_sent": "Transaktion erfolgreich gesendet",
|
||||||
"transaction_sent_msg": "Transaktion gesendet!",
|
"transaction_sent_msg": "Transaktion gesendet!",
|
||||||
@@ -1335,13 +1524,24 @@
|
|||||||
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
|
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
|
||||||
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
|
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
|
||||||
"tt_block_explorer": "Den DragonX Block-Explorer im Browser öffnen",
|
"tt_block_explorer": "Den DragonX Block-Explorer im Browser öffnen",
|
||||||
"tt_blur": "Unschärfe-Stärke (0%% = aus, 100%% = maximum)",
|
"tt_blur": "Unschärfe-Stärke (0% = aus, 100% = maximum)",
|
||||||
"tt_change_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
|
"tt_change_pass": "Die Wallet-Verschlüsselungspassphrase ändern",
|
||||||
"tt_change_pin": "Ihre Entsperr-PIN ä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_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_copy_diagnostics": "Kopiert eine Support-Übersicht (Version, Daemon-/Wallet-/Log-Status – keine Geheimnisse) in die Zwischenablage",
|
||||||
"tt_custom_fees": "Manuelle Gebühreneingabe beim Senden von Transaktionen aktivieren",
|
"tt_custom_fees": "Manuelle Gebühreneingabe beim Senden von Transaktionen aktivieren",
|
||||||
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
|
"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_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_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_collapse": "Debug-Protokollierungsoptionen einklappen",
|
||||||
"tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen",
|
"tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen",
|
||||||
@@ -1359,15 +1559,39 @@
|
|||||||
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
||||||
"tt_language": "Schnittstellensprache der Wallet-UI",
|
"tt_language": "Schnittstellensprache der Wallet-UI",
|
||||||
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
||||||
|
"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_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_lock": "Die Wallet sofort sperren",
|
||||||
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
|
"tt_low_spec": "Alle aufwendigen visuellen Effekte deaktivieren\\nHotkey: Ctrl+Shift+Down",
|
||||||
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
|
"tt_merge": "Mehrere UTXOs einer Adresse zusammenführen",
|
||||||
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)",
|
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)",
|
||||||
"tt_noise": "Körnungstextur-Intensität (0%% = aus, 100%% = maximum)",
|
"tt_noise": "Körnungstextur-Intensität (0% = aus, 100% = maximum)",
|
||||||
"tt_open_app_dir": "Den ObsidianDragon-Ordner (Einstellungen, Themes, Logs) im Dateimanager öffnen",
|
"tt_open_app_dir": "Den ObsidianDragon-Ordner (Einstellungen, Themes, Logs) im Dateimanager öffnen",
|
||||||
"tt_open_data_dir": "Den Ordner mit Ihren Wallet- und Blockchain-Daten im Dateimanager öffnen",
|
"tt_open_data_dir": "Den Ordner mit Ihren Wallet- und Blockchain-Daten im Dateimanager öffnen",
|
||||||
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
|
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
|
||||||
|
"tt_open_log_folder": "Öffnet den Ordner mit den Debug- und Absturzprotokollen",
|
||||||
"tt_reduce_motion": "Animierte Übergänge und Saldo-Lerp für Barrierefreiheit deaktivieren",
|
"tt_reduce_motion": "Animierte Übergänge und Saldo-Lerp für Barrierefreiheit deaktivieren",
|
||||||
"tt_remove_encrypt": "Verschlüsselung entfernen und Wallet ungeschützt speichern",
|
"tt_remove_encrypt": "Verschlüsselung entfernen und Wallet ungeschützt speichern",
|
||||||
"tt_remove_pin": "PIN entfernen und Passphrase zum Entsperren erfordern",
|
"tt_remove_pin": "PIN entfernen und Passphrase zum Entsperren erfordern",
|
||||||
@@ -1380,12 +1604,17 @@
|
|||||||
"tt_rpc_host": "Hostname des DragonX-Daemons",
|
"tt_rpc_host": "Hostname des DragonX-Daemons",
|
||||||
"tt_rpc_pass": "RPC-Authentifizierungspasswort",
|
"tt_rpc_pass": "RPC-Authentifizierungspasswort",
|
||||||
"tt_rpc_port": "Port für RPC-Verbindungen des Daemons",
|
"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_rpc_user": "RPC-Authentifizierungsbenutzername",
|
||||||
"tt_save_settings": "Alle Einstellungen auf der Festplatte speichern",
|
"tt_save_settings": "Alle Einstellungen auf der Festplatte speichern",
|
||||||
"tt_save_ztx": "Z-Adresse-Transaktionsverlauf lokal für schnelleres Laden 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_scan_themes": "Nach neuen Themes suchen.\\nTheme-Ordner ablegen in:\\n%s",
|
||||||
"tt_scanline": "CRT-Scanlinieneffekt in der Konsole",
|
"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_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_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_set_pin": "Eine 4-8-stellige PIN für schnelles Entsperren festlegen",
|
||||||
"tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben",
|
"tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben",
|
||||||
@@ -1397,13 +1626,14 @@
|
|||||||
"tt_theme_hotkey": "Hotkey: Ctrl+Links/Rechts zum Wechseln der Themes",
|
"tt_theme_hotkey": "Hotkey: Ctrl+Links/Rechts zum Wechseln der Themes",
|
||||||
"tt_tor": "Daemon-Verbindungen für Anonymität über das Tor-Netzwerk leiten",
|
"tt_tor": "Daemon-Verbindungen für Anonymität über das Tor-Netzwerk leiten",
|
||||||
"tt_tx_url": "Basis-URL zum Anzeigen von Transaktionen in einem Block-Explorer",
|
"tt_tx_url": "Basis-URL zum Anzeigen von Transaktionen in einem Block-Explorer",
|
||||||
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100%% = vollständig undurchsichtig, niedriger = durchsichtiger)",
|
"tt_ui_opacity": "Karten- und Seitenleisten-Deckkraft (100% = vollständig undurchsichtig, niedriger = durchsichtiger)",
|
||||||
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
|
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
|
||||||
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
|
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
|
||||||
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
|
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
|
||||||
"tt_website": "Die DragonX-Website öffnen",
|
"tt_website": "Die DragonX-Website öffnen",
|
||||||
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
||||||
"tt_wizard": "Den Ersteinrichtungsassistenten erneut ausführen\\nDer Daemon wird neu gestartet",
|
"tt_wizard": "Den Ersteinrichtungsassistenten erneut ausführen\\nDer Daemon wird neu gestartet",
|
||||||
|
"tx_chat_badge": "Nachricht",
|
||||||
"tx_confirmations": "%d Bestätigungen",
|
"tx_confirmations": "%d Bestätigungen",
|
||||||
"tx_details_title": "Transaktionsdetails",
|
"tx_details_title": "Transaktionsdetails",
|
||||||
"tx_from_address": "Von Adresse:",
|
"tx_from_address": "Von Adresse:",
|
||||||
@@ -1449,6 +1679,7 @@
|
|||||||
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
||||||
"validate_ownership": "Eigentum:",
|
"validate_ownership": "Eigentum:",
|
||||||
"validate_results": "Ergebnisse:",
|
"validate_results": "Ergebnisse:",
|
||||||
|
"validate_results_placeholder": "Ergebnisse erscheinen hier",
|
||||||
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
||||||
"validate_status": "Status:",
|
"validate_status": "Status:",
|
||||||
"validate_title": "Adresse validieren",
|
"validate_title": "Adresse validieren",
|
||||||
@@ -1472,6 +1703,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ Weiteren Ordner nach Wallets durchsuchen…",
|
"wallets_add_folder_toggle": "+ Weiteren Ordner nach Wallets durchsuchen…",
|
||||||
"wallets_badge_encrypted": "Verschlüsselt (passphrasengeschützt)",
|
"wallets_badge_encrypted": "Verschlüsselt (passphrasengeschützt)",
|
||||||
"wallets_badge_encrypted_short": "Verschlüsselt",
|
"wallets_badge_encrypted_short": "Verschlüsselt",
|
||||||
|
"wallets_badge_hd": "HD-Wallet – Seed-Phrase ohne Öffnen nicht bestätigbar",
|
||||||
|
"wallets_badge_hd_short": "HD-Wallet",
|
||||||
"wallets_badge_legacy": "Legacy-Wallet (keine Seed-Phrase)",
|
"wallets_badge_legacy": "Legacy-Wallet (keine Seed-Phrase)",
|
||||||
"wallets_badge_legacy_short": "Legacy",
|
"wallets_badge_legacy_short": "Legacy",
|
||||||
"wallets_badge_seed": "Seed-Phrase-Wallet (HD)",
|
"wallets_badge_seed": "Seed-Phrase-Wallet (HD)",
|
||||||
@@ -1587,6 +1820,7 @@
|
|||||||
"xmrig_loading_releases": "Releases werden geladen…",
|
"xmrig_loading_releases": "Releases werden geladen…",
|
||||||
"xmrig_none": "keiner",
|
"xmrig_none": "keiner",
|
||||||
"xmrig_reinstall": "Neu installieren",
|
"xmrig_reinstall": "Neu installieren",
|
||||||
|
"xmrig_releases": "xmrig-Releases",
|
||||||
"xmrig_stop_mining_first": "Stoppen Sie das Mining, bevor Sie den Miner aktualisieren.",
|
"xmrig_stop_mining_first": "Stoppen Sie das Mining, bevor Sie den Miner aktualisieren.",
|
||||||
"xmrig_unavailable_body": "Für diese Plattform ist kein Miner-Build verfügbar.",
|
"xmrig_unavailable_body": "Für diese Plattform ist kein Miner-Build verfügbar.",
|
||||||
"xmrig_unavailable_title": "Miner-Updates nicht verfügbar",
|
"xmrig_unavailable_title": "Miner-Updates nicht verfügbar",
|
||||||
|
|||||||
278
res/lang/es.json
278
res/lang/es.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "AVANZADO",
|
"advanced": "AVANZADO",
|
||||||
"advanced_effects": "Efectos Avanzados...",
|
"advanced_effects": "Efectos Avanzados...",
|
||||||
"ago": "atrás",
|
"ago": "atrás",
|
||||||
|
"alerts_clear": "Borrar historial de alertas",
|
||||||
|
"alerts_history_tooltip": "Alertas recientes",
|
||||||
|
"alerts_none": "Aún no hay alertas",
|
||||||
|
"alerts_recent": "ALERTAS RECIENTES",
|
||||||
"all_filter": "Todos",
|
"all_filter": "Todos",
|
||||||
"allow_custom_fees": "Permitir comisiones personalizadas",
|
"allow_custom_fees": "Permitir comisiones personalizadas",
|
||||||
"amount": "Cantidad",
|
"amount": "Cantidad",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender bloqueó el minero",
|
"av_title": "Windows Defender bloqueó el minero",
|
||||||
"available": "Disponible",
|
"available": "Disponible",
|
||||||
"backup_backing_up": "Respaldando...",
|
"backup_backing_up": "Respaldando...",
|
||||||
|
"backup_col_backup": "COPIA DE SEGURIDAD",
|
||||||
|
"backup_col_export": "EXPORTAR",
|
||||||
|
"backup_col_import": "IMPORTAR Y RESTAURAR",
|
||||||
"backup_create": "Crear Respaldo",
|
"backup_create": "Crear Respaldo",
|
||||||
"backup_created": "Respaldo de cartera creado",
|
"backup_created": "Respaldo de cartera creado",
|
||||||
"backup_data": "RESPALDO Y DATOS",
|
"backup_data": "RESPALDO Y DATOS",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "Saldo",
|
"balance": "Saldo",
|
||||||
"balance_history_collecting": "Historial de saldo — recopilando datos...",
|
"balance_history_collecting": "Historial de saldo — recopilando datos...",
|
||||||
"balance_layout": "Diseño de Saldo",
|
"balance_layout": "Diseño de Saldo",
|
||||||
|
"balance_layout_switched": "Diseño: %s",
|
||||||
|
"balance_mining_rate": "Minando %s",
|
||||||
"balance_shielded_fmt": "Protegido: %.8f",
|
"balance_shielded_fmt": "Protegido: %.8f",
|
||||||
|
"balance_syncing_pct": "Sincronizando %.1f%%",
|
||||||
"balance_transparent_fmt": "Transparente: %.8f",
|
"balance_transparent_fmt": "Transparente: %.8f",
|
||||||
"ban": "Bloquear",
|
"ban": "Bloquear",
|
||||||
"banned_peers": "Nodos Bloqueados",
|
"banned_peers": "Nodos Bloqueados",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "Verificando sumas de comprobación...",
|
"bootstrap_verifying": "Verificando sumas de comprobación...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
||||||
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
|
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
|
||||||
|
"byte_count_fmt": "%zu / %zu bytes",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"change_pass_confirm": "Confirmar nueva:",
|
"change_pass_confirm": "Confirmar nueva:",
|
||||||
"change_pass_current": "Frase de contraseña actual:",
|
"change_pass_current": "Frase de contraseña actual:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "Cambiar frase de contraseña",
|
"change_pass_title": "Cambiar frase de contraseña",
|
||||||
"characters": "caracteres",
|
"characters": "caracteres",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"chat_accent_amber": "Ámbar",
|
||||||
|
"chat_accent_blue": "Azul",
|
||||||
|
"chat_accent_green": "Verde",
|
||||||
|
"chat_accent_pink": "Rosa",
|
||||||
|
"chat_accent_purple": "Morado",
|
||||||
|
"chat_accent_theme": "Tema",
|
||||||
|
"chat_add_contact": "Añadir contacto",
|
||||||
|
"chat_awaiting_key": "Esperando respuesta",
|
||||||
|
"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_cancel": "Cancelar",
|
||||||
|
"chat_contact_added": "Contacto añadido: renómbralo en Contactos",
|
||||||
"chat_contact_request": "solicitud de contacto",
|
"chat_contact_request": "solicitud de contacto",
|
||||||
|
"chat_copy_address_tip": "Clic para copiar la dirección",
|
||||||
|
"chat_density_comfortable": "Cómoda",
|
||||||
|
"chat_density_compact": "Compacta",
|
||||||
|
"chat_emoji_color": "Color",
|
||||||
|
"chat_emoji_mono": "Monocromo",
|
||||||
|
"chat_emoji_search": "Buscar emoji",
|
||||||
"chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.",
|
"chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.",
|
||||||
|
"chat_empty_start": "Inicia una con \"Nueva conversación\".",
|
||||||
|
"chat_empty_title": "Aún no hay conversaciones",
|
||||||
|
"chat_export": "Exportar chat…",
|
||||||
|
"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",
|
||||||
|
"chat_jump_latest": "Recientes",
|
||||||
|
"chat_len_over": "Mensaje demasiado largo",
|
||||||
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
|
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
|
||||||
"chat_new_button": "Nueva conversación",
|
"chat_mute": "Silenciar",
|
||||||
|
"chat_new_button": "Nuevo chat",
|
||||||
"chat_new_message": "Mensaje",
|
"chat_new_message": "Mensaje",
|
||||||
|
"chat_new_message_toast": "Nuevo mensaje de chat cifrado",
|
||||||
"chat_new_send": "Enviar solicitud",
|
"chat_new_send": "Enviar solicitud",
|
||||||
"chat_new_title": "Nueva conversación",
|
"chat_new_title": "Nuevo chat",
|
||||||
"chat_new_zaddr": "Dirección z del destinatario",
|
"chat_new_zaddr": "Dirección z del destinatario",
|
||||||
|
"chat_no_matches": "Ninguna conversación coincide con tu búsqueda.",
|
||||||
|
"chat_no_z_contacts": "Aún no hay contactos con dirección blindada",
|
||||||
|
"chat_opt_bubble_accent": "Color de burbuja",
|
||||||
|
"chat_opt_bubble_style": "Estilo de burbuja",
|
||||||
|
"chat_opt_density": "Densidad de mensajes",
|
||||||
|
"chat_opt_emoji": "Estilo de emoji",
|
||||||
|
"chat_opt_enter_sends": "Enter envía el mensaje",
|
||||||
|
"chat_opt_font_size": "Tamaño del texto",
|
||||||
|
"chat_opt_global_clock": "Formato de reloj global",
|
||||||
|
"chat_opt_poll": "Frecuencia de sondeo",
|
||||||
|
"chat_opt_timestamp": "Marcas de tiempo",
|
||||||
|
"chat_pick_contact": "Elegir de contactos…",
|
||||||
|
"chat_rename": "Renombrar contacto",
|
||||||
|
"chat_rename_hint": "Nombre del contacto",
|
||||||
|
"chat_renamed": "Contacto renombrado",
|
||||||
|
"chat_retry": "Reintentar",
|
||||||
|
"chat_search": "Buscar conversaciones",
|
||||||
|
"chat_sec_appearance": "APARIENCIA",
|
||||||
|
"chat_sec_messaging": "MENSAJES",
|
||||||
"chat_select_hint": "Selecciona una conversación para verla.",
|
"chat_select_hint": "Selecciona una conversación para verla.",
|
||||||
"chat_send": "Enviar",
|
"chat_send": "Enviar",
|
||||||
"chat_send_failed": "no enviado",
|
"chat_send_failed": "no enviado",
|
||||||
|
"chat_sending": "enviando…",
|
||||||
|
"chat_settings_done": "Listo",
|
||||||
|
"chat_settings_section": "CHAT Y CONTACTOS",
|
||||||
|
"chat_settings_tip": "Personalizar chat",
|
||||||
|
"chat_settings_title": "Ajustes de chat",
|
||||||
|
"chat_show_hidden": "Ver ocultos",
|
||||||
|
"chat_time_now": "ahora",
|
||||||
"chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).",
|
"chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).",
|
||||||
"chat_toast_lite_busy": "Ya hay un envío en curso, o no hay ningún monedero abierto.",
|
"chat_toast_lite_busy": "Ya hay un envío en curso, o no hay ningún monedero abierto.",
|
||||||
|
"chat_toast_need_funds": "Necesitas un pequeño saldo blindado para enviar chats (para cubrir la comisión).",
|
||||||
"chat_toast_no_zaddr": "No hay ninguna dirección z disponible desde la que enviar el chat.",
|
"chat_toast_no_zaddr": "No hay ninguna dirección z disponible desde la que enviar el chat.",
|
||||||
"chat_toast_not_connected": "Sin conexión: mensaje de chat no enviado.",
|
"chat_toast_not_connected": "Sin conexión: mensaje de chat no enviado.",
|
||||||
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
|
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
|
||||||
"chat_toast_request_queued": "Solicitud de contacto en cola.",
|
"chat_toast_request_queued": "Solicitud de contacto en cola.",
|
||||||
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
|
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
|
||||||
|
"chat_today": "Hoy",
|
||||||
|
"chat_ts_12h": "12 horas",
|
||||||
|
"chat_ts_24h": "24 horas",
|
||||||
|
"chat_ts_global": "Seguir global",
|
||||||
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_unhide": "Mostrar",
|
||||||
|
"chat_unmute": "Reactivar",
|
||||||
|
"chat_verify_key": "Clave de identidad: compárala para verificar",
|
||||||
"chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.",
|
"chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.",
|
||||||
|
"chat_yesterday": "Ayer",
|
||||||
"chat_you": "Tú",
|
"chat_you": "Tú",
|
||||||
"choose_icon": "Elegir Icono",
|
"choose_icon": "Elegir Icono",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "Clic para copiar dirección",
|
"click_copy_address": "Clic para copiar dirección",
|
||||||
"click_copy_uri": "Clic para copiar URI",
|
"click_copy_uri": "Clic para copiar URI",
|
||||||
"click_to_copy": "Clic para copiar",
|
"click_to_copy": "Clic para copiar",
|
||||||
|
"clock_format": "Formato de hora",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
"conf_count": "%d conf",
|
"conf_count": "%d conf",
|
||||||
"confirm_and_send": "Confirmar y Enviar",
|
"confirm_and_send": "Confirmar y Enviar",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "App",
|
"console_app": "App",
|
||||||
"console_auto_scroll": "Auto-desplazamiento",
|
"console_auto_scroll": "Auto-desplazamiento",
|
||||||
"console_available_commands": "Comandos disponibles:",
|
"console_available_commands": "Comandos disponibles:",
|
||||||
|
"console_backend_reference": "Referencia de Comandos del Backend",
|
||||||
|
"console_backend_unavailable": "Sin backend",
|
||||||
"console_capturing_output": "Capturando salida del daemon...",
|
"console_capturing_output": "Capturando salida del daemon...",
|
||||||
|
"console_cat_advanced": "Avanzado",
|
||||||
"console_cat_blockchain": "Blockchain",
|
"console_cat_blockchain": "Blockchain",
|
||||||
"console_cat_control": "Control",
|
"console_cat_control": "Control",
|
||||||
|
"console_cat_keys": "Claves y seguridad",
|
||||||
"console_cat_mining": "Minería",
|
"console_cat_mining": "Minería",
|
||||||
"console_cat_network": "Red",
|
"console_cat_network": "Red",
|
||||||
"console_cat_raw_transactions": "Transacciones sin procesar",
|
"console_cat_raw_transactions": "Transacciones sin procesar",
|
||||||
|
"console_cat_send": "Enviar",
|
||||||
|
"console_cat_sync": "Sincronización",
|
||||||
"console_cat_utility": "Utilidades",
|
"console_cat_utility": "Utilidades",
|
||||||
"console_cat_wallet": "Cartera",
|
"console_cat_wallet": "Cartera",
|
||||||
"console_clear": "Limpiar",
|
"console_clear": "Limpiar",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - Mostrar este mensaje de ayuda",
|
"console_help_help": " help - Mostrar este mensaje de ayuda",
|
||||||
"console_help_setgenerate": " setgenerate - Controlar minería",
|
"console_help_setgenerate": " setgenerate - Controlar minería",
|
||||||
"console_help_stop": " stop - Detener el daemon",
|
"console_help_stop": " stop - Detener el daemon",
|
||||||
|
"console_last_error": "Último error:",
|
||||||
"console_line_count": "%zu líneas",
|
"console_line_count": "%zu líneas",
|
||||||
"console_matches": "coincidencias",
|
"console_matches": "coincidencias",
|
||||||
"console_new_lines": "%d nuevas líneas",
|
"console_new_lines": "%d nuevas líneas",
|
||||||
"console_no_daemon": "Sin daemon",
|
"console_no_daemon": "Sin daemon",
|
||||||
|
"console_no_output": "(sin salida)",
|
||||||
"console_not_connected": "Error: No conectado al daemon",
|
"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_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.",
|
||||||
"console_ref_builds": "Genera",
|
"console_ref_builds": "Genera",
|
||||||
"console_ref_cancel": "Cancelar",
|
"console_ref_cancel": "Cancelar",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
|
"console_ref_run_confirm": "¿Ejecutar %s ahora? Es un comando con consecuencias.",
|
||||||
"console_ref_search_hint": "Buscar por nombre o tarea…",
|
"console_ref_search_hint": "Buscar por nombre o tarea…",
|
||||||
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
|
"console_ref_select_hint": "Selecciona un comando para ver qué hace.",
|
||||||
|
"console_ref_value": "valor",
|
||||||
"console_rpc_reference": "Referencia de Comandos RPC",
|
"console_rpc_reference": "Referencia de Comandos RPC",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "Líneas de consola",
|
"console_scanline": "Líneas de consola",
|
||||||
"console_search_commands": "Buscar comandos...",
|
"console_search_commands": "Buscar comandos...",
|
||||||
"console_select_all": "Seleccionar Todo",
|
"console_select_all": "Seleccionar Todo",
|
||||||
"console_show_app_output": "Mostrar las líneas de registro de la cartera [app]",
|
"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_daemon_output": "Mostrar salida del daemon",
|
||||||
"console_show_errors_only": "Mostrar solo errores",
|
"console_show_errors_only": "Mostrar solo errores",
|
||||||
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "Detenido",
|
"console_status_stopped": "Detenido",
|
||||||
"console_status_stopping": "Deteniendo",
|
"console_status_stopping": "Deteniendo",
|
||||||
"console_status_unknown": "Desconocido",
|
"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_tab_completion": "Tab para completar",
|
||||||
"console_text_colors": "Colores de texto",
|
"console_text_colors": "Colores de texto",
|
||||||
"console_toggle_accents": "Alternar acentos de color de línea",
|
"console_toggle_accents": "Alternar acentos de color de línea",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "Activado: este contacto permanece visible sin importar qué cartera cargues. Desactivado: pertenece solo a la cartera actual.",
|
"contact_global_tt": "Activado: este contacto permanece visible sin importar qué cartera cargues. Desactivado: pertenece solo a la cartera actual.",
|
||||||
"contact_preview_addr": "La dirección aparecerá aquí",
|
"contact_preview_addr": "La dirección aparecerá aquí",
|
||||||
"contact_preview_name": "Nombre del contacto",
|
"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": "Contactos",
|
||||||
|
"contacts_avatar_shape": "Forma del avatar",
|
||||||
|
"contacts_list_scale": "Escala de la lista",
|
||||||
"contacts_search_no_match": "No hay contactos coincidentes",
|
"contacts_search_no_match": "No hay contactos coincidentes",
|
||||||
"contacts_search_placeholder": "Buscar contactos...",
|
"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!",
|
"copied": "¡Copiado!",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copy_address": "Copiar Dirección Completa",
|
"copy_address": "Copiar Dirección Completa",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "Incluido",
|
"daemon_bundled": "Incluido",
|
||||||
"daemon_install_bundled": "Instalar integrado",
|
"daemon_install_bundled": "Instalar integrado",
|
||||||
"daemon_installed": "Instalado",
|
"daemon_installed": "Instalado",
|
||||||
|
"daemon_maintenance_label": "MANTENIMIENTO",
|
||||||
"daemon_none_bundled": "ninguno en esta compilación",
|
"daemon_none_bundled": "ninguno en esta compilación",
|
||||||
"daemon_not_installed": "no instalado",
|
"daemon_not_installed": "no instalado",
|
||||||
"daemon_status_differ": "El binario instalado difiere de la versión incluida.",
|
"daemon_status_differ": "El binario instalado difiere de la versión incluida.",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "Más reciente:",
|
"daemon_update_latest": "Más reciente:",
|
||||||
"daemon_update_loading": "Cargando versiones…",
|
"daemon_update_loading": "Cargando versiones…",
|
||||||
"daemon_update_now": "Actualizar ahora",
|
"daemon_update_now": "Actualizar ahora",
|
||||||
|
"daemon_update_prompt_title": "¿Actualizar el daemon del nodo?",
|
||||||
"daemon_update_reinstall": "Reinstalar",
|
"daemon_update_reinstall": "Reinstalar",
|
||||||
"daemon_update_restart_note": "Reinicia el daemon para empezar a ejecutar la nueva versión.",
|
"daemon_update_restart_note": "Reinicia el daemon para empezar a ejecutar la nueva versión.",
|
||||||
"daemon_update_restart_now": "Reiniciar el daemon ahora",
|
"daemon_update_restart_now": "Reiniciar el daemon ahora",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "La descarga se verifica frente al SHA-256 publicado de la versión y una firma ed25519 fijada antes de instalarla.",
|
"daemon_update_verify_note": "La descarga se verifica frente al SHA-256 publicado de la versión y una firma ed25519 fijada antes de instalarla.",
|
||||||
"daemon_update_verifying": "Verificando…",
|
"daemon_update_verifying": "Verificando…",
|
||||||
"daemon_update_version": "Versión:",
|
"daemon_update_version": "Versión:",
|
||||||
|
"daemon_updates_label": "ACTUALIZACIONES",
|
||||||
"daemon_version": "Daemon",
|
"daemon_version": "Daemon",
|
||||||
"dark": "Oscuro",
|
"dark": "Oscuro",
|
||||||
|
"data_stale_prefix": "Actualizado",
|
||||||
|
"data_stale_tooltip": "El saldo puede estar desactualizado: la cartera no ha recibido una actualización reciente. Comprueba la conexión con tu nodo.",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
"date_label": "Fecha:",
|
"date_label": "Fecha:",
|
||||||
"debug_logging": "REGISTRO DE DEPURACIÓN",
|
"debug_logging": "REGISTRO DE DEPURACIÓN",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "Descargar Bootstrap",
|
"download_bootstrap": "Descargar Bootstrap",
|
||||||
"dragonx_green": "DragonX (Verde)",
|
"dragonx_green": "DragonX (Verde)",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
|
"empty_wallet_keys_suffix": "claves",
|
||||||
|
"empty_wallet_open_manager": "Abrir administrador de carteras",
|
||||||
|
"empty_wallet_restore": "Restaurar mi cartera",
|
||||||
|
"empty_wallet_salvage_body": "Esta cartera está vacía porque una reparación automática anterior apartó tu cartera original como copia de seguridad. Tus monedas casi con certeza están en esa copia, no perdidas. Restáurala para volver a cargar tus fondos: no se elimina nada; primero se aparta el archivo actual.",
|
||||||
|
"empty_wallet_salvage_headline": "Tus monedas están a salvo en un archivo de copia de seguridad.",
|
||||||
|
"empty_wallet_salvage_title": "Es posible que tu cartera haya sido reparada",
|
||||||
|
"empty_wallet_warning_body": "Esta cartera no tiene direcciones ni fondos, pero otro archivo de cartera en tu carpeta de DragonX contiene claves. Lo más probable es que tus monedas estén ahí, no perdidas. Abre el administrador de carteras para cambiar a la cartera que tiene tus fondos.",
|
||||||
|
"empty_wallet_warning_dismiss": "No volver a avisar para esta cartera",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "Detiene este aviso solo para el archivo de cartera actual. Si más tarde cambias a otra cartera vacía, podría avisarte de nuevo.",
|
||||||
|
"empty_wallet_warning_headline": "Es posible que haya abierto la cartera equivocada.",
|
||||||
|
"empty_wallet_warning_title": "Esta cartera está vacía",
|
||||||
"enc_confirm": "Confirmar:",
|
"enc_confirm": "Confirmar:",
|
||||||
"enc_desc": "Cifrar tu monedero protege tus claves privadas con una frase de contraseña. Tras el cifrado, el daemon se reiniciará.",
|
"enc_desc": "Cifrar tu monedero protege tus claves privadas con una frase de contraseña. Tras el cifrado, el daemon se reiniciará.",
|
||||||
"enc_encrypting": "Cifrando el monedero...",
|
"enc_encrypting": "Cifrando el monedero...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "Claro",
|
"light": "Claro",
|
||||||
"lite_account_label": "Cuenta",
|
"lite_account_label": "Cuenta",
|
||||||
"lite_action": "Acción",
|
"lite_action": "Acción",
|
||||||
|
"lite_backend_unavailable": "Backend de billetera Lite no disponible",
|
||||||
"lite_backup_keys": "Respaldo y claves",
|
"lite_backup_keys": "Respaldo y claves",
|
||||||
"lite_birthday_backup": "Cumpleaños: %llu (respalda esto también)",
|
"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_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_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_console_help_passthrough": "Cualquier otra entrada se ejecuta como un comando de consola de la cartera lite.",
|
||||||
"lite_copy": "Copiar",
|
"lite_copy": "Copiar",
|
||||||
|
"lite_could_not_start": "No se pudo iniciar la operación",
|
||||||
"lite_could_not_write": "No se pudo escribir ",
|
"lite_could_not_write": "No se pudo escribir ",
|
||||||
"lite_encrypt_wallet": "Cifrar cartera",
|
"lite_encrypt_wallet": "Cifrar cartera",
|
||||||
"lite_encryption_removed": "Cifrado eliminado",
|
"lite_encryption_removed": "Cifrado eliminado",
|
||||||
|
"lite_enter_all_seed_words": "Ingrese las 24 palabras semilla para restaurar (se obtuvieron %d)",
|
||||||
|
"lite_enter_wallet_path": "Ingrese una ruta de billetera",
|
||||||
"lite_hide_wipe": "Ocultar y borrar",
|
"lite_hide_wipe": "Ocultar y borrar",
|
||||||
"lite_import": "Importar",
|
"lite_import": "Importar",
|
||||||
"lite_import_key_label": "Importar clave",
|
"lite_import_key_label": "Importar clave",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://tu-servidor-lite",
|
"lite_net_add_url_hint": "https://tu-servidor-lite",
|
||||||
"lite_net_checking": "comprobando…",
|
"lite_net_checking": "comprobando…",
|
||||||
"lite_net_connected": "Conectado",
|
"lite_net_connected": "Conectado",
|
||||||
|
"lite_net_connecting": "Conectando…",
|
||||||
"lite_net_custom": "Personalizado",
|
"lite_net_custom": "Personalizado",
|
||||||
"lite_net_disconnected": "No conectado",
|
"lite_net_disconnected": "No conectado",
|
||||||
"lite_net_hidden_section": "Servidores ocultos",
|
"lite_net_hidden_section": "Servidores ocultos",
|
||||||
@@ -638,6 +765,9 @@
|
|||||||
"lite_working": "Trabajando…",
|
"lite_working": "Trabajando…",
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
"loading_addresses": "Cargando direcciones...",
|
"loading_addresses": "Cargando direcciones...",
|
||||||
|
"loading_stall_body": "El daemon lleva %.0f s inicializándose. Esto puede ser normal tras una actualización o en el primer inicio (cargando el índice de bloques o reescaneando); se conectará automáticamente cuando esté listo.",
|
||||||
|
"loading_stall_hint": "¿Sigue bloqueado? Abre Ajustes y usa Reiniciar daemon, o revisa la Consola para más detalles.",
|
||||||
|
"loading_stall_title": "Está tardando más de lo esperado",
|
||||||
"loading_transactions": "Cargando transacciones",
|
"loading_transactions": "Cargando transacciones",
|
||||||
"local_hashrate": "Tasa Hash Local",
|
"local_hashrate": "Tasa Hash Local",
|
||||||
"low_spec_mode": "Modo bajo rendimiento",
|
"low_spec_mode": "Modo bajo rendimiento",
|
||||||
@@ -654,6 +784,9 @@
|
|||||||
"market_cap": "Cap. de Mercado",
|
"market_cap": "Cap. de Mercado",
|
||||||
"market_cap_short": "Cap.",
|
"market_cap_short": "Cap.",
|
||||||
"market_chart_loading": "Cargando historial de precios",
|
"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_1d": "1D",
|
||||||
"market_iv_1h": "1H",
|
"market_iv_1h": "1H",
|
||||||
"market_iv_1m": "1M",
|
"market_iv_1m": "1M",
|
||||||
@@ -662,13 +795,18 @@
|
|||||||
"market_no_history": "No hay historial de precios disponible",
|
"market_no_history": "No hay historial de precios disponible",
|
||||||
"market_no_price": "Sin datos de precio",
|
"market_no_price": "Sin datos de precio",
|
||||||
"market_now": "Ahora",
|
"market_now": "Ahora",
|
||||||
|
"market_opt_chart_style": "Estilo de gráfico",
|
||||||
"market_pct_shielded": "%.0f%% Protegido",
|
"market_pct_shielded": "%.0f%% Protegido",
|
||||||
"market_portfolio": "PORTAFOLIO",
|
"market_portfolio": "PORTAFOLIO",
|
||||||
"market_price_loading": "Cargando datos de precio...",
|
"market_price_loading": "Cargando datos de precio...",
|
||||||
"market_price_unavailable": "Datos de precio no disponibles",
|
"market_price_unavailable": "Datos de precio no disponibles",
|
||||||
"market_refresh_price": "Actualizar datos de precio",
|
"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": "Cambiar a velas",
|
||||||
|
"market_style_candle_label": "Velas",
|
||||||
"market_style_line": "Cambiar a gráfico de líneas",
|
"market_style_line": "Cambiar a gráfico de líneas",
|
||||||
|
"market_style_line_label": "Línea",
|
||||||
"market_trade_on": "Operar en %s",
|
"market_trade_on": "Operar en %s",
|
||||||
"market_updated": "\\xc2\\xb7 Actualizado %s",
|
"market_updated": "\\xc2\\xb7 Actualizado %s",
|
||||||
"market_vol_short": "Vol",
|
"market_vol_short": "Vol",
|
||||||
@@ -762,6 +900,7 @@
|
|||||||
"mining_difficulty_copied": "Dificultad copiada",
|
"mining_difficulty_copied": "Dificultad copiada",
|
||||||
"mining_est_block": "Bloque Est.",
|
"mining_est_block": "Bloque Est.",
|
||||||
"mining_est_daily": "Diario Est.",
|
"mining_est_daily": "Diario Est.",
|
||||||
|
"mining_est_daily_pool_sub": "equivalente solo aproximado, antes de la comisión del pool",
|
||||||
"mining_filter_all": "Todos",
|
"mining_filter_all": "Todos",
|
||||||
"mining_filter_tip_all": "Mostrar todas las ganancias",
|
"mining_filter_tip_all": "Mostrar todas las ganancias",
|
||||||
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
|
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
|
||||||
@@ -790,10 +929,12 @@
|
|||||||
"mining_open_in_explorer": "Abrir en explorador",
|
"mining_open_in_explorer": "Abrir en explorador",
|
||||||
"mining_payout_address": "Dirección de Pago",
|
"mining_payout_address": "Dirección de Pago",
|
||||||
"mining_payout_foreign": "⚠ Esta dirección de pago no está en tu cartera actual — las recompensas minadas irían a otra cartera. Actualízala si cambiaste de cartera.",
|
"mining_payout_foreign": "⚠ Esta dirección de pago no está en tu cartera actual — las recompensas minadas irían a otra cartera. Actualízala si cambiaste de cartera.",
|
||||||
|
"mining_payout_invalid": "No es una dirección DragonX válida — corrígela antes de empezar, o se pierden las recompensas de minería.",
|
||||||
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
|
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
|
||||||
"mining_pool": "Pool",
|
"mining_pool": "Pool",
|
||||||
"mining_pool_fee": "Comisión",
|
"mining_pool_fee": "Comisión",
|
||||||
"mining_pool_hashrate": "Hashrate del Pool",
|
"mining_pool_hashrate": "Hashrate del Pool",
|
||||||
|
"mining_pool_needs_payout_tooltip": "Ingrese primero una dirección de pago (genere una dirección Z)",
|
||||||
"mining_pool_url": "URL del Pool",
|
"mining_pool_url": "URL del Pool",
|
||||||
"mining_pools_header": "POOLS",
|
"mining_pools_header": "POOLS",
|
||||||
"mining_recent_blocks": "BLOQUES RECIENTES",
|
"mining_recent_blocks": "BLOQUES RECIENTES",
|
||||||
@@ -823,6 +964,9 @@
|
|||||||
"mining_syncing_tooltip": "El blockchain está sincronizando...",
|
"mining_syncing_tooltip": "El blockchain está sincronizando...",
|
||||||
"mining_tag": " · Minería",
|
"mining_tag": " · Minería",
|
||||||
"mining_threads": "Hilos de Minería",
|
"mining_threads": "Hilos de Minería",
|
||||||
|
"mining_threads_input_tooltip": "Escribe un número exacto de hilos (pulsa Enter para aplicar)",
|
||||||
|
"mining_threads_minus_tooltip": "Menos hilos",
|
||||||
|
"mining_threads_plus_tooltip": "Más hilos",
|
||||||
"mining_to_save": "para guardar",
|
"mining_to_save": "para guardar",
|
||||||
"mining_today": "Hoy",
|
"mining_today": "Hoy",
|
||||||
"mining_uptime": "Tiempo activo",
|
"mining_uptime": "Tiempo activo",
|
||||||
@@ -849,6 +993,11 @@
|
|||||||
"no_transactions": "No se encontraron transacciones",
|
"no_transactions": "No se encontraron transacciones",
|
||||||
"no_transactions_yet": "Aún no hay transacciones",
|
"no_transactions_yet": "Aún no hay transacciones",
|
||||||
"node": "NODO",
|
"node": "NODO",
|
||||||
|
"node_banner_crashed_title": "El nodo se detuvo inesperadamente",
|
||||||
|
"node_banner_lite_open_failed": "No se pudo abrir tu monedero",
|
||||||
|
"node_banner_offline_title": "No conectado al nodo DragonX",
|
||||||
|
"node_banner_reconnect": "Reconectar",
|
||||||
|
"node_banner_restart": "Reiniciar nodo",
|
||||||
"node_security": "NODO Y SEGURIDAD",
|
"node_security": "NODO Y SEGURIDAD",
|
||||||
"noise": "Ruido",
|
"noise": "Ruido",
|
||||||
"not_connected": "No conectado al daemon...",
|
"not_connected": "No conectado al daemon...",
|
||||||
@@ -972,11 +1121,12 @@
|
|||||||
"portfolio_spark_min": "Minuto",
|
"portfolio_spark_min": "Minuto",
|
||||||
"portfolio_spark_month": "Mes",
|
"portfolio_spark_month": "Mes",
|
||||||
"portfolio_spark_week": "Semana",
|
"portfolio_spark_week": "Semana",
|
||||||
"portfolio_style_compact": "Filas compactas",
|
"portfolio_style_compact": "Tabla",
|
||||||
"portfolio_style_detailed": "Filas detalladas",
|
"portfolio_style_detailed": "Tarjetas",
|
||||||
"portfolio_style_featured": "Filas destacadas",
|
"portfolio_style_featured": "Destacado",
|
||||||
"portfolio_style_label": "Estilo de cartera",
|
"portfolio_style_label": "Estilo de cartera",
|
||||||
"portfolio_untitled": "Sin título",
|
"portfolio_untitled": "Sin título",
|
||||||
|
"portfolio_wallet_loading": "Espera a que la cartera termine de cargar para añadir un grupo.",
|
||||||
"price_chart": "Gráfico de Precios",
|
"price_chart": "Gráfico de Precios",
|
||||||
"privacy_great": "¡Excelente privacidad!",
|
"privacy_great": "¡Excelente privacidad!",
|
||||||
"privacy_low": "Privacidad baja: protege los fondos",
|
"privacy_low": "Privacidad baja: protege los fondos",
|
||||||
@@ -986,6 +1136,8 @@
|
|||||||
"qr_failed": "Error al generar código QR",
|
"qr_failed": "Error al generar código QR",
|
||||||
"qr_title": "Código QR",
|
"qr_title": "Código QR",
|
||||||
"qr_unavailable": "QR no disponible",
|
"qr_unavailable": "QR no disponible",
|
||||||
|
"quick_receive": "Recepción rápida",
|
||||||
|
"quick_send": "Envío rápido",
|
||||||
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
||||||
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
||||||
"ram_system_gb": "Sistema: %.1f / %.0f GB",
|
"ram_system_gb": "Sistema: %.1f / %.0f GB",
|
||||||
@@ -1035,6 +1187,7 @@
|
|||||||
"rpc_connection": "Conexión RPC...",
|
"rpc_connection": "Conexión RPC...",
|
||||||
"rpc_host": "Host RPC",
|
"rpc_host": "Host RPC",
|
||||||
"rpc_pass": "Contraseña",
|
"rpc_pass": "Contraseña",
|
||||||
|
"rpc_plaintext_remote_warning": "El RPC remoto está usando HTTP sin cifrar. Agregue rpctls=1 a DRAGONX.conf si su daemon admite TLS.",
|
||||||
"rpc_port": "Puerto",
|
"rpc_port": "Puerto",
|
||||||
"rpc_user": "Usuario",
|
"rpc_user": "Usuario",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
@@ -1049,6 +1202,8 @@
|
|||||||
"sb_connecting_external": "Conectando a daemon externo...",
|
"sb_connecting_external": "Conectando a daemon externo...",
|
||||||
"sb_connecting_generic": "Conectando al daemon...",
|
"sb_connecting_generic": "Conectando al daemon...",
|
||||||
"sb_daemon_crashed": "El daemon se bloqueó %d veces",
|
"sb_daemon_crashed": "El daemon se bloqueó %d veces",
|
||||||
|
"sb_daemon_extract_failed": "No se pudieron escribir los archivos del daemon: comprueba el espacio libre en disco y los permisos.",
|
||||||
|
"sb_daemon_files_failed": "No se pudieron escribir los archivos del daemon en %s: comprueba el espacio libre en disco y los permisos.",
|
||||||
"sb_daemon_not_found": "Daemon no encontrado",
|
"sb_daemon_not_found": "Daemon no encontrado",
|
||||||
"sb_daemon_start_failed": "No se pudo iniciar dragonxd",
|
"sb_daemon_start_failed": "No se pudo iniciar dragonxd",
|
||||||
"sb_dragonxd_running": "dragonxd ejecutándose",
|
"sb_dragonxd_running": "dragonxd ejecutándose",
|
||||||
@@ -1064,6 +1219,7 @@
|
|||||||
"sb_net_mhs": "Red: %.2f MH/s",
|
"sb_net_mhs": "Red: %.2f MH/s",
|
||||||
"sb_no_conf": "DRAGONX.conf no encontrado",
|
"sb_no_conf": "DRAGONX.conf no encontrado",
|
||||||
"sb_peers": "Pares: %zu",
|
"sb_peers": "Pares: %zu",
|
||||||
|
"sb_plaintext_remote_blocked": "Se rechaza enviar credenciales RPC en texto plano a un host remoto. Añade rpcallowplaintext=1 a DRAGONX.conf para permitirlo, o habilita TLS con rpctls=1.",
|
||||||
"sb_rescanning": "Reescaneando",
|
"sb_rescanning": "Reescaneando",
|
||||||
"sb_rescanning_pct": "Reescaneando %.0f%%",
|
"sb_rescanning_pct": "Reescaneando %.0f%%",
|
||||||
"sb_restarting_daemon": "Reiniciando daemon...",
|
"sb_restarting_daemon": "Reiniciando daemon...",
|
||||||
@@ -1077,6 +1233,7 @@
|
|||||||
"sb_waiting_daemon_err": "Esperando a dragonxd — %s",
|
"sb_waiting_daemon_err": "Esperando a dragonxd — %s",
|
||||||
"sb_warming_up": "Calentando...",
|
"sb_warming_up": "Calentando...",
|
||||||
"sb_witness_cache": "Reconstruyendo testigos",
|
"sb_witness_cache": "Reconstruyendo testigos",
|
||||||
|
"scale_effects": "ESCALA Y EFECTOS",
|
||||||
"screenshot_open_dir": "Abrir ubicación",
|
"screenshot_open_dir": "Abrir ubicación",
|
||||||
"screenshot_sweep": "Ejecutar barrido de capturas",
|
"screenshot_sweep": "Ejecutar barrido de capturas",
|
||||||
"screenshot_sweep_desc": "Recorre cada tema en cada pestaña y guarda una captura de pantalla de cada una en subcarpetas por pestaña dentro de la carpeta de capturas del directorio de configuración (sobrescribiendo el barrido anterior). Se ejecuta durante unos segundos.",
|
"screenshot_sweep_desc": "Recorre cada tema en cada pestaña y guarda una captura de pantalla de cada una en subcarpetas por pestaña dentro de la carpeta de capturas del directorio de configuración (sobrescribiendo el barrido anterior). Se ejecuta durante unos segundos.",
|
||||||
@@ -1141,6 +1298,7 @@
|
|||||||
"send_tooltip_not_connected": "No conectado al daemon",
|
"send_tooltip_not_connected": "No conectado al daemon",
|
||||||
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
|
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
|
||||||
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
|
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
|
||||||
|
"send_tooltip_view_only": "Dirección de solo vista — sin clave de gasto, no se puede enviar",
|
||||||
"send_total": "Total",
|
"send_total": "Total",
|
||||||
"send_transaction": "Enviar Transacción",
|
"send_transaction": "Enviar Transacción",
|
||||||
"send_tx_failed": "Error en la transacción",
|
"send_tx_failed": "Error en la transacción",
|
||||||
@@ -1160,16 +1318,16 @@
|
|||||||
"sent_filter": "Enviado",
|
"sent_filter": "Enviado",
|
||||||
"sent_type": "Enviado",
|
"sent_type": "Enviado",
|
||||||
"sent_upper": "ENVIADO",
|
"sent_upper": "ENVIADO",
|
||||||
"set_label": "Establecer Etiqueta...",
|
"set_label": "Establecer Etiqueta",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.",
|
"settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.",
|
||||||
"settings_acrylic_level": "Nivel de acrílico:",
|
"settings_acrylic_level": "Nivel de acrílico:",
|
||||||
"settings_address_book": "Libreta de direcciones...",
|
"settings_address_book": "Libreta de direcciones…",
|
||||||
"settings_auto_detected": "Autodetectado de DRAGONX.conf",
|
"settings_auto_detected": "Autodetectado de DRAGONX.conf",
|
||||||
"settings_auto_lock": "BLOQUEO AUTOMÁTICO",
|
"settings_auto_lock": "BLOQUEO AUTOMÁTICO",
|
||||||
"settings_auto_shield_desc": "Mover automáticamente fondos transparentes a direcciones blindadas",
|
"settings_auto_shield_desc": "Mover automáticamente fondos transparentes a direcciones blindadas",
|
||||||
"settings_auto_shield_funds": "Blindar fondos transparentes automáticamente",
|
"settings_auto_shield_funds": "Blindar fondos transparentes automáticamente",
|
||||||
"settings_backup": "Respaldo...",
|
"settings_backup": "Respaldo…",
|
||||||
"settings_block_explorer_urls": "URLs del explorador de bloques",
|
"settings_block_explorer_urls": "URLs del explorador de bloques",
|
||||||
"settings_builtin": "Integrado",
|
"settings_builtin": "Integrado",
|
||||||
"settings_change_passphrase": "Cambiar contraseña",
|
"settings_change_passphrase": "Cambiar contraseña",
|
||||||
@@ -1180,60 +1338,71 @@
|
|||||||
"settings_configure_explorer": "Configurar enlaces de explorador de bloques externo",
|
"settings_configure_explorer": "Configurar enlaces de explorador de bloques externo",
|
||||||
"settings_configure_rpc": "Configurar conexión al daemon dragonxd",
|
"settings_configure_rpc": "Configurar conexión al daemon dragonxd",
|
||||||
"settings_connection": "Conexión",
|
"settings_connection": "Conexión",
|
||||||
|
"settings_copy_diagnostics": "Copiar diagnósticos",
|
||||||
"settings_copyright": "Copyright 2024-2026 Desarrolladores de DragonX | Licencia GPLv3",
|
"settings_copyright": "Copyright 2024-2026 Desarrolladores de DragonX | Licencia GPLv3",
|
||||||
"settings_custom": "Personalizado",
|
"settings_custom": "Personalizado",
|
||||||
"settings_data_dir": "Dir. de datos:",
|
"settings_data_dir": "Dir. de datos",
|
||||||
"settings_debug_changed": "Categorías de depuración cambiadas — reinicie el daemon para aplicar",
|
"settings_debug_changed": "Categorías de depuración cambiadas — reinicie el daemon para aplicar",
|
||||||
"settings_debug_restart_note": "Los cambios surten efecto después de reiniciar el daemon.",
|
"settings_debug_restart_note": "Los cambios surten efecto después de reiniciar el daemon.",
|
||||||
"settings_debug_select": "Seleccione categorías para habilitar el registro de depuración del daemon (flags -debug=).",
|
"settings_debug_select": "Seleccione categorías para habilitar el registro de depuración del daemon (flags -debug=).",
|
||||||
|
"settings_diagnostics_copied": "Diagnósticos copiados al portapapeles",
|
||||||
"settings_encrypt_first_pin": "Primero cifre la billetera para habilitar el PIN",
|
"settings_encrypt_first_pin": "Primero cifre la billetera para habilitar el PIN",
|
||||||
"settings_encrypt_wallet": "Cifrar billetera",
|
"settings_encrypt_wallet": "Cifrar billetera",
|
||||||
"settings_explorer_hint": "Las URLs deben incluir una barra final. Se añadirá el txid/dirección.",
|
"settings_explorer_hint": "Las URLs deben incluir una barra final. Se añadirá el txid/dirección.",
|
||||||
"settings_export_all": "Exportar todo...",
|
"settings_export_all": "Exportar todo…",
|
||||||
"settings_export_csv": "Exportar CSV...",
|
"settings_export_csv": "Exportar CSV…",
|
||||||
"settings_export_key": "Exportar clave...",
|
"settings_export_key": "Exportar clave…",
|
||||||
"settings_gradient_bg": "Fondo degradado",
|
"settings_gradient_bg": "Fondo degradado",
|
||||||
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
|
"settings_gradient_desc": "Reemplazar fondos con texturas por degradados suaves",
|
||||||
"settings_idle_after": "después de",
|
"settings_idle_after": "después de",
|
||||||
"settings_import_key": "Importar Clave Privada...",
|
"settings_import_key": "Importar Clave Privada…",
|
||||||
"settings_import_viewkey": "Importar clave de visualización...",
|
"settings_import_viewkey": "Importar clave de visualización…",
|
||||||
"settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
|
"settings_language_note": "Nota: Parte del texto requiere reinicio para actualizarse",
|
||||||
"settings_lock_now": "Bloquear ahora",
|
"settings_lock_now": "Bloquear ahora",
|
||||||
"settings_locked": "Bloqueado",
|
"settings_locked": "Bloqueado",
|
||||||
"settings_merge_to_address": "Fusionar a dirección...",
|
"settings_merge_to_address": "Fusionar a dirección…",
|
||||||
"settings_noise_opacity": "Opacidad de ruido:",
|
"settings_noise_opacity": "Opacidad de ruido:",
|
||||||
|
"settings_not_connected": "No conectado al daemon",
|
||||||
"settings_not_encrypted": "Sin cifrar",
|
"settings_not_encrypted": "Sin cifrar",
|
||||||
"settings_not_found": "No encontrado",
|
"settings_not_found": "No encontrado",
|
||||||
"settings_open_app_dir": "Abrir carpeta de la aplicación",
|
"settings_open_app_dir": "Abrir carpeta de la aplicación",
|
||||||
"settings_open_data_dir": "Abrir carpeta de datos",
|
"settings_open_data_dir": "Abrir carpeta de datos",
|
||||||
|
"settings_open_log_folder": "Abrir carpeta de registros",
|
||||||
"settings_other": "Otros",
|
"settings_other": "Otros",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "Privacidad",
|
"settings_privacy": "Privacidad",
|
||||||
"settings_quick_unlock_pin": "PIN de desbloqueo rápido",
|
"settings_quick_unlock_pin": "PIN de desbloqueo rápido",
|
||||||
"settings_reduce_transparency": "Reducir transparencia",
|
"settings_reduce_transparency": "Reducir transparencia",
|
||||||
|
"settings_reloaded": "Configuración recargada desde el disco",
|
||||||
"settings_remove_encryption": "Quitar cifrado",
|
"settings_remove_encryption": "Quitar cifrado",
|
||||||
"settings_remove_pin": "Quitar PIN",
|
"settings_remove_pin": "Quitar PIN",
|
||||||
"settings_request_payment": "Solicitar pago...",
|
"settings_request_payment": "Solicitar pago…",
|
||||||
"settings_rescan_desc": "Reescanear la cadena de bloques en busca de transacciones faltantes",
|
"settings_rescan_desc": "Reescanear la cadena de bloques en busca de transacciones faltantes",
|
||||||
"settings_restart_daemon": "Reiniciar daemon",
|
"settings_restart_daemon": "Reiniciar daemon",
|
||||||
"settings_rpc_connection": "Conexión RPC",
|
"settings_rpc_connection": "Conexión RPC",
|
||||||
|
"settings_rpc_error_prefix": "Error de RPC: ",
|
||||||
"settings_rpc_note": "Nota: Los ajustes de conexión se detectan automáticamente desde DRAGONX.conf",
|
"settings_rpc_note": "Nota: Los ajustes de conexión se detectan automáticamente desde DRAGONX.conf",
|
||||||
|
"settings_rpc_ok": "Conexión RPC correcta",
|
||||||
"settings_save_shielded_desc": "Almacena transacciones z-addr en un archivo local para visualización",
|
"settings_save_shielded_desc": "Almacena transacciones z-addr en un archivo local para visualización",
|
||||||
"settings_save_shielded_local": "Guardar historial de transacciones blindadas localmente",
|
"settings_save_shielded_local": "Guardar historial de transacciones blindadas localmente",
|
||||||
|
"settings_saved": "Configuración guardada",
|
||||||
"settings_set_pin": "Establecer PIN",
|
"settings_set_pin": "Establecer PIN",
|
||||||
"settings_shield_mining": "Blindar minería...",
|
"settings_shield_mining": "Blindar minería…",
|
||||||
"settings_solid_colors_desc": "Usar colores sólidos en lugar de efectos de desenfoque (accesibilidad)",
|
"settings_solid_colors_desc": "Usar colores sólidos en lugar de efectos de desenfoque (accesibilidad)",
|
||||||
|
"settings_theme_refreshed": "Lista de temas actualizada",
|
||||||
"settings_tor_desc": "Enrutar todas las conexiones a través de Tor para mayor privacidad",
|
"settings_tor_desc": "Enrutar todas las conexiones a través de Tor para mayor privacidad",
|
||||||
"settings_unlocked": "Desbloqueado",
|
"settings_unlocked": "Desbloqueado",
|
||||||
"settings_use_tor_network": "Usar Tor para conexiones de red",
|
"settings_use_tor_network": "Usar Tor para conexiones de red",
|
||||||
"settings_validate_address": "Validar dirección...",
|
"settings_validate_address": "Validar dirección…",
|
||||||
"settings_visual_effects": "Efectos visuales",
|
"settings_visual_effects": "Efectos visuales",
|
||||||
"settings_wallet_file_size": "Tamaño del archivo de billetera: %s",
|
"settings_wallet_file_size": "Tamaño del archivo de billetera: %s",
|
||||||
"settings_wallet_info": "Información de billetera",
|
"settings_wallet_info": "Información de billetera",
|
||||||
"settings_wallet_location": "Ubicación de billetera: %s",
|
"settings_wallet_location": "Ubicación de billetera: %s",
|
||||||
"settings_wallet_maintenance": "Mantenimiento de billetera",
|
"settings_wallet_maintenance": "Mantenimiento de billetera",
|
||||||
"settings_wallet_not_found": "Archivo de billetera no encontrado",
|
"settings_wallet_not_found": "Archivo de billetera no encontrado",
|
||||||
"settings_wallet_size_label": "Tamaño de billetera:",
|
"settings_wallet_size_label": "Tamaño de billetera",
|
||||||
|
"settings_ztx_cleared": "Historial de transacciones Z borrado",
|
||||||
|
"settings_ztx_not_found": "No se encontró archivo de historial",
|
||||||
"setup_wizard": "Asistente de Configuración",
|
"setup_wizard": "Asistente de Configuración",
|
||||||
"share": "Compartir",
|
"share": "Compartir",
|
||||||
"shield_check_status": "Verificar Estado",
|
"shield_check_status": "Verificar Estado",
|
||||||
@@ -1292,6 +1461,23 @@
|
|||||||
"sweep_to": "Barrido a:",
|
"sweep_to": "Barrido a:",
|
||||||
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
|
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
|
||||||
"sweep_tx": "Transacción:",
|
"sweep_tx": "Transacción:",
|
||||||
|
"switch_corrupt_body": "Esta cartera parece dañada: el nodo no pudo abrirla. Restáurala desde una copia de seguridad, vuelve a crearla o intenta repararla.",
|
||||||
|
"switch_corrupt_repair": "Intentar reparar (salvage)",
|
||||||
|
"switch_progress_background": "Continuar en segundo plano",
|
||||||
|
"switch_progress_default_wallet": "Cartera predeterminada",
|
||||||
|
"switch_progress_elapsed": "Transcurrido",
|
||||||
|
"switch_progress_external_wallet": "Cartera externa",
|
||||||
|
"switch_progress_failed_title": "Error al cambiar de cartera",
|
||||||
|
"switch_progress_from_label": "desde",
|
||||||
|
"switch_progress_hint": "Un apagado ordenado puede tardar hasta un minuto.",
|
||||||
|
"switch_progress_reconnecting": "Reconectando",
|
||||||
|
"switch_progress_starting": "Iniciando el nodo con la nueva cartera",
|
||||||
|
"switch_progress_stopping": "Deteniendo el nodo actual",
|
||||||
|
"switch_progress_title": "Cambiando de cartera",
|
||||||
|
"switch_stopnode_body": "Cambiar de cartera reinicia el nodo con la cartera seleccionada. El nodo en ejecución se detendrá y se reiniciará con la nueva cartera; si lo dejaste en marcha a propósito, volverá a iniciarse automáticamente.",
|
||||||
|
"switch_stopnode_confirm": "Detener nodo y cambiar",
|
||||||
|
"switch_stopnode_title": "¿Detener el nodo en ejecución?",
|
||||||
|
"switch_stopnode_warn": "Ya hay un nodo en ejecución que esta cartera no inició.",
|
||||||
"syncing": "Sincronizando...",
|
"syncing": "Sincronizando...",
|
||||||
"t_address": "Dirección T",
|
"t_address": "Dirección T",
|
||||||
"t_addresses": "Direcciones T",
|
"t_addresses": "Direcciones T",
|
||||||
@@ -1299,6 +1485,7 @@
|
|||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"theme_effects": "Efectos de tema",
|
"theme_effects": "Efectos de tema",
|
||||||
"theme_language": "TEMA E IDIOMA",
|
"theme_language": "TEMA E IDIOMA",
|
||||||
|
"tile_click_to_open": "Clic para abrir",
|
||||||
"time_days_ago": "hace %d días",
|
"time_days_ago": "hace %d días",
|
||||||
"time_hours_ago": "hace %d horas",
|
"time_hours_ago": "hace %d horas",
|
||||||
"time_minutes_ago": "hace %d minutos",
|
"time_minutes_ago": "hace %d minutos",
|
||||||
@@ -1313,7 +1500,9 @@
|
|||||||
"to_upper": "PARA",
|
"to_upper": "PARA",
|
||||||
"tools": "HERRAMIENTAS",
|
"tools": "HERRAMIENTAS",
|
||||||
"tools_actions": "Herramientas y Acciones...",
|
"tools_actions": "Herramientas y Acciones...",
|
||||||
|
"tools_actions_hdr": "HERRAMIENTAS Y ACCIONES",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"total_balance_label": "Saldo Total",
|
||||||
"transaction_id": "ID DE TRANSACCIÓN",
|
"transaction_id": "ID DE TRANSACCIÓN",
|
||||||
"transaction_sent": "Transacción enviada exitosamente",
|
"transaction_sent": "Transacción enviada exitosamente",
|
||||||
"transaction_sent_msg": "¡Transacción enviada!",
|
"transaction_sent_msg": "¡Transacción enviada!",
|
||||||
@@ -1335,13 +1524,24 @@
|
|||||||
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad",
|
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad",
|
||||||
"tt_backup": "Crear una copia de seguridad de su wallet.dat",
|
"tt_backup": "Crear una copia de seguridad de su wallet.dat",
|
||||||
"tt_block_explorer": "Abrir el explorador de bloques DragonX en su navegador",
|
"tt_block_explorer": "Abrir el explorador de bloques DragonX en su navegador",
|
||||||
"tt_blur": "Cantidad de desenfoque (0%% = apagado, 100%% = máximo)",
|
"tt_blur": "Cantidad de desenfoque (0% = apagado, 100% = máximo)",
|
||||||
"tt_change_pass": "Cambiar la contraseña de cifrado de la billetera",
|
"tt_change_pass": "Cambiar la contraseña de cifrado de la billetera",
|
||||||
"tt_change_pin": "Cambiar su PIN de desbloqueo",
|
"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_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_copy_diagnostics": "Copia al portapapeles un resumen para soporte (versión, estado de daemon/cartera/registros, sin datos secretos)",
|
||||||
"tt_custom_fees": "Habilitar entrada manual de comisiones al enviar transacciones",
|
"tt_custom_fees": "Habilitar entrada manual de comisiones al enviar transacciones",
|
||||||
"tt_custom_theme": "Tema personalizado activo",
|
"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_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_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_collapse": "Colapsar opciones de registro de depuración",
|
||||||
"tt_debug_expand": "Expandir opciones de registro de depuración",
|
"tt_debug_expand": "Expandir opciones de registro de depuración",
|
||||||
@@ -1359,15 +1559,39 @@
|
|||||||
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
||||||
"tt_language": "Idioma de la interfaz de la billetera",
|
"tt_language": "Idioma de la interfaz de la billetera",
|
||||||
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
||||||
|
"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_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_lock": "Bloquear la billetera inmediatamente",
|
||||||
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
|
"tt_low_spec": "Desactivar todos los efectos visuales pesados\\nAtajo: Ctrl+Shift+Down",
|
||||||
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
|
"tt_merge": "Consolidar múltiples UTXOs en una dirección",
|
||||||
"tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)",
|
"tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)",
|
||||||
"tt_noise": "Intensidad de textura granulada (0%% = apagado, 100%% = máximo)",
|
"tt_noise": "Intensidad de textura granulada (0% = apagado, 100% = máximo)",
|
||||||
"tt_open_app_dir": "Abrir la carpeta de ObsidianDragon (configuración, temas, registros) en el explorador de archivos",
|
"tt_open_app_dir": "Abrir la carpeta de ObsidianDragon (configuración, temas, registros) en el explorador de archivos",
|
||||||
"tt_open_data_dir": "Abre en el gestor de archivos la carpeta con los datos de tu cartera y de la blockchain",
|
"tt_open_data_dir": "Abre en el gestor de archivos la carpeta con los datos de tu cartera y de la blockchain",
|
||||||
"tt_open_dir": "Clic para abrir en explorador de archivos",
|
"tt_open_dir": "Clic para abrir en explorador de archivos",
|
||||||
|
"tt_open_log_folder": "Abre la carpeta que contiene los registros de depuración y de fallos",
|
||||||
"tt_reduce_motion": "Desactivar transiciones animadas y lerp de saldo para accesibilidad",
|
"tt_reduce_motion": "Desactivar transiciones animadas y lerp de saldo para accesibilidad",
|
||||||
"tt_remove_encrypt": "Quitar cifrado y almacenar la billetera sin protección",
|
"tt_remove_encrypt": "Quitar cifrado y almacenar la billetera sin protección",
|
||||||
"tt_remove_pin": "Quitar PIN y requerir contraseña para desbloquear",
|
"tt_remove_pin": "Quitar PIN y requerir contraseña para desbloquear",
|
||||||
@@ -1380,12 +1604,17 @@
|
|||||||
"tt_rpc_host": "Nombre de host del daemon DragonX",
|
"tt_rpc_host": "Nombre de host del daemon DragonX",
|
||||||
"tt_rpc_pass": "Contraseña de autenticación RPC",
|
"tt_rpc_pass": "Contraseña de autenticación RPC",
|
||||||
"tt_rpc_port": "Puerto para conexiones RPC del daemon",
|
"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_rpc_user": "Nombre de usuario de autenticación RPC",
|
||||||
"tt_save_settings": "Guardar todas las configuraciones en disco",
|
"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_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_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_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_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_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_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",
|
"tt_shield_mining": "Mover recompensas de minería transparentes a una dirección blindada",
|
||||||
@@ -1397,13 +1626,14 @@
|
|||||||
"tt_theme_hotkey": "Atajo: Ctrl+Izquierda/Derecha para cambiar temas",
|
"tt_theme_hotkey": "Atajo: Ctrl+Izquierda/Derecha para cambiar temas",
|
||||||
"tt_tor": "Enrutar conexiones del daemon a través de la red Tor para anonimato",
|
"tt_tor": "Enrutar conexiones del daemon a través de la red Tor para anonimato",
|
||||||
"tt_tx_url": "URL base para ver transacciones en un explorador de bloques",
|
"tt_tx_url": "URL base para ver transacciones en un explorador de bloques",
|
||||||
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100%% = totalmente opaco, menor = más transparente)",
|
"tt_ui_opacity": "Opacidad de tarjetas y barra lateral (100% = totalmente opaco, menor = más transparente)",
|
||||||
"tt_validate": "Comprobar si una dirección DragonX es válida",
|
"tt_validate": "Comprobar si una dirección DragonX es válida",
|
||||||
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
|
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
|
||||||
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
|
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
|
||||||
"tt_website": "Abrir el sitio web de DragonX",
|
"tt_website": "Abrir el sitio web de DragonX",
|
||||||
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
"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",
|
"tt_wizard": "Volver a ejecutar el asistente de configuración inicial\\nEl daemon será reiniciado",
|
||||||
|
"tx_chat_badge": "Mensaje",
|
||||||
"tx_confirmations": "%d confirmaciones",
|
"tx_confirmations": "%d confirmaciones",
|
||||||
"tx_details_title": "Detalles de Transacción",
|
"tx_details_title": "Detalles de Transacción",
|
||||||
"tx_from_address": "Dirección Origen:",
|
"tx_from_address": "Dirección Origen:",
|
||||||
@@ -1449,6 +1679,7 @@
|
|||||||
"validate_not_mine": "No es propiedad de esta cartera",
|
"validate_not_mine": "No es propiedad de esta cartera",
|
||||||
"validate_ownership": "Propiedad:",
|
"validate_ownership": "Propiedad:",
|
||||||
"validate_results": "Resultados:",
|
"validate_results": "Resultados:",
|
||||||
|
"validate_results_placeholder": "Los resultados aparecerán aquí",
|
||||||
"validate_shielded_type": "Protegida (dirección z)",
|
"validate_shielded_type": "Protegida (dirección z)",
|
||||||
"validate_status": "Estado:",
|
"validate_status": "Estado:",
|
||||||
"validate_title": "Validar Dirección",
|
"validate_title": "Validar Dirección",
|
||||||
@@ -1472,6 +1703,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ Buscar wallets en otra carpeta…",
|
"wallets_add_folder_toggle": "+ Buscar wallets en otra carpeta…",
|
||||||
"wallets_badge_encrypted": "Cifrada (protegida con contraseña)",
|
"wallets_badge_encrypted": "Cifrada (protegida con contraseña)",
|
||||||
"wallets_badge_encrypted_short": "Cifrada",
|
"wallets_badge_encrypted_short": "Cifrada",
|
||||||
|
"wallets_badge_hd": "Cartera HD: no se puede confirmar la frase semilla sin abrirla",
|
||||||
|
"wallets_badge_hd_short": "Cartera HD",
|
||||||
"wallets_badge_legacy": "Billetera heredada (sin frase semilla)",
|
"wallets_badge_legacy": "Billetera heredada (sin frase semilla)",
|
||||||
"wallets_badge_legacy_short": "Heredada",
|
"wallets_badge_legacy_short": "Heredada",
|
||||||
"wallets_badge_seed": "Billetera con frase semilla (HD)",
|
"wallets_badge_seed": "Billetera con frase semilla (HD)",
|
||||||
@@ -1587,6 +1820,7 @@
|
|||||||
"xmrig_loading_releases": "Cargando versiones…",
|
"xmrig_loading_releases": "Cargando versiones…",
|
||||||
"xmrig_none": "ninguno",
|
"xmrig_none": "ninguno",
|
||||||
"xmrig_reinstall": "Reinstalar",
|
"xmrig_reinstall": "Reinstalar",
|
||||||
|
"xmrig_releases": "versiones de xmrig",
|
||||||
"xmrig_stop_mining_first": "Detén la minería antes de actualizar el minero.",
|
"xmrig_stop_mining_first": "Detén la minería antes de actualizar el minero.",
|
||||||
"xmrig_unavailable_body": "No hay ninguna versión del minero disponible para esta plataforma.",
|
"xmrig_unavailable_body": "No hay ninguna versión del minero disponible para esta plataforma.",
|
||||||
"xmrig_unavailable_title": "Actualizaciones del minero no disponibles",
|
"xmrig_unavailable_title": "Actualizaciones del minero no disponibles",
|
||||||
|
|||||||
278
res/lang/fr.json
278
res/lang/fr.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "AVANCÉ",
|
"advanced": "AVANCÉ",
|
||||||
"advanced_effects": "Effets avancés...",
|
"advanced_effects": "Effets avancés...",
|
||||||
"ago": "passé",
|
"ago": "passé",
|
||||||
|
"alerts_clear": "Effacer l'historique des alertes",
|
||||||
|
"alerts_history_tooltip": "Alertes récentes",
|
||||||
|
"alerts_none": "Aucune alerte pour l'instant",
|
||||||
|
"alerts_recent": "ALERTES RÉCENTES",
|
||||||
"all_filter": "Tout",
|
"all_filter": "Tout",
|
||||||
"allow_custom_fees": "Autoriser les frais personnalisés",
|
"allow_custom_fees": "Autoriser les frais personnalisés",
|
||||||
"amount": "Montant",
|
"amount": "Montant",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender a bloqué le mineur",
|
"av_title": "Windows Defender a bloqué le mineur",
|
||||||
"available": "Disponible",
|
"available": "Disponible",
|
||||||
"backup_backing_up": "Sauvegarde en cours...",
|
"backup_backing_up": "Sauvegarde en cours...",
|
||||||
|
"backup_col_backup": "SAUVEGARDE",
|
||||||
|
"backup_col_export": "EXPORTER",
|
||||||
|
"backup_col_import": "IMPORTER ET RESTAURER",
|
||||||
"backup_create": "Créer une sauvegarde",
|
"backup_create": "Créer une sauvegarde",
|
||||||
"backup_created": "Sauvegarde du portefeuille créée",
|
"backup_created": "Sauvegarde du portefeuille créée",
|
||||||
"backup_data": "SAUVEGARDE & DONNÉES",
|
"backup_data": "SAUVEGARDE & DONNÉES",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "Solde",
|
"balance": "Solde",
|
||||||
"balance_history_collecting": "Historique du solde — collecte des données...",
|
"balance_history_collecting": "Historique du solde — collecte des données...",
|
||||||
"balance_layout": "Disposition du solde",
|
"balance_layout": "Disposition du solde",
|
||||||
|
"balance_layout_switched": "Disposition : %s",
|
||||||
|
"balance_mining_rate": "Minage %s",
|
||||||
"balance_shielded_fmt": "Blindé : %.8f",
|
"balance_shielded_fmt": "Blindé : %.8f",
|
||||||
|
"balance_syncing_pct": "Synchronisation %.1f%%",
|
||||||
"balance_transparent_fmt": "Transparent : %.8f",
|
"balance_transparent_fmt": "Transparent : %.8f",
|
||||||
"ban": "Bannir",
|
"ban": "Bannir",
|
||||||
"banned_peers": "Pairs bannis",
|
"banned_peers": "Pairs bannis",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "Vérification des sommes de contrôle...",
|
"bootstrap_verifying": "Vérification des sommes de contrôle...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat est protégé)",
|
"bootstrap_wallet_protected": "(wallet.dat est protégé)",
|
||||||
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
|
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
|
||||||
|
"byte_count_fmt": "%zu / %zu octets",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"change_pass_confirm": "Confirmer la nouvelle :",
|
"change_pass_confirm": "Confirmer la nouvelle :",
|
||||||
"change_pass_current": "Phrase secrète actuelle :",
|
"change_pass_current": "Phrase secrète actuelle :",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "Changer la phrase secrète",
|
"change_pass_title": "Changer la phrase secrète",
|
||||||
"characters": "caractères",
|
"characters": "caractères",
|
||||||
"chat": "Discussion",
|
"chat": "Discussion",
|
||||||
|
"chat_accent_amber": "Ambre",
|
||||||
|
"chat_accent_blue": "Bleu",
|
||||||
|
"chat_accent_green": "Vert",
|
||||||
|
"chat_accent_pink": "Rose",
|
||||||
|
"chat_accent_purple": "Violet",
|
||||||
|
"chat_accent_theme": "Thème",
|
||||||
|
"chat_add_contact": "Ajouter un contact",
|
||||||
|
"chat_awaiting_key": "En attente de réponse",
|
||||||
|
"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_cancel": "Annuler",
|
||||||
|
"chat_contact_added": "Contact ajouté — renommez-le dans Contacts",
|
||||||
"chat_contact_request": "demande de contact",
|
"chat_contact_request": "demande de contact",
|
||||||
|
"chat_copy_address_tip": "Cliquer pour copier l'adresse",
|
||||||
|
"chat_density_comfortable": "Confortable",
|
||||||
|
"chat_density_compact": "Compacte",
|
||||||
|
"chat_emoji_color": "Couleur",
|
||||||
|
"chat_emoji_mono": "Monochrome",
|
||||||
|
"chat_emoji_search": "Rechercher un emoji",
|
||||||
"chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.",
|
"chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.",
|
||||||
|
"chat_empty_start": "Commencez-en une avec « Nouvelle conversation ».",
|
||||||
|
"chat_empty_title": "Aucune conversation pour l'instant",
|
||||||
|
"chat_export": "Exporter le chat…",
|
||||||
|
"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",
|
||||||
|
"chat_jump_latest": "Récents",
|
||||||
|
"chat_len_over": "Message trop long",
|
||||||
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
|
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
|
||||||
"chat_new_button": "Nouvelle conversation",
|
"chat_mute": "Muet",
|
||||||
|
"chat_new_button": "Nouvelle discussion",
|
||||||
"chat_new_message": "Message",
|
"chat_new_message": "Message",
|
||||||
|
"chat_new_message_toast": "Nouveau message chiffré",
|
||||||
"chat_new_send": "Envoyer la demande",
|
"chat_new_send": "Envoyer la demande",
|
||||||
"chat_new_title": "Nouvelle conversation",
|
"chat_new_title": "Nouvelle discussion",
|
||||||
"chat_new_zaddr": "Adresse Z du destinataire",
|
"chat_new_zaddr": "Adresse Z du destinataire",
|
||||||
|
"chat_no_matches": "Aucune conversation ne correspond à votre recherche.",
|
||||||
|
"chat_no_z_contacts": "Aucun contact avec adresse blindée pour l'instant",
|
||||||
|
"chat_opt_bubble_accent": "Couleur de bulle",
|
||||||
|
"chat_opt_bubble_style": "Style de bulle",
|
||||||
|
"chat_opt_density": "Densité des messages",
|
||||||
|
"chat_opt_emoji": "Style d'emoji",
|
||||||
|
"chat_opt_enter_sends": "Entrée envoie le message",
|
||||||
|
"chat_opt_font_size": "Taille du texte",
|
||||||
|
"chat_opt_global_clock": "Format d'horloge global",
|
||||||
|
"chat_opt_poll": "Fréquence d'actualisation",
|
||||||
|
"chat_opt_timestamp": "Horodatage",
|
||||||
|
"chat_pick_contact": "Choisir dans les contacts…",
|
||||||
|
"chat_rename": "Renommer le contact",
|
||||||
|
"chat_rename_hint": "Nom du contact",
|
||||||
|
"chat_renamed": "Contact renommé",
|
||||||
|
"chat_retry": "Réessayer",
|
||||||
|
"chat_search": "Rechercher des conversations",
|
||||||
|
"chat_sec_appearance": "APPARENCE",
|
||||||
|
"chat_sec_messaging": "MESSAGERIE",
|
||||||
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",
|
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",
|
||||||
"chat_send": "Envoyer",
|
"chat_send": "Envoyer",
|
||||||
"chat_send_failed": "non envoyé",
|
"chat_send_failed": "non envoyé",
|
||||||
|
"chat_sending": "envoi…",
|
||||||
|
"chat_settings_done": "Terminé",
|
||||||
|
"chat_settings_section": "CHAT ET CONTACTS",
|
||||||
|
"chat_settings_tip": "Personnaliser le chat",
|
||||||
|
"chat_settings_title": "Paramètres du chat",
|
||||||
|
"chat_show_hidden": "Afficher masqués",
|
||||||
|
"chat_time_now": "à l'instant",
|
||||||
"chat_toast_compose_failed": "Impossible de composer le message (trop long ?).",
|
"chat_toast_compose_failed": "Impossible de composer le message (trop long ?).",
|
||||||
"chat_toast_lite_busy": "Un envoi est déjà en cours, ou aucun portefeuille n'est ouvert.",
|
"chat_toast_lite_busy": "Un envoi est déjà en cours, ou aucun portefeuille n'est ouvert.",
|
||||||
|
"chat_toast_need_funds": "Un petit solde blindé est nécessaire pour envoyer des messages (pour couvrir les frais).",
|
||||||
"chat_toast_no_zaddr": "Aucune adresse Z disponible pour envoyer le message.",
|
"chat_toast_no_zaddr": "Aucune adresse Z disponible pour envoyer le message.",
|
||||||
"chat_toast_not_connected": "Non connecté — message non envoyé.",
|
"chat_toast_not_connected": "Non connecté — message non envoyé.",
|
||||||
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
|
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
|
||||||
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
|
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
|
||||||
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
|
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
|
||||||
|
"chat_today": "Aujourd'hui",
|
||||||
|
"chat_ts_12h": "12 heures",
|
||||||
|
"chat_ts_24h": "24 heures",
|
||||||
|
"chat_ts_global": "Suivre global",
|
||||||
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_unhide": "Afficher",
|
||||||
|
"chat_unmute": "Réactiver",
|
||||||
|
"chat_verify_key": "Clé d'identité — comparez pour vérifier",
|
||||||
"chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.",
|
"chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.",
|
||||||
|
"chat_yesterday": "Hier",
|
||||||
"chat_you": "Vous",
|
"chat_you": "Vous",
|
||||||
"choose_icon": "Choisir une icône",
|
"choose_icon": "Choisir une icône",
|
||||||
"clear": "Effacer",
|
"clear": "Effacer",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "Cliquez pour copier l'adresse",
|
"click_copy_address": "Cliquez pour copier l'adresse",
|
||||||
"click_copy_uri": "Cliquez pour copier l'URI",
|
"click_copy_uri": "Cliquez pour copier l'URI",
|
||||||
"click_to_copy": "Cliquez pour copier",
|
"click_to_copy": "Cliquez pour copier",
|
||||||
|
"clock_format": "Format d'horloge",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"conf_count": "%d conf.",
|
"conf_count": "%d conf.",
|
||||||
"confirm_and_send": "Confirmer & Envoyer",
|
"confirm_and_send": "Confirmer & Envoyer",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "App",
|
"console_app": "App",
|
||||||
"console_auto_scroll": "Défilement auto",
|
"console_auto_scroll": "Défilement auto",
|
||||||
"console_available_commands": "Commandes disponibles :",
|
"console_available_commands": "Commandes disponibles :",
|
||||||
|
"console_backend_reference": "Référence des commandes du backend",
|
||||||
|
"console_backend_unavailable": "Aucun backend",
|
||||||
"console_capturing_output": "Capture de la sortie du daemon...",
|
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||||
|
"console_cat_advanced": "Avancé",
|
||||||
"console_cat_blockchain": "Blockchain",
|
"console_cat_blockchain": "Blockchain",
|
||||||
"console_cat_control": "Contrôle",
|
"console_cat_control": "Contrôle",
|
||||||
|
"console_cat_keys": "Clés et sécurité",
|
||||||
"console_cat_mining": "Minage",
|
"console_cat_mining": "Minage",
|
||||||
"console_cat_network": "Réseau",
|
"console_cat_network": "Réseau",
|
||||||
"console_cat_raw_transactions": "Transactions brutes",
|
"console_cat_raw_transactions": "Transactions brutes",
|
||||||
|
"console_cat_send": "Envoyer",
|
||||||
|
"console_cat_sync": "Synchronisation",
|
||||||
"console_cat_utility": "Utilitaires",
|
"console_cat_utility": "Utilitaires",
|
||||||
"console_cat_wallet": "Portefeuille",
|
"console_cat_wallet": "Portefeuille",
|
||||||
"console_clear": "Effacer",
|
"console_clear": "Effacer",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - Afficher ce message d'aide",
|
"console_help_help": " help - Afficher ce message d'aide",
|
||||||
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
||||||
"console_help_stop": " stop - Arrêter le daemon",
|
"console_help_stop": " stop - Arrêter le daemon",
|
||||||
|
"console_last_error": "Dernière erreur :",
|
||||||
"console_line_count": "%zu lignes",
|
"console_line_count": "%zu lignes",
|
||||||
"console_matches": "correspondances",
|
"console_matches": "correspondances",
|
||||||
"console_new_lines": "%d nouvelles lignes",
|
"console_new_lines": "%d nouvelles lignes",
|
||||||
"console_no_daemon": "Pas de daemon",
|
"console_no_daemon": "Pas de daemon",
|
||||||
|
"console_no_output": "(aucune sortie)",
|
||||||
"console_not_connected": "Erreur : Non connecté au daemon",
|
"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_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.",
|
||||||
"console_ref_builds": "Génère",
|
"console_ref_builds": "Génère",
|
||||||
"console_ref_cancel": "Annuler",
|
"console_ref_cancel": "Annuler",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "Exécuter %s maintenant ? C'est une commande à conséquences.",
|
"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_search_hint": "Rechercher par nom ou tâche…",
|
||||||
"console_ref_select_hint": "Sélectionnez une commande pour voir ce qu'elle fait.",
|
"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_reference": "Référence des commandes RPC",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "Scanline de la console",
|
"console_scanline": "Scanline de la console",
|
||||||
"console_search_commands": "Rechercher des commandes...",
|
"console_search_commands": "Rechercher des commandes...",
|
||||||
"console_select_all": "Tout sélectionner",
|
"console_select_all": "Tout sélectionner",
|
||||||
"console_show_app_output": "Afficher les lignes du journal du portefeuille [app]",
|
"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_daemon_output": "Afficher la sortie du daemon",
|
||||||
"console_show_errors_only": "Afficher uniquement les erreurs",
|
"console_show_errors_only": "Afficher uniquement les erreurs",
|
||||||
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "Arrêté",
|
"console_status_stopped": "Arrêté",
|
||||||
"console_status_stopping": "Arrêt",
|
"console_status_stopping": "Arrêt",
|
||||||
"console_status_unknown": "Inconnu",
|
"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_tab_completion": "Tab pour compléter",
|
||||||
"console_text_colors": "Couleurs du texte",
|
"console_text_colors": "Couleurs du texte",
|
||||||
"console_toggle_accents": "Basculer les accents de couleur des lignes",
|
"console_toggle_accents": "Basculer les accents de couleur des lignes",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "Activé : ce contact reste visible quel que soit le portefeuille chargé. Désactivé : il appartient au portefeuille actuel uniquement.",
|
"contact_global_tt": "Activé : ce contact reste visible quel que soit le portefeuille chargé. Désactivé : il appartient au portefeuille actuel uniquement.",
|
||||||
"contact_preview_addr": "L'adresse apparaîtra ici",
|
"contact_preview_addr": "L'adresse apparaîtra ici",
|
||||||
"contact_preview_name": "Nom du contact",
|
"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": "Contacts",
|
||||||
|
"contacts_avatar_shape": "Forme de l'avatar",
|
||||||
|
"contacts_list_scale": "Échelle de la liste",
|
||||||
"contacts_search_no_match": "Aucun contact correspondant",
|
"contacts_search_no_match": "Aucun contact correspondant",
|
||||||
"contacts_search_placeholder": "Rechercher des contacts...",
|
"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é !",
|
"copied": "Copié !",
|
||||||
"copy": "Copier",
|
"copy": "Copier",
|
||||||
"copy_address": "Copier l'adresse complète",
|
"copy_address": "Copier l'adresse complète",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "Intégré",
|
"daemon_bundled": "Intégré",
|
||||||
"daemon_install_bundled": "Installer la version intégrée",
|
"daemon_install_bundled": "Installer la version intégrée",
|
||||||
"daemon_installed": "Installé",
|
"daemon_installed": "Installé",
|
||||||
|
"daemon_maintenance_label": "MAINTENANCE",
|
||||||
"daemon_none_bundled": "aucun dans cette version",
|
"daemon_none_bundled": "aucun dans cette version",
|
||||||
"daemon_not_installed": "non installé",
|
"daemon_not_installed": "non installé",
|
||||||
"daemon_status_differ": "Le binaire installé diffère de la version intégrée.",
|
"daemon_status_differ": "Le binaire installé diffère de la version intégrée.",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "Dernière :",
|
"daemon_update_latest": "Dernière :",
|
||||||
"daemon_update_loading": "Chargement des versions…",
|
"daemon_update_loading": "Chargement des versions…",
|
||||||
"daemon_update_now": "Mettre à jour",
|
"daemon_update_now": "Mettre à jour",
|
||||||
|
"daemon_update_prompt_title": "Mettre à jour le démon du nœud ?",
|
||||||
"daemon_update_reinstall": "Réinstaller",
|
"daemon_update_reinstall": "Réinstaller",
|
||||||
"daemon_update_restart_note": "Redémarrez le daemon pour lancer la nouvelle version.",
|
"daemon_update_restart_note": "Redémarrez le daemon pour lancer la nouvelle version.",
|
||||||
"daemon_update_restart_now": "Redémarrer le daemon maintenant",
|
"daemon_update_restart_now": "Redémarrer le daemon maintenant",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "Le téléchargement est vérifié par rapport au SHA-256 publié de la version et à une signature ed25519 épinglée avant l'installation.",
|
"daemon_update_verify_note": "Le téléchargement est vérifié par rapport au SHA-256 publié de la version et à une signature ed25519 épinglée avant l'installation.",
|
||||||
"daemon_update_verifying": "Vérification…",
|
"daemon_update_verifying": "Vérification…",
|
||||||
"daemon_update_version": "Version :",
|
"daemon_update_version": "Version :",
|
||||||
|
"daemon_updates_label": "MISES À JOUR",
|
||||||
"daemon_version": "Daemon",
|
"daemon_version": "Daemon",
|
||||||
"dark": "Sombre",
|
"dark": "Sombre",
|
||||||
|
"data_stale_prefix": "Mis à jour",
|
||||||
|
"data_stale_tooltip": "Le solde est peut-être obsolète — le portefeuille n'a pas reçu de mise à jour récente. Vérifiez la connexion à votre nœud.",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"date_label": "Date :",
|
"date_label": "Date :",
|
||||||
"debug_logging": "JOURNALISATION DE DÉBOGAGE",
|
"debug_logging": "JOURNALISATION DE DÉBOGAGE",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "Télécharger Bootstrap",
|
"download_bootstrap": "Télécharger Bootstrap",
|
||||||
"dragonx_green": "DragonX (Vert)",
|
"dragonx_green": "DragonX (Vert)",
|
||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
|
"empty_wallet_keys_suffix": "clés",
|
||||||
|
"empty_wallet_open_manager": "Ouvrir le gestionnaire de portefeuilles",
|
||||||
|
"empty_wallet_restore": "Restaurer mon portefeuille",
|
||||||
|
"empty_wallet_salvage_body": "Ce portefeuille est vide car une réparation automatique antérieure a mis votre portefeuille d'origine de côté comme sauvegarde. Vos pièces se trouvent presque certainement dans cette sauvegarde, elles ne sont pas perdues. Restaurez-la pour recharger vos fonds — rien n'est supprimé ; le fichier actuel est d'abord mis de côté.",
|
||||||
|
"empty_wallet_salvage_headline": "Vos pièces sont en sécurité dans un fichier de sauvegarde.",
|
||||||
|
"empty_wallet_salvage_title": "Votre portefeuille a peut-être été réparé",
|
||||||
|
"empty_wallet_warning_body": "Ce portefeuille n'a aucune adresse ni fonds, mais un autre fichier de portefeuille dans votre dossier DragonX contient des clés. Vos pièces s'y trouvent très probablement, elles ne sont pas perdues. Ouvrez le gestionnaire de portefeuilles pour passer au portefeuille qui contient vos fonds.",
|
||||||
|
"empty_wallet_warning_dismiss": "Ne plus avertir pour ce portefeuille",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "Arrête cet avertissement uniquement pour le fichier de portefeuille actuel. Si vous passez plus tard à un autre portefeuille vide, il pourra avertir à nouveau.",
|
||||||
|
"empty_wallet_warning_headline": "Vous avez peut-être ouvert le mauvais portefeuille.",
|
||||||
|
"empty_wallet_warning_title": "Ce portefeuille est vide",
|
||||||
"enc_confirm": "Confirmer :",
|
"enc_confirm": "Confirmer :",
|
||||||
"enc_desc": "Chiffrer votre portefeuille protège vos clés privées avec une phrase secrète. Après le chiffrement, le daemon redémarrera.",
|
"enc_desc": "Chiffrer votre portefeuille protège vos clés privées avec une phrase secrète. Après le chiffrement, le daemon redémarrera.",
|
||||||
"enc_encrypting": "Chiffrement du portefeuille...",
|
"enc_encrypting": "Chiffrement du portefeuille...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "Clair",
|
"light": "Clair",
|
||||||
"lite_account_label": "Compte",
|
"lite_account_label": "Compte",
|
||||||
"lite_action": "Action",
|
"lite_action": "Action",
|
||||||
|
"lite_backend_unavailable": "Backend du portefeuille léger indisponible",
|
||||||
"lite_backup_keys": "Sauvegarde et clés",
|
"lite_backup_keys": "Sauvegarde et clés",
|
||||||
"lite_birthday_backup": "Date de création : %llu (à sauvegarder également)",
|
"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_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_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_console_help_passthrough": "Toute autre entrée est exécutée comme une commande de la console du portefeuille lite.",
|
||||||
"lite_copy": "Copier",
|
"lite_copy": "Copier",
|
||||||
|
"lite_could_not_start": "Impossible de démarrer l'opération",
|
||||||
"lite_could_not_write": "Impossible d'écrire ",
|
"lite_could_not_write": "Impossible d'écrire ",
|
||||||
"lite_encrypt_wallet": "Chiffrer le portefeuille",
|
"lite_encrypt_wallet": "Chiffrer le portefeuille",
|
||||||
"lite_encryption_removed": "Chiffrement supprimé",
|
"lite_encryption_removed": "Chiffrement supprimé",
|
||||||
|
"lite_enter_all_seed_words": "Entrez les 24 mots de la phrase de récupération (%d obtenus)",
|
||||||
|
"lite_enter_wallet_path": "Entrez un chemin de portefeuille",
|
||||||
"lite_hide_wipe": "Masquer et effacer",
|
"lite_hide_wipe": "Masquer et effacer",
|
||||||
"lite_import": "Importer",
|
"lite_import": "Importer",
|
||||||
"lite_import_key_label": "Importer une clé",
|
"lite_import_key_label": "Importer une clé",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://votre-serveur-lite",
|
"lite_net_add_url_hint": "https://votre-serveur-lite",
|
||||||
"lite_net_checking": "vérification…",
|
"lite_net_checking": "vérification…",
|
||||||
"lite_net_connected": "Connecté",
|
"lite_net_connected": "Connecté",
|
||||||
|
"lite_net_connecting": "Connexion…",
|
||||||
"lite_net_custom": "Personnalisé",
|
"lite_net_custom": "Personnalisé",
|
||||||
"lite_net_disconnected": "Non connecté",
|
"lite_net_disconnected": "Non connecté",
|
||||||
"lite_net_hidden_section": "Serveurs masqués",
|
"lite_net_hidden_section": "Serveurs masqués",
|
||||||
@@ -638,6 +765,9 @@
|
|||||||
"lite_working": "En cours…",
|
"lite_working": "En cours…",
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"loading_addresses": "Chargement des adresses...",
|
"loading_addresses": "Chargement des adresses...",
|
||||||
|
"loading_stall_body": "Le démon s'initialise depuis %.0f s. Cela peut être normal après une mise à jour ou au premier lancement (chargement de l'index des blocs ou nouvelle analyse) — la connexion se fera automatiquement une fois prêt.",
|
||||||
|
"loading_stall_hint": "Toujours bloqué ? Ouvrez les Paramètres et utilisez Redémarrer le démon, ou consultez la Console pour plus de détails.",
|
||||||
|
"loading_stall_title": "Cela prend plus de temps que prévu",
|
||||||
"loading_transactions": "Chargement des transactions",
|
"loading_transactions": "Chargement des transactions",
|
||||||
"local_hashrate": "Hashrate local",
|
"local_hashrate": "Hashrate local",
|
||||||
"low_spec_mode": "Mode économie",
|
"low_spec_mode": "Mode économie",
|
||||||
@@ -654,6 +784,9 @@
|
|||||||
"market_cap": "Capitalisation",
|
"market_cap": "Capitalisation",
|
||||||
"market_cap_short": "Cap.",
|
"market_cap_short": "Cap.",
|
||||||
"market_chart_loading": "Chargement de l'historique des prix",
|
"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_1d": "1J",
|
||||||
"market_iv_1h": "1H",
|
"market_iv_1h": "1H",
|
||||||
"market_iv_1m": "1M",
|
"market_iv_1m": "1M",
|
||||||
@@ -662,13 +795,18 @@
|
|||||||
"market_no_history": "Aucun historique de prix disponible",
|
"market_no_history": "Aucun historique de prix disponible",
|
||||||
"market_no_price": "Pas de données de prix",
|
"market_no_price": "Pas de données de prix",
|
||||||
"market_now": "Maintenant",
|
"market_now": "Maintenant",
|
||||||
|
"market_opt_chart_style": "Style du graphique",
|
||||||
"market_pct_shielded": "%.0f%% Blindé",
|
"market_pct_shielded": "%.0f%% Blindé",
|
||||||
"market_portfolio": "PORTEFEUILLE",
|
"market_portfolio": "PORTEFEUILLE",
|
||||||
"market_price_loading": "Chargement des données de prix...",
|
"market_price_loading": "Chargement des données de prix...",
|
||||||
"market_price_unavailable": "Données de prix indisponibles",
|
"market_price_unavailable": "Données de prix indisponibles",
|
||||||
"market_refresh_price": "Actualiser les données de prix",
|
"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": "Passer aux chandeliers",
|
||||||
|
"market_style_candle_label": "Chandelier",
|
||||||
"market_style_line": "Passer au graphique en ligne",
|
"market_style_line": "Passer au graphique en ligne",
|
||||||
|
"market_style_line_label": "Ligne",
|
||||||
"market_trade_on": "Échanger sur %s",
|
"market_trade_on": "Échanger sur %s",
|
||||||
"market_updated": "\\xc2\\xb7 Mis à jour %s",
|
"market_updated": "\\xc2\\xb7 Mis à jour %s",
|
||||||
"market_vol_short": "Vol",
|
"market_vol_short": "Vol",
|
||||||
@@ -762,6 +900,7 @@
|
|||||||
"mining_difficulty_copied": "Difficulté copiée",
|
"mining_difficulty_copied": "Difficulté copiée",
|
||||||
"mining_est_block": "Bloc est.",
|
"mining_est_block": "Bloc est.",
|
||||||
"mining_est_daily": "Est. quotidien",
|
"mining_est_daily": "Est. quotidien",
|
||||||
|
"mining_est_daily_pool_sub": "équivalent solo approximatif, avant les frais du pool",
|
||||||
"mining_filter_all": "Tout",
|
"mining_filter_all": "Tout",
|
||||||
"mining_filter_tip_all": "Afficher tous les gains",
|
"mining_filter_tip_all": "Afficher tous les gains",
|
||||||
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
|
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
|
||||||
@@ -790,10 +929,12 @@
|
|||||||
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
|
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
|
||||||
"mining_payout_address": "Adresse de paiement",
|
"mining_payout_address": "Adresse de paiement",
|
||||||
"mining_payout_foreign": "⚠ Cette adresse de paiement ne fait pas partie de votre portefeuille actuel — les récompenses minées iraient vers un autre portefeuille. Mettez-la à jour si vous avez changé de portefeuille.",
|
"mining_payout_foreign": "⚠ Cette adresse de paiement ne fait pas partie de votre portefeuille actuel — les récompenses minées iraient vers un autre portefeuille. Mettez-la à jour si vous avez changé de portefeuille.",
|
||||||
|
"mining_payout_invalid": "Adresse DragonX invalide — corrigez-la avant de démarrer, sinon les récompenses de minage sont perdues.",
|
||||||
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
|
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
|
||||||
"mining_pool": "Pool",
|
"mining_pool": "Pool",
|
||||||
"mining_pool_fee": "Frais",
|
"mining_pool_fee": "Frais",
|
||||||
"mining_pool_hashrate": "Hashrate du pool",
|
"mining_pool_hashrate": "Hashrate du pool",
|
||||||
|
"mining_pool_needs_payout_tooltip": "Entrez d'abord une adresse de paiement (générez une adresse Z)",
|
||||||
"mining_pool_url": "URL du pool",
|
"mining_pool_url": "URL du pool",
|
||||||
"mining_pools_header": "POOLS",
|
"mining_pools_header": "POOLS",
|
||||||
"mining_recent_blocks": "BLOCS RÉCENTS",
|
"mining_recent_blocks": "BLOCS RÉCENTS",
|
||||||
@@ -823,6 +964,9 @@
|
|||||||
"mining_syncing_tooltip": "La blockchain se synchronise...",
|
"mining_syncing_tooltip": "La blockchain se synchronise...",
|
||||||
"mining_tag": " · Minage",
|
"mining_tag": " · Minage",
|
||||||
"mining_threads": "Threads de minage",
|
"mining_threads": "Threads de minage",
|
||||||
|
"mining_threads_input_tooltip": "Saisissez un nombre exact de threads (Entrée pour appliquer)",
|
||||||
|
"mining_threads_minus_tooltip": "Moins de threads",
|
||||||
|
"mining_threads_plus_tooltip": "Plus de threads",
|
||||||
"mining_to_save": "pour enregistrer",
|
"mining_to_save": "pour enregistrer",
|
||||||
"mining_today": "Aujourd'hui",
|
"mining_today": "Aujourd'hui",
|
||||||
"mining_uptime": "Temps de fonctionnement",
|
"mining_uptime": "Temps de fonctionnement",
|
||||||
@@ -849,6 +993,11 @@
|
|||||||
"no_transactions": "Aucune transaction trouvée",
|
"no_transactions": "Aucune transaction trouvée",
|
||||||
"no_transactions_yet": "Aucune transaction pour le moment",
|
"no_transactions_yet": "Aucune transaction pour le moment",
|
||||||
"node": "NŒUD",
|
"node": "NŒUD",
|
||||||
|
"node_banner_crashed_title": "Le nœud s'est arrêté de façon inattendue",
|
||||||
|
"node_banner_lite_open_failed": "Impossible d'ouvrir votre portefeuille",
|
||||||
|
"node_banner_offline_title": "Non connecté au nœud DragonX",
|
||||||
|
"node_banner_reconnect": "Reconnecter",
|
||||||
|
"node_banner_restart": "Redémarrer le nœud",
|
||||||
"node_security": "NŒUD & SÉCURITÉ",
|
"node_security": "NŒUD & SÉCURITÉ",
|
||||||
"noise": "Bruit",
|
"noise": "Bruit",
|
||||||
"not_connected": "Non connecté au daemon...",
|
"not_connected": "Non connecté au daemon...",
|
||||||
@@ -972,11 +1121,12 @@
|
|||||||
"portfolio_spark_min": "Minute",
|
"portfolio_spark_min": "Minute",
|
||||||
"portfolio_spark_month": "Mois",
|
"portfolio_spark_month": "Mois",
|
||||||
"portfolio_spark_week": "Semaine",
|
"portfolio_spark_week": "Semaine",
|
||||||
"portfolio_style_compact": "Lignes compactes",
|
"portfolio_style_compact": "Tableau",
|
||||||
"portfolio_style_detailed": "Lignes détaillées",
|
"portfolio_style_detailed": "Cartes",
|
||||||
"portfolio_style_featured": "Lignes en vedette",
|
"portfolio_style_featured": "En vedette",
|
||||||
"portfolio_style_label": "Style du portefeuille",
|
"portfolio_style_label": "Style du portefeuille",
|
||||||
"portfolio_untitled": "Sans titre",
|
"portfolio_untitled": "Sans titre",
|
||||||
|
"portfolio_wallet_loading": "Attendez la fin du chargement du portefeuille pour ajouter un groupe.",
|
||||||
"price_chart": "Graphique des prix",
|
"price_chart": "Graphique des prix",
|
||||||
"privacy_great": "Excellente confidentialité !",
|
"privacy_great": "Excellente confidentialité !",
|
||||||
"privacy_low": "Faible confidentialité — blindez vos fonds",
|
"privacy_low": "Faible confidentialité — blindez vos fonds",
|
||||||
@@ -986,6 +1136,8 @@
|
|||||||
"qr_failed": "Échec de la génération du code QR",
|
"qr_failed": "Échec de la génération du code QR",
|
||||||
"qr_title": "Code QR",
|
"qr_title": "Code QR",
|
||||||
"qr_unavailable": "QR indisponible",
|
"qr_unavailable": "QR indisponible",
|
||||||
|
"quick_receive": "Réception rapide",
|
||||||
|
"quick_send": "Envoi rapide",
|
||||||
"ram_daemon_gb": "Daemon : %.1f Go (%s)",
|
"ram_daemon_gb": "Daemon : %.1f Go (%s)",
|
||||||
"ram_daemon_mb": "Daemon : %.0f Mo (%s)",
|
"ram_daemon_mb": "Daemon : %.0f Mo (%s)",
|
||||||
"ram_system_gb": "Système : %.1f / %.0f Go",
|
"ram_system_gb": "Système : %.1f / %.0f Go",
|
||||||
@@ -1035,6 +1187,7 @@
|
|||||||
"rpc_connection": "Connexion RPC...",
|
"rpc_connection": "Connexion RPC...",
|
||||||
"rpc_host": "Hôte RPC",
|
"rpc_host": "Hôte RPC",
|
||||||
"rpc_pass": "Mot de passe",
|
"rpc_pass": "Mot de passe",
|
||||||
|
"rpc_plaintext_remote_warning": "Le RPC distant utilise du HTTP en clair. Ajoutez rpctls=1 à DRAGONX.conf si votre démon prend en charge TLS.",
|
||||||
"rpc_port": "Port",
|
"rpc_port": "Port",
|
||||||
"rpc_user": "Nom d'utilisateur",
|
"rpc_user": "Nom d'utilisateur",
|
||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
@@ -1049,6 +1202,8 @@
|
|||||||
"sb_connecting_external": "Connexion au daemon externe...",
|
"sb_connecting_external": "Connexion au daemon externe...",
|
||||||
"sb_connecting_generic": "Connexion au daemon...",
|
"sb_connecting_generic": "Connexion au daemon...",
|
||||||
"sb_daemon_crashed": "Le daemon a planté %d fois",
|
"sb_daemon_crashed": "Le daemon a planté %d fois",
|
||||||
|
"sb_daemon_extract_failed": "Échec de l'écriture des fichiers du démon — vérifiez l'espace disque libre et les permissions.",
|
||||||
|
"sb_daemon_files_failed": "Échec de l'écriture des fichiers du démon dans %s — vérifiez l'espace disque libre et les permissions.",
|
||||||
"sb_daemon_not_found": "Daemon introuvable",
|
"sb_daemon_not_found": "Daemon introuvable",
|
||||||
"sb_daemon_start_failed": "Impossible de démarrer dragonxd",
|
"sb_daemon_start_failed": "Impossible de démarrer dragonxd",
|
||||||
"sb_dragonxd_running": "dragonxd en cours",
|
"sb_dragonxd_running": "dragonxd en cours",
|
||||||
@@ -1064,6 +1219,7 @@
|
|||||||
"sb_net_mhs": "Rés: %.2f MH/s",
|
"sb_net_mhs": "Rés: %.2f MH/s",
|
||||||
"sb_no_conf": "DRAGONX.conf introuvable",
|
"sb_no_conf": "DRAGONX.conf introuvable",
|
||||||
"sb_peers": "Pairs : %zu",
|
"sb_peers": "Pairs : %zu",
|
||||||
|
"sb_plaintext_remote_blocked": "Refus d'envoyer les identifiants RPC en clair vers un hôte distant. Ajoutez rpcallowplaintext=1 à DRAGONX.conf pour l'autoriser, ou activez TLS avec rpctls=1.",
|
||||||
"sb_rescanning": "Rescan",
|
"sb_rescanning": "Rescan",
|
||||||
"sb_rescanning_pct": "Rescan %.0f%%",
|
"sb_rescanning_pct": "Rescan %.0f%%",
|
||||||
"sb_restarting_daemon": "Redémarrage du daemon...",
|
"sb_restarting_daemon": "Redémarrage du daemon...",
|
||||||
@@ -1077,6 +1233,7 @@
|
|||||||
"sb_waiting_daemon_err": "En attente de dragonxd — %s",
|
"sb_waiting_daemon_err": "En attente de dragonxd — %s",
|
||||||
"sb_warming_up": "Démarrage...",
|
"sb_warming_up": "Démarrage...",
|
||||||
"sb_witness_cache": "Reconstruction des témoins",
|
"sb_witness_cache": "Reconstruction des témoins",
|
||||||
|
"scale_effects": "ÉCHELLE ET EFFETS",
|
||||||
"screenshot_open_dir": "Ouvrir l'emplacement",
|
"screenshot_open_dir": "Ouvrir l'emplacement",
|
||||||
"screenshot_sweep": "Lancer la capture d'écran",
|
"screenshot_sweep": "Lancer la capture d'écran",
|
||||||
"screenshot_sweep_desc": "Parcourt chaque thème sur chaque onglet et enregistre une capture d'écran de chacun dans des sous-dossiers par onglet, sous le dossier screenshots du répertoire de configuration (en écrasant le balayage précédent). Dure quelques secondes.",
|
"screenshot_sweep_desc": "Parcourt chaque thème sur chaque onglet et enregistre une capture d'écran de chacun dans des sous-dossiers par onglet, sous le dossier screenshots du répertoire de configuration (en écrasant le balayage précédent). Dure quelques secondes.",
|
||||||
@@ -1141,6 +1298,7 @@
|
|||||||
"send_tooltip_not_connected": "Non connecté au daemon",
|
"send_tooltip_not_connected": "Non connecté au daemon",
|
||||||
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
|
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
|
||||||
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
|
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
|
||||||
|
"send_tooltip_view_only": "Adresse en lecture seule — pas de clé de dépense, envoi impossible",
|
||||||
"send_total": "Total",
|
"send_total": "Total",
|
||||||
"send_transaction": "Envoyer la transaction",
|
"send_transaction": "Envoyer la transaction",
|
||||||
"send_tx_failed": "Transaction échouée",
|
"send_tx_failed": "Transaction échouée",
|
||||||
@@ -1160,16 +1318,16 @@
|
|||||||
"sent_filter": "Envoyé",
|
"sent_filter": "Envoyé",
|
||||||
"sent_type": "Envoyé",
|
"sent_type": "Envoyé",
|
||||||
"sent_upper": "ENVOYÉ",
|
"sent_upper": "ENVOYÉ",
|
||||||
"set_label": "Définir le libellé...",
|
"set_label": "Définir le libellé",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.",
|
"settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.",
|
||||||
"settings_acrylic_level": "Niveau acrylique :",
|
"settings_acrylic_level": "Niveau acrylique :",
|
||||||
"settings_address_book": "Carnet d'adresses...",
|
"settings_address_book": "Carnet d'adresses…",
|
||||||
"settings_auto_detected": "Détecté automatiquement depuis DRAGONX.conf",
|
"settings_auto_detected": "Détecté automatiquement depuis DRAGONX.conf",
|
||||||
"settings_auto_lock": "VERROUILLAGE AUTO",
|
"settings_auto_lock": "VERROUILLAGE AUTO",
|
||||||
"settings_auto_shield_desc": "Déplacer automatiquement les fonds transparents vers des adresses blindées",
|
"settings_auto_shield_desc": "Déplacer automatiquement les fonds transparents vers des adresses blindées",
|
||||||
"settings_auto_shield_funds": "Blindage automatique des fonds transparents",
|
"settings_auto_shield_funds": "Blindage automatique des fonds transparents",
|
||||||
"settings_backup": "Sauvegarde...",
|
"settings_backup": "Sauvegarde…",
|
||||||
"settings_block_explorer_urls": "URLs de l'explorateur de blocs",
|
"settings_block_explorer_urls": "URLs de l'explorateur de blocs",
|
||||||
"settings_builtin": "Intégré",
|
"settings_builtin": "Intégré",
|
||||||
"settings_change_passphrase": "Changer la phrase secrète",
|
"settings_change_passphrase": "Changer la phrase secrète",
|
||||||
@@ -1180,60 +1338,71 @@
|
|||||||
"settings_configure_explorer": "Configurer les liens vers l'explorateur de blocs externe",
|
"settings_configure_explorer": "Configurer les liens vers l'explorateur de blocs externe",
|
||||||
"settings_configure_rpc": "Configurer la connexion au daemon dragonxd",
|
"settings_configure_rpc": "Configurer la connexion au daemon dragonxd",
|
||||||
"settings_connection": "Connexion",
|
"settings_connection": "Connexion",
|
||||||
|
"settings_copy_diagnostics": "Copier les diagnostics",
|
||||||
"settings_copyright": "Copyright 2024-2026 Développeurs DragonX | Licence GPLv3",
|
"settings_copyright": "Copyright 2024-2026 Développeurs DragonX | Licence GPLv3",
|
||||||
"settings_custom": "Personnalisé",
|
"settings_custom": "Personnalisé",
|
||||||
"settings_data_dir": "Rép. de données :",
|
"settings_data_dir": "Rép. de données ",
|
||||||
"settings_debug_changed": "Catégories de débogage modifiées — redémarrez le daemon pour appliquer",
|
"settings_debug_changed": "Catégories de débogage modifiées — redémarrez le daemon pour appliquer",
|
||||||
"settings_debug_restart_note": "Les modifications prennent effet après le redémarrage du daemon.",
|
"settings_debug_restart_note": "Les modifications prennent effet après le redémarrage du daemon.",
|
||||||
"settings_debug_select": "Sélectionnez les catégories pour activer la journalisation de débogage du daemon (flags -debug=).",
|
"settings_debug_select": "Sélectionnez les catégories pour activer la journalisation de débogage du daemon (flags -debug=).",
|
||||||
|
"settings_diagnostics_copied": "Diagnostics copiés dans le presse-papiers",
|
||||||
"settings_encrypt_first_pin": "Chiffrez d'abord le portefeuille pour activer le PIN",
|
"settings_encrypt_first_pin": "Chiffrez d'abord le portefeuille pour activer le PIN",
|
||||||
"settings_encrypt_wallet": "Chiffrer le portefeuille",
|
"settings_encrypt_wallet": "Chiffrer le portefeuille",
|
||||||
"settings_explorer_hint": "Les URLs doivent inclure une barre oblique finale. Le txid/adresse sera ajouté.",
|
"settings_explorer_hint": "Les URLs doivent inclure une barre oblique finale. Le txid/adresse sera ajouté.",
|
||||||
"settings_export_all": "Tout exporter...",
|
"settings_export_all": "Tout exporter…",
|
||||||
"settings_export_csv": "Exporter CSV...",
|
"settings_export_csv": "Exporter CSV…",
|
||||||
"settings_export_key": "Exporter la clé...",
|
"settings_export_key": "Exporter la clé…",
|
||||||
"settings_gradient_bg": "Fond dégradé",
|
"settings_gradient_bg": "Fond dégradé",
|
||||||
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
|
"settings_gradient_desc": "Remplacer les arrière-plans texturés par des dégradés lisses",
|
||||||
"settings_idle_after": "après",
|
"settings_idle_after": "après",
|
||||||
"settings_import_key": "Importer une clé privée...",
|
"settings_import_key": "Importer une clé privée…",
|
||||||
"settings_import_viewkey": "Importer la clé de visualisation...",
|
"settings_import_viewkey": "Importer la clé de visualisation…",
|
||||||
"settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
|
"settings_language_note": "Remarque : Certains textes nécessitent un redémarrage pour se mettre à jour",
|
||||||
"settings_lock_now": "Verrouiller maintenant",
|
"settings_lock_now": "Verrouiller maintenant",
|
||||||
"settings_locked": "Verrouillé",
|
"settings_locked": "Verrouillé",
|
||||||
"settings_merge_to_address": "Fusionner vers l'adresse...",
|
"settings_merge_to_address": "Fusionner vers l'adresse…",
|
||||||
"settings_noise_opacity": "Opacité du bruit :",
|
"settings_noise_opacity": "Opacité du bruit :",
|
||||||
|
"settings_not_connected": "Non connecté au démon",
|
||||||
"settings_not_encrypted": "Non chiffré",
|
"settings_not_encrypted": "Non chiffré",
|
||||||
"settings_not_found": "Non trouvé",
|
"settings_not_found": "Non trouvé",
|
||||||
"settings_open_app_dir": "Ouvrir le dossier de l'application",
|
"settings_open_app_dir": "Ouvrir le dossier de l'application",
|
||||||
"settings_open_data_dir": "Ouvrir le dossier de données",
|
"settings_open_data_dir": "Ouvrir le dossier de données",
|
||||||
|
"settings_open_log_folder": "Ouvrir le dossier des journaux",
|
||||||
"settings_other": "Autres",
|
"settings_other": "Autres",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "Confidentialité",
|
"settings_privacy": "Confidentialité",
|
||||||
"settings_quick_unlock_pin": "PIN de déverrouillage rapide",
|
"settings_quick_unlock_pin": "PIN de déverrouillage rapide",
|
||||||
"settings_reduce_transparency": "Réduire la transparence",
|
"settings_reduce_transparency": "Réduire la transparence",
|
||||||
|
"settings_reloaded": "Paramètres rechargés depuis le disque",
|
||||||
"settings_remove_encryption": "Supprimer le chiffrement",
|
"settings_remove_encryption": "Supprimer le chiffrement",
|
||||||
"settings_remove_pin": "Supprimer le PIN",
|
"settings_remove_pin": "Supprimer le PIN",
|
||||||
"settings_request_payment": "Demander un paiement...",
|
"settings_request_payment": "Demander un paiement…",
|
||||||
"settings_rescan_desc": "Rescanner la blockchain pour les transactions manquantes",
|
"settings_rescan_desc": "Rescanner la blockchain pour les transactions manquantes",
|
||||||
"settings_restart_daemon": "Redémarrer le daemon",
|
"settings_restart_daemon": "Redémarrer le daemon",
|
||||||
"settings_rpc_connection": "Connexion RPC",
|
"settings_rpc_connection": "Connexion RPC",
|
||||||
|
"settings_rpc_error_prefix": "Erreur RPC : ",
|
||||||
"settings_rpc_note": "Remarque : Les paramètres de connexion sont généralement détectés automatiquement depuis DRAGONX.conf",
|
"settings_rpc_note": "Remarque : Les paramètres de connexion sont généralement détectés automatiquement depuis DRAGONX.conf",
|
||||||
|
"settings_rpc_ok": "Connexion RPC OK",
|
||||||
"settings_save_shielded_desc": "Stocke les transactions z-addr dans un fichier local pour consultation",
|
"settings_save_shielded_desc": "Stocke les transactions z-addr dans un fichier local pour consultation",
|
||||||
"settings_save_shielded_local": "Enregistrer l'historique des transactions blindées localement",
|
"settings_save_shielded_local": "Enregistrer l'historique des transactions blindées localement",
|
||||||
|
"settings_saved": "Paramètres enregistrés",
|
||||||
"settings_set_pin": "Définir le PIN",
|
"settings_set_pin": "Définir le PIN",
|
||||||
"settings_shield_mining": "Blindage minage...",
|
"settings_shield_mining": "Blindage minage…",
|
||||||
"settings_solid_colors_desc": "Utiliser des couleurs unies au lieu des effets de flou (accessibilité)",
|
"settings_solid_colors_desc": "Utiliser des couleurs unies au lieu des effets de flou (accessibilité)",
|
||||||
|
"settings_theme_refreshed": "Liste des thèmes actualisée",
|
||||||
"settings_tor_desc": "Acheminer toutes les connexions via Tor pour une confidentialité renforcée",
|
"settings_tor_desc": "Acheminer toutes les connexions via Tor pour une confidentialité renforcée",
|
||||||
"settings_unlocked": "Déverrouillé",
|
"settings_unlocked": "Déverrouillé",
|
||||||
"settings_use_tor_network": "Utiliser Tor pour les connexions réseau",
|
"settings_use_tor_network": "Utiliser Tor pour les connexions réseau",
|
||||||
"settings_validate_address": "Valider l'adresse...",
|
"settings_validate_address": "Valider l'adresse…",
|
||||||
"settings_visual_effects": "Effets visuels",
|
"settings_visual_effects": "Effets visuels",
|
||||||
"settings_wallet_file_size": "Taille du fichier portefeuille : %s",
|
"settings_wallet_file_size": "Taille du fichier portefeuille : %s",
|
||||||
"settings_wallet_info": "Informations du portefeuille",
|
"settings_wallet_info": "Informations du portefeuille",
|
||||||
"settings_wallet_location": "Emplacement du portefeuille : %s",
|
"settings_wallet_location": "Emplacement du portefeuille : %s",
|
||||||
"settings_wallet_maintenance": "Maintenance du portefeuille",
|
"settings_wallet_maintenance": "Maintenance du portefeuille",
|
||||||
"settings_wallet_not_found": "Fichier portefeuille introuvable",
|
"settings_wallet_not_found": "Fichier portefeuille introuvable",
|
||||||
"settings_wallet_size_label": "Taille du portefeuille :",
|
"settings_wallet_size_label": "Taille du portefeuille ",
|
||||||
|
"settings_ztx_cleared": "Historique des transactions Z effacé",
|
||||||
|
"settings_ztx_not_found": "Aucun fichier d'historique trouvé",
|
||||||
"setup_wizard": "Assistant de configuration",
|
"setup_wizard": "Assistant de configuration",
|
||||||
"share": "Partager",
|
"share": "Partager",
|
||||||
"shield_check_status": "Vérifier le statut",
|
"shield_check_status": "Vérifier le statut",
|
||||||
@@ -1292,6 +1461,23 @@
|
|||||||
"sweep_to": "Balayé vers :",
|
"sweep_to": "Balayé vers :",
|
||||||
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
|
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
|
||||||
"sweep_tx": "Transaction :",
|
"sweep_tx": "Transaction :",
|
||||||
|
"switch_corrupt_body": "Ce portefeuille semble corrompu — le nœud n'a pas pu l'ouvrir. Restaurez-le depuis une sauvegarde, recréez-le ou tentez de le réparer.",
|
||||||
|
"switch_corrupt_repair": "Tenter une réparation (salvage)",
|
||||||
|
"switch_progress_background": "Continuer en arrière-plan",
|
||||||
|
"switch_progress_default_wallet": "Portefeuille par défaut",
|
||||||
|
"switch_progress_elapsed": "Écoulé",
|
||||||
|
"switch_progress_external_wallet": "Portefeuille externe",
|
||||||
|
"switch_progress_failed_title": "Échec du changement de portefeuille",
|
||||||
|
"switch_progress_from_label": "depuis",
|
||||||
|
"switch_progress_hint": "Un arrêt propre peut prendre jusqu'à une minute.",
|
||||||
|
"switch_progress_reconnecting": "Reconnexion",
|
||||||
|
"switch_progress_starting": "Démarrage du nœud sur le nouveau portefeuille",
|
||||||
|
"switch_progress_stopping": "Arrêt du nœud actuel",
|
||||||
|
"switch_progress_title": "Changement de portefeuille",
|
||||||
|
"switch_stopnode_body": "Changer de portefeuille redémarre le nœud sur le portefeuille sélectionné. Le nœud en cours sera arrêté puis relancé sur le nouveau portefeuille — si vous l'avez laissé tourner exprès, il redémarre automatiquement.",
|
||||||
|
"switch_stopnode_confirm": "Arrêter le nœud et changer",
|
||||||
|
"switch_stopnode_title": "Arrêter le nœud en cours d'exécution ?",
|
||||||
|
"switch_stopnode_warn": "Un nœud que ce portefeuille n'a pas démarré est déjà en cours d'exécution.",
|
||||||
"syncing": "Synchronisation...",
|
"syncing": "Synchronisation...",
|
||||||
"t_address": "Adresse T",
|
"t_address": "Adresse T",
|
||||||
"t_addresses": "Adresses T",
|
"t_addresses": "Adresses T",
|
||||||
@@ -1299,6 +1485,7 @@
|
|||||||
"theme": "Thème",
|
"theme": "Thème",
|
||||||
"theme_effects": "Effets de thème",
|
"theme_effects": "Effets de thème",
|
||||||
"theme_language": "THÈME & LANGUE",
|
"theme_language": "THÈME & LANGUE",
|
||||||
|
"tile_click_to_open": "Cliquer pour ouvrir",
|
||||||
"time_days_ago": "il y a %d jours",
|
"time_days_ago": "il y a %d jours",
|
||||||
"time_hours_ago": "il y a %d heures",
|
"time_hours_ago": "il y a %d heures",
|
||||||
"time_minutes_ago": "il y a %d minutes",
|
"time_minutes_ago": "il y a %d minutes",
|
||||||
@@ -1313,7 +1500,9 @@
|
|||||||
"to_upper": "À",
|
"to_upper": "À",
|
||||||
"tools": "OUTILS",
|
"tools": "OUTILS",
|
||||||
"tools_actions": "Outils & Actions...",
|
"tools_actions": "Outils & Actions...",
|
||||||
|
"tools_actions_hdr": "OUTILS ET ACTIONS",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"total_balance_label": "Solde total",
|
||||||
"transaction_id": "ID DE TRANSACTION",
|
"transaction_id": "ID DE TRANSACTION",
|
||||||
"transaction_sent": "Transaction envoyée avec succès",
|
"transaction_sent": "Transaction envoyée avec succès",
|
||||||
"transaction_sent_msg": "Transaction envoyée !",
|
"transaction_sent_msg": "Transaction envoyée !",
|
||||||
@@ -1335,13 +1524,24 @@
|
|||||||
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité",
|
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité",
|
||||||
"tt_backup": "Créer une sauvegarde de votre wallet.dat",
|
"tt_backup": "Créer une sauvegarde de votre wallet.dat",
|
||||||
"tt_block_explorer": "Ouvrir l'explorateur de blocs DragonX dans votre navigateur",
|
"tt_block_explorer": "Ouvrir l'explorateur de blocs DragonX dans votre navigateur",
|
||||||
"tt_blur": "Quantité de flou (0%% = désactivé, 100%% = maximum)",
|
"tt_blur": "Quantité de flou (0% = désactivé, 100% = maximum)",
|
||||||
"tt_change_pass": "Changer la phrase secrète de chiffrement du portefeuille",
|
"tt_change_pass": "Changer la phrase secrète de chiffrement du portefeuille",
|
||||||
"tt_change_pin": "Changer votre PIN de déverrouillage",
|
"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_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_copy_diagnostics": "Copie un récapitulatif de support (version, état daemon/portefeuille/journaux — sans données secrètes) dans le presse-papiers",
|
||||||
"tt_custom_fees": "Activer la saisie manuelle des frais lors de l'envoi de transactions",
|
"tt_custom_fees": "Activer la saisie manuelle des frais lors de l'envoi de transactions",
|
||||||
"tt_custom_theme": "Thème personnalisé actif",
|
"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_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_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_collapse": "Réduire les options de journalisation de débogage",
|
||||||
"tt_debug_expand": "Développer les options de journalisation de débogage",
|
"tt_debug_expand": "Développer les options de journalisation de débogage",
|
||||||
@@ -1359,15 +1559,39 @@
|
|||||||
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
||||||
"tt_language": "Langue de l'interface du portefeuille",
|
"tt_language": "Langue de l'interface du portefeuille",
|
||||||
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
||||||
|
"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_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_lock": "Verrouiller le portefeuille immédiatement",
|
||||||
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
|
"tt_low_spec": "Désactiver tous les effets visuels lourds\\nRaccourci : Ctrl+Shift+Down",
|
||||||
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
|
"tt_merge": "Consolider plusieurs UTXOs vers une adresse",
|
||||||
"tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)",
|
"tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)",
|
||||||
"tt_noise": "Intensité de texture grainée (0%% = désactivé, 100%% = maximum)",
|
"tt_noise": "Intensité de texture grainée (0% = désactivé, 100% = maximum)",
|
||||||
"tt_open_app_dir": "Ouvrir le dossier ObsidianDragon (paramètres, thèmes, journaux) dans le gestionnaire de fichiers",
|
"tt_open_app_dir": "Ouvrir le dossier ObsidianDragon (paramètres, thèmes, journaux) dans le gestionnaire de fichiers",
|
||||||
"tt_open_data_dir": "Ouvrir le dossier contenant les données de votre portefeuille et de la blockchain dans le gestionnaire de fichiers",
|
"tt_open_data_dir": "Ouvrir le dossier contenant les données de votre portefeuille et de la blockchain dans le gestionnaire de fichiers",
|
||||||
"tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
|
"tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
|
||||||
|
"tt_open_log_folder": "Ouvre le dossier contenant les journaux de débogage et de plantage",
|
||||||
"tt_reduce_motion": "Désactiver les transitions animées et le lerp de solde pour l'accessibilité",
|
"tt_reduce_motion": "Désactiver les transitions animées et le lerp de solde pour l'accessibilité",
|
||||||
"tt_remove_encrypt": "Supprimer le chiffrement et stocker le portefeuille sans protection",
|
"tt_remove_encrypt": "Supprimer le chiffrement et stocker le portefeuille sans protection",
|
||||||
"tt_remove_pin": "Supprimer le PIN et exiger la phrase secrète pour déverrouiller",
|
"tt_remove_pin": "Supprimer le PIN et exiger la phrase secrète pour déverrouiller",
|
||||||
@@ -1380,12 +1604,17 @@
|
|||||||
"tt_rpc_host": "Nom d'hôte du daemon DragonX",
|
"tt_rpc_host": "Nom d'hôte du daemon DragonX",
|
||||||
"tt_rpc_pass": "Mot de passe d'authentification RPC",
|
"tt_rpc_pass": "Mot de passe d'authentification RPC",
|
||||||
"tt_rpc_port": "Port pour les connexions RPC du daemon",
|
"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_rpc_user": "Nom d'utilisateur d'authentification RPC",
|
||||||
"tt_save_settings": "Enregistrer tous les paramètres sur le disque",
|
"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_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_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_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_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_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_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",
|
"tt_shield_mining": "Déplacer les récompenses de minage transparentes vers une adresse blindée",
|
||||||
@@ -1397,13 +1626,14 @@
|
|||||||
"tt_theme_hotkey": "Raccourci : Ctrl+Gauche/Droite pour changer de thème",
|
"tt_theme_hotkey": "Raccourci : Ctrl+Gauche/Droite pour changer de thème",
|
||||||
"tt_tor": "Acheminer les connexions du daemon via le réseau Tor pour l'anonymat",
|
"tt_tor": "Acheminer les connexions du daemon via le réseau Tor pour l'anonymat",
|
||||||
"tt_tx_url": "URL de base pour consulter les transactions dans un explorateur de blocs",
|
"tt_tx_url": "URL de base pour consulter les transactions dans un explorateur de blocs",
|
||||||
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100%% = entièrement opaque, plus bas = plus transparent)",
|
"tt_ui_opacity": "Opacité des cartes et de la barre latérale (100% = entièrement opaque, plus bas = plus transparent)",
|
||||||
"tt_validate": "Vérifier si une adresse DragonX est valide",
|
"tt_validate": "Vérifier si une adresse DragonX est valide",
|
||||||
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
|
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
|
||||||
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
|
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
|
||||||
"tt_website": "Ouvrir le site web DragonX",
|
"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_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é",
|
"tt_wizard": "Relancer l'assistant de configuration initiale\\nLe daemon sera redémarré",
|
||||||
|
"tx_chat_badge": "Message",
|
||||||
"tx_confirmations": "%d confirmations",
|
"tx_confirmations": "%d confirmations",
|
||||||
"tx_details_title": "Détails de la transaction",
|
"tx_details_title": "Détails de la transaction",
|
||||||
"tx_from_address": "Adresse d'origine :",
|
"tx_from_address": "Adresse d'origine :",
|
||||||
@@ -1449,6 +1679,7 @@
|
|||||||
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
||||||
"validate_ownership": "Propriété :",
|
"validate_ownership": "Propriété :",
|
||||||
"validate_results": "Résultats :",
|
"validate_results": "Résultats :",
|
||||||
|
"validate_results_placeholder": "Les résultats apparaîtront ici",
|
||||||
"validate_shielded_type": "Blindée (z-adresse)",
|
"validate_shielded_type": "Blindée (z-adresse)",
|
||||||
"validate_status": "Statut :",
|
"validate_status": "Statut :",
|
||||||
"validate_title": "Valider l'adresse",
|
"validate_title": "Valider l'adresse",
|
||||||
@@ -1472,6 +1703,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ Analyser un autre dossier pour les portefeuilles…",
|
"wallets_add_folder_toggle": "+ Analyser un autre dossier pour les portefeuilles…",
|
||||||
"wallets_badge_encrypted": "Chiffré (protégé par phrase secrète)",
|
"wallets_badge_encrypted": "Chiffré (protégé par phrase secrète)",
|
||||||
"wallets_badge_encrypted_short": "Chiffré",
|
"wallets_badge_encrypted_short": "Chiffré",
|
||||||
|
"wallets_badge_hd": "Portefeuille HD — impossible de confirmer une phrase de récupération sans l'ouvrir",
|
||||||
|
"wallets_badge_hd_short": "Portefeuille HD",
|
||||||
"wallets_badge_legacy": "Portefeuille hérité (sans phrase de récupération)",
|
"wallets_badge_legacy": "Portefeuille hérité (sans phrase de récupération)",
|
||||||
"wallets_badge_legacy_short": "Hérité",
|
"wallets_badge_legacy_short": "Hérité",
|
||||||
"wallets_badge_seed": "Portefeuille à phrase de récupération (HD)",
|
"wallets_badge_seed": "Portefeuille à phrase de récupération (HD)",
|
||||||
@@ -1587,6 +1820,7 @@
|
|||||||
"xmrig_loading_releases": "Chargement des versions…",
|
"xmrig_loading_releases": "Chargement des versions…",
|
||||||
"xmrig_none": "aucun",
|
"xmrig_none": "aucun",
|
||||||
"xmrig_reinstall": "Réinstaller",
|
"xmrig_reinstall": "Réinstaller",
|
||||||
|
"xmrig_releases": "versions de xmrig",
|
||||||
"xmrig_stop_mining_first": "Arrêtez le minage avant de mettre à jour le mineur.",
|
"xmrig_stop_mining_first": "Arrêtez le minage avant de mettre à jour le mineur.",
|
||||||
"xmrig_unavailable_body": "Aucune version du mineur n'est disponible pour cette plateforme.",
|
"xmrig_unavailable_body": "Aucune version du mineur n'est disponible pour cette plateforme.",
|
||||||
"xmrig_unavailable_title": "Mises à jour du mineur indisponibles",
|
"xmrig_unavailable_title": "Mises à jour du mineur indisponibles",
|
||||||
|
|||||||
271
res/lang/ja.json
271
res/lang/ja.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "詳細設定",
|
"advanced": "詳細設定",
|
||||||
"advanced_effects": "高度なエフェクト...",
|
"advanced_effects": "高度なエフェクト...",
|
||||||
"ago": "前",
|
"ago": "前",
|
||||||
|
"alerts_clear": "通知履歴を消去",
|
||||||
|
"alerts_history_tooltip": "最近の通知",
|
||||||
|
"alerts_none": "通知はまだありません",
|
||||||
|
"alerts_recent": "最近の通知",
|
||||||
"all_filter": "すべて",
|
"all_filter": "すべて",
|
||||||
"allow_custom_fees": "カスタム手数料を許可",
|
"allow_custom_fees": "カスタム手数料を許可",
|
||||||
"amount": "金額",
|
"amount": "金額",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender がマイナーをブロックしました",
|
"av_title": "Windows Defender がマイナーをブロックしました",
|
||||||
"available": "利用可能",
|
"available": "利用可能",
|
||||||
"backup_backing_up": "バックアップ中...",
|
"backup_backing_up": "バックアップ中...",
|
||||||
|
"backup_col_backup": "バックアップ",
|
||||||
|
"backup_col_export": "エクスポート",
|
||||||
|
"backup_col_import": "インポートと復元",
|
||||||
"backup_create": "バックアップを作成",
|
"backup_create": "バックアップを作成",
|
||||||
"backup_created": "ウォレットのバックアップを作成しました",
|
"backup_created": "ウォレットのバックアップを作成しました",
|
||||||
"backup_data": "バックアップとデータ",
|
"backup_data": "バックアップとデータ",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "残高",
|
"balance": "残高",
|
||||||
"balance_history_collecting": "残高履歴 — データを収集中...",
|
"balance_history_collecting": "残高履歴 — データを収集中...",
|
||||||
"balance_layout": "残高レイアウト",
|
"balance_layout": "残高レイアウト",
|
||||||
|
"balance_layout_switched": "レイアウト: %s",
|
||||||
|
"balance_mining_rate": "マイニング中 %s",
|
||||||
"balance_shielded_fmt": "シールド: %.8f",
|
"balance_shielded_fmt": "シールド: %.8f",
|
||||||
|
"balance_syncing_pct": "同期中 %.1f%%",
|
||||||
"balance_transparent_fmt": "透明: %.8f",
|
"balance_transparent_fmt": "透明: %.8f",
|
||||||
"ban": "ブロック",
|
"ban": "ブロック",
|
||||||
"banned_peers": "ブロック済みピア",
|
"banned_peers": "ブロック済みピア",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "チェックサムを検証中...",
|
"bootstrap_verifying": "チェックサムを検証中...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat は保護されています)",
|
"bootstrap_wallet_protected": "(wallet.dat は保護されています)",
|
||||||
"bootstrap_warning": "既存のブロックデータ(blocks、chainstate、notarizations)は削除され置き換えられます。wallet.dat は変更・削除されません。",
|
"bootstrap_warning": "既存のブロックデータ(blocks、chainstate、notarizations)は削除され置き換えられます。wallet.dat は変更・削除されません。",
|
||||||
|
"byte_count_fmt": "%zu / %zu バイト",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"change_pass_confirm": "新しいパスフレーズ(確認):",
|
"change_pass_confirm": "新しいパスフレーズ(確認):",
|
||||||
"change_pass_current": "現在のパスフレーズ:",
|
"change_pass_current": "現在のパスフレーズ:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "パスフレーズを変更",
|
"change_pass_title": "パスフレーズを変更",
|
||||||
"characters": "文字",
|
"characters": "文字",
|
||||||
"chat": "チャット",
|
"chat": "チャット",
|
||||||
|
"chat_accent_amber": "琥珀",
|
||||||
|
"chat_accent_blue": "青",
|
||||||
|
"chat_accent_green": "緑",
|
||||||
|
"chat_accent_pink": "ピンク",
|
||||||
|
"chat_accent_purple": "紫",
|
||||||
|
"chat_accent_theme": "テーマ",
|
||||||
|
"chat_add_contact": "連絡先に追加",
|
||||||
|
"chat_awaiting_key": "返信待ち",
|
||||||
|
"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_cancel": "キャンセル",
|
||||||
|
"chat_contact_added": "連絡先を追加しました — 連絡先で名前を変更できます",
|
||||||
"chat_contact_request": "連絡リクエスト",
|
"chat_contact_request": "連絡リクエスト",
|
||||||
|
"chat_copy_address_tip": "クリックしてアドレスをコピー",
|
||||||
|
"chat_density_comfortable": "ゆったり",
|
||||||
|
"chat_density_compact": "コンパクト",
|
||||||
|
"chat_emoji_color": "カラー",
|
||||||
|
"chat_emoji_mono": "モノクロ",
|
||||||
|
"chat_emoji_search": "絵文字を検索",
|
||||||
"chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。",
|
"chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。",
|
||||||
|
"chat_empty_start": "「新しい会話」から始めましょう。",
|
||||||
|
"chat_empty_title": "会話はまだありません",
|
||||||
|
"chat_export": "チャットをエクスポート…",
|
||||||
|
"chat_export_done": "会話をエクスポートしました",
|
||||||
|
"chat_export_failed": "エクスポートファイルを書き込めませんでした。",
|
||||||
|
"chat_export_warn": "復号したメッセージを平文で保存します。ファイルは安全に保管してください。",
|
||||||
|
"chat_filter": "チャット",
|
||||||
|
"chat_hidden_toast": "会話を非表示にしました — 新しいメッセージが届くと再表示されます",
|
||||||
|
"chat_hide": "非表示",
|
||||||
|
"chat_hide_hidden": "非表示を隠す",
|
||||||
|
"chat_jump_latest": "最新",
|
||||||
|
"chat_len_over": "メッセージが長すぎます",
|
||||||
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
|
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
|
||||||
"chat_new_button": "新しい会話",
|
"chat_mute": "ミュート",
|
||||||
|
"chat_new_button": "新しいチャット",
|
||||||
"chat_new_message": "メッセージ",
|
"chat_new_message": "メッセージ",
|
||||||
|
"chat_new_message_toast": "新しい暗号化チャットメッセージ",
|
||||||
"chat_new_send": "リクエストを送信",
|
"chat_new_send": "リクエストを送信",
|
||||||
"chat_new_title": "新しい会話",
|
"chat_new_title": "新しいチャット",
|
||||||
"chat_new_zaddr": "宛先Zアドレス",
|
"chat_new_zaddr": "宛先Zアドレス",
|
||||||
|
"chat_no_matches": "検索に一致する会話がありません。",
|
||||||
|
"chat_no_z_contacts": "シールドアドレスの連絡先はまだありません",
|
||||||
|
"chat_opt_bubble_accent": "吹き出しの色",
|
||||||
|
"chat_opt_bubble_style": "吹き出しスタイル",
|
||||||
|
"chat_opt_density": "メッセージ密度",
|
||||||
|
"chat_opt_emoji": "絵文字スタイル",
|
||||||
|
"chat_opt_enter_sends": "Enterで送信",
|
||||||
|
"chat_opt_font_size": "文字サイズ",
|
||||||
|
"chat_opt_global_clock": "全体の時刻形式",
|
||||||
|
"chat_opt_poll": "取得間隔",
|
||||||
|
"chat_opt_timestamp": "タイムスタンプ",
|
||||||
|
"chat_pick_contact": "連絡先から選択…",
|
||||||
|
"chat_rename": "連絡先の名前を変更",
|
||||||
|
"chat_rename_hint": "連絡先名",
|
||||||
|
"chat_renamed": "連絡先の名前を変更しました",
|
||||||
|
"chat_retry": "再送信",
|
||||||
|
"chat_search": "会話を検索",
|
||||||
|
"chat_sec_appearance": "外観",
|
||||||
|
"chat_sec_messaging": "メッセージ",
|
||||||
"chat_select_hint": "表示する会話を選択してください。",
|
"chat_select_hint": "表示する会話を選択してください。",
|
||||||
"chat_send": "送信",
|
"chat_send": "送信",
|
||||||
"chat_send_failed": "未送信",
|
"chat_send_failed": "未送信",
|
||||||
|
"chat_sending": "送信中…",
|
||||||
|
"chat_settings_done": "完了",
|
||||||
|
"chat_settings_section": "チャットと連絡先",
|
||||||
|
"chat_settings_tip": "チャットのカスタマイズ",
|
||||||
|
"chat_settings_title": "チャット設定",
|
||||||
|
"chat_show_hidden": "非表示を表示",
|
||||||
|
"chat_time_now": "たった今",
|
||||||
"chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。",
|
"chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。",
|
||||||
"chat_toast_lite_busy": "すでに送信処理が進行中か、ウォレットが開かれていません。",
|
"chat_toast_lite_busy": "すでに送信処理が進行中か、ウォレットが開かれていません。",
|
||||||
|
"chat_toast_need_funds": "チャットを送信するには、手数料を賄うための少額のシールド残高が必要です。",
|
||||||
"chat_toast_no_zaddr": "送信元に使えるZアドレスがありません。",
|
"chat_toast_no_zaddr": "送信元に使えるZアドレスがありません。",
|
||||||
"chat_toast_not_connected": "未接続 — チャットメッセージは送信されませんでした。",
|
"chat_toast_not_connected": "未接続 — チャットメッセージは送信されませんでした。",
|
||||||
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
||||||
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
||||||
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
||||||
|
"chat_today": "今日",
|
||||||
|
"chat_ts_12h": "12時間",
|
||||||
|
"chat_ts_24h": "24時間",
|
||||||
|
"chat_ts_global": "全体設定に従う",
|
||||||
|
"chat_ts_global_short": "全体",
|
||||||
|
"chat_unhide": "再表示",
|
||||||
|
"chat_unmute": "ミュート解除",
|
||||||
|
"chat_verify_key": "識別鍵 — 照合して確認",
|
||||||
"chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。",
|
"chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。",
|
||||||
|
"chat_yesterday": "昨日",
|
||||||
"chat_you": "自分",
|
"chat_you": "自分",
|
||||||
"choose_icon": "アイコンを選択",
|
"choose_icon": "アイコンを選択",
|
||||||
"clear": "クリア",
|
"clear": "クリア",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "クリックしてアドレスをコピー",
|
"click_copy_address": "クリックしてアドレスをコピー",
|
||||||
"click_copy_uri": "クリックしてURIをコピー",
|
"click_copy_uri": "クリックしてURIをコピー",
|
||||||
"click_to_copy": "クリックしてコピー",
|
"click_to_copy": "クリックしてコピー",
|
||||||
|
"clock_format": "時刻形式",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"conf_count": "%d 確認",
|
"conf_count": "%d 確認",
|
||||||
"confirm_and_send": "確認して送金",
|
"confirm_and_send": "確認して送金",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "アプリ",
|
"console_app": "アプリ",
|
||||||
"console_auto_scroll": "自動スクロール",
|
"console_auto_scroll": "自動スクロール",
|
||||||
"console_available_commands": "利用可能なコマンド:",
|
"console_available_commands": "利用可能なコマンド:",
|
||||||
|
"console_backend_reference": "バックエンドコマンドリファレンス",
|
||||||
|
"console_backend_unavailable": "バックエンドなし",
|
||||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||||
|
"console_cat_advanced": "詳細設定",
|
||||||
"console_cat_blockchain": "ブロックチェーン",
|
"console_cat_blockchain": "ブロックチェーン",
|
||||||
"console_cat_control": "制御",
|
"console_cat_control": "制御",
|
||||||
|
"console_cat_keys": "鍵とセキュリティ",
|
||||||
"console_cat_mining": "マイニング",
|
"console_cat_mining": "マイニング",
|
||||||
"console_cat_network": "ネットワーク",
|
"console_cat_network": "ネットワーク",
|
||||||
"console_cat_raw_transactions": "生トランザクション",
|
"console_cat_raw_transactions": "生トランザクション",
|
||||||
|
"console_cat_send": "送金",
|
||||||
|
"console_cat_sync": "同期",
|
||||||
"console_cat_utility": "ユーティリティ",
|
"console_cat_utility": "ユーティリティ",
|
||||||
"console_cat_wallet": "ウォレット",
|
"console_cat_wallet": "ウォレット",
|
||||||
"console_clear": "クリア",
|
"console_clear": "クリア",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - このヘルプを表示",
|
"console_help_help": " help - このヘルプを表示",
|
||||||
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||||
"console_help_stop": " stop - デーモンを停止",
|
"console_help_stop": " stop - デーモンを停止",
|
||||||
|
"console_last_error": "最後のエラー:",
|
||||||
"console_line_count": "%zu 行",
|
"console_line_count": "%zu 行",
|
||||||
"console_matches": "件一致",
|
"console_matches": "件一致",
|
||||||
"console_new_lines": "%d 新しい行",
|
"console_new_lines": "%d 新しい行",
|
||||||
"console_no_daemon": "デーモンなし",
|
"console_no_daemon": "デーモンなし",
|
||||||
|
"console_no_output": "(出力なし)",
|
||||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||||
|
"console_not_connected_lite": "エラー:ウォレットが開かれていません",
|
||||||
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
||||||
"console_ref_builds": "生成",
|
"console_ref_builds": "生成",
|
||||||
"console_ref_cancel": "キャンセル",
|
"console_ref_cancel": "キャンセル",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
|
"console_ref_run_confirm": "%s を今すぐ実行しますか? 影響の大きいコマンドです。",
|
||||||
"console_ref_search_hint": "名前または用途で検索…",
|
"console_ref_search_hint": "名前または用途で検索…",
|
||||||
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
|
"console_ref_select_hint": "コマンドを選ぶと内容が表示されます。",
|
||||||
|
"console_ref_value": "値",
|
||||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "コンソールスキャンライン",
|
"console_scanline": "コンソールスキャンライン",
|
||||||
"console_search_commands": "コマンドを検索...",
|
"console_search_commands": "コマンドを検索...",
|
||||||
"console_select_all": "すべて選択",
|
"console_select_all": "すべて選択",
|
||||||
"console_show_app_output": "[app] ウォレットのログ行を表示",
|
"console_show_app_output": "[app] ウォレットのログ行を表示",
|
||||||
|
"console_show_backend_ref": "バックエンドコマンドリファレンスを表示",
|
||||||
"console_show_daemon_output": "デーモン出力を表示",
|
"console_show_daemon_output": "デーモン出力を表示",
|
||||||
"console_show_errors_only": "エラーのみ表示",
|
"console_show_errors_only": "エラーのみ表示",
|
||||||
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "停止済み",
|
"console_status_stopped": "停止済み",
|
||||||
"console_status_stopping": "停止中",
|
"console_status_stopping": "停止中",
|
||||||
"console_status_unknown": "不明",
|
"console_status_unknown": "不明",
|
||||||
|
"console_stop_confirm_node": "'stop' はノードを停止し、ウォレットを切断します。確認するにはもう一度 'stop' と入力してください。",
|
||||||
"console_tab_completion": "Tabで補完",
|
"console_tab_completion": "Tabで補完",
|
||||||
"console_text_colors": "テキスト色",
|
"console_text_colors": "テキスト色",
|
||||||
"console_toggle_accents": "行のカラーアクセントを切り替え",
|
"console_toggle_accents": "行のカラーアクセントを切り替え",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "オン:この連絡先はどのウォレットを読み込んでも表示されます。オフ:現在のウォレットにのみ属します。",
|
"contact_global_tt": "オン:この連絡先はどのウォレットを読み込んでも表示されます。オフ:現在のウォレットにのみ属します。",
|
||||||
"contact_preview_addr": "ここにアドレスが表示されます",
|
"contact_preview_addr": "ここにアドレスが表示されます",
|
||||||
"contact_preview_name": "連絡先名",
|
"contact_preview_name": "連絡先名",
|
||||||
|
"contact_wallet_loading": "ウォレットを読み込み中です。「すべてのウォレットに表示」にチェックするか、少し待ってから再試行してください。",
|
||||||
"contacts": "連絡先",
|
"contacts": "連絡先",
|
||||||
|
"contacts_avatar_shape": "アバターの形",
|
||||||
|
"contacts_list_scale": "リストの拡大率",
|
||||||
"contacts_search_no_match": "一致する連絡先がありません",
|
"contacts_search_no_match": "一致する連絡先がありません",
|
||||||
"contacts_search_placeholder": "連絡先を検索...",
|
"contacts_search_placeholder": "連絡先を検索...",
|
||||||
|
"contacts_settings_tip": "連絡先のカスタマイズ",
|
||||||
|
"contacts_settings_title": "連絡先設定",
|
||||||
|
"contacts_shape_circle": "円",
|
||||||
|
"contacts_shape_square": "四角",
|
||||||
|
"contacts_shape_tab": "左タブ",
|
||||||
"copied": "コピーしました!",
|
"copied": "コピーしました!",
|
||||||
"copy": "コピー",
|
"copy": "コピー",
|
||||||
"copy_address": "完全なアドレスをコピー",
|
"copy_address": "完全なアドレスをコピー",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "バンドル版",
|
"daemon_bundled": "バンドル版",
|
||||||
"daemon_install_bundled": "バンドル版をインストール",
|
"daemon_install_bundled": "バンドル版をインストール",
|
||||||
"daemon_installed": "インストール済み",
|
"daemon_installed": "インストール済み",
|
||||||
|
"daemon_maintenance_label": "メンテナンス",
|
||||||
"daemon_none_bundled": "このビルドにはなし",
|
"daemon_none_bundled": "このビルドにはなし",
|
||||||
"daemon_not_installed": "未インストール",
|
"daemon_not_installed": "未インストール",
|
||||||
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
|
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "最新:",
|
"daemon_update_latest": "最新:",
|
||||||
"daemon_update_loading": "リリースを読み込み中…",
|
"daemon_update_loading": "リリースを読み込み中…",
|
||||||
"daemon_update_now": "今すぐ更新",
|
"daemon_update_now": "今すぐ更新",
|
||||||
|
"daemon_update_prompt_title": "ノードデーモンを更新しますか?",
|
||||||
"daemon_update_reinstall": "再インストール",
|
"daemon_update_reinstall": "再インストール",
|
||||||
"daemon_update_restart_note": "新しいバージョンを実行するにはデーモンを再起動してください。",
|
"daemon_update_restart_note": "新しいバージョンを実行するにはデーモンを再起動してください。",
|
||||||
"daemon_update_restart_now": "今すぐデーモンを再起動",
|
"daemon_update_restart_now": "今すぐデーモンを再起動",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
|
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
|
||||||
"daemon_update_verifying": "検証中…",
|
"daemon_update_verifying": "検証中…",
|
||||||
"daemon_update_version": "バージョン:",
|
"daemon_update_version": "バージョン:",
|
||||||
|
"daemon_updates_label": "アップデート",
|
||||||
"daemon_version": "デーモン",
|
"daemon_version": "デーモン",
|
||||||
"dark": "ダーク",
|
"dark": "ダーク",
|
||||||
|
"data_stale_prefix": "更新",
|
||||||
|
"data_stale_tooltip": "残高が最新でない可能性があります。ウォレットは最近更新を受信していません。ノード接続を確認してください。",
|
||||||
"date": "日付",
|
"date": "日付",
|
||||||
"date_label": "日付:",
|
"date_label": "日付:",
|
||||||
"debug_logging": "デバッグログ",
|
"debug_logging": "デバッグログ",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "ブートストラップをダウンロード",
|
"download_bootstrap": "ブートストラップをダウンロード",
|
||||||
"dragonx_green": "DragonX(グリーン)",
|
"dragonx_green": "DragonX(グリーン)",
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
|
"empty_wallet_keys_suffix": "個の鍵",
|
||||||
|
"empty_wallet_open_manager": "ウォレットマネージャーを開く",
|
||||||
|
"empty_wallet_restore": "ウォレットを復元",
|
||||||
|
"empty_wallet_salvage_body": "このウォレットが空なのは、以前の自動修復によって元のウォレットがバックアップとして脇に保存されたためです。コインはほぼ確実にそのバックアップの中にあり、失われていません。復元すれば資金を再び読み込めます。何も削除されません。現在のファイルは先に脇へ保存されます。",
|
||||||
|
"empty_wallet_salvage_headline": "コインはバックアップファイルに安全に保管されています。",
|
||||||
|
"empty_wallet_salvage_title": "ウォレットが修復された可能性があります",
|
||||||
|
"empty_wallet_warning_body": "このウォレットにはアドレスも資金もありませんが、DragonX フォルダー内の別のウォレットファイルに鍵が含まれています。コインはおそらくそちらにあり、失われていません。ウォレットマネージャーを開いて、資金のあるウォレットに切り替えてください。",
|
||||||
|
"empty_wallet_warning_dismiss": "このウォレットでは今後警告しない",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "現在のウォレットファイルに対してのみこの警告を停止します。後で別の空のウォレットに切り替えると、再び警告される場合があります。",
|
||||||
|
"empty_wallet_warning_headline": "間違ったウォレットを開いた可能性があります。",
|
||||||
|
"empty_wallet_warning_title": "このウォレットは空です",
|
||||||
"enc_confirm": "確認:",
|
"enc_confirm": "確認:",
|
||||||
"enc_desc": "ウォレットを暗号化すると、パスフレーズで秘密鍵が保護されます。暗号化後、デーモンが再起動します。",
|
"enc_desc": "ウォレットを暗号化すると、パスフレーズで秘密鍵が保護されます。暗号化後、デーモンが再起動します。",
|
||||||
"enc_encrypting": "ウォレットを暗号化しています...",
|
"enc_encrypting": "ウォレットを暗号化しています...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "ライト",
|
"light": "ライト",
|
||||||
"lite_account_label": "アカウント",
|
"lite_account_label": "アカウント",
|
||||||
"lite_action": "アクション",
|
"lite_action": "アクション",
|
||||||
|
"lite_backend_unavailable": "ライトウォレットのバックエンドが利用できません",
|
||||||
"lite_backup_keys": "バックアップと鍵",
|
"lite_backup_keys": "バックアップと鍵",
|
||||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||||
"lite_birthday_label": "バースデー",
|
"lite_birthday_label": "バースデー",
|
||||||
|
"lite_console_backend_commands": "バックエンドコマンド:",
|
||||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||||
"lite_copy": "コピー",
|
"lite_copy": "コピー",
|
||||||
|
"lite_could_not_start": "操作を開始できませんでした",
|
||||||
"lite_could_not_write": "書き込めませんでした ",
|
"lite_could_not_write": "書き込めませんでした ",
|
||||||
"lite_encrypt_wallet": "ウォレットを暗号化",
|
"lite_encrypt_wallet": "ウォレットを暗号化",
|
||||||
"lite_encryption_removed": "暗号化を解除しました",
|
"lite_encryption_removed": "暗号化を解除しました",
|
||||||
|
"lite_enter_all_seed_words": "復元するには24個のシードワードをすべて入力してください(現在 %d 個)",
|
||||||
|
"lite_enter_wallet_path": "ウォレットのパスを入力してください",
|
||||||
"lite_hide_wipe": "非表示にして消去",
|
"lite_hide_wipe": "非表示にして消去",
|
||||||
"lite_import": "インポート",
|
"lite_import": "インポート",
|
||||||
"lite_import_key_label": "鍵をインポート",
|
"lite_import_key_label": "鍵をインポート",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://your-lite-server",
|
"lite_net_add_url_hint": "https://your-lite-server",
|
||||||
"lite_net_checking": "確認中…",
|
"lite_net_checking": "確認中…",
|
||||||
"lite_net_connected": "接続済み",
|
"lite_net_connected": "接続済み",
|
||||||
|
"lite_net_connecting": "接続中…",
|
||||||
"lite_net_custom": "カスタム",
|
"lite_net_custom": "カスタム",
|
||||||
"lite_net_disconnected": "未接続",
|
"lite_net_disconnected": "未接続",
|
||||||
"lite_net_hidden_section": "非表示のサーバー",
|
"lite_net_hidden_section": "非表示のサーバー",
|
||||||
@@ -638,6 +765,9 @@
|
|||||||
"lite_working": "処理中…",
|
"lite_working": "処理中…",
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
"loading_addresses": "アドレスを読み込み中...",
|
"loading_addresses": "アドレスを読み込み中...",
|
||||||
|
"loading_stall_body": "デーモンは %.0f 秒間初期化しています。アップデート後や初回起動時(ブロックインデックスの読み込みや再スキャン)は正常な場合があります。準備ができ次第、自動的に接続します。",
|
||||||
|
"loading_stall_hint": "まだ動かない場合は、設定を開いて「デーモンを再起動」を使うか、コンソールで詳細を確認してください。",
|
||||||
|
"loading_stall_title": "予想より時間がかかっています",
|
||||||
"loading_transactions": "トランザクションを読み込み中",
|
"loading_transactions": "トランザクションを読み込み中",
|
||||||
"local_hashrate": "ローカルハッシュレート",
|
"local_hashrate": "ローカルハッシュレート",
|
||||||
"low_spec_mode": "省電力モード",
|
"low_spec_mode": "省電力モード",
|
||||||
@@ -654,6 +784,9 @@
|
|||||||
"market_cap": "時価総額",
|
"market_cap": "時価総額",
|
||||||
"market_cap_short": "時価総額",
|
"market_cap_short": "時価総額",
|
||||||
"market_chart_loading": "価格履歴を読み込み中",
|
"market_chart_loading": "価格履歴を読み込み中",
|
||||||
|
"market_col_name": "名前",
|
||||||
|
"market_col_trend": "トレンド",
|
||||||
|
"market_col_value": "価値",
|
||||||
"market_iv_1d": "1日",
|
"market_iv_1d": "1日",
|
||||||
"market_iv_1h": "1時間",
|
"market_iv_1h": "1時間",
|
||||||
"market_iv_1m": "1ヶ月",
|
"market_iv_1m": "1ヶ月",
|
||||||
@@ -662,13 +795,18 @@
|
|||||||
"market_no_history": "価格履歴がありません",
|
"market_no_history": "価格履歴がありません",
|
||||||
"market_no_price": "価格データなし",
|
"market_no_price": "価格データなし",
|
||||||
"market_now": "現在",
|
"market_now": "現在",
|
||||||
|
"market_opt_chart_style": "チャートスタイル",
|
||||||
"market_pct_shielded": "%.0f%% シールド済み",
|
"market_pct_shielded": "%.0f%% シールド済み",
|
||||||
"market_portfolio": "ポートフォリオ",
|
"market_portfolio": "ポートフォリオ",
|
||||||
"market_price_loading": "価格データを読み込み中...",
|
"market_price_loading": "価格データを読み込み中...",
|
||||||
"market_price_unavailable": "価格データが利用できません",
|
"market_price_unavailable": "価格データが利用できません",
|
||||||
"market_refresh_price": "価格データを更新",
|
"market_refresh_price": "価格データを更新",
|
||||||
|
"market_settings_tip": "マーケットオプション",
|
||||||
|
"market_settings_title": "マーケット設定",
|
||||||
"market_style_candle": "ローソク足に切り替え",
|
"market_style_candle": "ローソク足に切り替え",
|
||||||
|
"market_style_candle_label": "ローソク足",
|
||||||
"market_style_line": "折れ線チャートに切り替え",
|
"market_style_line": "折れ線チャートに切り替え",
|
||||||
|
"market_style_line_label": "ライン",
|
||||||
"market_trade_on": "%s で取引",
|
"market_trade_on": "%s で取引",
|
||||||
"market_updated": "\\xc2\\xb7 更新: %s",
|
"market_updated": "\\xc2\\xb7 更新: %s",
|
||||||
"market_vol_short": "出来高",
|
"market_vol_short": "出来高",
|
||||||
@@ -762,6 +900,7 @@
|
|||||||
"mining_difficulty_copied": "難易度をコピーしました",
|
"mining_difficulty_copied": "難易度をコピーしました",
|
||||||
"mining_est_block": "予測ブロック",
|
"mining_est_block": "予測ブロック",
|
||||||
"mining_est_daily": "予測日収",
|
"mining_est_daily": "予測日収",
|
||||||
|
"mining_est_daily_pool_sub": "おおよそのソロ換算(プール手数料前)",
|
||||||
"mining_filter_all": "すべて",
|
"mining_filter_all": "すべて",
|
||||||
"mining_filter_tip_all": "すべての収益を表示",
|
"mining_filter_tip_all": "すべての収益を表示",
|
||||||
"mining_filter_tip_pool": "プール収益のみ表示",
|
"mining_filter_tip_pool": "プール収益のみ表示",
|
||||||
@@ -790,10 +929,12 @@
|
|||||||
"mining_open_in_explorer": "エクスプローラーで開く",
|
"mining_open_in_explorer": "エクスプローラーで開く",
|
||||||
"mining_payout_address": "支払いアドレス",
|
"mining_payout_address": "支払いアドレス",
|
||||||
"mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。",
|
"mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。",
|
||||||
|
"mining_payout_invalid": "有効な DragonX アドレスではありません — 開始前に修正してください。さもないとマイニング報酬が失われます。",
|
||||||
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
||||||
"mining_pool": "プール",
|
"mining_pool": "プール",
|
||||||
"mining_pool_fee": "手数料",
|
"mining_pool_fee": "手数料",
|
||||||
"mining_pool_hashrate": "プールハッシュレート",
|
"mining_pool_hashrate": "プールハッシュレート",
|
||||||
|
"mining_pool_needs_payout_tooltip": "先に支払い先アドレスを入力してください(Zアドレスを生成)",
|
||||||
"mining_pool_url": "プールURL",
|
"mining_pool_url": "プールURL",
|
||||||
"mining_pools_header": "プール",
|
"mining_pools_header": "プール",
|
||||||
"mining_recent_blocks": "最近のブロック",
|
"mining_recent_blocks": "最近のブロック",
|
||||||
@@ -823,6 +964,9 @@
|
|||||||
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
||||||
"mining_tag": " · マイニング",
|
"mining_tag": " · マイニング",
|
||||||
"mining_threads": "マイニングスレッド",
|
"mining_threads": "マイニングスレッド",
|
||||||
|
"mining_threads_input_tooltip": "正確なスレッド数を入力(Enter で適用)",
|
||||||
|
"mining_threads_minus_tooltip": "スレッドを減らす",
|
||||||
|
"mining_threads_plus_tooltip": "スレッドを増やす",
|
||||||
"mining_to_save": "保存する",
|
"mining_to_save": "保存する",
|
||||||
"mining_today": "今日",
|
"mining_today": "今日",
|
||||||
"mining_uptime": "稼働時間",
|
"mining_uptime": "稼働時間",
|
||||||
@@ -849,6 +993,11 @@
|
|||||||
"no_transactions": "取引が見つかりません",
|
"no_transactions": "取引が見つかりません",
|
||||||
"no_transactions_yet": "まだ取引がありません",
|
"no_transactions_yet": "まだ取引がありません",
|
||||||
"node": "ノード",
|
"node": "ノード",
|
||||||
|
"node_banner_crashed_title": "ノードが予期せず停止しました",
|
||||||
|
"node_banner_lite_open_failed": "ウォレットを開けませんでした",
|
||||||
|
"node_banner_offline_title": "DragonX ノードに接続されていません",
|
||||||
|
"node_banner_reconnect": "再接続",
|
||||||
|
"node_banner_restart": "ノードを再起動",
|
||||||
"node_security": "ノードとセキュリティ",
|
"node_security": "ノードとセキュリティ",
|
||||||
"noise": "ノイズ",
|
"noise": "ノイズ",
|
||||||
"not_connected": "デーモンに未接続...",
|
"not_connected": "デーモンに未接続...",
|
||||||
@@ -972,11 +1121,12 @@
|
|||||||
"portfolio_spark_min": "分",
|
"portfolio_spark_min": "分",
|
||||||
"portfolio_spark_month": "月",
|
"portfolio_spark_month": "月",
|
||||||
"portfolio_spark_week": "週",
|
"portfolio_spark_week": "週",
|
||||||
"portfolio_style_compact": "コンパクト行",
|
"portfolio_style_compact": "テーブル",
|
||||||
"portfolio_style_detailed": "詳細行",
|
"portfolio_style_detailed": "カード",
|
||||||
"portfolio_style_featured": "注目行",
|
"portfolio_style_featured": "スポットライト",
|
||||||
"portfolio_style_label": "ポートフォリオスタイル",
|
"portfolio_style_label": "ポートフォリオスタイル",
|
||||||
"portfolio_untitled": "無題",
|
"portfolio_untitled": "無題",
|
||||||
|
"portfolio_wallet_loading": "ウォレットの読み込みが終わってからグループを追加してください。",
|
||||||
"price_chart": "価格チャート",
|
"price_chart": "価格チャート",
|
||||||
"privacy_great": "優れたプライバシーです!",
|
"privacy_great": "優れたプライバシーです!",
|
||||||
"privacy_low": "プライバシーが低い — 資金をシールドしてください",
|
"privacy_low": "プライバシーが低い — 資金をシールドしてください",
|
||||||
@@ -986,6 +1136,8 @@
|
|||||||
"qr_failed": "QRコードの生成に失敗しました",
|
"qr_failed": "QRコードの生成に失敗しました",
|
||||||
"qr_title": "QRコード",
|
"qr_title": "QRコード",
|
||||||
"qr_unavailable": "QR利用不可",
|
"qr_unavailable": "QR利用不可",
|
||||||
|
"quick_receive": "クイック受取",
|
||||||
|
"quick_send": "クイック送金",
|
||||||
"ram_daemon_gb": "デーモン:%.1f GB (%s)",
|
"ram_daemon_gb": "デーモン:%.1f GB (%s)",
|
||||||
"ram_daemon_mb": "デーモン:%.0f MB (%s)",
|
"ram_daemon_mb": "デーモン:%.0f MB (%s)",
|
||||||
"ram_system_gb": "システム:%.1f / %.0f GB",
|
"ram_system_gb": "システム:%.1f / %.0f GB",
|
||||||
@@ -1035,6 +1187,7 @@
|
|||||||
"rpc_connection": "RPC接続...",
|
"rpc_connection": "RPC接続...",
|
||||||
"rpc_host": "RPCホスト",
|
"rpc_host": "RPCホスト",
|
||||||
"rpc_pass": "パスワード",
|
"rpc_pass": "パスワード",
|
||||||
|
"rpc_plaintext_remote_warning": "リモートRPCは暗号化されていないHTTPを使用しています。デーモンがTLSに対応している場合は、DRAGONX.confにrpctls=1を追加してください。",
|
||||||
"rpc_port": "ポート",
|
"rpc_port": "ポート",
|
||||||
"rpc_user": "ユーザー名",
|
"rpc_user": "ユーザー名",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
@@ -1077,6 +1230,7 @@
|
|||||||
"sb_waiting_daemon_err": "dragonxd を待機中 — %s",
|
"sb_waiting_daemon_err": "dragonxd を待機中 — %s",
|
||||||
"sb_warming_up": "ウォームアップ中...",
|
"sb_warming_up": "ウォームアップ中...",
|
||||||
"sb_witness_cache": "ウィットネスを再構築中",
|
"sb_witness_cache": "ウィットネスを再構築中",
|
||||||
|
"scale_effects": "スケールとエフェクト",
|
||||||
"screenshot_open_dir": "場所を開く",
|
"screenshot_open_dir": "場所を開く",
|
||||||
"screenshot_sweep": "スクリーンショットスイープを実行",
|
"screenshot_sweep": "スクリーンショットスイープを実行",
|
||||||
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
|
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
|
||||||
@@ -1141,6 +1295,7 @@
|
|||||||
"send_tooltip_not_connected": "デーモンに未接続",
|
"send_tooltip_not_connected": "デーモンに未接続",
|
||||||
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
|
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
|
||||||
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
|
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
|
||||||
|
"send_tooltip_view_only": "閲覧専用アドレス — 送金鍵がないため送金できません",
|
||||||
"send_total": "合計",
|
"send_total": "合計",
|
||||||
"send_transaction": "取引を送信",
|
"send_transaction": "取引を送信",
|
||||||
"send_tx_failed": "取引に失敗しました",
|
"send_tx_failed": "取引に失敗しました",
|
||||||
@@ -1160,16 +1315,16 @@
|
|||||||
"sent_filter": "送信済み",
|
"sent_filter": "送信済み",
|
||||||
"sent_type": "送信済み",
|
"sent_type": "送信済み",
|
||||||
"sent_upper": "送信済み",
|
"sent_upper": "送信済み",
|
||||||
"set_label": "ラベルを設定...",
|
"set_label": "ラベルを設定",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
|
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
|
||||||
"settings_acrylic_level": "アクリルレベル:",
|
"settings_acrylic_level": "アクリルレベル:",
|
||||||
"settings_address_book": "アドレス帳...",
|
"settings_address_book": "アドレス帳…",
|
||||||
"settings_auto_detected": "DRAGONX.conf から自動検出",
|
"settings_auto_detected": "DRAGONX.conf から自動検出",
|
||||||
"settings_auto_lock": "オートロック",
|
"settings_auto_lock": "オートロック",
|
||||||
"settings_auto_shield_desc": "透明資金を自動的にシールドアドレスに移動",
|
"settings_auto_shield_desc": "透明資金を自動的にシールドアドレスに移動",
|
||||||
"settings_auto_shield_funds": "透明資金を自動シールド",
|
"settings_auto_shield_funds": "透明資金を自動シールド",
|
||||||
"settings_backup": "バックアップ...",
|
"settings_backup": "バックアップ…",
|
||||||
"settings_block_explorer_urls": "ブロックエクスプローラーURL",
|
"settings_block_explorer_urls": "ブロックエクスプローラーURL",
|
||||||
"settings_builtin": "内蔵",
|
"settings_builtin": "内蔵",
|
||||||
"settings_change_passphrase": "パスフレーズを変更",
|
"settings_change_passphrase": "パスフレーズを変更",
|
||||||
@@ -1180,53 +1335,62 @@
|
|||||||
"settings_configure_explorer": "外部ブロックエクスプローラーリンクを設定",
|
"settings_configure_explorer": "外部ブロックエクスプローラーリンクを設定",
|
||||||
"settings_configure_rpc": "dragonxd デーモンへの接続を設定",
|
"settings_configure_rpc": "dragonxd デーモンへの接続を設定",
|
||||||
"settings_connection": "接続",
|
"settings_connection": "接続",
|
||||||
|
"settings_copy_diagnostics": "診断情報をコピー",
|
||||||
"settings_copyright": "Copyright 2024-2026 DragonX 開発者 | GPLv3 ライセンス",
|
"settings_copyright": "Copyright 2024-2026 DragonX 開発者 | GPLv3 ライセンス",
|
||||||
"settings_custom": "カスタム",
|
"settings_custom": "カスタム",
|
||||||
"settings_data_dir": "データディレクトリ:",
|
"settings_data_dir": "データディレクトリ:",
|
||||||
"settings_debug_changed": "デバッグカテゴリが変更されました — デーモンを再起動して適用",
|
"settings_debug_changed": "デバッグカテゴリが変更されました — デーモンを再起動して適用",
|
||||||
"settings_debug_restart_note": "変更はデーモンの再起動後に有効になります。",
|
"settings_debug_restart_note": "変更はデーモンの再起動後に有効になります。",
|
||||||
"settings_debug_select": "デーモンのデバッグログを有効にするカテゴリを選択(-debug= フラグ)。",
|
"settings_debug_select": "デーモンのデバッグログを有効にするカテゴリを選択(-debug= フラグ)。",
|
||||||
|
"settings_diagnostics_copied": "診断情報をクリップボードにコピーしました",
|
||||||
"settings_encrypt_first_pin": "PIN を有効にするには、まずウォレットを暗号化してください",
|
"settings_encrypt_first_pin": "PIN を有効にするには、まずウォレットを暗号化してください",
|
||||||
"settings_encrypt_wallet": "ウォレットを暗号化",
|
"settings_encrypt_wallet": "ウォレットを暗号化",
|
||||||
"settings_explorer_hint": "URLには末尾のスラッシュを含めてください。txid/アドレスが追加されます。",
|
"settings_explorer_hint": "URLには末尾のスラッシュを含めてください。txid/アドレスが追加されます。",
|
||||||
"settings_export_all": "すべてエクスポート...",
|
"settings_export_all": "すべてエクスポート…",
|
||||||
"settings_export_csv": "CSV エクスポート...",
|
"settings_export_csv": "CSV エクスポート…",
|
||||||
"settings_export_key": "鍵をエクスポート...",
|
"settings_export_key": "鍵をエクスポート…",
|
||||||
"settings_gradient_bg": "グラデーション背景",
|
"settings_gradient_bg": "グラデーション背景",
|
||||||
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
|
"settings_gradient_desc": "テクスチャ背景を滑らかなグラデーションに置換",
|
||||||
"settings_idle_after": "経過後",
|
"settings_idle_after": "経過後",
|
||||||
"settings_import_key": "秘密鍵をインポート...",
|
"settings_import_key": "秘密鍵をインポート…",
|
||||||
"settings_import_viewkey": "閲覧鍵をインポート...",
|
"settings_import_viewkey": "閲覧鍵をインポート…",
|
||||||
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
|
"settings_language_note": "注意:一部のテキストは更新に再起動が必要です",
|
||||||
"settings_lock_now": "今すぐロック",
|
"settings_lock_now": "今すぐロック",
|
||||||
"settings_locked": "ロック済み",
|
"settings_locked": "ロック済み",
|
||||||
"settings_merge_to_address": "アドレスにマージ...",
|
"settings_merge_to_address": "アドレスにマージ…",
|
||||||
"settings_noise_opacity": "ノイズ不透明度:",
|
"settings_noise_opacity": "ノイズ不透明度:",
|
||||||
|
"settings_not_connected": "デーモンに接続されていません",
|
||||||
"settings_not_encrypted": "暗号化されていません",
|
"settings_not_encrypted": "暗号化されていません",
|
||||||
"settings_not_found": "見つかりません",
|
"settings_not_found": "見つかりません",
|
||||||
"settings_open_app_dir": "アプリフォルダを開く",
|
"settings_open_app_dir": "アプリフォルダを開く",
|
||||||
"settings_open_data_dir": "データフォルダを開く",
|
"settings_open_data_dir": "データフォルダを開く",
|
||||||
|
"settings_open_log_folder": "ログフォルダを開く",
|
||||||
"settings_other": "その他",
|
"settings_other": "その他",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "プライバシー",
|
"settings_privacy": "プライバシー",
|
||||||
"settings_quick_unlock_pin": "クイックアンロック PIN",
|
"settings_quick_unlock_pin": "クイックアンロック PIN",
|
||||||
"settings_reduce_transparency": "透明度を下げる",
|
"settings_reduce_transparency": "透明度を下げる",
|
||||||
|
"settings_reloaded": "ディスクから設定を再読み込みしました",
|
||||||
"settings_remove_encryption": "暗号化を解除",
|
"settings_remove_encryption": "暗号化を解除",
|
||||||
"settings_remove_pin": "PIN を削除",
|
"settings_remove_pin": "PIN を削除",
|
||||||
"settings_request_payment": "支払い請求...",
|
"settings_request_payment": "支払い請求…",
|
||||||
"settings_rescan_desc": "欠落したトランザクションのためにブロックチェーンを再スキャン",
|
"settings_rescan_desc": "欠落したトランザクションのためにブロックチェーンを再スキャン",
|
||||||
"settings_restart_daemon": "デーモンを再起動",
|
"settings_restart_daemon": "デーモンを再起動",
|
||||||
"settings_rpc_connection": "RPC 接続",
|
"settings_rpc_connection": "RPC 接続",
|
||||||
|
"settings_rpc_error_prefix": "RPCエラー: ",
|
||||||
"settings_rpc_note": "注意:接続設定は通常 DRAGONX.conf から自動検出されます",
|
"settings_rpc_note": "注意:接続設定は通常 DRAGONX.conf から自動検出されます",
|
||||||
|
"settings_rpc_ok": "RPC接続は正常です",
|
||||||
"settings_save_shielded_desc": "z-addr トランザクションをローカルファイルに保存して表示",
|
"settings_save_shielded_desc": "z-addr トランザクションをローカルファイルに保存して表示",
|
||||||
"settings_save_shielded_local": "シールドトランザクション履歴をローカルに保存",
|
"settings_save_shielded_local": "シールドトランザクション履歴をローカルに保存",
|
||||||
|
"settings_saved": "設定を保存しました",
|
||||||
"settings_set_pin": "PIN を設定",
|
"settings_set_pin": "PIN を設定",
|
||||||
"settings_shield_mining": "マイニングシールド...",
|
"settings_shield_mining": "マイニングシールド…",
|
||||||
"settings_solid_colors_desc": "ぼかし効果の代わりに単色を使用(アクセシビリティ)",
|
"settings_solid_colors_desc": "ぼかし効果の代わりに単色を使用(アクセシビリティ)",
|
||||||
|
"settings_theme_refreshed": "テーマ一覧を更新しました",
|
||||||
"settings_tor_desc": "プライバシー向上のため全接続を Tor 経由にする",
|
"settings_tor_desc": "プライバシー向上のため全接続を Tor 経由にする",
|
||||||
"settings_unlocked": "ロック解除",
|
"settings_unlocked": "ロック解除",
|
||||||
"settings_use_tor_network": "ネットワーク接続に Tor を使用",
|
"settings_use_tor_network": "ネットワーク接続に Tor を使用",
|
||||||
"settings_validate_address": "アドレス検証...",
|
"settings_validate_address": "アドレス検証…",
|
||||||
"settings_visual_effects": "視覚効果",
|
"settings_visual_effects": "視覚効果",
|
||||||
"settings_wallet_file_size": "ウォレットファイルサイズ:%s",
|
"settings_wallet_file_size": "ウォレットファイルサイズ:%s",
|
||||||
"settings_wallet_info": "ウォレット情報",
|
"settings_wallet_info": "ウォレット情報",
|
||||||
@@ -1234,6 +1398,8 @@
|
|||||||
"settings_wallet_maintenance": "ウォレットメンテナンス",
|
"settings_wallet_maintenance": "ウォレットメンテナンス",
|
||||||
"settings_wallet_not_found": "ウォレットファイルが見つかりません",
|
"settings_wallet_not_found": "ウォレットファイルが見つかりません",
|
||||||
"settings_wallet_size_label": "ウォレットサイズ:",
|
"settings_wallet_size_label": "ウォレットサイズ:",
|
||||||
|
"settings_ztx_cleared": "Zトランザクション履歴を消去しました",
|
||||||
|
"settings_ztx_not_found": "履歴ファイルが見つかりません",
|
||||||
"setup_wizard": "セットアップウィザード",
|
"setup_wizard": "セットアップウィザード",
|
||||||
"share": "共有",
|
"share": "共有",
|
||||||
"shield_check_status": "ステータスを確認",
|
"shield_check_status": "ステータスを確認",
|
||||||
@@ -1292,6 +1458,23 @@
|
|||||||
"sweep_to": "集約先:",
|
"sweep_to": "集約先:",
|
||||||
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
|
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
|
||||||
"sweep_tx": "取引:",
|
"sweep_tx": "取引:",
|
||||||
|
"switch_corrupt_body": "このウォレットは破損しているようです。ノードが開けませんでした。バックアップから復元するか、作り直すか、修復を試してください。",
|
||||||
|
"switch_corrupt_repair": "修復を試す(salvage)",
|
||||||
|
"switch_progress_background": "バックグラウンドで続行",
|
||||||
|
"switch_progress_default_wallet": "既定のウォレット",
|
||||||
|
"switch_progress_elapsed": "経過",
|
||||||
|
"switch_progress_external_wallet": "外部ウォレット",
|
||||||
|
"switch_progress_failed_title": "ウォレットの切り替えに失敗しました",
|
||||||
|
"switch_progress_from_label": "元:",
|
||||||
|
"switch_progress_hint": "正常なシャットダウンには最大1分かかることがあります。",
|
||||||
|
"switch_progress_reconnecting": "再接続しています",
|
||||||
|
"switch_progress_starting": "新しいウォレットでノードを起動しています",
|
||||||
|
"switch_progress_stopping": "現在のノードを停止しています",
|
||||||
|
"switch_progress_title": "ウォレットを切り替え中",
|
||||||
|
"switch_stopnode_body": "ウォレットを切り替えると、選択したウォレットでノードが再起動します。実行中のノードは停止され、新しいウォレットで再起動されます。意図的に起動したままにしていた場合は、自動的に復帰します。",
|
||||||
|
"switch_stopnode_confirm": "ノードを停止して切り替え",
|
||||||
|
"switch_stopnode_title": "実行中のノードを停止しますか?",
|
||||||
|
"switch_stopnode_warn": "このウォレットが起動していないノードがすでに実行中です。",
|
||||||
"syncing": "同期中...",
|
"syncing": "同期中...",
|
||||||
"t_address": "Tアドレス",
|
"t_address": "Tアドレス",
|
||||||
"t_addresses": "Tアドレス",
|
"t_addresses": "Tアドレス",
|
||||||
@@ -1299,6 +1482,7 @@
|
|||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"theme_effects": "テーマ効果",
|
"theme_effects": "テーマ効果",
|
||||||
"theme_language": "テーマと言語",
|
"theme_language": "テーマと言語",
|
||||||
|
"tile_click_to_open": "クリックして開く",
|
||||||
"time_days_ago": "%d日前",
|
"time_days_ago": "%d日前",
|
||||||
"time_hours_ago": "%d時間前",
|
"time_hours_ago": "%d時間前",
|
||||||
"time_minutes_ago": "%d分前",
|
"time_minutes_ago": "%d分前",
|
||||||
@@ -1313,7 +1497,9 @@
|
|||||||
"to_upper": "宛先",
|
"to_upper": "宛先",
|
||||||
"tools": "ツール",
|
"tools": "ツール",
|
||||||
"tools_actions": "ツールとアクション...",
|
"tools_actions": "ツールとアクション...",
|
||||||
|
"tools_actions_hdr": "ツールと操作",
|
||||||
"total": "合計",
|
"total": "合計",
|
||||||
|
"total_balance_label": "総残高",
|
||||||
"transaction_id": "取引ID",
|
"transaction_id": "取引ID",
|
||||||
"transaction_sent": "取引の送信に成功しました",
|
"transaction_sent": "取引の送信に成功しました",
|
||||||
"transaction_sent_msg": "取引を送信しました!",
|
"transaction_sent_msg": "取引を送信しました!",
|
||||||
@@ -1335,13 +1521,24 @@
|
|||||||
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
|
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
|
||||||
"tt_backup": "wallet.dat のバックアップを作成",
|
"tt_backup": "wallet.dat のバックアップを作成",
|
||||||
"tt_block_explorer": "ブラウザで DragonX ブロックエクスプローラーを開く",
|
"tt_block_explorer": "ブラウザで DragonX ブロックエクスプローラーを開く",
|
||||||
"tt_blur": "ぼかし量(0%% = オフ、100%% = 最大)",
|
"tt_blur": "ぼかし量(0% = オフ、100% = 最大)",
|
||||||
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
|
"tt_change_pass": "ウォレットの暗号化パスフレーズを変更",
|
||||||
"tt_change_pin": "アンロック PIN を変更",
|
"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_clear_ztx": "ローカルにキャッシュされた z-トランザクション履歴を削除",
|
||||||
|
"tt_clock_format": "24時間または12時間表示(アプリ全体)。チャットで上書きできます。",
|
||||||
|
"tt_copy_diagnostics": "サポート用の概要(バージョン、デーモン/ウォレット/ログの状態 — 秘密情報なし)をクリップボードにコピーします",
|
||||||
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
||||||
"tt_custom_theme": "カスタムテーマがアクティブ",
|
"tt_custom_theme": "カスタムテーマがアクティブ",
|
||||||
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
||||||
|
"tt_daemon_refresh": "上に表示されているインストール済みおよび同梱のdragonxdのバージョン、サイズ、日付を再読み込みします",
|
||||||
"tt_daemon_update_check": "プロジェクトの Gitea から最新の dragonxd フルノードをダウンロードして検証し、再起動して適用します",
|
"tt_daemon_update_check": "プロジェクトの Gitea から最新の dragonxd フルノードをダウンロードして検証し、再起動して適用します",
|
||||||
"tt_debug_collapse": "デバッグログオプションを折りたたむ",
|
"tt_debug_collapse": "デバッグログオプションを折りたたむ",
|
||||||
"tt_debug_expand": "デバッグログオプションを展開",
|
"tt_debug_expand": "デバッグログオプションを展開",
|
||||||
@@ -1359,15 +1556,39 @@
|
|||||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||||
"tt_language": "ウォレット UI のインターフェース言語",
|
"tt_language": "ウォレット UI のインターフェース言語",
|
||||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
"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_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_lock": "ウォレットを即座にロック",
|
||||||
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
||||||
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
||||||
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
|
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
|
||||||
"tt_noise": "グレインテクスチャ強度(0%% = オフ、100%% = 最大)",
|
"tt_noise": "グレインテクスチャ強度(0% = オフ、100% = 最大)",
|
||||||
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
|
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
|
||||||
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
|
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
|
||||||
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
|
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
|
||||||
|
"tt_open_log_folder": "デバッグログとクラッシュログが入ったフォルダを開きます",
|
||||||
"tt_reduce_motion": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
|
"tt_reduce_motion": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
|
||||||
"tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存",
|
"tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存",
|
||||||
"tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求",
|
"tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求",
|
||||||
@@ -1380,12 +1601,17 @@
|
|||||||
"tt_rpc_host": "DragonX デーモンのホスト名",
|
"tt_rpc_host": "DragonX デーモンのホスト名",
|
||||||
"tt_rpc_pass": "RPC 認証パスワード",
|
"tt_rpc_pass": "RPC 認証パスワード",
|
||||||
"tt_rpc_port": "デーモン RPC 接続用ポート",
|
"tt_rpc_port": "デーモン RPC 接続用ポート",
|
||||||
|
"tt_rpc_toggle": "デーモンの読み取り専用のRPC接続情報(ホスト、ポート、ユーザー、パスワード)を表示または非表示にします",
|
||||||
"tt_rpc_user": "RPC 認証ユーザー名",
|
"tt_rpc_user": "RPC 認証ユーザー名",
|
||||||
"tt_save_settings": "すべての設定をディスクに保存",
|
"tt_save_settings": "すべての設定をディスクに保存",
|
||||||
"tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み",
|
"tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み",
|
||||||
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s",
|
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s",
|
||||||
"tt_scanline": "コンソールでの CRT スキャンライン効果",
|
"tt_scanline": "コンソールでの CRT スキャンライン効果",
|
||||||
|
"tt_screenshot_open_dir": "スクリーンショットフォルダ(設定ディレクトリ内)をファイルマネージャーで開きます",
|
||||||
|
"tt_screenshot_sweep": "すべてのタブですべてのテーマを順に切り替え、それぞれのスクリーンショットを設定のスクリーンショットフォルダに保存します(前回の実行を上書きします)",
|
||||||
|
"tt_screenshot_sweep_full": "テーマの実行と同様ですが、一時的なオフラインのデモウォレットデータを使って、すべてのモーダル/ダイアログ/フローも撮影します",
|
||||||
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
|
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
|
||||||
|
"tt_seed_demo_chat": "実行でUIを撮影できるように、チャットタブにサンプルの会話を挿入します。メモリ上のみで、再起動で消えます",
|
||||||
"tt_seed_migrate": "新しいシードフレーズウォレットを作成し、資金をそこへ移動します",
|
"tt_seed_migrate": "新しいシードフレーズウォレットを作成し、資金をそこへ移動します",
|
||||||
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
|
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
|
||||||
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
|
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
|
||||||
@@ -1397,13 +1623,14 @@
|
|||||||
"tt_theme_hotkey": "ホットキー:Ctrl+左/右でテーマを切り替え",
|
"tt_theme_hotkey": "ホットキー:Ctrl+左/右でテーマを切り替え",
|
||||||
"tt_tor": "匿名性のためにデーモン接続を Tor ネットワーク経由でルーティング",
|
"tt_tor": "匿名性のためにデーモン接続を Tor ネットワーク経由でルーティング",
|
||||||
"tt_tx_url": "ブロックエクスプローラーでトランザクションを表示するためのベース URL",
|
"tt_tx_url": "ブロックエクスプローラーでトランザクションを表示するためのベース URL",
|
||||||
"tt_ui_opacity": "カードとサイドバーの不透明度(100%% = 完全不透明、低い = より透過)",
|
"tt_ui_opacity": "カードとサイドバーの不透明度(100% = 完全不透明、低い = より透過)",
|
||||||
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
||||||
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
||||||
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
||||||
"tt_website": "DragonX ウェブサイトを開く",
|
"tt_website": "DragonX ウェブサイトを開く",
|
||||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||||
"tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます",
|
"tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます",
|
||||||
|
"tx_chat_badge": "メッセージ",
|
||||||
"tx_confirmations": "%d 確認",
|
"tx_confirmations": "%d 確認",
|
||||||
"tx_details_title": "取引の詳細",
|
"tx_details_title": "取引の詳細",
|
||||||
"tx_from_address": "送信元アドレス:",
|
"tx_from_address": "送信元アドレス:",
|
||||||
@@ -1449,6 +1676,7 @@
|
|||||||
"validate_not_mine": "このウォレットに属していません",
|
"validate_not_mine": "このウォレットに属していません",
|
||||||
"validate_ownership": "所有者:",
|
"validate_ownership": "所有者:",
|
||||||
"validate_results": "結果:",
|
"validate_results": "結果:",
|
||||||
|
"validate_results_placeholder": "ここに結果が表示されます",
|
||||||
"validate_shielded_type": "シールド(zアドレス)",
|
"validate_shielded_type": "シールド(zアドレス)",
|
||||||
"validate_status": "ステータス:",
|
"validate_status": "ステータス:",
|
||||||
"validate_title": "アドレスを検証",
|
"validate_title": "アドレスを検証",
|
||||||
@@ -1472,6 +1700,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ 別のフォルダーをウォレット検索…",
|
"wallets_add_folder_toggle": "+ 別のフォルダーをウォレット検索…",
|
||||||
"wallets_badge_encrypted": "暗号化済み(パスフレーズ保護)",
|
"wallets_badge_encrypted": "暗号化済み(パスフレーズ保護)",
|
||||||
"wallets_badge_encrypted_short": "暗号化",
|
"wallets_badge_encrypted_short": "暗号化",
|
||||||
|
"wallets_badge_hd": "HDウォレット — 開かないとシードフレーズを確認できません",
|
||||||
|
"wallets_badge_hd_short": "HDウォレット",
|
||||||
"wallets_badge_legacy": "レガシーウォレット(シードフレーズなし)",
|
"wallets_badge_legacy": "レガシーウォレット(シードフレーズなし)",
|
||||||
"wallets_badge_legacy_short": "レガシー",
|
"wallets_badge_legacy_short": "レガシー",
|
||||||
"wallets_badge_seed": "シードフレーズウォレット (HD)",
|
"wallets_badge_seed": "シードフレーズウォレット (HD)",
|
||||||
@@ -1587,6 +1817,7 @@
|
|||||||
"xmrig_loading_releases": "リリースを読み込み中…",
|
"xmrig_loading_releases": "リリースを読み込み中…",
|
||||||
"xmrig_none": "なし",
|
"xmrig_none": "なし",
|
||||||
"xmrig_reinstall": "再インストール",
|
"xmrig_reinstall": "再インストール",
|
||||||
|
"xmrig_releases": "xmrig リリース",
|
||||||
"xmrig_stop_mining_first": "マイナーを更新する前にマイニングを停止してください。",
|
"xmrig_stop_mining_first": "マイナーを更新する前にマイニングを停止してください。",
|
||||||
"xmrig_unavailable_body": "このプラットフォーム向けのマイナービルドは利用できません。",
|
"xmrig_unavailable_body": "このプラットフォーム向けのマイナービルドは利用できません。",
|
||||||
"xmrig_unavailable_title": "マイナーの更新は利用できません",
|
"xmrig_unavailable_title": "マイナーの更新は利用できません",
|
||||||
|
|||||||
277
res/lang/ko.json
277
res/lang/ko.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "고급 설정",
|
"advanced": "고급 설정",
|
||||||
"advanced_effects": "고급 효과...",
|
"advanced_effects": "고급 효과...",
|
||||||
"ago": "전",
|
"ago": "전",
|
||||||
|
"alerts_clear": "알림 기록 지우기",
|
||||||
|
"alerts_history_tooltip": "최근 알림",
|
||||||
|
"alerts_none": "아직 알림이 없습니다",
|
||||||
|
"alerts_recent": "최근 알림",
|
||||||
"all_filter": "전체",
|
"all_filter": "전체",
|
||||||
"allow_custom_fees": "사용자 정의 수수료 허용",
|
"allow_custom_fees": "사용자 정의 수수료 허용",
|
||||||
"amount": "금액",
|
"amount": "금액",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender가 채굴기를 차단했습니다",
|
"av_title": "Windows Defender가 채굴기를 차단했습니다",
|
||||||
"available": "사용 가능",
|
"available": "사용 가능",
|
||||||
"backup_backing_up": "백업 중...",
|
"backup_backing_up": "백업 중...",
|
||||||
|
"backup_col_backup": "백업",
|
||||||
|
"backup_col_export": "내보내기",
|
||||||
|
"backup_col_import": "가져오기 및 복원",
|
||||||
"backup_create": "백업 생성",
|
"backup_create": "백업 생성",
|
||||||
"backup_created": "지갑 백업이 생성되었습니다",
|
"backup_created": "지갑 백업이 생성되었습니다",
|
||||||
"backup_data": "백업 및 데이터",
|
"backup_data": "백업 및 데이터",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "잔액",
|
"balance": "잔액",
|
||||||
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
|
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
|
||||||
"balance_layout": "잔액 레이아웃",
|
"balance_layout": "잔액 레이아웃",
|
||||||
|
"balance_layout_switched": "레이아웃: %s",
|
||||||
|
"balance_mining_rate": "채굴 중 %s",
|
||||||
"balance_shielded_fmt": "차폐: %.8f",
|
"balance_shielded_fmt": "차폐: %.8f",
|
||||||
|
"balance_syncing_pct": "동기화 중 %.1f%%",
|
||||||
"balance_transparent_fmt": "투명: %.8f",
|
"balance_transparent_fmt": "투명: %.8f",
|
||||||
"ban": "차단",
|
"ban": "차단",
|
||||||
"banned_peers": "차단된 피어",
|
"banned_peers": "차단된 피어",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "체크섬 확인 중...",
|
"bootstrap_verifying": "체크섬 확인 중...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat 보호됨)",
|
"bootstrap_wallet_protected": "(wallet.dat 보호됨)",
|
||||||
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
|
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
|
||||||
|
"byte_count_fmt": "%zu / %zu 바이트",
|
||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"change_pass_confirm": "새 암호 확인:",
|
"change_pass_confirm": "새 암호 확인:",
|
||||||
"change_pass_current": "현재 암호:",
|
"change_pass_current": "현재 암호:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "암호 변경",
|
"change_pass_title": "암호 변경",
|
||||||
"characters": "문자",
|
"characters": "문자",
|
||||||
"chat": "채팅",
|
"chat": "채팅",
|
||||||
|
"chat_accent_amber": "황색",
|
||||||
|
"chat_accent_blue": "파랑",
|
||||||
|
"chat_accent_green": "초록",
|
||||||
|
"chat_accent_pink": "분홍",
|
||||||
|
"chat_accent_purple": "보라",
|
||||||
|
"chat_accent_theme": "테마",
|
||||||
|
"chat_add_contact": "연락처 추가",
|
||||||
|
"chat_awaiting_key": "답장 대기 중",
|
||||||
|
"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_cancel": "취소",
|
||||||
|
"chat_contact_added": "연락처 추가됨 — 연락처에서 이름을 변경하세요",
|
||||||
"chat_contact_request": "연락 요청",
|
"chat_contact_request": "연락 요청",
|
||||||
|
"chat_copy_address_tip": "클릭하여 주소 복사",
|
||||||
|
"chat_density_comfortable": "편안하게",
|
||||||
|
"chat_density_compact": "촘촘하게",
|
||||||
|
"chat_emoji_color": "컬러",
|
||||||
|
"chat_emoji_mono": "단색",
|
||||||
|
"chat_emoji_search": "이모지 검색",
|
||||||
"chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.",
|
"chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.",
|
||||||
|
"chat_empty_start": "\"새 대화\"로 시작하세요.",
|
||||||
|
"chat_empty_title": "아직 대화가 없습니다",
|
||||||
|
"chat_export": "채팅 내보내기…",
|
||||||
|
"chat_export_done": "대화를 내보냈습니다",
|
||||||
|
"chat_export_failed": "내보내기 파일을 쓸 수 없습니다.",
|
||||||
|
"chat_export_warn": "복호화된 메시지를 일반 텍스트로 저장합니다. 파일을 안전하게 보관하세요.",
|
||||||
|
"chat_filter": "채팅",
|
||||||
|
"chat_hidden_toast": "대화를 숨겼습니다 — 새 메시지가 오면 다시 표시됩니다",
|
||||||
|
"chat_hide": "숨기기",
|
||||||
|
"chat_hide_hidden": "숨김 접기",
|
||||||
|
"chat_jump_latest": "최신",
|
||||||
|
"chat_len_over": "메시지가 너무 깁니다",
|
||||||
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
|
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
|
||||||
"chat_new_button": "새 대화",
|
"chat_mute": "음소거",
|
||||||
|
"chat_new_button": "새 채팅",
|
||||||
"chat_new_message": "메시지",
|
"chat_new_message": "메시지",
|
||||||
|
"chat_new_message_toast": "새 암호화 채팅 메시지",
|
||||||
"chat_new_send": "요청 보내기",
|
"chat_new_send": "요청 보내기",
|
||||||
"chat_new_title": "새 대화",
|
"chat_new_title": "새 채팅",
|
||||||
"chat_new_zaddr": "받는 사람 z-주소",
|
"chat_new_zaddr": "받는 사람 z-주소",
|
||||||
|
"chat_no_matches": "검색과 일치하는 대화가 없습니다.",
|
||||||
|
"chat_no_z_contacts": "보호 주소 연락처가 아직 없습니다",
|
||||||
|
"chat_opt_bubble_accent": "말풍선 색상",
|
||||||
|
"chat_opt_bubble_style": "말풍선 스타일",
|
||||||
|
"chat_opt_density": "메시지 밀도",
|
||||||
|
"chat_opt_emoji": "이모지 스타일",
|
||||||
|
"chat_opt_enter_sends": "Enter로 전송",
|
||||||
|
"chat_opt_font_size": "글자 크기",
|
||||||
|
"chat_opt_global_clock": "전역 시간 형식",
|
||||||
|
"chat_opt_poll": "폴링 주기",
|
||||||
|
"chat_opt_timestamp": "타임스탬프",
|
||||||
|
"chat_pick_contact": "연락처에서 선택…",
|
||||||
|
"chat_rename": "연락처 이름 변경",
|
||||||
|
"chat_rename_hint": "연락처 이름",
|
||||||
|
"chat_renamed": "연락처 이름이 변경되었습니다",
|
||||||
|
"chat_retry": "다시 시도",
|
||||||
|
"chat_search": "대화 검색",
|
||||||
|
"chat_sec_appearance": "모양",
|
||||||
|
"chat_sec_messaging": "메시지",
|
||||||
"chat_select_hint": "볼 대화를 선택하세요.",
|
"chat_select_hint": "볼 대화를 선택하세요.",
|
||||||
"chat_send": "전송",
|
"chat_send": "전송",
|
||||||
"chat_send_failed": "전송 안 됨",
|
"chat_send_failed": "전송 안 됨",
|
||||||
|
"chat_sending": "전송 중…",
|
||||||
|
"chat_settings_done": "완료",
|
||||||
|
"chat_settings_section": "채팅 및 연락처",
|
||||||
|
"chat_settings_tip": "채팅 사용자 지정",
|
||||||
|
"chat_settings_title": "채팅 설정",
|
||||||
|
"chat_show_hidden": "숨김 보기",
|
||||||
|
"chat_time_now": "방금",
|
||||||
"chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).",
|
"chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).",
|
||||||
"chat_toast_lite_busy": "이미 전송이 진행 중이거나 열린 지갑이 없습니다.",
|
"chat_toast_lite_busy": "이미 전송이 진행 중이거나 열린 지갑이 없습니다.",
|
||||||
|
"chat_toast_need_funds": "채팅을 보내려면 수수료를 낼 소액의 보호 잔액이 필요합니다.",
|
||||||
"chat_toast_no_zaddr": "채팅을 보낼 z-주소가 없습니다.",
|
"chat_toast_no_zaddr": "채팅을 보낼 z-주소가 없습니다.",
|
||||||
"chat_toast_not_connected": "연결되지 않음 — 채팅 메시지가 전송되지 않았습니다.",
|
"chat_toast_not_connected": "연결되지 않음 — 채팅 메시지가 전송되지 않았습니다.",
|
||||||
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
||||||
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
||||||
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
||||||
|
"chat_today": "오늘",
|
||||||
|
"chat_ts_12h": "12시간",
|
||||||
|
"chat_ts_24h": "24시간",
|
||||||
|
"chat_ts_global": "전역 설정 따르기",
|
||||||
|
"chat_ts_global_short": "전역",
|
||||||
|
"chat_unhide": "다시 표시",
|
||||||
|
"chat_unmute": "음소거 해제",
|
||||||
|
"chat_verify_key": "신원 키 — 비교하여 확인",
|
||||||
"chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.",
|
"chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.",
|
||||||
|
"chat_yesterday": "어제",
|
||||||
"chat_you": "나",
|
"chat_you": "나",
|
||||||
"choose_icon": "아이콘 선택",
|
"choose_icon": "아이콘 선택",
|
||||||
"clear": "지우기",
|
"clear": "지우기",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "클릭하여 주소 복사",
|
"click_copy_address": "클릭하여 주소 복사",
|
||||||
"click_copy_uri": "클릭하여 URI 복사",
|
"click_copy_uri": "클릭하여 URI 복사",
|
||||||
"click_to_copy": "복사하려면 클릭",
|
"click_to_copy": "복사하려면 클릭",
|
||||||
|
"clock_format": "시간 형식",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
"conf_count": "%d 확인",
|
"conf_count": "%d 확인",
|
||||||
"confirm_and_send": "확인 후 전송",
|
"confirm_and_send": "확인 후 전송",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "앱",
|
"console_app": "앱",
|
||||||
"console_auto_scroll": "자동 스크롤",
|
"console_auto_scroll": "자동 스크롤",
|
||||||
"console_available_commands": "사용 가능한 명령어:",
|
"console_available_commands": "사용 가능한 명령어:",
|
||||||
|
"console_backend_reference": "백엔드 명령어 참조",
|
||||||
|
"console_backend_unavailable": "백엔드 없음",
|
||||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||||
|
"console_cat_advanced": "고급",
|
||||||
"console_cat_blockchain": "블록체인",
|
"console_cat_blockchain": "블록체인",
|
||||||
"console_cat_control": "제어",
|
"console_cat_control": "제어",
|
||||||
|
"console_cat_keys": "키 및 보안",
|
||||||
"console_cat_mining": "채굴",
|
"console_cat_mining": "채굴",
|
||||||
"console_cat_network": "네트워크",
|
"console_cat_network": "네트워크",
|
||||||
"console_cat_raw_transactions": "원시 트랜잭션",
|
"console_cat_raw_transactions": "원시 트랜잭션",
|
||||||
|
"console_cat_send": "보내기",
|
||||||
|
"console_cat_sync": "동기화",
|
||||||
"console_cat_utility": "유틸리티",
|
"console_cat_utility": "유틸리티",
|
||||||
"console_cat_wallet": "지갑",
|
"console_cat_wallet": "지갑",
|
||||||
"console_clear": "지우기",
|
"console_clear": "지우기",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - 도움말 표시",
|
"console_help_help": " help - 도움말 표시",
|
||||||
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||||
"console_help_stop": " stop - 데몬 중지",
|
"console_help_stop": " stop - 데몬 중지",
|
||||||
|
"console_last_error": "마지막 오류:",
|
||||||
"console_line_count": "%zu줄",
|
"console_line_count": "%zu줄",
|
||||||
"console_matches": "일치",
|
"console_matches": "일치",
|
||||||
"console_new_lines": "%d 새 줄",
|
"console_new_lines": "%d 새 줄",
|
||||||
"console_no_daemon": "데몬 없음",
|
"console_no_daemon": "데몬 없음",
|
||||||
|
"console_no_output": "(출력 없음)",
|
||||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||||
|
"console_not_connected_lite": "오류: 열린 지갑 없음",
|
||||||
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
||||||
"console_ref_builds": "생성",
|
"console_ref_builds": "생성",
|
||||||
"console_ref_cancel": "취소",
|
"console_ref_cancel": "취소",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
|
"console_ref_run_confirm": "%s 을(를) 지금 실행할까요? 영향이 큰 명령입니다.",
|
||||||
"console_ref_search_hint": "이름 또는 용도로 검색…",
|
"console_ref_search_hint": "이름 또는 용도로 검색…",
|
||||||
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
|
"console_ref_select_hint": "명령을 선택하면 설명이 표시됩니다.",
|
||||||
|
"console_ref_value": "값",
|
||||||
"console_rpc_reference": "RPC 명령어 참조",
|
"console_rpc_reference": "RPC 명령어 참조",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "콘솔 스캔라인",
|
"console_scanline": "콘솔 스캔라인",
|
||||||
"console_search_commands": "명령어 검색...",
|
"console_search_commands": "명령어 검색...",
|
||||||
"console_select_all": "모두 선택",
|
"console_select_all": "모두 선택",
|
||||||
"console_show_app_output": "[app] 지갑 로그 줄 표시",
|
"console_show_app_output": "[app] 지갑 로그 줄 표시",
|
||||||
|
"console_show_backend_ref": "백엔드 명령어 참조 표시",
|
||||||
"console_show_daemon_output": "데몬 출력 표시",
|
"console_show_daemon_output": "데몬 출력 표시",
|
||||||
"console_show_errors_only": "오류만 표시",
|
"console_show_errors_only": "오류만 표시",
|
||||||
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "중지됨",
|
"console_status_stopped": "중지됨",
|
||||||
"console_status_stopping": "중지 중",
|
"console_status_stopping": "중지 중",
|
||||||
"console_status_unknown": "알 수 없음",
|
"console_status_unknown": "알 수 없음",
|
||||||
|
"console_stop_confirm_node": "'stop'은 노드를 종료하고 지갑 연결을 끊습니다. 확인하려면 'stop'을 다시 입력하세요.",
|
||||||
"console_tab_completion": "Tab으로 자동 완성",
|
"console_tab_completion": "Tab으로 자동 완성",
|
||||||
"console_text_colors": "텍스트 색상",
|
"console_text_colors": "텍스트 색상",
|
||||||
"console_toggle_accents": "줄 색상 강조 전환",
|
"console_toggle_accents": "줄 색상 강조 전환",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "켜짐: 어떤 지갑을 불러오든 이 연락처가 계속 표시됩니다. 꺼짐: 현재 지갑에만 속합니다.",
|
"contact_global_tt": "켜짐: 어떤 지갑을 불러오든 이 연락처가 계속 표시됩니다. 꺼짐: 현재 지갑에만 속합니다.",
|
||||||
"contact_preview_addr": "여기에 주소가 표시됩니다",
|
"contact_preview_addr": "여기에 주소가 표시됩니다",
|
||||||
"contact_preview_name": "연락처 이름",
|
"contact_preview_name": "연락처 이름",
|
||||||
|
"contact_wallet_loading": "지갑을 아직 불러오는 중입니다 — “모든 지갑에 표시”를 선택하거나 잠시 후 다시 시도하세요.",
|
||||||
"contacts": "연락처",
|
"contacts": "연락처",
|
||||||
|
"contacts_avatar_shape": "아바타 모양",
|
||||||
|
"contacts_list_scale": "목록 배율",
|
||||||
"contacts_search_no_match": "일치하는 연락처 없음",
|
"contacts_search_no_match": "일치하는 연락처 없음",
|
||||||
"contacts_search_placeholder": "연락처 검색...",
|
"contacts_search_placeholder": "연락처 검색...",
|
||||||
|
"contacts_settings_tip": "연락처 사용자 지정",
|
||||||
|
"contacts_settings_title": "연락처 설정",
|
||||||
|
"contacts_shape_circle": "원",
|
||||||
|
"contacts_shape_square": "사각형",
|
||||||
|
"contacts_shape_tab": "왼쪽 탭",
|
||||||
"copied": "복사됨!",
|
"copied": "복사됨!",
|
||||||
"copy": "복사",
|
"copy": "복사",
|
||||||
"copy_address": "전체 주소 복사",
|
"copy_address": "전체 주소 복사",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "번들",
|
"daemon_bundled": "번들",
|
||||||
"daemon_install_bundled": "번들 버전 설치",
|
"daemon_install_bundled": "번들 버전 설치",
|
||||||
"daemon_installed": "설치됨",
|
"daemon_installed": "설치됨",
|
||||||
|
"daemon_maintenance_label": "유지 관리",
|
||||||
"daemon_none_bundled": "이 빌드에 없음",
|
"daemon_none_bundled": "이 빌드에 없음",
|
||||||
"daemon_not_installed": "설치되지 않음",
|
"daemon_not_installed": "설치되지 않음",
|
||||||
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
|
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "최신:",
|
"daemon_update_latest": "최신:",
|
||||||
"daemon_update_loading": "릴리스 불러오는 중…",
|
"daemon_update_loading": "릴리스 불러오는 중…",
|
||||||
"daemon_update_now": "지금 업데이트",
|
"daemon_update_now": "지금 업데이트",
|
||||||
|
"daemon_update_prompt_title": "노드 데몬을 업데이트하시겠습니까?",
|
||||||
"daemon_update_reinstall": "다시 설치",
|
"daemon_update_reinstall": "다시 설치",
|
||||||
"daemon_update_restart_note": "새 버전을 실행하려면 데몬을 재시작하세요.",
|
"daemon_update_restart_note": "새 버전을 실행하려면 데몬을 재시작하세요.",
|
||||||
"daemon_update_restart_now": "지금 데몬 재시작",
|
"daemon_update_restart_now": "지금 데몬 재시작",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
|
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
|
||||||
"daemon_update_verifying": "확인 중…",
|
"daemon_update_verifying": "확인 중…",
|
||||||
"daemon_update_version": "버전:",
|
"daemon_update_version": "버전:",
|
||||||
|
"daemon_updates_label": "업데이트",
|
||||||
"daemon_version": "데몬",
|
"daemon_version": "데몬",
|
||||||
"dark": "다크",
|
"dark": "다크",
|
||||||
|
"data_stale_prefix": "업데이트",
|
||||||
|
"data_stale_tooltip": "잔액이 오래되었을 수 있습니다 — 지갑이 최근에 업데이트를 받지 못했습니다. 노드 연결을 확인하세요.",
|
||||||
"date": "날짜",
|
"date": "날짜",
|
||||||
"date_label": "날짜:",
|
"date_label": "날짜:",
|
||||||
"debug_logging": "디버그 로깅",
|
"debug_logging": "디버그 로깅",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "부트스트랩 다운로드",
|
"download_bootstrap": "부트스트랩 다운로드",
|
||||||
"dragonx_green": "DragonX(그린)",
|
"dragonx_green": "DragonX(그린)",
|
||||||
"edit": "편집",
|
"edit": "편집",
|
||||||
|
"empty_wallet_keys_suffix": "개 키",
|
||||||
|
"empty_wallet_open_manager": "지갑 관리자 열기",
|
||||||
|
"empty_wallet_restore": "내 지갑 복원",
|
||||||
|
"empty_wallet_salvage_body": "이 지갑이 비어 있는 것은 이전의 자동 복구가 원본 지갑을 백업으로 따로 보관했기 때문입니다. 코인은 거의 확실히 그 백업에 있으며 사라지지 않았습니다. 복원하면 자금을 다시 불러올 수 있습니다. 아무것도 삭제되지 않으며, 현재 파일은 먼저 따로 보관됩니다.",
|
||||||
|
"empty_wallet_salvage_headline": "코인은 백업 파일에 안전하게 보관되어 있습니다.",
|
||||||
|
"empty_wallet_salvage_title": "지갑이 복구되었을 수 있습니다",
|
||||||
|
"empty_wallet_warning_body": "이 지갑에는 주소도 자금도 없지만, DragonX 폴더의 다른 지갑 파일에 키가 들어 있습니다. 코인은 대부분 그 안에 있으며 사라진 것이 아닙니다. 지갑 관리자를 열어 자금이 있는 지갑으로 전환하세요.",
|
||||||
|
"empty_wallet_warning_dismiss": "이 지갑에 대해 다시 경고하지 않기",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "현재 지갑 파일에 대해서만 이 경고를 중지합니다. 나중에 다른 빈 지갑으로 전환하면 다시 경고할 수 있습니다.",
|
||||||
|
"empty_wallet_warning_headline": "잘못된 지갑을 열었을 수 있습니다.",
|
||||||
|
"empty_wallet_warning_title": "이 지갑은 비어 있습니다",
|
||||||
"enc_confirm": "확인:",
|
"enc_confirm": "확인:",
|
||||||
"enc_desc": "지갑을 암호화하면 암호로 개인 키를 보호합니다. 암호화 후 데몬이 다시 시작됩니다.",
|
"enc_desc": "지갑을 암호화하면 암호로 개인 키를 보호합니다. 암호화 후 데몬이 다시 시작됩니다.",
|
||||||
"enc_encrypting": "지갑을 암호화하는 중...",
|
"enc_encrypting": "지갑을 암호화하는 중...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "라이트",
|
"light": "라이트",
|
||||||
"lite_account_label": "계정",
|
"lite_account_label": "계정",
|
||||||
"lite_action": "작업",
|
"lite_action": "작업",
|
||||||
|
"lite_backend_unavailable": "라이트 지갑 백엔드를 사용할 수 없습니다",
|
||||||
"lite_backup_keys": "백업 및 키",
|
"lite_backup_keys": "백업 및 키",
|
||||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||||
"lite_birthday_label": "생일 블록",
|
"lite_birthday_label": "생일 블록",
|
||||||
|
"lite_console_backend_commands": "백엔드 명령:",
|
||||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||||
"lite_copy": "복사",
|
"lite_copy": "복사",
|
||||||
|
"lite_could_not_start": "작업을 시작할 수 없습니다",
|
||||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||||
"lite_encrypt_wallet": "지갑 암호화",
|
"lite_encrypt_wallet": "지갑 암호화",
|
||||||
"lite_encryption_removed": "암호화가 제거되었습니다",
|
"lite_encryption_removed": "암호화가 제거되었습니다",
|
||||||
|
"lite_enter_all_seed_words": "복구하려면 24개의 시드 단어를 모두 입력하세요 (현재 %d개)",
|
||||||
|
"lite_enter_wallet_path": "지갑 경로를 입력하세요",
|
||||||
"lite_hide_wipe": "숨기고 삭제",
|
"lite_hide_wipe": "숨기고 삭제",
|
||||||
"lite_import": "가져오기",
|
"lite_import": "가져오기",
|
||||||
"lite_import_key_label": "키 가져오기",
|
"lite_import_key_label": "키 가져오기",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://your-lite-server",
|
"lite_net_add_url_hint": "https://your-lite-server",
|
||||||
"lite_net_checking": "확인 중…",
|
"lite_net_checking": "확인 중…",
|
||||||
"lite_net_connected": "연결됨",
|
"lite_net_connected": "연결됨",
|
||||||
|
"lite_net_connecting": "연결 중…",
|
||||||
"lite_net_custom": "사용자 지정",
|
"lite_net_custom": "사용자 지정",
|
||||||
"lite_net_disconnected": "연결되지 않음",
|
"lite_net_disconnected": "연결되지 않음",
|
||||||
"lite_net_hidden_section": "숨겨진 서버",
|
"lite_net_hidden_section": "숨겨진 서버",
|
||||||
@@ -638,6 +765,8 @@
|
|||||||
"lite_working": "작업 중…",
|
"lite_working": "작업 중…",
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
"loading_addresses": "주소 로딩 중...",
|
"loading_addresses": "주소 로딩 중...",
|
||||||
|
"loading_stall_body": "데몬이 %.0f초 동안 초기화 중입니다. 업데이트 후나 첫 실행 시(블록 인덱스 로드 또는 재스캔)에는 정상일 수 있습니다. 준비되면 자동으로 연결됩니다.",
|
||||||
|
"loading_stall_title": "예상보다 오래 걸리고 있습니다",
|
||||||
"loading_transactions": "거래를 불러오는 중",
|
"loading_transactions": "거래를 불러오는 중",
|
||||||
"local_hashrate": "로컬 해시레이트",
|
"local_hashrate": "로컬 해시레이트",
|
||||||
"low_spec_mode": "저사양 모드",
|
"low_spec_mode": "저사양 모드",
|
||||||
@@ -654,6 +783,9 @@
|
|||||||
"market_cap": "시가총액",
|
"market_cap": "시가총액",
|
||||||
"market_cap_short": "시총",
|
"market_cap_short": "시총",
|
||||||
"market_chart_loading": "가격 기록 불러오는 중",
|
"market_chart_loading": "가격 기록 불러오는 중",
|
||||||
|
"market_col_name": "이름",
|
||||||
|
"market_col_trend": "추세",
|
||||||
|
"market_col_value": "가치",
|
||||||
"market_iv_1d": "1일",
|
"market_iv_1d": "1일",
|
||||||
"market_iv_1h": "1시간",
|
"market_iv_1h": "1시간",
|
||||||
"market_iv_1m": "1개월",
|
"market_iv_1m": "1개월",
|
||||||
@@ -662,13 +794,18 @@
|
|||||||
"market_no_history": "가격 내역 없음",
|
"market_no_history": "가격 내역 없음",
|
||||||
"market_no_price": "가격 데이터 없음",
|
"market_no_price": "가격 데이터 없음",
|
||||||
"market_now": "현재",
|
"market_now": "현재",
|
||||||
|
"market_opt_chart_style": "차트 스타일",
|
||||||
"market_pct_shielded": "%.0f%% 차폐됨",
|
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||||
"market_portfolio": "포트폴리오",
|
"market_portfolio": "포트폴리오",
|
||||||
"market_price_loading": "가격 데이터를 불러오는 중...",
|
"market_price_loading": "가격 데이터를 불러오는 중...",
|
||||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||||
"market_refresh_price": "가격 데이터 새로고침",
|
"market_refresh_price": "가격 데이터 새로고침",
|
||||||
|
"market_settings_tip": "마켓 옵션",
|
||||||
|
"market_settings_title": "마켓 설정",
|
||||||
"market_style_candle": "캔들차트로 전환",
|
"market_style_candle": "캔들차트로 전환",
|
||||||
|
"market_style_candle_label": "캔들",
|
||||||
"market_style_line": "선형 차트로 전환",
|
"market_style_line": "선형 차트로 전환",
|
||||||
|
"market_style_line_label": "라인",
|
||||||
"market_trade_on": "%s에서 거래",
|
"market_trade_on": "%s에서 거래",
|
||||||
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
|
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
|
||||||
"market_vol_short": "거래량",
|
"market_vol_short": "거래량",
|
||||||
@@ -762,6 +899,7 @@
|
|||||||
"mining_difficulty_copied": "난이도가 복사되었습니다",
|
"mining_difficulty_copied": "난이도가 복사되었습니다",
|
||||||
"mining_est_block": "예상 블록",
|
"mining_est_block": "예상 블록",
|
||||||
"mining_est_daily": "예상 일일 수익",
|
"mining_est_daily": "예상 일일 수익",
|
||||||
|
"mining_est_daily_pool_sub": "대략적인 솔로 환산, 풀 수수료 전",
|
||||||
"mining_filter_all": "전체",
|
"mining_filter_all": "전체",
|
||||||
"mining_filter_tip_all": "모든 수익 표시",
|
"mining_filter_tip_all": "모든 수익 표시",
|
||||||
"mining_filter_tip_pool": "풀 수익만 표시",
|
"mining_filter_tip_pool": "풀 수익만 표시",
|
||||||
@@ -790,10 +928,12 @@
|
|||||||
"mining_open_in_explorer": "탐색기에서 열기",
|
"mining_open_in_explorer": "탐색기에서 열기",
|
||||||
"mining_payout_address": "지급 주소",
|
"mining_payout_address": "지급 주소",
|
||||||
"mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.",
|
"mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.",
|
||||||
|
"mining_payout_invalid": "유효한 DragonX 주소가 아닙니다 — 시작하기 전에 수정하세요. 그렇지 않으면 채굴 보상이 사라집니다.",
|
||||||
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
||||||
"mining_pool": "풀",
|
"mining_pool": "풀",
|
||||||
"mining_pool_fee": "수수료",
|
"mining_pool_fee": "수수료",
|
||||||
"mining_pool_hashrate": "풀 해시레이트",
|
"mining_pool_hashrate": "풀 해시레이트",
|
||||||
|
"mining_pool_needs_payout_tooltip": "먼저 지급 주소를 입력하세요 (Z 주소를 생성하세요)",
|
||||||
"mining_pool_url": "풀 URL",
|
"mining_pool_url": "풀 URL",
|
||||||
"mining_pools_header": "풀",
|
"mining_pools_header": "풀",
|
||||||
"mining_recent_blocks": "최근 블록",
|
"mining_recent_blocks": "최근 블록",
|
||||||
@@ -823,6 +963,9 @@
|
|||||||
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
||||||
"mining_tag": " · 채굴",
|
"mining_tag": " · 채굴",
|
||||||
"mining_threads": "채굴 스레드",
|
"mining_threads": "채굴 스레드",
|
||||||
|
"mining_threads_input_tooltip": "정확한 스레드 수 입력 (Enter로 적용)",
|
||||||
|
"mining_threads_minus_tooltip": "스레드 줄이기",
|
||||||
|
"mining_threads_plus_tooltip": "스레드 늘리기",
|
||||||
"mining_to_save": "저장하려면",
|
"mining_to_save": "저장하려면",
|
||||||
"mining_today": "오늘",
|
"mining_today": "오늘",
|
||||||
"mining_uptime": "가동 시간",
|
"mining_uptime": "가동 시간",
|
||||||
@@ -849,6 +992,11 @@
|
|||||||
"no_transactions": "거래 내역이 없습니다",
|
"no_transactions": "거래 내역이 없습니다",
|
||||||
"no_transactions_yet": "아직 거래 내역이 없습니다",
|
"no_transactions_yet": "아직 거래 내역이 없습니다",
|
||||||
"node": "노드",
|
"node": "노드",
|
||||||
|
"node_banner_crashed_title": "노드가 예기치 않게 중지되었습니다",
|
||||||
|
"node_banner_lite_open_failed": "지갑을 열 수 없습니다",
|
||||||
|
"node_banner_offline_title": "DragonX 노드에 연결되지 않음",
|
||||||
|
"node_banner_reconnect": "재연결",
|
||||||
|
"node_banner_restart": "노드 재시작",
|
||||||
"node_security": "노드 및 보안",
|
"node_security": "노드 및 보안",
|
||||||
"noise": "노이즈",
|
"noise": "노이즈",
|
||||||
"not_connected": "데몬에 연결되지 않음...",
|
"not_connected": "데몬에 연결되지 않음...",
|
||||||
@@ -972,11 +1120,12 @@
|
|||||||
"portfolio_spark_min": "분",
|
"portfolio_spark_min": "분",
|
||||||
"portfolio_spark_month": "월",
|
"portfolio_spark_month": "월",
|
||||||
"portfolio_spark_week": "주",
|
"portfolio_spark_week": "주",
|
||||||
"portfolio_style_compact": "간결한 행",
|
"portfolio_style_compact": "테이블",
|
||||||
"portfolio_style_detailed": "상세 행",
|
"portfolio_style_detailed": "카드",
|
||||||
"portfolio_style_featured": "강조 행",
|
"portfolio_style_featured": "스포트라이트",
|
||||||
"portfolio_style_label": "포트폴리오 스타일",
|
"portfolio_style_label": "포트폴리오 스타일",
|
||||||
"portfolio_untitled": "제목 없음",
|
"portfolio_untitled": "제목 없음",
|
||||||
|
"portfolio_wallet_loading": "지갑 로딩이 끝난 후 그룹을 추가하세요.",
|
||||||
"price_chart": "가격 차트",
|
"price_chart": "가격 차트",
|
||||||
"privacy_great": "프라이버시가 우수합니다!",
|
"privacy_great": "프라이버시가 우수합니다!",
|
||||||
"privacy_low": "낮은 프라이버시 — 자금을 차폐하세요",
|
"privacy_low": "낮은 프라이버시 — 자금을 차폐하세요",
|
||||||
@@ -986,6 +1135,8 @@
|
|||||||
"qr_failed": "QR 코드 생성 실패",
|
"qr_failed": "QR 코드 생성 실패",
|
||||||
"qr_title": "QR 코드",
|
"qr_title": "QR 코드",
|
||||||
"qr_unavailable": "QR 사용 불가",
|
"qr_unavailable": "QR 사용 불가",
|
||||||
|
"quick_receive": "빠른 받기",
|
||||||
|
"quick_send": "빠른 보내기",
|
||||||
"ram_daemon_gb": "데몬: %.1f GB (%s)",
|
"ram_daemon_gb": "데몬: %.1f GB (%s)",
|
||||||
"ram_daemon_mb": "데몬: %.0f MB (%s)",
|
"ram_daemon_mb": "데몬: %.0f MB (%s)",
|
||||||
"ram_system_gb": "시스템: %.1f / %.0f GB",
|
"ram_system_gb": "시스템: %.1f / %.0f GB",
|
||||||
@@ -1035,6 +1186,7 @@
|
|||||||
"rpc_connection": "RPC 연결...",
|
"rpc_connection": "RPC 연결...",
|
||||||
"rpc_host": "RPC 호스트",
|
"rpc_host": "RPC 호스트",
|
||||||
"rpc_pass": "비밀번호",
|
"rpc_pass": "비밀번호",
|
||||||
|
"rpc_plaintext_remote_warning": "원격 RPC가 암호화되지 않은 HTTP를 사용하고 있습니다. 데몬이 TLS를 지원하면 DRAGONX.conf에 rpctls=1을 추가하세요.",
|
||||||
"rpc_port": "포트",
|
"rpc_port": "포트",
|
||||||
"rpc_user": "사용자명",
|
"rpc_user": "사용자명",
|
||||||
"save": "저장",
|
"save": "저장",
|
||||||
@@ -1049,6 +1201,8 @@
|
|||||||
"sb_connecting_external": "외부 데몬에 연결 중...",
|
"sb_connecting_external": "외부 데몬에 연결 중...",
|
||||||
"sb_connecting_generic": "데몬에 연결 중...",
|
"sb_connecting_generic": "데몬에 연결 중...",
|
||||||
"sb_daemon_crashed": "데몬이 %d회 충돌함",
|
"sb_daemon_crashed": "데몬이 %d회 충돌함",
|
||||||
|
"sb_daemon_extract_failed": "데몬 파일을 쓰지 못했습니다. 디스크 여유 공간과 권한을 확인하세요.",
|
||||||
|
"sb_daemon_files_failed": "%s에 데몬 파일을 쓰지 못했습니다. 디스크 여유 공간과 권한을 확인하세요.",
|
||||||
"sb_daemon_not_found": "데몬을 찾을 수 없음",
|
"sb_daemon_not_found": "데몬을 찾을 수 없음",
|
||||||
"sb_daemon_start_failed": "dragonxd를 시작할 수 없습니다",
|
"sb_daemon_start_failed": "dragonxd를 시작할 수 없습니다",
|
||||||
"sb_dragonxd_running": "dragonxd 실행 중",
|
"sb_dragonxd_running": "dragonxd 실행 중",
|
||||||
@@ -1064,6 +1218,7 @@
|
|||||||
"sb_net_mhs": "네트: %.2f MH/s",
|
"sb_net_mhs": "네트: %.2f MH/s",
|
||||||
"sb_no_conf": "DRAGONX.conf를 찾을 수 없음",
|
"sb_no_conf": "DRAGONX.conf를 찾을 수 없음",
|
||||||
"sb_peers": "피어: %zu",
|
"sb_peers": "피어: %zu",
|
||||||
|
"sb_plaintext_remote_blocked": "원격 호스트로 RPC 자격 증명을 평문으로 보내는 것을 거부했습니다. 허용하려면 DRAGONX.conf에 rpcallowplaintext=1을 추가하거나 rpctls=1로 TLS를 활성화하세요.",
|
||||||
"sb_rescanning": "재스캔",
|
"sb_rescanning": "재스캔",
|
||||||
"sb_rescanning_pct": "재스캔 %.0f%%",
|
"sb_rescanning_pct": "재스캔 %.0f%%",
|
||||||
"sb_restarting_daemon": "데몬 재시작 중...",
|
"sb_restarting_daemon": "데몬 재시작 중...",
|
||||||
@@ -1077,6 +1232,7 @@
|
|||||||
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
|
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
|
||||||
"sb_warming_up": "워밍업 중...",
|
"sb_warming_up": "워밍업 중...",
|
||||||
"sb_witness_cache": "증인 재구축 중",
|
"sb_witness_cache": "증인 재구축 중",
|
||||||
|
"scale_effects": "배율 및 효과",
|
||||||
"screenshot_open_dir": "위치 열기",
|
"screenshot_open_dir": "위치 열기",
|
||||||
"screenshot_sweep": "스크린샷 스윕 실행",
|
"screenshot_sweep": "스크린샷 스윕 실행",
|
||||||
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
|
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
|
||||||
@@ -1141,6 +1297,7 @@
|
|||||||
"send_tooltip_not_connected": "데몬에 연결되지 않음",
|
"send_tooltip_not_connected": "데몬에 연결되지 않음",
|
||||||
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
|
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
|
||||||
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
|
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
|
||||||
|
"send_tooltip_view_only": "조회 전용 주소 — 지출 키가 없어 보낼 수 없습니다",
|
||||||
"send_total": "합계",
|
"send_total": "합계",
|
||||||
"send_transaction": "거래 전송",
|
"send_transaction": "거래 전송",
|
||||||
"send_tx_failed": "거래 실패",
|
"send_tx_failed": "거래 실패",
|
||||||
@@ -1160,16 +1317,16 @@
|
|||||||
"sent_filter": "전송됨",
|
"sent_filter": "전송됨",
|
||||||
"sent_type": "전송됨",
|
"sent_type": "전송됨",
|
||||||
"sent_upper": "전송됨",
|
"sent_upper": "전송됨",
|
||||||
"set_label": "라벨 설정...",
|
"set_label": "라벨 설정",
|
||||||
"settings": "설정",
|
"settings": "설정",
|
||||||
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
|
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
|
||||||
"settings_acrylic_level": "아크릴 레벨:",
|
"settings_acrylic_level": "아크릴 레벨:",
|
||||||
"settings_address_book": "주소록...",
|
"settings_address_book": "주소록…",
|
||||||
"settings_auto_detected": "DRAGONX.conf에서 자동 감지",
|
"settings_auto_detected": "DRAGONX.conf에서 자동 감지",
|
||||||
"settings_auto_lock": "자동 잠금",
|
"settings_auto_lock": "자동 잠금",
|
||||||
"settings_auto_shield_desc": "투명 자금을 자동으로 차폐 주소로 이동",
|
"settings_auto_shield_desc": "투명 자금을 자동으로 차폐 주소로 이동",
|
||||||
"settings_auto_shield_funds": "투명 자금 자동 차폐",
|
"settings_auto_shield_funds": "투명 자금 자동 차폐",
|
||||||
"settings_backup": "백업...",
|
"settings_backup": "백업…",
|
||||||
"settings_block_explorer_urls": "블록 탐색기 URL",
|
"settings_block_explorer_urls": "블록 탐색기 URL",
|
||||||
"settings_builtin": "내장",
|
"settings_builtin": "내장",
|
||||||
"settings_change_passphrase": "비밀번호 변경",
|
"settings_change_passphrase": "비밀번호 변경",
|
||||||
@@ -1180,60 +1337,71 @@
|
|||||||
"settings_configure_explorer": "외부 블록 탐색기 링크 구성",
|
"settings_configure_explorer": "외부 블록 탐색기 링크 구성",
|
||||||
"settings_configure_rpc": "dragonxd 데몬 연결 구성",
|
"settings_configure_rpc": "dragonxd 데몬 연결 구성",
|
||||||
"settings_connection": "연결",
|
"settings_connection": "연결",
|
||||||
|
"settings_copy_diagnostics": "진단 정보 복사",
|
||||||
"settings_copyright": "Copyright 2024-2026 DragonX 개발자 | GPLv3 라이선스",
|
"settings_copyright": "Copyright 2024-2026 DragonX 개발자 | GPLv3 라이선스",
|
||||||
"settings_custom": "사용자 지정",
|
"settings_custom": "사용자 지정",
|
||||||
"settings_data_dir": "데이터 디렉터리:",
|
"settings_data_dir": "데이터 디렉터리",
|
||||||
"settings_debug_changed": "디버그 카테고리가 변경되었습니다 — 데몬을 재시작하여 적용",
|
"settings_debug_changed": "디버그 카테고리가 변경되었습니다 — 데몬을 재시작하여 적용",
|
||||||
"settings_debug_restart_note": "변경 사항은 데몬을 다시 시작한 후에 적용됩니다.",
|
"settings_debug_restart_note": "변경 사항은 데몬을 다시 시작한 후에 적용됩니다.",
|
||||||
"settings_debug_select": "데몬 디버그 로깅을 활성화할 카테고리를 선택하세요 (-debug= 플래그).",
|
"settings_debug_select": "데몬 디버그 로깅을 활성화할 카테고리를 선택하세요 (-debug= 플래그).",
|
||||||
|
"settings_diagnostics_copied": "진단 정보를 클립보드에 복사했습니다",
|
||||||
"settings_encrypt_first_pin": "PIN을 활성화하려면 먼저 지갑을 암호화하세요",
|
"settings_encrypt_first_pin": "PIN을 활성화하려면 먼저 지갑을 암호화하세요",
|
||||||
"settings_encrypt_wallet": "지갑 암호화",
|
"settings_encrypt_wallet": "지갑 암호화",
|
||||||
"settings_explorer_hint": "URL에 후행 슬래시를 포함해야 합니다. txid/주소가 추가됩니다.",
|
"settings_explorer_hint": "URL에 후행 슬래시를 포함해야 합니다. txid/주소가 추가됩니다.",
|
||||||
"settings_export_all": "모두 내보내기...",
|
"settings_export_all": "모두 내보내기…",
|
||||||
"settings_export_csv": "CSV 내보내기...",
|
"settings_export_csv": "CSV 내보내기…",
|
||||||
"settings_export_key": "키 내보내기...",
|
"settings_export_key": "키 내보내기…",
|
||||||
"settings_gradient_bg": "그라데이션 배경",
|
"settings_gradient_bg": "그라데이션 배경",
|
||||||
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
|
"settings_gradient_desc": "텍스처 배경을 부드러운 그라데이션으로 교체",
|
||||||
"settings_idle_after": "후",
|
"settings_idle_after": "후",
|
||||||
"settings_import_key": "개인 키 가져오기...",
|
"settings_import_key": "개인 키 가져오기…",
|
||||||
"settings_import_viewkey": "조회 키 가져오기...",
|
"settings_import_viewkey": "조회 키 가져오기…",
|
||||||
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
|
"settings_language_note": "참고: 일부 텍스트는 업데이트하려면 다시 시작해야 합니다",
|
||||||
"settings_lock_now": "지금 잠금",
|
"settings_lock_now": "지금 잠금",
|
||||||
"settings_locked": "잠김",
|
"settings_locked": "잠김",
|
||||||
"settings_merge_to_address": "주소로 병합...",
|
"settings_merge_to_address": "주소로 병합…",
|
||||||
"settings_noise_opacity": "노이즈 불투명도:",
|
"settings_noise_opacity": "노이즈 불투명도:",
|
||||||
|
"settings_not_connected": "데몬에 연결되지 않음",
|
||||||
"settings_not_encrypted": "암호화되지 않음",
|
"settings_not_encrypted": "암호화되지 않음",
|
||||||
"settings_not_found": "찾을 수 없음",
|
"settings_not_found": "찾을 수 없음",
|
||||||
"settings_open_app_dir": "앱 폴더 열기",
|
"settings_open_app_dir": "앱 폴더 열기",
|
||||||
"settings_open_data_dir": "데이터 폴더 열기",
|
"settings_open_data_dir": "데이터 폴더 열기",
|
||||||
|
"settings_open_log_folder": "로그 폴더 열기",
|
||||||
"settings_other": "기타",
|
"settings_other": "기타",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "개인 정보",
|
"settings_privacy": "개인 정보",
|
||||||
"settings_quick_unlock_pin": "빠른 잠금 해제 PIN",
|
"settings_quick_unlock_pin": "빠른 잠금 해제 PIN",
|
||||||
"settings_reduce_transparency": "투명도 줄이기",
|
"settings_reduce_transparency": "투명도 줄이기",
|
||||||
|
"settings_reloaded": "디스크에서 설정을 다시 불러왔습니다",
|
||||||
"settings_remove_encryption": "암호화 제거",
|
"settings_remove_encryption": "암호화 제거",
|
||||||
"settings_remove_pin": "PIN 제거",
|
"settings_remove_pin": "PIN 제거",
|
||||||
"settings_request_payment": "결제 요청...",
|
"settings_request_payment": "결제 요청…",
|
||||||
"settings_rescan_desc": "누락된 거래를 찾기 위해 블록체인 재스캔",
|
"settings_rescan_desc": "누락된 거래를 찾기 위해 블록체인 재스캔",
|
||||||
"settings_restart_daemon": "데몬 재시작",
|
"settings_restart_daemon": "데몬 재시작",
|
||||||
"settings_rpc_connection": "RPC 연결",
|
"settings_rpc_connection": "RPC 연결",
|
||||||
|
"settings_rpc_error_prefix": "RPC 오류: ",
|
||||||
"settings_rpc_note": "참고: 연결 설정은 보통 DRAGONX.conf에서 자동 감지됩니다",
|
"settings_rpc_note": "참고: 연결 설정은 보통 DRAGONX.conf에서 자동 감지됩니다",
|
||||||
|
"settings_rpc_ok": "RPC 연결 정상",
|
||||||
"settings_save_shielded_desc": "z-addr 거래를 로컬 파일에 저장하여 조회",
|
"settings_save_shielded_desc": "z-addr 거래를 로컬 파일에 저장하여 조회",
|
||||||
"settings_save_shielded_local": "차폐 거래 기록을 로컬에 저장",
|
"settings_save_shielded_local": "차폐 거래 기록을 로컬에 저장",
|
||||||
|
"settings_saved": "설정이 저장되었습니다",
|
||||||
"settings_set_pin": "PIN 설정",
|
"settings_set_pin": "PIN 설정",
|
||||||
"settings_shield_mining": "채굴 차폐...",
|
"settings_shield_mining": "채굴 차폐…",
|
||||||
"settings_solid_colors_desc": "블러 효과 대신 단색 사용 (접근성)",
|
"settings_solid_colors_desc": "블러 효과 대신 단색 사용 (접근성)",
|
||||||
|
"settings_theme_refreshed": "테마 목록을 새로고침했습니다",
|
||||||
"settings_tor_desc": "향상된 개인 정보 보호를 위해 모든 연결을 Tor를 통해 라우팅",
|
"settings_tor_desc": "향상된 개인 정보 보호를 위해 모든 연결을 Tor를 통해 라우팅",
|
||||||
"settings_unlocked": "잠금 해제",
|
"settings_unlocked": "잠금 해제",
|
||||||
"settings_use_tor_network": "네트워크 연결에 Tor 사용",
|
"settings_use_tor_network": "네트워크 연결에 Tor 사용",
|
||||||
"settings_validate_address": "주소 확인...",
|
"settings_validate_address": "주소 확인…",
|
||||||
"settings_visual_effects": "시각 효과",
|
"settings_visual_effects": "시각 효과",
|
||||||
"settings_wallet_file_size": "지갑 파일 크기: %s",
|
"settings_wallet_file_size": "지갑 파일 크기: %s",
|
||||||
"settings_wallet_info": "지갑 정보",
|
"settings_wallet_info": "지갑 정보",
|
||||||
"settings_wallet_location": "지갑 위치: %s",
|
"settings_wallet_location": "지갑 위치: %s",
|
||||||
"settings_wallet_maintenance": "지갑 유지보수",
|
"settings_wallet_maintenance": "지갑 유지보수",
|
||||||
"settings_wallet_not_found": "지갑 파일을 찾을 수 없음",
|
"settings_wallet_not_found": "지갑 파일을 찾을 수 없음",
|
||||||
"settings_wallet_size_label": "지갑 크기:",
|
"settings_wallet_size_label": "지갑 크기",
|
||||||
|
"settings_ztx_cleared": "Z-거래 내역이 삭제되었습니다",
|
||||||
|
"settings_ztx_not_found": "내역 파일을 찾을 수 없습니다",
|
||||||
"setup_wizard": "설정 마법사",
|
"setup_wizard": "설정 마법사",
|
||||||
"share": "공유",
|
"share": "공유",
|
||||||
"shield_check_status": "상태 확인",
|
"shield_check_status": "상태 확인",
|
||||||
@@ -1292,6 +1460,23 @@
|
|||||||
"sweep_to": "쓸어담은 주소:",
|
"sweep_to": "쓸어담은 주소:",
|
||||||
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
|
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
|
||||||
"sweep_tx": "거래:",
|
"sweep_tx": "거래:",
|
||||||
|
"switch_corrupt_body": "이 지갑이 손상된 것 같습니다. 노드가 열 수 없습니다. 백업에서 복원하거나 다시 만들거나 복구를 시도하세요.",
|
||||||
|
"switch_corrupt_repair": "복구 시도(salvage)",
|
||||||
|
"switch_progress_background": "백그라운드에서 계속",
|
||||||
|
"switch_progress_default_wallet": "기본 지갑",
|
||||||
|
"switch_progress_elapsed": "경과",
|
||||||
|
"switch_progress_external_wallet": "외부 지갑",
|
||||||
|
"switch_progress_failed_title": "지갑 전환 실패",
|
||||||
|
"switch_progress_from_label": "이전:",
|
||||||
|
"switch_progress_hint": "정상 종료에는 최대 1분이 걸릴 수 있습니다.",
|
||||||
|
"switch_progress_reconnecting": "다시 연결하는 중",
|
||||||
|
"switch_progress_starting": "새 지갑으로 노드를 시작하는 중",
|
||||||
|
"switch_progress_stopping": "현재 노드를 중지하는 중",
|
||||||
|
"switch_progress_title": "지갑 전환 중",
|
||||||
|
"switch_stopnode_body": "지갑을 전환하면 선택한 지갑으로 노드가 다시 시작됩니다. 실행 중인 노드가 중지되고 새 지갑으로 다시 시작됩니다. 일부러 계속 실행해 두었다면 자동으로 다시 켜집니다.",
|
||||||
|
"switch_stopnode_confirm": "노드 중지 후 전환",
|
||||||
|
"switch_stopnode_title": "실행 중인 노드를 중지할까요?",
|
||||||
|
"switch_stopnode_warn": "이 지갑이 시작하지 않은 노드가 이미 실행 중입니다.",
|
||||||
"syncing": "동기화 중...",
|
"syncing": "동기화 중...",
|
||||||
"t_address": "T 주소",
|
"t_address": "T 주소",
|
||||||
"t_addresses": "T 주소",
|
"t_addresses": "T 주소",
|
||||||
@@ -1299,6 +1484,7 @@
|
|||||||
"theme": "테마",
|
"theme": "테마",
|
||||||
"theme_effects": "테마 효과",
|
"theme_effects": "테마 효과",
|
||||||
"theme_language": "테마 및 언어",
|
"theme_language": "테마 및 언어",
|
||||||
|
"tile_click_to_open": "클릭하여 열기",
|
||||||
"time_days_ago": "%d일 전",
|
"time_days_ago": "%d일 전",
|
||||||
"time_hours_ago": "%d시간 전",
|
"time_hours_ago": "%d시간 전",
|
||||||
"time_minutes_ago": "%d분 전",
|
"time_minutes_ago": "%d분 전",
|
||||||
@@ -1313,7 +1499,9 @@
|
|||||||
"to_upper": "받는 곳",
|
"to_upper": "받는 곳",
|
||||||
"tools": "도구",
|
"tools": "도구",
|
||||||
"tools_actions": "도구 및 작업...",
|
"tools_actions": "도구 및 작업...",
|
||||||
|
"tools_actions_hdr": "도구 및 작업",
|
||||||
"total": "합계",
|
"total": "합계",
|
||||||
|
"total_balance_label": "총 잔액",
|
||||||
"transaction_id": "거래 ID",
|
"transaction_id": "거래 ID",
|
||||||
"transaction_sent": "거래 전송 성공",
|
"transaction_sent": "거래 전송 성공",
|
||||||
"transaction_sent_msg": "거래가 전송되었습니다!",
|
"transaction_sent_msg": "거래가 전송되었습니다!",
|
||||||
@@ -1335,13 +1523,24 @@
|
|||||||
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
|
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
|
||||||
"tt_backup": "wallet.dat 백업 만들기",
|
"tt_backup": "wallet.dat 백업 만들기",
|
||||||
"tt_block_explorer": "브라우저에서 DragonX 블록 탐색기 열기",
|
"tt_block_explorer": "브라우저에서 DragonX 블록 탐색기 열기",
|
||||||
"tt_blur": "블러 양 (0%% = 끔, 100%% = 최대)",
|
"tt_blur": "블러 양 (0% = 끔, 100% = 최대)",
|
||||||
"tt_change_pass": "지갑 암호화 비밀번호 변경",
|
"tt_change_pass": "지갑 암호화 비밀번호 변경",
|
||||||
"tt_change_pin": "잠금 해제 PIN 변경",
|
"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_clear_ztx": "로컬에 캐시된 z-트랜잭션 기록 삭제",
|
||||||
|
"tt_clock_format": "24시간 또는 12시간 형식(앱 전체). 채팅에서 재정의할 수 있습니다.",
|
||||||
|
"tt_copy_diagnostics": "지원용 요약(버전, 데몬/지갑/로그 상태 — 비밀 정보 없음)을 클립보드에 복사합니다",
|
||||||
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
||||||
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
||||||
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
||||||
|
"tt_daemon_refresh": "위에 표시된 설치 및 번들 dragonxd의 버전, 크기, 날짜를 다시 읽어옵니다",
|
||||||
"tt_daemon_update_check": "프로젝트 Gitea에서 최신 dragonxd 풀 노드를 다운로드하고 검증한 다음, 재시작하여 적용합니다",
|
"tt_daemon_update_check": "프로젝트 Gitea에서 최신 dragonxd 풀 노드를 다운로드하고 검증한 다음, 재시작하여 적용합니다",
|
||||||
"tt_debug_collapse": "디버그 로깅 옵션 접기",
|
"tt_debug_collapse": "디버그 로깅 옵션 접기",
|
||||||
"tt_debug_expand": "디버그 로깅 옵션 펼치기",
|
"tt_debug_expand": "디버그 로깅 옵션 펼치기",
|
||||||
@@ -1359,15 +1558,39 @@
|
|||||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||||
"tt_language": "지갑 UI 인터페이스 언어",
|
"tt_language": "지갑 UI 인터페이스 언어",
|
||||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
"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_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_lock": "지갑 즉시 잠금",
|
||||||
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
||||||
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
||||||
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
|
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
|
||||||
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
|
"tt_noise": "그레인 텍스처 강도 (0% = 끔, 100% = 최대)",
|
||||||
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
|
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
|
||||||
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
|
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
|
||||||
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
|
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
|
||||||
|
"tt_open_log_folder": "디버그 및 충돌 로그가 있는 폴더를 엽니다",
|
||||||
"tt_reduce_motion": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화",
|
"tt_reduce_motion": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화",
|
||||||
"tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장",
|
"tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장",
|
||||||
"tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구",
|
"tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구",
|
||||||
@@ -1380,12 +1603,17 @@
|
|||||||
"tt_rpc_host": "DragonX 데몬 호스트 이름",
|
"tt_rpc_host": "DragonX 데몬 호스트 이름",
|
||||||
"tt_rpc_pass": "RPC 인증 비밀번호",
|
"tt_rpc_pass": "RPC 인증 비밀번호",
|
||||||
"tt_rpc_port": "데몬 RPC 연결 포트",
|
"tt_rpc_port": "데몬 RPC 연결 포트",
|
||||||
|
"tt_rpc_toggle": "데몬의 읽기 전용 RPC 연결 정보(호스트, 포트, 사용자, 비밀번호)를 표시하거나 숨깁니다",
|
||||||
"tt_rpc_user": "RPC 인증 사용자 이름",
|
"tt_rpc_user": "RPC 인증 사용자 이름",
|
||||||
"tt_save_settings": "모든 설정을 디스크에 저장",
|
"tt_save_settings": "모든 설정을 디스크에 저장",
|
||||||
"tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩",
|
"tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩",
|
||||||
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
|
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
|
||||||
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
|
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
|
||||||
|
"tt_screenshot_open_dir": "파일 관리자에서 screenshots 폴더(설정 디렉터리 아래)를 엽니다",
|
||||||
|
"tt_screenshot_sweep": "모든 탭에서 모든 테마를 순환하며 각각의 스크린샷을 설정 screenshots 폴더에 저장합니다(마지막 스윕을 덮어씀)",
|
||||||
|
"tt_screenshot_sweep_full": "테마 스윕과 유사하지만 임시 오프라인 데모 지갑 데이터를 사용하여 모든 모달 / 대화 상자 / 흐름도 캡처합니다",
|
||||||
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
|
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
|
||||||
|
"tt_seed_demo_chat": "스윕이 UI를 캡처하도록 샘플 대화를 채팅 탭에 삽입합니다; 메모리에만 저장되며 재시작 시 사라집니다",
|
||||||
"tt_seed_migrate": "새 시드 문구 지갑을 만들고 자금을 그곳으로 옮깁니다",
|
"tt_seed_migrate": "새 시드 문구 지갑을 만들고 자금을 그곳으로 옮깁니다",
|
||||||
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
|
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
|
||||||
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
|
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
|
||||||
@@ -1397,13 +1625,14 @@
|
|||||||
"tt_theme_hotkey": "단축키: Ctrl+왼쪽/오른쪽으로 테마 전환",
|
"tt_theme_hotkey": "단축키: Ctrl+왼쪽/오른쪽으로 테마 전환",
|
||||||
"tt_tor": "익명성을 위해 데몬 연결을 Tor 네트워크를 통해 라우팅",
|
"tt_tor": "익명성을 위해 데몬 연결을 Tor 네트워크를 통해 라우팅",
|
||||||
"tt_tx_url": "블록 탐색기에서 거래를 보기 위한 기본 URL",
|
"tt_tx_url": "블록 탐색기에서 거래를 보기 위한 기본 URL",
|
||||||
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100%% = 완전 불투명, 낮을수록 더 투명)",
|
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100% = 완전 불투명, 낮을수록 더 투명)",
|
||||||
"tt_validate": "DragonX 주소가 유효한지 확인",
|
"tt_validate": "DragonX 주소가 유효한지 확인",
|
||||||
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
||||||
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
||||||
"tt_website": "DragonX 웹사이트 열기",
|
"tt_website": "DragonX 웹사이트 열기",
|
||||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||||
"tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다",
|
"tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다",
|
||||||
|
"tx_chat_badge": "메시지",
|
||||||
"tx_confirmations": "%d 확인",
|
"tx_confirmations": "%d 확인",
|
||||||
"tx_details_title": "거래 상세",
|
"tx_details_title": "거래 상세",
|
||||||
"tx_from_address": "보낸 주소:",
|
"tx_from_address": "보낸 주소:",
|
||||||
@@ -1449,6 +1678,7 @@
|
|||||||
"validate_not_mine": "이 지갑에 속하지 않음",
|
"validate_not_mine": "이 지갑에 속하지 않음",
|
||||||
"validate_ownership": "소유자:",
|
"validate_ownership": "소유자:",
|
||||||
"validate_results": "결과:",
|
"validate_results": "결과:",
|
||||||
|
"validate_results_placeholder": "결과가 여기에 표시됩니다",
|
||||||
"validate_shielded_type": "차폐 (z 주소)",
|
"validate_shielded_type": "차폐 (z 주소)",
|
||||||
"validate_status": "상태:",
|
"validate_status": "상태:",
|
||||||
"validate_title": "주소 검증",
|
"validate_title": "주소 검증",
|
||||||
@@ -1472,6 +1702,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ 다른 폴더에서 지갑 검색…",
|
"wallets_add_folder_toggle": "+ 다른 폴더에서 지갑 검색…",
|
||||||
"wallets_badge_encrypted": "암호화됨 (암호로 보호됨)",
|
"wallets_badge_encrypted": "암호화됨 (암호로 보호됨)",
|
||||||
"wallets_badge_encrypted_short": "암호화됨",
|
"wallets_badge_encrypted_short": "암호화됨",
|
||||||
|
"wallets_badge_hd": "HD 지갑 — 열지 않으면 시드 문구를 확인할 수 없습니다",
|
||||||
|
"wallets_badge_hd_short": "HD 지갑",
|
||||||
"wallets_badge_legacy": "레거시 지갑 (시드 문구 없음)",
|
"wallets_badge_legacy": "레거시 지갑 (시드 문구 없음)",
|
||||||
"wallets_badge_legacy_short": "레거시",
|
"wallets_badge_legacy_short": "레거시",
|
||||||
"wallets_badge_seed": "시드 문구 지갑 (HD)",
|
"wallets_badge_seed": "시드 문구 지갑 (HD)",
|
||||||
@@ -1587,6 +1819,7 @@
|
|||||||
"xmrig_loading_releases": "릴리스를 불러오는 중…",
|
"xmrig_loading_releases": "릴리스를 불러오는 중…",
|
||||||
"xmrig_none": "없음",
|
"xmrig_none": "없음",
|
||||||
"xmrig_reinstall": "재설치",
|
"xmrig_reinstall": "재설치",
|
||||||
|
"xmrig_releases": "xmrig 릴리스",
|
||||||
"xmrig_stop_mining_first": "채굴기를 업데이트하기 전에 채굴을 중지하세요.",
|
"xmrig_stop_mining_first": "채굴기를 업데이트하기 전에 채굴을 중지하세요.",
|
||||||
"xmrig_unavailable_body": "이 플랫폼에서 사용 가능한 채굴기 빌드가 없습니다.",
|
"xmrig_unavailable_body": "이 플랫폼에서 사용 가능한 채굴기 빌드가 없습니다.",
|
||||||
"xmrig_unavailable_title": "채굴기 업데이트를 사용할 수 없습니다",
|
"xmrig_unavailable_title": "채굴기 업데이트를 사용할 수 없습니다",
|
||||||
|
|||||||
278
res/lang/pt.json
278
res/lang/pt.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "AVANÇADO",
|
"advanced": "AVANÇADO",
|
||||||
"advanced_effects": "Efeitos Avançados...",
|
"advanced_effects": "Efeitos Avançados...",
|
||||||
"ago": "atrás",
|
"ago": "atrás",
|
||||||
|
"alerts_clear": "Limpar histórico de alertas",
|
||||||
|
"alerts_history_tooltip": "Alertas recentes",
|
||||||
|
"alerts_none": "Ainda não há alertas",
|
||||||
|
"alerts_recent": "ALERTAS RECENTES",
|
||||||
"all_filter": "Todos",
|
"all_filter": "Todos",
|
||||||
"allow_custom_fees": "Permitir taxas personalizadas",
|
"allow_custom_fees": "Permitir taxas personalizadas",
|
||||||
"amount": "Valor",
|
"amount": "Valor",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender bloqueou o minerador",
|
"av_title": "Windows Defender bloqueou o minerador",
|
||||||
"available": "Disponível",
|
"available": "Disponível",
|
||||||
"backup_backing_up": "Fazendo backup...",
|
"backup_backing_up": "Fazendo backup...",
|
||||||
|
"backup_col_backup": "BACKUP",
|
||||||
|
"backup_col_export": "EXPORTAR",
|
||||||
|
"backup_col_import": "IMPORTAR E RESTAURAR",
|
||||||
"backup_create": "Criar Backup",
|
"backup_create": "Criar Backup",
|
||||||
"backup_created": "Backup da carteira criado",
|
"backup_created": "Backup da carteira criado",
|
||||||
"backup_data": "BACKUP & DADOS",
|
"backup_data": "BACKUP & DADOS",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "Saldo",
|
"balance": "Saldo",
|
||||||
"balance_history_collecting": "Histórico de saldo — coletando dados...",
|
"balance_history_collecting": "Histórico de saldo — coletando dados...",
|
||||||
"balance_layout": "Layout do Saldo",
|
"balance_layout": "Layout do Saldo",
|
||||||
|
"balance_layout_switched": "Layout: %s",
|
||||||
|
"balance_mining_rate": "Minerando %s",
|
||||||
"balance_shielded_fmt": "Blindado: %.8f",
|
"balance_shielded_fmt": "Blindado: %.8f",
|
||||||
|
"balance_syncing_pct": "Sincronizando %.1f%%",
|
||||||
"balance_transparent_fmt": "Transparente: %.8f",
|
"balance_transparent_fmt": "Transparente: %.8f",
|
||||||
"ban": "Banir",
|
"ban": "Banir",
|
||||||
"banned_peers": "Pares Banidos",
|
"banned_peers": "Pares Banidos",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "Verificando somas de verificação...",
|
"bootstrap_verifying": "Verificando somas de verificação...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
|
||||||
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
|
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
|
||||||
|
"byte_count_fmt": "%zu / %zu bytes",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"change_pass_confirm": "Confirmar nova:",
|
"change_pass_confirm": "Confirmar nova:",
|
||||||
"change_pass_current": "Senha atual:",
|
"change_pass_current": "Senha atual:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "Alterar senha",
|
"change_pass_title": "Alterar senha",
|
||||||
"characters": "caracteres",
|
"characters": "caracteres",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"chat_accent_amber": "Âmbar",
|
||||||
|
"chat_accent_blue": "Azul",
|
||||||
|
"chat_accent_green": "Verde",
|
||||||
|
"chat_accent_pink": "Rosa",
|
||||||
|
"chat_accent_purple": "Roxo",
|
||||||
|
"chat_accent_theme": "Tema",
|
||||||
|
"chat_add_contact": "Adicionar contato",
|
||||||
|
"chat_awaiting_key": "Aguardando resposta",
|
||||||
|
"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_cancel": "Cancelar",
|
||||||
|
"chat_contact_added": "Contato adicionado — renomeie em Contatos",
|
||||||
"chat_contact_request": "solicitação de contato",
|
"chat_contact_request": "solicitação de contato",
|
||||||
|
"chat_copy_address_tip": "Clique para copiar o endereço",
|
||||||
|
"chat_density_comfortable": "Confortável",
|
||||||
|
"chat_density_compact": "Compacta",
|
||||||
|
"chat_emoji_color": "Colorido",
|
||||||
|
"chat_emoji_mono": "Monocromático",
|
||||||
|
"chat_emoji_search": "Pesquisar emoji",
|
||||||
"chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.",
|
"chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.",
|
||||||
|
"chat_empty_start": "Inicie uma com \"Nova conversa\".",
|
||||||
|
"chat_empty_title": "Ainda não há conversas",
|
||||||
|
"chat_export": "Exportar conversa…",
|
||||||
|
"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",
|
||||||
|
"chat_jump_latest": "Recentes",
|
||||||
|
"chat_len_over": "Mensagem muito longa",
|
||||||
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
|
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
|
||||||
"chat_new_button": "Nova conversa",
|
"chat_mute": "Silenciar",
|
||||||
|
"chat_new_button": "Novo chat",
|
||||||
"chat_new_message": "Mensagem",
|
"chat_new_message": "Mensagem",
|
||||||
|
"chat_new_message_toast": "Nova mensagem de chat criptografada",
|
||||||
"chat_new_send": "Enviar solicitação",
|
"chat_new_send": "Enviar solicitação",
|
||||||
"chat_new_title": "Nova conversa",
|
"chat_new_title": "Novo chat",
|
||||||
"chat_new_zaddr": "Endereço-z do destinatário",
|
"chat_new_zaddr": "Endereço-z do destinatário",
|
||||||
|
"chat_no_matches": "Nenhuma conversa corresponde à sua pesquisa.",
|
||||||
|
"chat_no_z_contacts": "Ainda não há contatos com endereço blindado",
|
||||||
|
"chat_opt_bubble_accent": "Cor do balão",
|
||||||
|
"chat_opt_bubble_style": "Estilo do balão",
|
||||||
|
"chat_opt_density": "Densidade das mensagens",
|
||||||
|
"chat_opt_emoji": "Estilo de emoji",
|
||||||
|
"chat_opt_enter_sends": "Enter envia a mensagem",
|
||||||
|
"chat_opt_font_size": "Tamanho do texto",
|
||||||
|
"chat_opt_global_clock": "Formato de relógio global",
|
||||||
|
"chat_opt_poll": "Taxa de atualização",
|
||||||
|
"chat_opt_timestamp": "Carimbos de data/hora",
|
||||||
|
"chat_pick_contact": "Escolher dos contatos…",
|
||||||
|
"chat_rename": "Renomear contato",
|
||||||
|
"chat_rename_hint": "Nome do contato",
|
||||||
|
"chat_renamed": "Contato renomeado",
|
||||||
|
"chat_retry": "Tentar novamente",
|
||||||
|
"chat_search": "Pesquisar conversas",
|
||||||
|
"chat_sec_appearance": "APARÊNCIA",
|
||||||
|
"chat_sec_messaging": "MENSAGENS",
|
||||||
"chat_select_hint": "Selecione uma conversa para visualizá-la.",
|
"chat_select_hint": "Selecione uma conversa para visualizá-la.",
|
||||||
"chat_send": "Enviar",
|
"chat_send": "Enviar",
|
||||||
"chat_send_failed": "não enviada",
|
"chat_send_failed": "não enviada",
|
||||||
|
"chat_sending": "enviando…",
|
||||||
|
"chat_settings_done": "Concluído",
|
||||||
|
"chat_settings_section": "CHAT E CONTATOS",
|
||||||
|
"chat_settings_tip": "Personalizar chat",
|
||||||
|
"chat_settings_title": "Configurações de chat",
|
||||||
|
"chat_show_hidden": "Ver ocultas",
|
||||||
|
"chat_time_now": "agora",
|
||||||
"chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).",
|
"chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).",
|
||||||
"chat_toast_lite_busy": "Já há um envio em andamento, ou nenhuma carteira está aberta.",
|
"chat_toast_lite_busy": "Já há um envio em andamento, ou nenhuma carteira está aberta.",
|
||||||
|
"chat_toast_need_funds": "É necessário um pequeno saldo blindado para enviar chats (para cobrir a taxa).",
|
||||||
"chat_toast_no_zaddr": "Nenhum endereço-z disponível para enviar o chat.",
|
"chat_toast_no_zaddr": "Nenhum endereço-z disponível para enviar o chat.",
|
||||||
"chat_toast_not_connected": "Não conectado — mensagem de chat não enviada.",
|
"chat_toast_not_connected": "Não conectado — mensagem de chat não enviada.",
|
||||||
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
|
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
|
||||||
"chat_toast_request_queued": "Solicitação de contato na fila.",
|
"chat_toast_request_queued": "Solicitação de contato na fila.",
|
||||||
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
|
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
|
||||||
|
"chat_today": "Hoje",
|
||||||
|
"chat_ts_12h": "12 horas",
|
||||||
|
"chat_ts_24h": "24 horas",
|
||||||
|
"chat_ts_global": "Seguir global",
|
||||||
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_unhide": "Mostrar",
|
||||||
|
"chat_unmute": "Reativar som",
|
||||||
|
"chat_verify_key": "Chave de identidade — compare para verificar",
|
||||||
"chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.",
|
"chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.",
|
||||||
|
"chat_yesterday": "Ontem",
|
||||||
"chat_you": "Você",
|
"chat_you": "Você",
|
||||||
"choose_icon": "Escolher Ícone",
|
"choose_icon": "Escolher Ícone",
|
||||||
"clear": "Limpar",
|
"clear": "Limpar",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "Clique para copiar o endereço",
|
"click_copy_address": "Clique para copiar o endereço",
|
||||||
"click_copy_uri": "Clique para copiar a URI",
|
"click_copy_uri": "Clique para copiar a URI",
|
||||||
"click_to_copy": "Clique para copiar",
|
"click_to_copy": "Clique para copiar",
|
||||||
|
"clock_format": "Formato de hora",
|
||||||
"close": "Fechar",
|
"close": "Fechar",
|
||||||
"conf_count": "%d conf.",
|
"conf_count": "%d conf.",
|
||||||
"confirm_and_send": "Confirmar & Enviar",
|
"confirm_and_send": "Confirmar & Enviar",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "App",
|
"console_app": "App",
|
||||||
"console_auto_scroll": "Rolagem automática",
|
"console_auto_scroll": "Rolagem automática",
|
||||||
"console_available_commands": "Comandos disponíveis:",
|
"console_available_commands": "Comandos disponíveis:",
|
||||||
|
"console_backend_reference": "Referência de Comandos do Backend",
|
||||||
|
"console_backend_unavailable": "Sem backend",
|
||||||
"console_capturing_output": "Capturando saída do daemon...",
|
"console_capturing_output": "Capturando saída do daemon...",
|
||||||
|
"console_cat_advanced": "Avançado",
|
||||||
"console_cat_blockchain": "Blockchain",
|
"console_cat_blockchain": "Blockchain",
|
||||||
"console_cat_control": "Controle",
|
"console_cat_control": "Controle",
|
||||||
|
"console_cat_keys": "Chaves e segurança",
|
||||||
"console_cat_mining": "Mineração",
|
"console_cat_mining": "Mineração",
|
||||||
"console_cat_network": "Rede",
|
"console_cat_network": "Rede",
|
||||||
"console_cat_raw_transactions": "Transações brutas",
|
"console_cat_raw_transactions": "Transações brutas",
|
||||||
|
"console_cat_send": "Enviar",
|
||||||
|
"console_cat_sync": "Sincronização",
|
||||||
"console_cat_utility": "Utilitários",
|
"console_cat_utility": "Utilitários",
|
||||||
"console_cat_wallet": "Carteira",
|
"console_cat_wallet": "Carteira",
|
||||||
"console_clear": "Limpar",
|
"console_clear": "Limpar",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - Mostrar esta mensagem de ajuda",
|
"console_help_help": " help - Mostrar esta mensagem de ajuda",
|
||||||
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
||||||
"console_help_stop": " stop - Parar o daemon",
|
"console_help_stop": " stop - Parar o daemon",
|
||||||
|
"console_last_error": "Último erro:",
|
||||||
"console_line_count": "%zu linhas",
|
"console_line_count": "%zu linhas",
|
||||||
"console_matches": "correspondências",
|
"console_matches": "correspondências",
|
||||||
"console_new_lines": "%d novas linhas",
|
"console_new_lines": "%d novas linhas",
|
||||||
"console_no_daemon": "Sem daemon",
|
"console_no_daemon": "Sem daemon",
|
||||||
|
"console_no_output": "(sem saída)",
|
||||||
"console_not_connected": "Erro: Não conectado ao daemon",
|
"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_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.",
|
||||||
"console_ref_builds": "Gera",
|
"console_ref_builds": "Gera",
|
||||||
"console_ref_cancel": "Cancelar",
|
"console_ref_cancel": "Cancelar",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
|
"console_ref_run_confirm": "Executar %s agora? Este é um comando com consequências.",
|
||||||
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
|
"console_ref_search_hint": "Pesquisar por nome ou tarefa…",
|
||||||
"console_ref_select_hint": "Selecione um comando para ver o que ele faz.",
|
"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_reference": "Referência de Comandos RPC",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "Scanline do console",
|
"console_scanline": "Scanline do console",
|
||||||
"console_search_commands": "Pesquisar comandos...",
|
"console_search_commands": "Pesquisar comandos...",
|
||||||
"console_select_all": "Selecionar Tudo",
|
"console_select_all": "Selecionar Tudo",
|
||||||
"console_show_app_output": "Mostrar linhas do log da carteira [app]",
|
"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_daemon_output": "Mostrar saída do daemon",
|
||||||
"console_show_errors_only": "Mostrar apenas erros",
|
"console_show_errors_only": "Mostrar apenas erros",
|
||||||
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "Parado",
|
"console_status_stopped": "Parado",
|
||||||
"console_status_stopping": "Parando",
|
"console_status_stopping": "Parando",
|
||||||
"console_status_unknown": "Desconhecido",
|
"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_tab_completion": "Tab para completar",
|
||||||
"console_text_colors": "Cores do texto",
|
"console_text_colors": "Cores do texto",
|
||||||
"console_toggle_accents": "Alternar destaques de cor das linhas",
|
"console_toggle_accents": "Alternar destaques de cor das linhas",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "Ativado: este contato permanece visível em qualquer carteira que você carregar. Desativado: ele pertence apenas à carteira atual.",
|
"contact_global_tt": "Ativado: este contato permanece visível em qualquer carteira que você carregar. Desativado: ele pertence apenas à carteira atual.",
|
||||||
"contact_preview_addr": "O endereço aparecerá aqui",
|
"contact_preview_addr": "O endereço aparecerá aqui",
|
||||||
"contact_preview_name": "Nome do contato",
|
"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": "Contatos",
|
||||||
|
"contacts_avatar_shape": "Forma do avatar",
|
||||||
|
"contacts_list_scale": "Escala da lista",
|
||||||
"contacts_search_no_match": "Nenhum contato correspondente",
|
"contacts_search_no_match": "Nenhum contato correspondente",
|
||||||
"contacts_search_placeholder": "Pesquisar contatos...",
|
"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!",
|
"copied": "Copiado!",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copy_address": "Copiar Endereço Completo",
|
"copy_address": "Copiar Endereço Completo",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "Empacotado",
|
"daemon_bundled": "Empacotado",
|
||||||
"daemon_install_bundled": "Instalar incluído",
|
"daemon_install_bundled": "Instalar incluído",
|
||||||
"daemon_installed": "Instalado",
|
"daemon_installed": "Instalado",
|
||||||
|
"daemon_maintenance_label": "MANUTENÇÃO",
|
||||||
"daemon_none_bundled": "nenhum nesta build",
|
"daemon_none_bundled": "nenhum nesta build",
|
||||||
"daemon_not_installed": "não instalado",
|
"daemon_not_installed": "não instalado",
|
||||||
"daemon_status_differ": "O binário instalado difere da versão empacotada.",
|
"daemon_status_differ": "O binário instalado difere da versão empacotada.",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "Mais recente:",
|
"daemon_update_latest": "Mais recente:",
|
||||||
"daemon_update_loading": "Carregando versões…",
|
"daemon_update_loading": "Carregando versões…",
|
||||||
"daemon_update_now": "Atualizar agora",
|
"daemon_update_now": "Atualizar agora",
|
||||||
|
"daemon_update_prompt_title": "Atualizar o daemon do node?",
|
||||||
"daemon_update_reinstall": "Reinstalar",
|
"daemon_update_reinstall": "Reinstalar",
|
||||||
"daemon_update_restart_note": "Reinicie o daemon para começar a executar a nova versão.",
|
"daemon_update_restart_note": "Reinicie o daemon para começar a executar a nova versão.",
|
||||||
"daemon_update_restart_now": "Reiniciar daemon agora",
|
"daemon_update_restart_now": "Reiniciar daemon agora",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "O download é verificado contra o SHA-256 publicado do lançamento e uma assinatura ed25519 fixada antes da instalação.",
|
"daemon_update_verify_note": "O download é verificado contra o SHA-256 publicado do lançamento e uma assinatura ed25519 fixada antes da instalação.",
|
||||||
"daemon_update_verifying": "Verificando…",
|
"daemon_update_verifying": "Verificando…",
|
||||||
"daemon_update_version": "Versão:",
|
"daemon_update_version": "Versão:",
|
||||||
|
"daemon_updates_label": "ATUALIZAÇÕES",
|
||||||
"daemon_version": "Daemon",
|
"daemon_version": "Daemon",
|
||||||
"dark": "Escuro",
|
"dark": "Escuro",
|
||||||
|
"data_stale_prefix": "Atualizado",
|
||||||
|
"data_stale_tooltip": "O saldo pode estar desatualizado — a carteira não recebeu uma atualização recente. Verifique a conexão com o seu nó.",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
"date_label": "Data:",
|
"date_label": "Data:",
|
||||||
"debug_logging": "REGISTRO DE DEPURAÇÃO",
|
"debug_logging": "REGISTRO DE DEPURAÇÃO",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "Baixar Bootstrap",
|
"download_bootstrap": "Baixar Bootstrap",
|
||||||
"dragonx_green": "DragonX (Verde)",
|
"dragonx_green": "DragonX (Verde)",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
|
"empty_wallet_keys_suffix": "chaves",
|
||||||
|
"empty_wallet_open_manager": "Abrir gerenciador de carteiras",
|
||||||
|
"empty_wallet_restore": "Restaurar minha carteira",
|
||||||
|
"empty_wallet_salvage_body": "Esta carteira está vazia porque um reparo automático anterior colocou sua carteira original de lado como backup. Suas moedas quase certamente estão nesse backup, não perdidas. Restaure-o para carregar seus fundos novamente — nada é excluído; o arquivo atual é guardado primeiro.",
|
||||||
|
"empty_wallet_salvage_headline": "Suas moedas estão seguras em um arquivo de backup.",
|
||||||
|
"empty_wallet_salvage_title": "Sua carteira pode ter sido reparada",
|
||||||
|
"empty_wallet_warning_body": "Esta carteira não tem endereços nem fundos, mas outro arquivo de carteira na sua pasta do DragonX contém chaves. Suas moedas provavelmente estão nele, não perdidas. Abra o gerenciador de carteiras para mudar para a carteira que contém seus fundos.",
|
||||||
|
"empty_wallet_warning_dismiss": "Não avisar novamente para esta carteira",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "Interrompe este aviso apenas para o arquivo de carteira atual. Se você mudar para outra carteira vazia mais tarde, poderá avisar novamente.",
|
||||||
|
"empty_wallet_warning_headline": "Você pode ter aberto a carteira errada.",
|
||||||
|
"empty_wallet_warning_title": "Esta carteira está vazia",
|
||||||
"enc_confirm": "Confirmar:",
|
"enc_confirm": "Confirmar:",
|
||||||
"enc_desc": "Criptografar sua carteira protege suas chaves privadas com uma senha. Após a criptografia, o daemon será reiniciado.",
|
"enc_desc": "Criptografar sua carteira protege suas chaves privadas com uma senha. Após a criptografia, o daemon será reiniciado.",
|
||||||
"enc_encrypting": "Criptografando a carteira...",
|
"enc_encrypting": "Criptografando a carteira...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "Claro",
|
"light": "Claro",
|
||||||
"lite_account_label": "Conta",
|
"lite_account_label": "Conta",
|
||||||
"lite_action": "Ação",
|
"lite_action": "Ação",
|
||||||
|
"lite_backend_unavailable": "Backend da carteira lite indisponível",
|
||||||
"lite_backup_keys": "Backup e chaves",
|
"lite_backup_keys": "Backup e chaves",
|
||||||
"lite_birthday_backup": "Aniversário: %llu (faça o backup disto também)",
|
"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_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_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_console_help_passthrough": "Qualquer outra entrada é executada como um comando de console da carteira leve.",
|
||||||
"lite_copy": "Copiar",
|
"lite_copy": "Copiar",
|
||||||
|
"lite_could_not_start": "Não foi possível iniciar a operação",
|
||||||
"lite_could_not_write": "Não foi possível gravar ",
|
"lite_could_not_write": "Não foi possível gravar ",
|
||||||
"lite_encrypt_wallet": "Criptografar carteira",
|
"lite_encrypt_wallet": "Criptografar carteira",
|
||||||
"lite_encryption_removed": "Criptografia removida",
|
"lite_encryption_removed": "Criptografia removida",
|
||||||
|
"lite_enter_all_seed_words": "Informe todas as 24 palavras-semente para restaurar (obtidas %d)",
|
||||||
|
"lite_enter_wallet_path": "Informe um caminho para a carteira",
|
||||||
"lite_hide_wipe": "Ocultar e apagar",
|
"lite_hide_wipe": "Ocultar e apagar",
|
||||||
"lite_import": "Importar",
|
"lite_import": "Importar",
|
||||||
"lite_import_key_label": "Importar chave",
|
"lite_import_key_label": "Importar chave",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://seu-servidor-lite",
|
"lite_net_add_url_hint": "https://seu-servidor-lite",
|
||||||
"lite_net_checking": "verificando…",
|
"lite_net_checking": "verificando…",
|
||||||
"lite_net_connected": "Conectado",
|
"lite_net_connected": "Conectado",
|
||||||
|
"lite_net_connecting": "Conectando…",
|
||||||
"lite_net_custom": "Personalizado",
|
"lite_net_custom": "Personalizado",
|
||||||
"lite_net_disconnected": "Não conectado",
|
"lite_net_disconnected": "Não conectado",
|
||||||
"lite_net_hidden_section": "Servidores ocultos",
|
"lite_net_hidden_section": "Servidores ocultos",
|
||||||
@@ -638,6 +765,9 @@
|
|||||||
"lite_working": "Processando…",
|
"lite_working": "Processando…",
|
||||||
"loading": "Carregando...",
|
"loading": "Carregando...",
|
||||||
"loading_addresses": "Carregando endereços...",
|
"loading_addresses": "Carregando endereços...",
|
||||||
|
"loading_stall_body": "O daemon está inicializando há %.0f s. Isso pode ser normal após uma atualização ou no primeiro início (carregando o índice de blocos ou reescaneando) — ele se conectará automaticamente quando estiver pronto.",
|
||||||
|
"loading_stall_hint": "Ainda travado? Abra as Configurações e use Reiniciar daemon, ou verifique o Console para mais detalhes.",
|
||||||
|
"loading_stall_title": "Está demorando mais do que o esperado",
|
||||||
"loading_transactions": "Carregando transações",
|
"loading_transactions": "Carregando transações",
|
||||||
"local_hashrate": "Hashrate Local",
|
"local_hashrate": "Hashrate Local",
|
||||||
"low_spec_mode": "Modo econômico",
|
"low_spec_mode": "Modo econômico",
|
||||||
@@ -654,6 +784,9 @@
|
|||||||
"market_cap": "Capitalização",
|
"market_cap": "Capitalização",
|
||||||
"market_cap_short": "Cap.",
|
"market_cap_short": "Cap.",
|
||||||
"market_chart_loading": "Carregando histórico de preços",
|
"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_1d": "1D",
|
||||||
"market_iv_1h": "1H",
|
"market_iv_1h": "1H",
|
||||||
"market_iv_1m": "1M",
|
"market_iv_1m": "1M",
|
||||||
@@ -662,13 +795,18 @@
|
|||||||
"market_no_history": "Nenhum histórico de preços disponível",
|
"market_no_history": "Nenhum histórico de preços disponível",
|
||||||
"market_no_price": "Sem dados de preço",
|
"market_no_price": "Sem dados de preço",
|
||||||
"market_now": "Agora",
|
"market_now": "Agora",
|
||||||
|
"market_opt_chart_style": "Estilo do gráfico",
|
||||||
"market_pct_shielded": "%.0f%% Blindado",
|
"market_pct_shielded": "%.0f%% Blindado",
|
||||||
"market_portfolio": "PORTFÓLIO",
|
"market_portfolio": "PORTFÓLIO",
|
||||||
"market_price_loading": "Carregando dados de preço...",
|
"market_price_loading": "Carregando dados de preço...",
|
||||||
"market_price_unavailable": "Dados de preço indisponíveis",
|
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||||
"market_refresh_price": "Atualizar dados de preço",
|
"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": "Mudar para velas",
|
||||||
|
"market_style_candle_label": "Velas",
|
||||||
"market_style_line": "Mudar para gráfico de linhas",
|
"market_style_line": "Mudar para gráfico de linhas",
|
||||||
|
"market_style_line_label": "Linha",
|
||||||
"market_trade_on": "Negociar no %s",
|
"market_trade_on": "Negociar no %s",
|
||||||
"market_updated": "\\xc2\\xb7 Atualizado %s",
|
"market_updated": "\\xc2\\xb7 Atualizado %s",
|
||||||
"market_vol_short": "Vol",
|
"market_vol_short": "Vol",
|
||||||
@@ -762,6 +900,7 @@
|
|||||||
"mining_difficulty_copied": "Dificuldade copiada",
|
"mining_difficulty_copied": "Dificuldade copiada",
|
||||||
"mining_est_block": "Bloco Est.",
|
"mining_est_block": "Bloco Est.",
|
||||||
"mining_est_daily": "Est. Diário",
|
"mining_est_daily": "Est. Diário",
|
||||||
|
"mining_est_daily_pool_sub": "equivalente solo aproximado, antes da taxa do pool",
|
||||||
"mining_filter_all": "Todos",
|
"mining_filter_all": "Todos",
|
||||||
"mining_filter_tip_all": "Mostrar todos os ganhos",
|
"mining_filter_tip_all": "Mostrar todos os ganhos",
|
||||||
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
|
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
|
||||||
@@ -790,10 +929,12 @@
|
|||||||
"mining_open_in_explorer": "Abrir no explorador",
|
"mining_open_in_explorer": "Abrir no explorador",
|
||||||
"mining_payout_address": "Endereço de Pagamento",
|
"mining_payout_address": "Endereço de Pagamento",
|
||||||
"mining_payout_foreign": "⚠ Este endereço de pagamento não está na sua carteira atual — as recompensas mineradas iriam para uma carteira diferente. Atualize-o se você trocou de carteira.",
|
"mining_payout_foreign": "⚠ Este endereço de pagamento não está na sua carteira atual — as recompensas mineradas iriam para uma carteira diferente. Atualize-o se você trocou de carteira.",
|
||||||
|
"mining_payout_invalid": "Endereço DragonX inválido — corrija antes de iniciar, ou as recompensas de mineração serão perdidas.",
|
||||||
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
|
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
|
||||||
"mining_pool": "Pool",
|
"mining_pool": "Pool",
|
||||||
"mining_pool_fee": "Taxa",
|
"mining_pool_fee": "Taxa",
|
||||||
"mining_pool_hashrate": "Hashrate do Pool",
|
"mining_pool_hashrate": "Hashrate do Pool",
|
||||||
|
"mining_pool_needs_payout_tooltip": "Informe primeiro um endereço de pagamento (gere um endereço Z)",
|
||||||
"mining_pool_url": "URL do Pool",
|
"mining_pool_url": "URL do Pool",
|
||||||
"mining_pools_header": "POOLS",
|
"mining_pools_header": "POOLS",
|
||||||
"mining_recent_blocks": "BLOCOS RECENTES",
|
"mining_recent_blocks": "BLOCOS RECENTES",
|
||||||
@@ -823,6 +964,9 @@
|
|||||||
"mining_syncing_tooltip": "Blockchain está sincronizando...",
|
"mining_syncing_tooltip": "Blockchain está sincronizando...",
|
||||||
"mining_tag": " · Mineração",
|
"mining_tag": " · Mineração",
|
||||||
"mining_threads": "Threads de Mineração",
|
"mining_threads": "Threads de Mineração",
|
||||||
|
"mining_threads_input_tooltip": "Digite um número exato de threads (Enter para aplicar)",
|
||||||
|
"mining_threads_minus_tooltip": "Menos threads",
|
||||||
|
"mining_threads_plus_tooltip": "Mais threads",
|
||||||
"mining_to_save": "para salvar",
|
"mining_to_save": "para salvar",
|
||||||
"mining_today": "Hoje",
|
"mining_today": "Hoje",
|
||||||
"mining_uptime": "Tempo Ativo",
|
"mining_uptime": "Tempo Ativo",
|
||||||
@@ -849,6 +993,11 @@
|
|||||||
"no_transactions": "Nenhuma transação encontrada",
|
"no_transactions": "Nenhuma transação encontrada",
|
||||||
"no_transactions_yet": "Nenhuma transação ainda",
|
"no_transactions_yet": "Nenhuma transação ainda",
|
||||||
"node": "NÓ",
|
"node": "NÓ",
|
||||||
|
"node_banner_crashed_title": "O nó parou inesperadamente",
|
||||||
|
"node_banner_lite_open_failed": "Não foi possível abrir sua carteira",
|
||||||
|
"node_banner_offline_title": "Não conectado ao nó DragonX",
|
||||||
|
"node_banner_reconnect": "Reconectar",
|
||||||
|
"node_banner_restart": "Reiniciar nó",
|
||||||
"node_security": "NÓ & SEGURANÇA",
|
"node_security": "NÓ & SEGURANÇA",
|
||||||
"noise": "Ruído",
|
"noise": "Ruído",
|
||||||
"not_connected": "Não conectado ao daemon...",
|
"not_connected": "Não conectado ao daemon...",
|
||||||
@@ -972,11 +1121,12 @@
|
|||||||
"portfolio_spark_min": "Minuto",
|
"portfolio_spark_min": "Minuto",
|
||||||
"portfolio_spark_month": "Mês",
|
"portfolio_spark_month": "Mês",
|
||||||
"portfolio_spark_week": "Semana",
|
"portfolio_spark_week": "Semana",
|
||||||
"portfolio_style_compact": "Linhas compactas",
|
"portfolio_style_compact": "Tabela",
|
||||||
"portfolio_style_detailed": "Linhas detalhadas",
|
"portfolio_style_detailed": "Cartões",
|
||||||
"portfolio_style_featured": "Linhas em destaque",
|
"portfolio_style_featured": "Destaque",
|
||||||
"portfolio_style_label": "Estilo do portfólio",
|
"portfolio_style_label": "Estilo do portfólio",
|
||||||
"portfolio_untitled": "Sem título",
|
"portfolio_untitled": "Sem título",
|
||||||
|
"portfolio_wallet_loading": "Aguarde a carteira terminar de carregar para adicionar um grupo.",
|
||||||
"price_chart": "Gráfico de Preços",
|
"price_chart": "Gráfico de Preços",
|
||||||
"privacy_great": "Ótima privacidade!",
|
"privacy_great": "Ótima privacidade!",
|
||||||
"privacy_low": "Baixa privacidade — blinde os fundos",
|
"privacy_low": "Baixa privacidade — blinde os fundos",
|
||||||
@@ -986,6 +1136,8 @@
|
|||||||
"qr_failed": "Falha ao gerar código QR",
|
"qr_failed": "Falha ao gerar código QR",
|
||||||
"qr_title": "Código QR",
|
"qr_title": "Código QR",
|
||||||
"qr_unavailable": "QR indisponível",
|
"qr_unavailable": "QR indisponível",
|
||||||
|
"quick_receive": "Recebimento rápido",
|
||||||
|
"quick_send": "Envio rápido",
|
||||||
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
"ram_daemon_gb": "Daemon: %.1f GB (%s)",
|
||||||
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
"ram_daemon_mb": "Daemon: %.0f MB (%s)",
|
||||||
"ram_system_gb": "Sistema: %.1f / %.0f GB",
|
"ram_system_gb": "Sistema: %.1f / %.0f GB",
|
||||||
@@ -1035,6 +1187,7 @@
|
|||||||
"rpc_connection": "Conexão RPC...",
|
"rpc_connection": "Conexão RPC...",
|
||||||
"rpc_host": "Host RPC",
|
"rpc_host": "Host RPC",
|
||||||
"rpc_pass": "Senha",
|
"rpc_pass": "Senha",
|
||||||
|
"rpc_plaintext_remote_warning": "O RPC remoto está usando HTTP em texto simples. Adicione rpctls=1 ao DRAGONX.conf se o seu daemon suportar TLS.",
|
||||||
"rpc_port": "Porta",
|
"rpc_port": "Porta",
|
||||||
"rpc_user": "Usuário",
|
"rpc_user": "Usuário",
|
||||||
"save": "Salvar",
|
"save": "Salvar",
|
||||||
@@ -1049,6 +1202,8 @@
|
|||||||
"sb_connecting_external": "Conectando ao daemon externo...",
|
"sb_connecting_external": "Conectando ao daemon externo...",
|
||||||
"sb_connecting_generic": "Conectando ao daemon...",
|
"sb_connecting_generic": "Conectando ao daemon...",
|
||||||
"sb_daemon_crashed": "O daemon travou %d vezes",
|
"sb_daemon_crashed": "O daemon travou %d vezes",
|
||||||
|
"sb_daemon_extract_failed": "Falha ao gravar os arquivos do daemon — verifique o espaço livre em disco e as permissões.",
|
||||||
|
"sb_daemon_files_failed": "Falha ao gravar os arquivos do daemon em %s — verifique o espaço livre em disco e as permissões.",
|
||||||
"sb_daemon_not_found": "Daemon não encontrado",
|
"sb_daemon_not_found": "Daemon não encontrado",
|
||||||
"sb_daemon_start_failed": "Não foi possível iniciar o dragonxd",
|
"sb_daemon_start_failed": "Não foi possível iniciar o dragonxd",
|
||||||
"sb_dragonxd_running": "dragonxd em execução",
|
"sb_dragonxd_running": "dragonxd em execução",
|
||||||
@@ -1064,6 +1219,7 @@
|
|||||||
"sb_net_mhs": "Rede: %.2f MH/s",
|
"sb_net_mhs": "Rede: %.2f MH/s",
|
||||||
"sb_no_conf": "DRAGONX.conf não encontrado",
|
"sb_no_conf": "DRAGONX.conf não encontrado",
|
||||||
"sb_peers": "Pares: %zu",
|
"sb_peers": "Pares: %zu",
|
||||||
|
"sb_plaintext_remote_blocked": "Recusando enviar credenciais RPC em texto simples para um host remoto. Adicione rpcallowplaintext=1 ao DRAGONX.conf para permitir, ou habilite TLS com rpctls=1.",
|
||||||
"sb_rescanning": "Reescaneando",
|
"sb_rescanning": "Reescaneando",
|
||||||
"sb_rescanning_pct": "Reescaneando %.0f%%",
|
"sb_rescanning_pct": "Reescaneando %.0f%%",
|
||||||
"sb_restarting_daemon": "Reiniciando daemon...",
|
"sb_restarting_daemon": "Reiniciando daemon...",
|
||||||
@@ -1077,6 +1233,7 @@
|
|||||||
"sb_waiting_daemon_err": "Aguardando dragonxd — %s",
|
"sb_waiting_daemon_err": "Aguardando dragonxd — %s",
|
||||||
"sb_warming_up": "Aquecendo...",
|
"sb_warming_up": "Aquecendo...",
|
||||||
"sb_witness_cache": "Reconstruindo testemunhas",
|
"sb_witness_cache": "Reconstruindo testemunhas",
|
||||||
|
"scale_effects": "ESCALA E EFEITOS",
|
||||||
"screenshot_open_dir": "Abrir local",
|
"screenshot_open_dir": "Abrir local",
|
||||||
"screenshot_sweep": "Executar varredura de capturas de tela",
|
"screenshot_sweep": "Executar varredura de capturas de tela",
|
||||||
"screenshot_sweep_desc": "Percorre cada tema em cada aba e salva uma captura de tela de cada um em subpastas por aba dentro da pasta de capturas de tela do diretório de configuração (sobrescrevendo a varredura anterior). É executado por alguns segundos.",
|
"screenshot_sweep_desc": "Percorre cada tema em cada aba e salva uma captura de tela de cada um em subpastas por aba dentro da pasta de capturas de tela do diretório de configuração (sobrescrevendo a varredura anterior). É executado por alguns segundos.",
|
||||||
@@ -1141,6 +1298,7 @@
|
|||||||
"send_tooltip_not_connected": "Não conectado ao daemon",
|
"send_tooltip_not_connected": "Não conectado ao daemon",
|
||||||
"send_tooltip_select_source": "Selecione primeiro um endereço de origem",
|
"send_tooltip_select_source": "Selecione primeiro um endereço de origem",
|
||||||
"send_tooltip_syncing": "Aguarde a sincronização da blockchain",
|
"send_tooltip_syncing": "Aguarde a sincronização da blockchain",
|
||||||
|
"send_tooltip_view_only": "Endereço somente visualização — sem chave de gasto, não é possível enviar",
|
||||||
"send_total": "Total",
|
"send_total": "Total",
|
||||||
"send_transaction": "Enviar Transação",
|
"send_transaction": "Enviar Transação",
|
||||||
"send_tx_failed": "Transação falhou",
|
"send_tx_failed": "Transação falhou",
|
||||||
@@ -1160,16 +1318,16 @@
|
|||||||
"sent_filter": "Enviado",
|
"sent_filter": "Enviado",
|
||||||
"sent_type": "Enviado",
|
"sent_type": "Enviado",
|
||||||
"sent_upper": "ENVIADO",
|
"sent_upper": "ENVIADO",
|
||||||
"set_label": "Definir Rótulo...",
|
"set_label": "Definir Rótulo",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.",
|
"settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.",
|
||||||
"settings_acrylic_level": "Nível acrílico:",
|
"settings_acrylic_level": "Nível acrílico:",
|
||||||
"settings_address_book": "Livro de endereços...",
|
"settings_address_book": "Livro de endereços…",
|
||||||
"settings_auto_detected": "Detectado automaticamente de DRAGONX.conf",
|
"settings_auto_detected": "Detectado automaticamente de DRAGONX.conf",
|
||||||
"settings_auto_lock": "BLOQUEIO AUTOMÁTICO",
|
"settings_auto_lock": "BLOQUEIO AUTOMÁTICO",
|
||||||
"settings_auto_shield_desc": "Mover automaticamente fundos transparentes para endereços blindados",
|
"settings_auto_shield_desc": "Mover automaticamente fundos transparentes para endereços blindados",
|
||||||
"settings_auto_shield_funds": "Blindar fundos transparentes automaticamente",
|
"settings_auto_shield_funds": "Blindar fundos transparentes automaticamente",
|
||||||
"settings_backup": "Backup...",
|
"settings_backup": "Backup…",
|
||||||
"settings_block_explorer_urls": "URLs do explorador de blocos",
|
"settings_block_explorer_urls": "URLs do explorador de blocos",
|
||||||
"settings_builtin": "Integrado",
|
"settings_builtin": "Integrado",
|
||||||
"settings_change_passphrase": "Alterar frase secreta",
|
"settings_change_passphrase": "Alterar frase secreta",
|
||||||
@@ -1180,60 +1338,71 @@
|
|||||||
"settings_configure_explorer": "Configurar links do explorador de blocos externo",
|
"settings_configure_explorer": "Configurar links do explorador de blocos externo",
|
||||||
"settings_configure_rpc": "Configurar conexão ao daemon dragonxd",
|
"settings_configure_rpc": "Configurar conexão ao daemon dragonxd",
|
||||||
"settings_connection": "Conexão",
|
"settings_connection": "Conexão",
|
||||||
|
"settings_copy_diagnostics": "Copiar diagnósticos",
|
||||||
"settings_copyright": "Copyright 2024-2026 Desenvolvedores DragonX | Licença GPLv3",
|
"settings_copyright": "Copyright 2024-2026 Desenvolvedores DragonX | Licença GPLv3",
|
||||||
"settings_custom": "Personalizado",
|
"settings_custom": "Personalizado",
|
||||||
"settings_data_dir": "Dir. de dados:",
|
"settings_data_dir": "Dir. de dados",
|
||||||
"settings_debug_changed": "Categorias de depuração alteradas — reinicie o daemon para aplicar",
|
"settings_debug_changed": "Categorias de depuração alteradas — reinicie o daemon para aplicar",
|
||||||
"settings_debug_restart_note": "As alterações entram em vigor após reiniciar o daemon.",
|
"settings_debug_restart_note": "As alterações entram em vigor após reiniciar o daemon.",
|
||||||
"settings_debug_select": "Selecione categorias para ativar o registro de depuração do daemon (flags -debug=).",
|
"settings_debug_select": "Selecione categorias para ativar o registro de depuração do daemon (flags -debug=).",
|
||||||
|
"settings_diagnostics_copied": "Diagnósticos copiados para a área de transferência",
|
||||||
"settings_encrypt_first_pin": "Encripte a carteira primeiro para ativar o PIN",
|
"settings_encrypt_first_pin": "Encripte a carteira primeiro para ativar o PIN",
|
||||||
"settings_encrypt_wallet": "Encriptar carteira",
|
"settings_encrypt_wallet": "Encriptar carteira",
|
||||||
"settings_explorer_hint": "As URLs devem incluir uma barra final. O txid/endereço será adicionado.",
|
"settings_explorer_hint": "As URLs devem incluir uma barra final. O txid/endereço será adicionado.",
|
||||||
"settings_export_all": "Exportar tudo...",
|
"settings_export_all": "Exportar tudo…",
|
||||||
"settings_export_csv": "Exportar CSV...",
|
"settings_export_csv": "Exportar CSV…",
|
||||||
"settings_export_key": "Exportar chave...",
|
"settings_export_key": "Exportar chave…",
|
||||||
"settings_gradient_bg": "Fundo gradiente",
|
"settings_gradient_bg": "Fundo gradiente",
|
||||||
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
|
"settings_gradient_desc": "Substituir fundos texturizados por gradientes suaves",
|
||||||
"settings_idle_after": "após",
|
"settings_idle_after": "após",
|
||||||
"settings_import_key": "Importar Chave Privada...",
|
"settings_import_key": "Importar Chave Privada…",
|
||||||
"settings_import_viewkey": "Importar chave de visualização...",
|
"settings_import_viewkey": "Importar chave de visualização…",
|
||||||
"settings_language_note": "Nota: Alguns textos requerem reinício para atualizar",
|
"settings_language_note": "Nota: Alguns textos requerem reinício para atualizar",
|
||||||
"settings_lock_now": "Bloquear agora",
|
"settings_lock_now": "Bloquear agora",
|
||||||
"settings_locked": "Bloqueado",
|
"settings_locked": "Bloqueado",
|
||||||
"settings_merge_to_address": "Fundir para endereço...",
|
"settings_merge_to_address": "Fundir para endereço…",
|
||||||
"settings_noise_opacity": "Opacidade do ruído:",
|
"settings_noise_opacity": "Opacidade do ruído:",
|
||||||
|
"settings_not_connected": "Não conectado ao daemon",
|
||||||
"settings_not_encrypted": "Não encriptado",
|
"settings_not_encrypted": "Não encriptado",
|
||||||
"settings_not_found": "Não encontrado",
|
"settings_not_found": "Não encontrado",
|
||||||
"settings_open_app_dir": "Abrir pasta do aplicativo",
|
"settings_open_app_dir": "Abrir pasta do aplicativo",
|
||||||
"settings_open_data_dir": "Abrir pasta de dados",
|
"settings_open_data_dir": "Abrir pasta de dados",
|
||||||
|
"settings_open_log_folder": "Abrir pasta de logs",
|
||||||
"settings_other": "Outros",
|
"settings_other": "Outros",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "Privacidade",
|
"settings_privacy": "Privacidade",
|
||||||
"settings_quick_unlock_pin": "PIN de desbloqueio rápido",
|
"settings_quick_unlock_pin": "PIN de desbloqueio rápido",
|
||||||
"settings_reduce_transparency": "Reduzir transparência",
|
"settings_reduce_transparency": "Reduzir transparência",
|
||||||
|
"settings_reloaded": "Configurações recarregadas do disco",
|
||||||
"settings_remove_encryption": "Remover encriptação",
|
"settings_remove_encryption": "Remover encriptação",
|
||||||
"settings_remove_pin": "Remover PIN",
|
"settings_remove_pin": "Remover PIN",
|
||||||
"settings_request_payment": "Solicitar pagamento...",
|
"settings_request_payment": "Solicitar pagamento…",
|
||||||
"settings_rescan_desc": "Reescanear a blockchain em busca de transações ausentes",
|
"settings_rescan_desc": "Reescanear a blockchain em busca de transações ausentes",
|
||||||
"settings_restart_daemon": "Reiniciar daemon",
|
"settings_restart_daemon": "Reiniciar daemon",
|
||||||
"settings_rpc_connection": "Conexão RPC",
|
"settings_rpc_connection": "Conexão RPC",
|
||||||
|
"settings_rpc_error_prefix": "Erro de RPC: ",
|
||||||
"settings_rpc_note": "Nota: As configurações de conexão são normalmente detectadas automaticamente do DRAGONX.conf",
|
"settings_rpc_note": "Nota: As configurações de conexão são normalmente detectadas automaticamente do DRAGONX.conf",
|
||||||
|
"settings_rpc_ok": "Conexão RPC OK",
|
||||||
"settings_save_shielded_desc": "Armazena transações z-addr em um arquivo local para visualização",
|
"settings_save_shielded_desc": "Armazena transações z-addr em um arquivo local para visualização",
|
||||||
"settings_save_shielded_local": "Salvar histórico de transações blindadas localmente",
|
"settings_save_shielded_local": "Salvar histórico de transações blindadas localmente",
|
||||||
|
"settings_saved": "Configurações salvas",
|
||||||
"settings_set_pin": "Definir PIN",
|
"settings_set_pin": "Definir PIN",
|
||||||
"settings_shield_mining": "Blindar mineração...",
|
"settings_shield_mining": "Blindar mineração…",
|
||||||
"settings_solid_colors_desc": "Usar cores sólidas em vez de efeitos de desfoque (acessibilidade)",
|
"settings_solid_colors_desc": "Usar cores sólidas em vez de efeitos de desfoque (acessibilidade)",
|
||||||
|
"settings_theme_refreshed": "Lista de temas atualizada",
|
||||||
"settings_tor_desc": "Rotear todas as conexões através do Tor para maior privacidade",
|
"settings_tor_desc": "Rotear todas as conexões através do Tor para maior privacidade",
|
||||||
"settings_unlocked": "Desbloqueado",
|
"settings_unlocked": "Desbloqueado",
|
||||||
"settings_use_tor_network": "Usar Tor para conexões de rede",
|
"settings_use_tor_network": "Usar Tor para conexões de rede",
|
||||||
"settings_validate_address": "Validar endereço...",
|
"settings_validate_address": "Validar endereço…",
|
||||||
"settings_visual_effects": "Efeitos visuais",
|
"settings_visual_effects": "Efeitos visuais",
|
||||||
"settings_wallet_file_size": "Tamanho do arquivo da carteira: %s",
|
"settings_wallet_file_size": "Tamanho do arquivo da carteira: %s",
|
||||||
"settings_wallet_info": "Informações da carteira",
|
"settings_wallet_info": "Informações da carteira",
|
||||||
"settings_wallet_location": "Localização da carteira: %s",
|
"settings_wallet_location": "Localização da carteira: %s",
|
||||||
"settings_wallet_maintenance": "Manutenção da carteira",
|
"settings_wallet_maintenance": "Manutenção da carteira",
|
||||||
"settings_wallet_not_found": "Arquivo da carteira não encontrado",
|
"settings_wallet_not_found": "Arquivo da carteira não encontrado",
|
||||||
"settings_wallet_size_label": "Tamanho da carteira:",
|
"settings_wallet_size_label": "Tamanho da carteira",
|
||||||
|
"settings_ztx_cleared": "Histórico de transações Z limpo",
|
||||||
|
"settings_ztx_not_found": "Nenhum arquivo de histórico encontrado",
|
||||||
"setup_wizard": "Assistente de Configuração",
|
"setup_wizard": "Assistente de Configuração",
|
||||||
"share": "Compartilhar",
|
"share": "Compartilhar",
|
||||||
"shield_check_status": "Verificar Status",
|
"shield_check_status": "Verificar Status",
|
||||||
@@ -1292,6 +1461,23 @@
|
|||||||
"sweep_to": "Varrido para:",
|
"sweep_to": "Varrido para:",
|
||||||
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
|
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
|
||||||
"sweep_tx": "Transação:",
|
"sweep_tx": "Transação:",
|
||||||
|
"switch_corrupt_body": "Esta carteira parece corrompida — o nó não conseguiu abri-la. Restaure de um backup, recrie-a ou tente repará-la.",
|
||||||
|
"switch_corrupt_repair": "Tentar reparar (salvage)",
|
||||||
|
"switch_progress_background": "Continuar em segundo plano",
|
||||||
|
"switch_progress_default_wallet": "Carteira padrão",
|
||||||
|
"switch_progress_elapsed": "Decorrido",
|
||||||
|
"switch_progress_external_wallet": "Carteira externa",
|
||||||
|
"switch_progress_failed_title": "Falha ao trocar de carteira",
|
||||||
|
"switch_progress_from_label": "de",
|
||||||
|
"switch_progress_hint": "Um desligamento normal pode levar até um minuto.",
|
||||||
|
"switch_progress_reconnecting": "Reconectando",
|
||||||
|
"switch_progress_starting": "Iniciando o nó com a nova carteira",
|
||||||
|
"switch_progress_stopping": "Parando o nó atual",
|
||||||
|
"switch_progress_title": "Trocando de carteira",
|
||||||
|
"switch_stopnode_body": "Trocar de carteira reinicia o nó com a carteira selecionada. O nó em execução será parado e reiniciado com a nova carteira — se você o deixou em execução de propósito, ele volta automaticamente.",
|
||||||
|
"switch_stopnode_confirm": "Parar nó e trocar",
|
||||||
|
"switch_stopnode_title": "Parar o nó em execução?",
|
||||||
|
"switch_stopnode_warn": "Já há um nó em execução que esta carteira não iniciou.",
|
||||||
"syncing": "Sincronizando...",
|
"syncing": "Sincronizando...",
|
||||||
"t_address": "Endereço T",
|
"t_address": "Endereço T",
|
||||||
"t_addresses": "Endereços T",
|
"t_addresses": "Endereços T",
|
||||||
@@ -1299,6 +1485,7 @@
|
|||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"theme_effects": "Efeitos de tema",
|
"theme_effects": "Efeitos de tema",
|
||||||
"theme_language": "TEMA E IDIOMA",
|
"theme_language": "TEMA E IDIOMA",
|
||||||
|
"tile_click_to_open": "Clique para abrir",
|
||||||
"time_days_ago": "há %d dias",
|
"time_days_ago": "há %d dias",
|
||||||
"time_hours_ago": "há %d horas",
|
"time_hours_ago": "há %d horas",
|
||||||
"time_minutes_ago": "há %d minutos",
|
"time_minutes_ago": "há %d minutos",
|
||||||
@@ -1313,7 +1500,9 @@
|
|||||||
"to_upper": "PARA",
|
"to_upper": "PARA",
|
||||||
"tools": "FERRAMENTAS",
|
"tools": "FERRAMENTAS",
|
||||||
"tools_actions": "Ferramentas e Ações...",
|
"tools_actions": "Ferramentas e Ações...",
|
||||||
|
"tools_actions_hdr": "FERRAMENTAS E AÇÕES",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"total_balance_label": "Saldo Total",
|
||||||
"transaction_id": "ID DA TRANSAÇÃO",
|
"transaction_id": "ID DA TRANSAÇÃO",
|
||||||
"transaction_sent": "Transação enviada com sucesso",
|
"transaction_sent": "Transação enviada com sucesso",
|
||||||
"transaction_sent_msg": "Transação enviada!",
|
"transaction_sent_msg": "Transação enviada!",
|
||||||
@@ -1335,13 +1524,24 @@
|
|||||||
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
|
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
|
||||||
"tt_backup": "Criar um backup do seu wallet.dat",
|
"tt_backup": "Criar um backup do seu wallet.dat",
|
||||||
"tt_block_explorer": "Abrir o explorador de blocos DragonX no seu navegador",
|
"tt_block_explorer": "Abrir o explorador de blocos DragonX no seu navegador",
|
||||||
"tt_blur": "Quantidade de desfoque (0%% = desligado, 100%% = máximo)",
|
"tt_blur": "Quantidade de desfoque (0% = desligado, 100% = máximo)",
|
||||||
"tt_change_pass": "Alterar a frase secreta de encriptação da carteira",
|
"tt_change_pass": "Alterar a frase secreta de encriptação da carteira",
|
||||||
"tt_change_pin": "Alterar seu PIN de desbloqueio",
|
"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_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_copy_diagnostics": "Copia um resumo para suporte (versão, estado do daemon/carteira/logs — sem segredos) para a área de transferência",
|
||||||
"tt_custom_fees": "Ativar entrada manual de taxas ao enviar transações",
|
"tt_custom_fees": "Ativar entrada manual de taxas ao enviar transações",
|
||||||
"tt_custom_theme": "Tema personalizado ativo",
|
"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_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_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_collapse": "Recolher opções de registro de depuração",
|
||||||
"tt_debug_expand": "Expandir opções de registro de depuração",
|
"tt_debug_expand": "Expandir opções de registro de depuração",
|
||||||
@@ -1359,15 +1559,39 @@
|
|||||||
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
||||||
"tt_language": "Idioma da interface da carteira",
|
"tt_language": "Idioma da interface da carteira",
|
||||||
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
||||||
|
"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_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_lock": "Bloquear a carteira imediatamente",
|
||||||
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
|
"tt_low_spec": "Desativar todos os efeitos visuais pesados\\nAtalho: Ctrl+Shift+Down",
|
||||||
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
|
"tt_merge": "Consolidar múltiplos UTXOs em um endereço",
|
||||||
"tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)",
|
"tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)",
|
||||||
"tt_noise": "Intensidade de textura granulada (0%% = desligado, 100%% = máximo)",
|
"tt_noise": "Intensidade de textura granulada (0% = desligado, 100% = máximo)",
|
||||||
"tt_open_app_dir": "Abrir a pasta ObsidianDragon (configurações, temas, logs) no gerenciador de arquivos",
|
"tt_open_app_dir": "Abrir a pasta ObsidianDragon (configurações, temas, logs) no gerenciador de arquivos",
|
||||||
"tt_open_data_dir": "Abrir a pasta com os dados da sua carteira e da blockchain no gerenciador de arquivos",
|
"tt_open_data_dir": "Abrir a pasta com os dados da sua carteira e da blockchain no gerenciador de arquivos",
|
||||||
"tt_open_dir": "Clique para abrir no explorador de arquivos",
|
"tt_open_dir": "Clique para abrir no explorador de arquivos",
|
||||||
|
"tt_open_log_folder": "Abre a pasta que contém os logs de depuração e de falhas",
|
||||||
"tt_reduce_motion": "Desativar transições animadas e lerp de saldo para acessibilidade",
|
"tt_reduce_motion": "Desativar transições animadas e lerp de saldo para acessibilidade",
|
||||||
"tt_remove_encrypt": "Remover encriptação e armazenar a carteira desprotegida",
|
"tt_remove_encrypt": "Remover encriptação e armazenar a carteira desprotegida",
|
||||||
"tt_remove_pin": "Remover PIN e exigir frase secreta para desbloquear",
|
"tt_remove_pin": "Remover PIN e exigir frase secreta para desbloquear",
|
||||||
@@ -1380,12 +1604,17 @@
|
|||||||
"tt_rpc_host": "Nome do host do daemon DragonX",
|
"tt_rpc_host": "Nome do host do daemon DragonX",
|
||||||
"tt_rpc_pass": "Senha de autenticação RPC",
|
"tt_rpc_pass": "Senha de autenticação RPC",
|
||||||
"tt_rpc_port": "Porta para conexões RPC do daemon",
|
"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_rpc_user": "Nome de usuário de autenticação RPC",
|
||||||
"tt_save_settings": "Salvar todas as configurações no disco",
|
"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_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_scan_themes": "Procurar novos temas.\\nColoque pastas de temas em:\\n%s",
|
||||||
"tt_scanline": "Efeito de linhas de varredura CRT no console",
|
"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_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_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_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",
|
"tt_shield_mining": "Mover recompensas de mineração transparentes para um endereço blindado",
|
||||||
@@ -1397,13 +1626,14 @@
|
|||||||
"tt_theme_hotkey": "Atalho: Ctrl+Esquerda/Direita para alternar temas",
|
"tt_theme_hotkey": "Atalho: Ctrl+Esquerda/Direita para alternar temas",
|
||||||
"tt_tor": "Rotear conexões do daemon através da rede Tor para anonimato",
|
"tt_tor": "Rotear conexões do daemon através da rede Tor para anonimato",
|
||||||
"tt_tx_url": "URL base para visualizar transações em um explorador de blocos",
|
"tt_tx_url": "URL base para visualizar transações em um explorador de blocos",
|
||||||
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100%% = totalmente opaco, menor = mais transparente)",
|
"tt_ui_opacity": "Opacidade de cartões e barra lateral (100% = totalmente opaco, menor = mais transparente)",
|
||||||
"tt_validate": "Verificar se um endereço DragonX é válido",
|
"tt_validate": "Verificar se um endereço DragonX é válido",
|
||||||
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
|
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
|
||||||
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
|
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
|
||||||
"tt_website": "Abrir o site do DragonX",
|
"tt_website": "Abrir o site do DragonX",
|
||||||
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
"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",
|
"tt_wizard": "Executar novamente o assistente de configuração inicial\\nO daemon será reiniciado",
|
||||||
|
"tx_chat_badge": "Mensagem",
|
||||||
"tx_confirmations": "%d confirmações",
|
"tx_confirmations": "%d confirmações",
|
||||||
"tx_details_title": "Detalhes da Transação",
|
"tx_details_title": "Detalhes da Transação",
|
||||||
"tx_from_address": "Endereço de Origem:",
|
"tx_from_address": "Endereço de Origem:",
|
||||||
@@ -1449,6 +1679,7 @@
|
|||||||
"validate_not_mine": "Não pertence a esta carteira",
|
"validate_not_mine": "Não pertence a esta carteira",
|
||||||
"validate_ownership": "Propriedade:",
|
"validate_ownership": "Propriedade:",
|
||||||
"validate_results": "Resultados:",
|
"validate_results": "Resultados:",
|
||||||
|
"validate_results_placeholder": "Os resultados aparecerão aqui",
|
||||||
"validate_shielded_type": "Blindado (z-endereço)",
|
"validate_shielded_type": "Blindado (z-endereço)",
|
||||||
"validate_status": "Status:",
|
"validate_status": "Status:",
|
||||||
"validate_title": "Validar Endereço",
|
"validate_title": "Validar Endereço",
|
||||||
@@ -1472,6 +1703,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ Procurar carteiras noutra pasta…",
|
"wallets_add_folder_toggle": "+ Procurar carteiras noutra pasta…",
|
||||||
"wallets_badge_encrypted": "Encriptada (protegida por senha)",
|
"wallets_badge_encrypted": "Encriptada (protegida por senha)",
|
||||||
"wallets_badge_encrypted_short": "Encriptada",
|
"wallets_badge_encrypted_short": "Encriptada",
|
||||||
|
"wallets_badge_hd": "Carteira HD — não é possível confirmar a frase-semente sem abri-la",
|
||||||
|
"wallets_badge_hd_short": "Carteira HD",
|
||||||
"wallets_badge_legacy": "Carteira legada (sem frase semente)",
|
"wallets_badge_legacy": "Carteira legada (sem frase semente)",
|
||||||
"wallets_badge_legacy_short": "Legada",
|
"wallets_badge_legacy_short": "Legada",
|
||||||
"wallets_badge_seed": "Carteira com frase semente (HD)",
|
"wallets_badge_seed": "Carteira com frase semente (HD)",
|
||||||
@@ -1587,6 +1820,7 @@
|
|||||||
"xmrig_loading_releases": "Carregando versões…",
|
"xmrig_loading_releases": "Carregando versões…",
|
||||||
"xmrig_none": "nenhum",
|
"xmrig_none": "nenhum",
|
||||||
"xmrig_reinstall": "Reinstalar",
|
"xmrig_reinstall": "Reinstalar",
|
||||||
|
"xmrig_releases": "versões do xmrig",
|
||||||
"xmrig_stop_mining_first": "Pare a mineração antes de atualizar o minerador.",
|
"xmrig_stop_mining_first": "Pare a mineração antes de atualizar o minerador.",
|
||||||
"xmrig_unavailable_body": "Nenhuma versão do minerador está disponível para esta plataforma.",
|
"xmrig_unavailable_body": "Nenhuma versão do minerador está disponível para esta plataforma.",
|
||||||
"xmrig_unavailable_title": "Atualizações do minerador indisponíveis",
|
"xmrig_unavailable_title": "Atualizações do minerador indisponíveis",
|
||||||
|
|||||||
278
res/lang/ru.json
278
res/lang/ru.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "ПРОЧЕЕ",
|
"advanced": "ПРОЧЕЕ",
|
||||||
"advanced_effects": "Расширенные эффекты...",
|
"advanced_effects": "Расширенные эффекты...",
|
||||||
"ago": "назад",
|
"ago": "назад",
|
||||||
|
"alerts_clear": "Очистить историю оповещений",
|
||||||
|
"alerts_history_tooltip": "Недавние оповещения",
|
||||||
|
"alerts_none": "Пока нет оповещений",
|
||||||
|
"alerts_recent": "НЕДАВНИЕ ОПОВЕЩЕНИЯ",
|
||||||
"all_filter": "Все",
|
"all_filter": "Все",
|
||||||
"allow_custom_fees": "Разрешить пользовательские комиссии",
|
"allow_custom_fees": "Разрешить пользовательские комиссии",
|
||||||
"amount": "Сумма",
|
"amount": "Сумма",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender заблокировал майнер",
|
"av_title": "Windows Defender заблокировал майнер",
|
||||||
"available": "Доступно",
|
"available": "Доступно",
|
||||||
"backup_backing_up": "Создание резервной копии...",
|
"backup_backing_up": "Создание резервной копии...",
|
||||||
|
"backup_col_backup": "РЕЗЕРВНАЯ КОПИЯ",
|
||||||
|
"backup_col_export": "ЭКСПОРТ",
|
||||||
|
"backup_col_import": "ИМПОРТ И ВОССТАНОВЛЕНИЕ",
|
||||||
"backup_create": "Создать резервную копию",
|
"backup_create": "Создать резервную копию",
|
||||||
"backup_created": "Резервная копия кошелька создана",
|
"backup_created": "Резервная копия кошелька создана",
|
||||||
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
|
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "Баланс",
|
"balance": "Баланс",
|
||||||
"balance_history_collecting": "История баланса — сбор данных...",
|
"balance_history_collecting": "История баланса — сбор данных...",
|
||||||
"balance_layout": "Макет баланса",
|
"balance_layout": "Макет баланса",
|
||||||
|
"balance_layout_switched": "Раскладка: %s",
|
||||||
|
"balance_mining_rate": "Майнинг %s",
|
||||||
"balance_shielded_fmt": "Экранировано: %.8f",
|
"balance_shielded_fmt": "Экранировано: %.8f",
|
||||||
|
"balance_syncing_pct": "Синхронизация %.1f%%",
|
||||||
"balance_transparent_fmt": "Прозрачный: %.8f",
|
"balance_transparent_fmt": "Прозрачный: %.8f",
|
||||||
"ban": "Заблокировать",
|
"ban": "Заблокировать",
|
||||||
"banned_peers": "Заблокированные узлы",
|
"banned_peers": "Заблокированные узлы",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "Проверка контрольных сумм...",
|
"bootstrap_verifying": "Проверка контрольных сумм...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat защищён)",
|
"bootstrap_wallet_protected": "(wallet.dat защищён)",
|
||||||
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
|
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
|
||||||
|
"byte_count_fmt": "%zu / %zu байт",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"change_pass_confirm": "Подтвердите новый:",
|
"change_pass_confirm": "Подтвердите новый:",
|
||||||
"change_pass_current": "Текущий пароль:",
|
"change_pass_current": "Текущий пароль:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "Сменить пароль",
|
"change_pass_title": "Сменить пароль",
|
||||||
"characters": "символов",
|
"characters": "символов",
|
||||||
"chat": "Чат",
|
"chat": "Чат",
|
||||||
|
"chat_accent_amber": "Янтарный",
|
||||||
|
"chat_accent_blue": "Синий",
|
||||||
|
"chat_accent_green": "Зелёный",
|
||||||
|
"chat_accent_pink": "Розовый",
|
||||||
|
"chat_accent_purple": "Фиолетовый",
|
||||||
|
"chat_accent_theme": "Тема",
|
||||||
|
"chat_add_contact": "Добавить контакт",
|
||||||
|
"chat_awaiting_key": "Ожидание ответа",
|
||||||
|
"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_cancel": "Отмена",
|
||||||
|
"chat_contact_added": "Контакт добавлен — переименуйте его в Контактах",
|
||||||
"chat_contact_request": "запрос контакта",
|
"chat_contact_request": "запрос контакта",
|
||||||
|
"chat_copy_address_tip": "Нажмите, чтобы скопировать адрес",
|
||||||
|
"chat_density_comfortable": "Свободная",
|
||||||
|
"chat_density_compact": "Компактная",
|
||||||
|
"chat_emoji_color": "Цветной",
|
||||||
|
"chat_emoji_mono": "Монохромный",
|
||||||
|
"chat_emoji_search": "Поиск эмодзи",
|
||||||
"chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.",
|
"chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.",
|
||||||
|
"chat_empty_start": "Начните новый с помощью «Новый разговор».",
|
||||||
|
"chat_empty_title": "Пока нет разговоров",
|
||||||
|
"chat_export": "Экспорт чата…",
|
||||||
|
"chat_export_done": "Разговор экспортирован",
|
||||||
|
"chat_export_failed": "Не удалось записать файл экспорта.",
|
||||||
|
"chat_export_warn": "Сохраняет расшифрованные сообщения в виде обычного текста. Храните файл в надёжном месте.",
|
||||||
|
"chat_filter": "Чат",
|
||||||
|
"chat_hidden_toast": "Разговор скрыт — новое сообщение вернёт его",
|
||||||
|
"chat_hide": "Скрыть",
|
||||||
|
"chat_hide_hidden": "Скрыть скрытые",
|
||||||
|
"chat_jump_latest": "Новые",
|
||||||
|
"chat_len_over": "Сообщение слишком длинное",
|
||||||
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
|
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
|
||||||
"chat_new_button": "Новая переписка",
|
"chat_mute": "Отключить уведомления",
|
||||||
|
"chat_new_button": "Новый чат",
|
||||||
"chat_new_message": "Сообщение",
|
"chat_new_message": "Сообщение",
|
||||||
|
"chat_new_message_toast": "Новое зашифрованное сообщение",
|
||||||
"chat_new_send": "Отправить запрос",
|
"chat_new_send": "Отправить запрос",
|
||||||
"chat_new_title": "Новая переписка",
|
"chat_new_title": "Новый чат",
|
||||||
"chat_new_zaddr": "Z-адрес получателя",
|
"chat_new_zaddr": "Z-адрес получателя",
|
||||||
|
"chat_no_matches": "Нет разговоров, соответствующих запросу.",
|
||||||
|
"chat_no_z_contacts": "Пока нет контактов с защищённым адресом",
|
||||||
|
"chat_opt_bubble_accent": "Цвет пузырька",
|
||||||
|
"chat_opt_bubble_style": "Стиль пузырька",
|
||||||
|
"chat_opt_density": "Плотность сообщений",
|
||||||
|
"chat_opt_emoji": "Стиль эмодзи",
|
||||||
|
"chat_opt_enter_sends": "Enter отправляет сообщение",
|
||||||
|
"chat_opt_font_size": "Размер текста",
|
||||||
|
"chat_opt_global_clock": "Глобальный формат времени",
|
||||||
|
"chat_opt_poll": "Частота опроса",
|
||||||
|
"chat_opt_timestamp": "Метки времени",
|
||||||
|
"chat_pick_contact": "Выбрать из контактов…",
|
||||||
|
"chat_rename": "Переименовать контакт",
|
||||||
|
"chat_rename_hint": "Имя контакта",
|
||||||
|
"chat_renamed": "Контакт переименован",
|
||||||
|
"chat_retry": "Повторить",
|
||||||
|
"chat_search": "Поиск разговоров",
|
||||||
|
"chat_sec_appearance": "ВИД",
|
||||||
|
"chat_sec_messaging": "СООБЩЕНИЯ",
|
||||||
"chat_select_hint": "Выберите переписку для просмотра.",
|
"chat_select_hint": "Выберите переписку для просмотра.",
|
||||||
"chat_send": "Отправить",
|
"chat_send": "Отправить",
|
||||||
"chat_send_failed": "не отправлено",
|
"chat_send_failed": "не отправлено",
|
||||||
|
"chat_sending": "отправка…",
|
||||||
|
"chat_settings_done": "Готово",
|
||||||
|
"chat_settings_section": "ЧАТ И КОНТАКТЫ",
|
||||||
|
"chat_settings_tip": "Настройка чата",
|
||||||
|
"chat_settings_title": "Настройки чата",
|
||||||
|
"chat_show_hidden": "Показать скрытые",
|
||||||
|
"chat_time_now": "сейчас",
|
||||||
"chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).",
|
"chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).",
|
||||||
"chat_toast_lite_busy": "Отправка уже выполняется, или кошелёк не открыт.",
|
"chat_toast_lite_busy": "Отправка уже выполняется, или кошелёк не открыт.",
|
||||||
|
"chat_toast_need_funds": "Для отправки сообщений нужен небольшой экранированный баланс (для оплаты комиссии).",
|
||||||
"chat_toast_no_zaddr": "Нет доступного Z-адреса для отправки сообщений.",
|
"chat_toast_no_zaddr": "Нет доступного Z-адреса для отправки сообщений.",
|
||||||
"chat_toast_not_connected": "Нет подключения — сообщение не отправлено.",
|
"chat_toast_not_connected": "Нет подключения — сообщение не отправлено.",
|
||||||
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
||||||
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
||||||
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
||||||
|
"chat_today": "Сегодня",
|
||||||
|
"chat_ts_12h": "12 часов",
|
||||||
|
"chat_ts_24h": "24 часа",
|
||||||
|
"chat_ts_global": "Как глобально",
|
||||||
|
"chat_ts_global_short": "Общий",
|
||||||
|
"chat_unhide": "Показать",
|
||||||
|
"chat_unmute": "Включить уведомления",
|
||||||
|
"chat_verify_key": "Ключ личности — сравните для проверки",
|
||||||
"chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.",
|
"chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.",
|
||||||
|
"chat_yesterday": "Вчера",
|
||||||
"chat_you": "Вы",
|
"chat_you": "Вы",
|
||||||
"choose_icon": "Выбрать иконку",
|
"choose_icon": "Выбрать иконку",
|
||||||
"clear": "Очистить",
|
"clear": "Очистить",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "Нажмите, чтобы скопировать адрес",
|
"click_copy_address": "Нажмите, чтобы скопировать адрес",
|
||||||
"click_copy_uri": "Нажмите, чтобы скопировать URI",
|
"click_copy_uri": "Нажмите, чтобы скопировать URI",
|
||||||
"click_to_copy": "Нажмите для копирования",
|
"click_to_copy": "Нажмите для копирования",
|
||||||
|
"clock_format": "Формат времени",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"conf_count": "%d подтв.",
|
"conf_count": "%d подтв.",
|
||||||
"confirm_and_send": "Подтвердить и отправить",
|
"confirm_and_send": "Подтвердить и отправить",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "Прил.",
|
"console_app": "Прил.",
|
||||||
"console_auto_scroll": "Авто-прокрутка",
|
"console_auto_scroll": "Авто-прокрутка",
|
||||||
"console_available_commands": "Доступные команды:",
|
"console_available_commands": "Доступные команды:",
|
||||||
|
"console_backend_reference": "Справочник команд бэкенда",
|
||||||
|
"console_backend_unavailable": "Нет бэкенда",
|
||||||
"console_capturing_output": "Захват вывода daemon...",
|
"console_capturing_output": "Захват вывода daemon...",
|
||||||
|
"console_cat_advanced": "Дополнительно",
|
||||||
"console_cat_blockchain": "Блокчейн",
|
"console_cat_blockchain": "Блокчейн",
|
||||||
"console_cat_control": "Управление",
|
"console_cat_control": "Управление",
|
||||||
|
"console_cat_keys": "Ключи и безопасность",
|
||||||
"console_cat_mining": "Майнинг",
|
"console_cat_mining": "Майнинг",
|
||||||
"console_cat_network": "Сеть",
|
"console_cat_network": "Сеть",
|
||||||
"console_cat_raw_transactions": "Сырые транзакции",
|
"console_cat_raw_transactions": "Сырые транзакции",
|
||||||
|
"console_cat_send": "Отправка",
|
||||||
|
"console_cat_sync": "Синхронизация",
|
||||||
"console_cat_utility": "Утилиты",
|
"console_cat_utility": "Утилиты",
|
||||||
"console_cat_wallet": "Кошелёк",
|
"console_cat_wallet": "Кошелёк",
|
||||||
"console_clear": "Очистить",
|
"console_clear": "Очистить",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - Показать эту справку",
|
"console_help_help": " help - Показать эту справку",
|
||||||
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
||||||
"console_help_stop": " stop - Остановить daemon",
|
"console_help_stop": " stop - Остановить daemon",
|
||||||
|
"console_last_error": "Последняя ошибка:",
|
||||||
"console_line_count": "%zu строк",
|
"console_line_count": "%zu строк",
|
||||||
"console_matches": "совпадений",
|
"console_matches": "совпадений",
|
||||||
"console_new_lines": "%d новых строк",
|
"console_new_lines": "%d новых строк",
|
||||||
"console_no_daemon": "Нет daemon",
|
"console_no_daemon": "Нет daemon",
|
||||||
|
"console_no_output": "(нет вывода)",
|
||||||
"console_not_connected": "Ошибка: Не подключено к daemon",
|
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||||
|
"console_not_connected_lite": "Ошибка: Нет открытого кошелька",
|
||||||
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
|
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
|
||||||
"console_ref_builds": "Формирует",
|
"console_ref_builds": "Формирует",
|
||||||
"console_ref_cancel": "Отмена",
|
"console_ref_cancel": "Отмена",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
|
"console_ref_run_confirm": "Выполнить %s сейчас? Это ответственная команда.",
|
||||||
"console_ref_search_hint": "Поиск по названию или задаче…",
|
"console_ref_search_hint": "Поиск по названию или задаче…",
|
||||||
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
|
"console_ref_select_hint": "Выберите команду, чтобы увидеть, что она делает.",
|
||||||
|
"console_ref_value": "значение",
|
||||||
"console_rpc_reference": "Справочник RPC-команд",
|
"console_rpc_reference": "Справочник RPC-команд",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "Скан-линия консоли",
|
"console_scanline": "Скан-линия консоли",
|
||||||
"console_search_commands": "Поиск команд...",
|
"console_search_commands": "Поиск команд...",
|
||||||
"console_select_all": "Выбрать всё",
|
"console_select_all": "Выбрать всё",
|
||||||
"console_show_app_output": "Показать строки журнала кошелька [app]",
|
"console_show_app_output": "Показать строки журнала кошелька [app]",
|
||||||
|
"console_show_backend_ref": "Показать справочник команд бэкенда",
|
||||||
"console_show_daemon_output": "Показать вывод daemon",
|
"console_show_daemon_output": "Показать вывод daemon",
|
||||||
"console_show_errors_only": "Показать только ошибки",
|
"console_show_errors_only": "Показать только ошибки",
|
||||||
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "Остановлен",
|
"console_status_stopped": "Остановлен",
|
||||||
"console_status_stopping": "Остановка",
|
"console_status_stopping": "Остановка",
|
||||||
"console_status_unknown": "Неизвестно",
|
"console_status_unknown": "Неизвестно",
|
||||||
|
"console_stop_confirm_node": "'stop' остановит узел и отключит кошелёк. Введите 'stop' ещё раз для подтверждения.",
|
||||||
"console_tab_completion": "Tab для дополнения",
|
"console_tab_completion": "Tab для дополнения",
|
||||||
"console_text_colors": "Цвета текста",
|
"console_text_colors": "Цвета текста",
|
||||||
"console_toggle_accents": "Переключить цветовые акценты строк",
|
"console_toggle_accents": "Переключить цветовые акценты строк",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "Вкл.: этот контакт остаётся видимым, какой бы кошелёк вы ни загрузили. Выкл.: он принадлежит только текущему кошельку.",
|
"contact_global_tt": "Вкл.: этот контакт остаётся видимым, какой бы кошелёк вы ни загрузили. Выкл.: он принадлежит только текущему кошельку.",
|
||||||
"contact_preview_addr": "Здесь появится адрес",
|
"contact_preview_addr": "Здесь появится адрес",
|
||||||
"contact_preview_name": "Имя контакта",
|
"contact_preview_name": "Имя контакта",
|
||||||
|
"contact_wallet_loading": "Кошелёк ещё загружается — отметьте «Показывать во всех кошельках» или повторите чуть позже.",
|
||||||
"contacts": "Контакты",
|
"contacts": "Контакты",
|
||||||
|
"contacts_avatar_shape": "Форма аватара",
|
||||||
|
"contacts_list_scale": "Масштаб списка",
|
||||||
"contacts_search_no_match": "Совпадающих контактов нет",
|
"contacts_search_no_match": "Совпадающих контактов нет",
|
||||||
"contacts_search_placeholder": "Поиск контактов...",
|
"contacts_search_placeholder": "Поиск контактов...",
|
||||||
|
"contacts_settings_tip": "Настройка контактов",
|
||||||
|
"contacts_settings_title": "Настройки контактов",
|
||||||
|
"contacts_shape_circle": "Круг",
|
||||||
|
"contacts_shape_square": "Квадрат",
|
||||||
|
"contacts_shape_tab": "Вкладка",
|
||||||
"copied": "Скопировано!",
|
"copied": "Скопировано!",
|
||||||
"copy": "Копировать",
|
"copy": "Копировать",
|
||||||
"copy_address": "Копировать полный адрес",
|
"copy_address": "Копировать полный адрес",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "Встроенный",
|
"daemon_bundled": "Встроенный",
|
||||||
"daemon_install_bundled": "Установить встроенную",
|
"daemon_install_bundled": "Установить встроенную",
|
||||||
"daemon_installed": "Установлено",
|
"daemon_installed": "Установлено",
|
||||||
|
"daemon_maintenance_label": "ОБСЛУЖИВАНИЕ",
|
||||||
"daemon_none_bundled": "нет в этой сборке",
|
"daemon_none_bundled": "нет в этой сборке",
|
||||||
"daemon_not_installed": "не установлен",
|
"daemon_not_installed": "не установлен",
|
||||||
"daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.",
|
"daemon_status_differ": "Установленный бинарный файл отличается от встроенной версии.",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "Последняя:",
|
"daemon_update_latest": "Последняя:",
|
||||||
"daemon_update_loading": "Загрузка релизов…",
|
"daemon_update_loading": "Загрузка релизов…",
|
||||||
"daemon_update_now": "Обновить сейчас",
|
"daemon_update_now": "Обновить сейчас",
|
||||||
|
"daemon_update_prompt_title": "Обновить демон узла?",
|
||||||
"daemon_update_reinstall": "Переустановить",
|
"daemon_update_reinstall": "Переустановить",
|
||||||
"daemon_update_restart_note": "Перезапустите daemon, чтобы начать работу с новой версией.",
|
"daemon_update_restart_note": "Перезапустите daemon, чтобы начать работу с новой версией.",
|
||||||
"daemon_update_restart_now": "Перезапустить демон сейчас",
|
"daemon_update_restart_now": "Перезапустить демон сейчас",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.",
|
"daemon_update_verify_note": "Перед установкой загрузка проверяется по опубликованному для релиза SHA-256 и закреплённой подписи ed25519.",
|
||||||
"daemon_update_verifying": "Проверка…",
|
"daemon_update_verifying": "Проверка…",
|
||||||
"daemon_update_version": "Версия:",
|
"daemon_update_version": "Версия:",
|
||||||
|
"daemon_updates_label": "ОБНОВЛЕНИЯ",
|
||||||
"daemon_version": "Демон",
|
"daemon_version": "Демон",
|
||||||
"dark": "Тёмная",
|
"dark": "Тёмная",
|
||||||
|
"data_stale_prefix": "Обновлено",
|
||||||
|
"data_stale_tooltip": "Баланс может быть устаревшим — кошелёк давно не получал обновлений. Проверьте подключение к узлу.",
|
||||||
"date": "Дата",
|
"date": "Дата",
|
||||||
"date_label": "Дата:",
|
"date_label": "Дата:",
|
||||||
"debug_logging": "ЖУРНАЛ ОТЛАДКИ",
|
"debug_logging": "ЖУРНАЛ ОТЛАДКИ",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "Скачать бутстрап",
|
"download_bootstrap": "Скачать бутстрап",
|
||||||
"dragonx_green": "DragonX (Зелёная)",
|
"dragonx_green": "DragonX (Зелёная)",
|
||||||
"edit": "Редактировать",
|
"edit": "Редактировать",
|
||||||
|
"empty_wallet_keys_suffix": "ключей",
|
||||||
|
"empty_wallet_open_manager": "Открыть менеджер кошельков",
|
||||||
|
"empty_wallet_restore": "Восстановить мой кошелёк",
|
||||||
|
"empty_wallet_salvage_body": "Этот кошелёк пуст, потому что предыдущее автоматическое восстановление отложило ваш исходный кошелёк в качестве резервной копии. Ваши монеты почти наверняка находятся в этой копии и не потеряны. Восстановите её, чтобы снова загрузить средства — ничего не удаляется; текущий файл сначала откладывается в сторону.",
|
||||||
|
"empty_wallet_salvage_headline": "Ваши монеты в безопасности в файле резервной копии.",
|
||||||
|
"empty_wallet_salvage_title": "Возможно, ваш кошелёк был восстановлен",
|
||||||
|
"empty_wallet_warning_body": "В этом кошельке нет адресов и средств, но другой файл кошелька в вашей папке DragonX содержит ключи. Ваши монеты, скорее всего, находятся в нём и не потеряны. Откройте менеджер кошельков, чтобы переключиться на кошелёк с вашими средствами.",
|
||||||
|
"empty_wallet_warning_dismiss": "Больше не предупреждать для этого кошелька",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "Останавливает это предупреждение только для текущего файла кошелька. Если позже вы переключитесь на другой пустой кошелёк, предупреждение может появиться снова.",
|
||||||
|
"empty_wallet_warning_headline": "Возможно, вы открыли не тот кошелёк.",
|
||||||
|
"empty_wallet_warning_title": "Этот кошелёк пуст",
|
||||||
"enc_confirm": "Подтвердите:",
|
"enc_confirm": "Подтвердите:",
|
||||||
"enc_desc": "Шифрование кошелька защищает ваши приватные ключи паролем. После шифрования демон перезапустится.",
|
"enc_desc": "Шифрование кошелька защищает ваши приватные ключи паролем. После шифрования демон перезапустится.",
|
||||||
"enc_encrypting": "Шифрование кошелька...",
|
"enc_encrypting": "Шифрование кошелька...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "Светлая",
|
"light": "Светлая",
|
||||||
"lite_account_label": "Аккаунт",
|
"lite_account_label": "Аккаунт",
|
||||||
"lite_action": "Действие",
|
"lite_action": "Действие",
|
||||||
|
"lite_backend_unavailable": "Бэкенд облегчённого кошелька недоступен",
|
||||||
"lite_backup_keys": "Резервная копия и ключи",
|
"lite_backup_keys": "Резервная копия и ключи",
|
||||||
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
||||||
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
||||||
"lite_birthday_label": "Дата рождения",
|
"lite_birthday_label": "Дата рождения",
|
||||||
|
"lite_console_backend_commands": "Команды бэкенда:",
|
||||||
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
||||||
"lite_copy": "Копировать",
|
"lite_copy": "Копировать",
|
||||||
|
"lite_could_not_start": "Не удалось запустить операцию",
|
||||||
"lite_could_not_write": "Не удалось записать ",
|
"lite_could_not_write": "Не удалось записать ",
|
||||||
"lite_encrypt_wallet": "Зашифровать кошелёк",
|
"lite_encrypt_wallet": "Зашифровать кошелёк",
|
||||||
"lite_encryption_removed": "Шифрование удалено",
|
"lite_encryption_removed": "Шифрование удалено",
|
||||||
|
"lite_enter_all_seed_words": "Введите все 24 слова seed-фразы для восстановления (введено %d)",
|
||||||
|
"lite_enter_wallet_path": "Введите путь к кошельку",
|
||||||
"lite_hide_wipe": "Скрыть и стереть",
|
"lite_hide_wipe": "Скрыть и стереть",
|
||||||
"lite_import": "Импорт",
|
"lite_import": "Импорт",
|
||||||
"lite_import_key_label": "Импортировать ключ",
|
"lite_import_key_label": "Импортировать ключ",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://your-lite-server",
|
"lite_net_add_url_hint": "https://your-lite-server",
|
||||||
"lite_net_checking": "проверка…",
|
"lite_net_checking": "проверка…",
|
||||||
"lite_net_connected": "Подключено",
|
"lite_net_connected": "Подключено",
|
||||||
|
"lite_net_connecting": "Подключение…",
|
||||||
"lite_net_custom": "Свой",
|
"lite_net_custom": "Свой",
|
||||||
"lite_net_disconnected": "Не подключено",
|
"lite_net_disconnected": "Не подключено",
|
||||||
"lite_net_hidden_section": "Скрытые серверы",
|
"lite_net_hidden_section": "Скрытые серверы",
|
||||||
@@ -638,6 +765,9 @@
|
|||||||
"lite_working": "Обработка…",
|
"lite_working": "Обработка…",
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
"loading_addresses": "Загрузка адресов...",
|
"loading_addresses": "Загрузка адресов...",
|
||||||
|
"loading_stall_body": "Демон инициализируется уже %.0f с. Это может быть нормально после обновления или при первом запуске (загрузка индекса блоков или повторное сканирование) — соединение установится автоматически, когда он будет готов.",
|
||||||
|
"loading_stall_hint": "Всё ещё не отвечает? Откройте Настройки и нажмите «Перезапустить демон» или посмотрите подробности в Консоли.",
|
||||||
|
"loading_stall_title": "Занимает больше времени, чем ожидалось",
|
||||||
"loading_transactions": "Загрузка транзакций",
|
"loading_transactions": "Загрузка транзакций",
|
||||||
"local_hashrate": "Локальный хешрейт",
|
"local_hashrate": "Локальный хешрейт",
|
||||||
"low_spec_mode": "Режим экономии",
|
"low_spec_mode": "Режим экономии",
|
||||||
@@ -654,6 +784,9 @@
|
|||||||
"market_cap": "Рыночная капитализация",
|
"market_cap": "Рыночная капитализация",
|
||||||
"market_cap_short": "Кап.",
|
"market_cap_short": "Кап.",
|
||||||
"market_chart_loading": "Загрузка истории цен",
|
"market_chart_loading": "Загрузка истории цен",
|
||||||
|
"market_col_name": "Название",
|
||||||
|
"market_col_trend": "Тренд",
|
||||||
|
"market_col_value": "Стоимость",
|
||||||
"market_iv_1d": "1Д",
|
"market_iv_1d": "1Д",
|
||||||
"market_iv_1h": "1Ч",
|
"market_iv_1h": "1Ч",
|
||||||
"market_iv_1m": "1М",
|
"market_iv_1m": "1М",
|
||||||
@@ -662,13 +795,18 @@
|
|||||||
"market_no_history": "Нет истории цен",
|
"market_no_history": "Нет истории цен",
|
||||||
"market_no_price": "Нет данных о ценах",
|
"market_no_price": "Нет данных о ценах",
|
||||||
"market_now": "Сейчас",
|
"market_now": "Сейчас",
|
||||||
|
"market_opt_chart_style": "Стиль графика",
|
||||||
"market_pct_shielded": "%.0f%% Экранировано",
|
"market_pct_shielded": "%.0f%% Экранировано",
|
||||||
"market_portfolio": "ПОРТФЕЛЬ",
|
"market_portfolio": "ПОРТФЕЛЬ",
|
||||||
"market_price_loading": "Загрузка данных о ценах...",
|
"market_price_loading": "Загрузка данных о ценах...",
|
||||||
"market_price_unavailable": "Данные о ценах недоступны",
|
"market_price_unavailable": "Данные о ценах недоступны",
|
||||||
"market_refresh_price": "Обновить данные о ценах",
|
"market_refresh_price": "Обновить данные о ценах",
|
||||||
|
"market_settings_tip": "Параметры рынка",
|
||||||
|
"market_settings_title": "Настройки рынка",
|
||||||
"market_style_candle": "Переключить на свечи",
|
"market_style_candle": "Переключить на свечи",
|
||||||
|
"market_style_candle_label": "Свечи",
|
||||||
"market_style_line": "Переключить на линейный график",
|
"market_style_line": "Переключить на линейный график",
|
||||||
|
"market_style_line_label": "Линия",
|
||||||
"market_trade_on": "Торговать на %s",
|
"market_trade_on": "Торговать на %s",
|
||||||
"market_updated": "\\xc2\\xb7 Обновлено %s",
|
"market_updated": "\\xc2\\xb7 Обновлено %s",
|
||||||
"market_vol_short": "Объём",
|
"market_vol_short": "Объём",
|
||||||
@@ -762,6 +900,7 @@
|
|||||||
"mining_difficulty_copied": "Сложность скопирована",
|
"mining_difficulty_copied": "Сложность скопирована",
|
||||||
"mining_est_block": "Расч. блок",
|
"mining_est_block": "Расч. блок",
|
||||||
"mining_est_daily": "Расч. за день",
|
"mining_est_daily": "Расч. за день",
|
||||||
|
"mining_est_daily_pool_sub": "примерный соло-эквивалент, до комиссии пула",
|
||||||
"mining_filter_all": "Все",
|
"mining_filter_all": "Все",
|
||||||
"mining_filter_tip_all": "Показать все доходы",
|
"mining_filter_tip_all": "Показать все доходы",
|
||||||
"mining_filter_tip_pool": "Показать только доходы пула",
|
"mining_filter_tip_pool": "Показать только доходы пула",
|
||||||
@@ -790,10 +929,12 @@
|
|||||||
"mining_open_in_explorer": "Открыть в обозревателе",
|
"mining_open_in_explorer": "Открыть в обозревателе",
|
||||||
"mining_payout_address": "Адрес выплат",
|
"mining_payout_address": "Адрес выплат",
|
||||||
"mining_payout_foreign": "⚠ Этот адрес выплат отсутствует в вашем текущем кошельке — намайненные вознаграждения будут отправлены в другой кошелёк. Обновите его, если вы сменили кошелёк.",
|
"mining_payout_foreign": "⚠ Этот адрес выплат отсутствует в вашем текущем кошельке — намайненные вознаграждения будут отправлены в другой кошелёк. Обновите его, если вы сменили кошелёк.",
|
||||||
|
"mining_payout_invalid": "Недействительный адрес DragonX — исправьте перед запуском, иначе награды за майнинг будут потеряны.",
|
||||||
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
|
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
|
||||||
"mining_pool": "Пул",
|
"mining_pool": "Пул",
|
||||||
"mining_pool_fee": "Комиссия",
|
"mining_pool_fee": "Комиссия",
|
||||||
"mining_pool_hashrate": "Хешрейт пула",
|
"mining_pool_hashrate": "Хешрейт пула",
|
||||||
|
"mining_pool_needs_payout_tooltip": "Сначала введите адрес для выплат (создайте Z-адрес)",
|
||||||
"mining_pool_url": "URL пула",
|
"mining_pool_url": "URL пула",
|
||||||
"mining_pools_header": "ПУЛЫ",
|
"mining_pools_header": "ПУЛЫ",
|
||||||
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
|
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
|
||||||
@@ -823,6 +964,9 @@
|
|||||||
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
|
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
|
||||||
"mining_tag": " · Майнинг",
|
"mining_tag": " · Майнинг",
|
||||||
"mining_threads": "Потоки майнинга",
|
"mining_threads": "Потоки майнинга",
|
||||||
|
"mining_threads_input_tooltip": "Введите точное число потоков (Enter для применения)",
|
||||||
|
"mining_threads_minus_tooltip": "Меньше потоков",
|
||||||
|
"mining_threads_plus_tooltip": "Больше потоков",
|
||||||
"mining_to_save": "для сохранения",
|
"mining_to_save": "для сохранения",
|
||||||
"mining_today": "Сегодня",
|
"mining_today": "Сегодня",
|
||||||
"mining_uptime": "Время работы",
|
"mining_uptime": "Время работы",
|
||||||
@@ -849,6 +993,11 @@
|
|||||||
"no_transactions": "Транзакции не найдены",
|
"no_transactions": "Транзакции не найдены",
|
||||||
"no_transactions_yet": "Транзакций пока нет",
|
"no_transactions_yet": "Транзакций пока нет",
|
||||||
"node": "УЗЕЛ",
|
"node": "УЗЕЛ",
|
||||||
|
"node_banner_crashed_title": "Узел неожиданно остановился",
|
||||||
|
"node_banner_lite_open_failed": "Не удалось открыть кошелёк",
|
||||||
|
"node_banner_offline_title": "Нет подключения к узлу DragonX",
|
||||||
|
"node_banner_reconnect": "Переподключить",
|
||||||
|
"node_banner_restart": "Перезапустить узел",
|
||||||
"node_security": "УЗЕЛ И БЕЗОПАСНОСТЬ",
|
"node_security": "УЗЕЛ И БЕЗОПАСНОСТЬ",
|
||||||
"noise": "Шум",
|
"noise": "Шум",
|
||||||
"not_connected": "Не подключено к daemon...",
|
"not_connected": "Не подключено к daemon...",
|
||||||
@@ -972,11 +1121,12 @@
|
|||||||
"portfolio_spark_min": "Минута",
|
"portfolio_spark_min": "Минута",
|
||||||
"portfolio_spark_month": "Месяц",
|
"portfolio_spark_month": "Месяц",
|
||||||
"portfolio_spark_week": "Неделя",
|
"portfolio_spark_week": "Неделя",
|
||||||
"portfolio_style_compact": "Компактные строки",
|
"portfolio_style_compact": "Таблица",
|
||||||
"portfolio_style_detailed": "Подробные строки",
|
"portfolio_style_detailed": "Карточки",
|
||||||
"portfolio_style_featured": "Избранные строки",
|
"portfolio_style_featured": "Витрина",
|
||||||
"portfolio_style_label": "Стиль портфеля",
|
"portfolio_style_label": "Стиль портфеля",
|
||||||
"portfolio_untitled": "Без названия",
|
"portfolio_untitled": "Без названия",
|
||||||
|
"portfolio_wallet_loading": "Дождитесь загрузки кошелька, чтобы добавить группу.",
|
||||||
"price_chart": "График цен",
|
"price_chart": "График цен",
|
||||||
"privacy_great": "Отличная конфиденциальность!",
|
"privacy_great": "Отличная конфиденциальность!",
|
||||||
"privacy_low": "Низкая конфиденциальность — экранируйте средства",
|
"privacy_low": "Низкая конфиденциальность — экранируйте средства",
|
||||||
@@ -986,6 +1136,8 @@
|
|||||||
"qr_failed": "Не удалось сгенерировать QR-код",
|
"qr_failed": "Не удалось сгенерировать QR-код",
|
||||||
"qr_title": "QR-код",
|
"qr_title": "QR-код",
|
||||||
"qr_unavailable": "QR недоступен",
|
"qr_unavailable": "QR недоступен",
|
||||||
|
"quick_receive": "Быстрый приём",
|
||||||
|
"quick_send": "Быстрая отправка",
|
||||||
"ram_daemon_gb": "Демон: %.1f ГБ (%s)",
|
"ram_daemon_gb": "Демон: %.1f ГБ (%s)",
|
||||||
"ram_daemon_mb": "Демон: %.0f МБ (%s)",
|
"ram_daemon_mb": "Демон: %.0f МБ (%s)",
|
||||||
"ram_system_gb": "Система: %.1f / %.0f ГБ",
|
"ram_system_gb": "Система: %.1f / %.0f ГБ",
|
||||||
@@ -1035,6 +1187,7 @@
|
|||||||
"rpc_connection": "RPC-подключение...",
|
"rpc_connection": "RPC-подключение...",
|
||||||
"rpc_host": "RPC-хост",
|
"rpc_host": "RPC-хост",
|
||||||
"rpc_pass": "Пароль",
|
"rpc_pass": "Пароль",
|
||||||
|
"rpc_plaintext_remote_warning": "Удалённый RPC использует незашифрованный HTTP. Добавьте rpctls=1 в DRAGONX.conf, если ваш демон поддерживает TLS.",
|
||||||
"rpc_port": "Порт",
|
"rpc_port": "Порт",
|
||||||
"rpc_user": "Имя пользователя",
|
"rpc_user": "Имя пользователя",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
@@ -1049,6 +1202,8 @@
|
|||||||
"sb_connecting_external": "Подключение к внешнему демону...",
|
"sb_connecting_external": "Подключение к внешнему демону...",
|
||||||
"sb_connecting_generic": "Подключение к демону...",
|
"sb_connecting_generic": "Подключение к демону...",
|
||||||
"sb_daemon_crashed": "Демон упал %d раз",
|
"sb_daemon_crashed": "Демон упал %d раз",
|
||||||
|
"sb_daemon_extract_failed": "Не удалось записать файлы демона — проверьте свободное место на диске и права доступа.",
|
||||||
|
"sb_daemon_files_failed": "Не удалось записать файлы демона в %s — проверьте свободное место на диске и права доступа.",
|
||||||
"sb_daemon_not_found": "Демон не найден",
|
"sb_daemon_not_found": "Демон не найден",
|
||||||
"sb_daemon_start_failed": "Не удалось запустить dragonxd",
|
"sb_daemon_start_failed": "Не удалось запустить dragonxd",
|
||||||
"sb_dragonxd_running": "dragonxd запущен",
|
"sb_dragonxd_running": "dragonxd запущен",
|
||||||
@@ -1064,6 +1219,7 @@
|
|||||||
"sb_net_mhs": "Сеть: %.2f MH/s",
|
"sb_net_mhs": "Сеть: %.2f MH/s",
|
||||||
"sb_no_conf": "DRAGONX.conf не найден",
|
"sb_no_conf": "DRAGONX.conf не найден",
|
||||||
"sb_peers": "Пиры: %zu",
|
"sb_peers": "Пиры: %zu",
|
||||||
|
"sb_plaintext_remote_blocked": "Отправка учётных данных RPC открытым текстом на удалённый узел запрещена. Добавьте rpcallowplaintext=1 в DRAGONX.conf, чтобы разрешить, или включите TLS с помощью rpctls=1.",
|
||||||
"sb_rescanning": "Пересканирование",
|
"sb_rescanning": "Пересканирование",
|
||||||
"sb_rescanning_pct": "Пересканирование %.0f%%",
|
"sb_rescanning_pct": "Пересканирование %.0f%%",
|
||||||
"sb_restarting_daemon": "Перезапуск демона...",
|
"sb_restarting_daemon": "Перезапуск демона...",
|
||||||
@@ -1077,6 +1233,7 @@
|
|||||||
"sb_waiting_daemon_err": "Ожидание dragonxd — %s",
|
"sb_waiting_daemon_err": "Ожидание dragonxd — %s",
|
||||||
"sb_warming_up": "Прогрев...",
|
"sb_warming_up": "Прогрев...",
|
||||||
"sb_witness_cache": "Перестроение свидетелей",
|
"sb_witness_cache": "Перестроение свидетелей",
|
||||||
|
"scale_effects": "МАСШТАБ И ЭФФЕКТЫ",
|
||||||
"screenshot_open_dir": "Открыть расположение",
|
"screenshot_open_dir": "Открыть расположение",
|
||||||
"screenshot_sweep": "Запустить прогон скриншотов",
|
"screenshot_sweep": "Запустить прогон скриншотов",
|
||||||
"screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.",
|
"screenshot_sweep_desc": "Перебирает каждую тему по всем вкладкам и сохраняет скриншот каждой в подпапки по вкладкам внутри папки screenshots в каталоге конфигурации (перезаписывая предыдущий проход). Выполняется несколько секунд.",
|
||||||
@@ -1141,6 +1298,7 @@
|
|||||||
"send_tooltip_not_connected": "Не подключено к daemon",
|
"send_tooltip_not_connected": "Не подключено к daemon",
|
||||||
"send_tooltip_select_source": "Сначала выберите адрес-источник",
|
"send_tooltip_select_source": "Сначала выберите адрес-источник",
|
||||||
"send_tooltip_syncing": "Дождитесь синхронизации блокчейна",
|
"send_tooltip_syncing": "Дождитесь синхронизации блокчейна",
|
||||||
|
"send_tooltip_view_only": "Адрес только для просмотра — нет ключа расходования, отправка невозможна",
|
||||||
"send_total": "Итого",
|
"send_total": "Итого",
|
||||||
"send_transaction": "Отправить транзакцию",
|
"send_transaction": "Отправить транзакцию",
|
||||||
"send_tx_failed": "Транзакция не удалась",
|
"send_tx_failed": "Транзакция не удалась",
|
||||||
@@ -1160,16 +1318,16 @@
|
|||||||
"sent_filter": "Отправлено",
|
"sent_filter": "Отправлено",
|
||||||
"sent_type": "Отправлено",
|
"sent_type": "Отправлено",
|
||||||
"sent_upper": "ОТПРАВЛЕНО",
|
"sent_upper": "ОТПРАВЛЕНО",
|
||||||
"set_label": "Установить метку...",
|
"set_label": "Установить метку",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
|
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
|
||||||
"settings_acrylic_level": "Уровень акрила:",
|
"settings_acrylic_level": "Уровень акрила:",
|
||||||
"settings_address_book": "Адресная книга...",
|
"settings_address_book": "Адресная книга…",
|
||||||
"settings_auto_detected": "Автоопределено из DRAGONX.conf",
|
"settings_auto_detected": "Автоопределено из DRAGONX.conf",
|
||||||
"settings_auto_lock": "АВТОБЛОКИРОВКА",
|
"settings_auto_lock": "АВТОБЛОКИРОВКА",
|
||||||
"settings_auto_shield_desc": "Автоматически перемещать прозрачные средства на экранированные адреса",
|
"settings_auto_shield_desc": "Автоматически перемещать прозрачные средства на экранированные адреса",
|
||||||
"settings_auto_shield_funds": "Автоматически экранировать прозрачные средства",
|
"settings_auto_shield_funds": "Автоматически экранировать прозрачные средства",
|
||||||
"settings_backup": "Резервная копия...",
|
"settings_backup": "Резервная копия…",
|
||||||
"settings_block_explorer_urls": "URL-адреса обозревателя блоков",
|
"settings_block_explorer_urls": "URL-адреса обозревателя блоков",
|
||||||
"settings_builtin": "Встроенные",
|
"settings_builtin": "Встроенные",
|
||||||
"settings_change_passphrase": "Сменить пароль",
|
"settings_change_passphrase": "Сменить пароль",
|
||||||
@@ -1180,60 +1338,71 @@
|
|||||||
"settings_configure_explorer": "Настроить ссылки внешнего обозревателя блоков",
|
"settings_configure_explorer": "Настроить ссылки внешнего обозревателя блоков",
|
||||||
"settings_configure_rpc": "Настроить подключение к демону dragonxd",
|
"settings_configure_rpc": "Настроить подключение к демону dragonxd",
|
||||||
"settings_connection": "Подключение",
|
"settings_connection": "Подключение",
|
||||||
|
"settings_copy_diagnostics": "Копировать диагностику",
|
||||||
"settings_copyright": "Copyright 2024-2026 Разработчики DragonX | Лицензия GPLv3",
|
"settings_copyright": "Copyright 2024-2026 Разработчики DragonX | Лицензия GPLv3",
|
||||||
"settings_custom": "Пользовательские",
|
"settings_custom": "Пользовательские",
|
||||||
"settings_data_dir": "Каталог данных:",
|
"settings_data_dir": "Каталог данных",
|
||||||
"settings_debug_changed": "Категории отладки изменены — перезапустите демон для применения",
|
"settings_debug_changed": "Категории отладки изменены — перезапустите демон для применения",
|
||||||
"settings_debug_restart_note": "Изменения вступают в силу после перезапуска демона.",
|
"settings_debug_restart_note": "Изменения вступают в силу после перезапуска демона.",
|
||||||
"settings_debug_select": "Выберите категории для включения журнала отладки демона (флаги -debug=).",
|
"settings_debug_select": "Выберите категории для включения журнала отладки демона (флаги -debug=).",
|
||||||
|
"settings_diagnostics_copied": "Диагностика скопирована в буфер обмена",
|
||||||
"settings_encrypt_first_pin": "Сначала зашифруйте кошелёк, чтобы включить PIN",
|
"settings_encrypt_first_pin": "Сначала зашифруйте кошелёк, чтобы включить PIN",
|
||||||
"settings_encrypt_wallet": "Зашифровать кошелёк",
|
"settings_encrypt_wallet": "Зашифровать кошелёк",
|
||||||
"settings_explorer_hint": "URL-адреса должны заканчиваться косой чертой. Txid/адрес будет добавлен.",
|
"settings_explorer_hint": "URL-адреса должны заканчиваться косой чертой. Txid/адрес будет добавлен.",
|
||||||
"settings_export_all": "Экспортировать все...",
|
"settings_export_all": "Экспортировать все…",
|
||||||
"settings_export_csv": "Экспорт CSV...",
|
"settings_export_csv": "Экспорт CSV…",
|
||||||
"settings_export_key": "Экспортировать ключ...",
|
"settings_export_key": "Экспортировать ключ…",
|
||||||
"settings_gradient_bg": "Градиент фона",
|
"settings_gradient_bg": "Градиент фона",
|
||||||
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
|
"settings_gradient_desc": "Заменить текстурные фоны плавными градиентами",
|
||||||
"settings_idle_after": "через",
|
"settings_idle_after": "через",
|
||||||
"settings_import_key": "Импорт приватного ключа...",
|
"settings_import_key": "Импорт приватного ключа…",
|
||||||
"settings_import_viewkey": "Импортировать ключ просмотра...",
|
"settings_import_viewkey": "Импортировать ключ просмотра…",
|
||||||
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
|
"settings_language_note": "Примечание: Некоторый текст требует перезапуска для обновления",
|
||||||
"settings_lock_now": "Заблокировать сейчас",
|
"settings_lock_now": "Заблокировать сейчас",
|
||||||
"settings_locked": "Заблокирован",
|
"settings_locked": "Заблокирован",
|
||||||
"settings_merge_to_address": "Объединить на адрес...",
|
"settings_merge_to_address": "Объединить на адрес…",
|
||||||
"settings_noise_opacity": "Непрозрачность шума:",
|
"settings_noise_opacity": "Непрозрачность шума:",
|
||||||
|
"settings_not_connected": "Нет соединения с демоном",
|
||||||
"settings_not_encrypted": "Не зашифрован",
|
"settings_not_encrypted": "Не зашифрован",
|
||||||
"settings_not_found": "Не найден",
|
"settings_not_found": "Не найден",
|
||||||
"settings_open_app_dir": "Открыть папку приложения",
|
"settings_open_app_dir": "Открыть папку приложения",
|
||||||
"settings_open_data_dir": "Открыть папку данных",
|
"settings_open_data_dir": "Открыть папку данных",
|
||||||
|
"settings_open_log_folder": "Открыть папку журналов",
|
||||||
"settings_other": "Прочее",
|
"settings_other": "Прочее",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "Конфиденциальность",
|
"settings_privacy": "Конфиденциальность",
|
||||||
"settings_quick_unlock_pin": "Быстрый PIN-код разблокировки",
|
"settings_quick_unlock_pin": "Быстрый PIN-код разблокировки",
|
||||||
"settings_reduce_transparency": "Уменьшить прозрачность",
|
"settings_reduce_transparency": "Уменьшить прозрачность",
|
||||||
|
"settings_reloaded": "Настройки перезагружены с диска",
|
||||||
"settings_remove_encryption": "Удалить шифрование",
|
"settings_remove_encryption": "Удалить шифрование",
|
||||||
"settings_remove_pin": "Удалить PIN",
|
"settings_remove_pin": "Удалить PIN",
|
||||||
"settings_request_payment": "Запросить платёж...",
|
"settings_request_payment": "Запросить платёж…",
|
||||||
"settings_rescan_desc": "Пересканировать блокчейн для поиска пропущенных транзакций",
|
"settings_rescan_desc": "Пересканировать блокчейн для поиска пропущенных транзакций",
|
||||||
"settings_restart_daemon": "Перезапустить демон",
|
"settings_restart_daemon": "Перезапустить демон",
|
||||||
"settings_rpc_connection": "RPC-соединение",
|
"settings_rpc_connection": "RPC-соединение",
|
||||||
|
"settings_rpc_error_prefix": "Ошибка RPC: ",
|
||||||
"settings_rpc_note": "Примечание: Настройки подключения обычно определяются автоматически из DRAGONX.conf",
|
"settings_rpc_note": "Примечание: Настройки подключения обычно определяются автоматически из DRAGONX.conf",
|
||||||
|
"settings_rpc_ok": "RPC-соединение в порядке",
|
||||||
"settings_save_shielded_desc": "Сохраняет z-addr транзакции в локальном файле для просмотра",
|
"settings_save_shielded_desc": "Сохраняет z-addr транзакции в локальном файле для просмотра",
|
||||||
"settings_save_shielded_local": "Сохранять историю защищённых транзакций локально",
|
"settings_save_shielded_local": "Сохранять историю защищённых транзакций локально",
|
||||||
|
"settings_saved": "Настройки сохранены",
|
||||||
"settings_set_pin": "Установить PIN",
|
"settings_set_pin": "Установить PIN",
|
||||||
"settings_shield_mining": "Экранировать майнинг...",
|
"settings_shield_mining": "Экранировать майнинг…",
|
||||||
"settings_solid_colors_desc": "Использовать сплошные цвета вместо эффектов размытия (доступность)",
|
"settings_solid_colors_desc": "Использовать сплошные цвета вместо эффектов размытия (доступность)",
|
||||||
|
"settings_theme_refreshed": "Список тем обновлён",
|
||||||
"settings_tor_desc": "Маршрутизировать все соединения через Tor для повышения конфиденциальности",
|
"settings_tor_desc": "Маршрутизировать все соединения через Tor для повышения конфиденциальности",
|
||||||
"settings_unlocked": "Разблокирован",
|
"settings_unlocked": "Разблокирован",
|
||||||
"settings_use_tor_network": "Использовать Tor для сетевых подключений",
|
"settings_use_tor_network": "Использовать Tor для сетевых подключений",
|
||||||
"settings_validate_address": "Проверить адрес...",
|
"settings_validate_address": "Проверить адрес…",
|
||||||
"settings_visual_effects": "Визуальные эффекты",
|
"settings_visual_effects": "Визуальные эффекты",
|
||||||
"settings_wallet_file_size": "Размер файла кошелька: %s",
|
"settings_wallet_file_size": "Размер файла кошелька: %s",
|
||||||
"settings_wallet_info": "Информация о кошельке",
|
"settings_wallet_info": "Информация о кошельке",
|
||||||
"settings_wallet_location": "Расположение кошелька: %s",
|
"settings_wallet_location": "Расположение кошелька: %s",
|
||||||
"settings_wallet_maintenance": "Обслуживание кошелька",
|
"settings_wallet_maintenance": "Обслуживание кошелька",
|
||||||
"settings_wallet_not_found": "Файл кошелька не найден",
|
"settings_wallet_not_found": "Файл кошелька не найден",
|
||||||
"settings_wallet_size_label": "Размер кошелька:",
|
"settings_wallet_size_label": "Размер кошелька",
|
||||||
|
"settings_ztx_cleared": "История Z-транзакций очищена",
|
||||||
|
"settings_ztx_not_found": "Файл истории не найден",
|
||||||
"setup_wizard": "Мастер настройки",
|
"setup_wizard": "Мастер настройки",
|
||||||
"share": "Поделиться",
|
"share": "Поделиться",
|
||||||
"shield_check_status": "Проверить статус",
|
"shield_check_status": "Проверить статус",
|
||||||
@@ -1292,6 +1461,23 @@
|
|||||||
"sweep_to": "Переведено на:",
|
"sweep_to": "Переведено на:",
|
||||||
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
|
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
|
||||||
"sweep_tx": "Транзакция:",
|
"sweep_tx": "Транзакция:",
|
||||||
|
"switch_corrupt_body": "Похоже, этот кошелёк повреждён — узел не смог его открыть. Восстановите из резервной копии, создайте заново или попробуйте восстановить.",
|
||||||
|
"switch_corrupt_repair": "Попробовать восстановить (salvage)",
|
||||||
|
"switch_progress_background": "Продолжить в фоне",
|
||||||
|
"switch_progress_default_wallet": "Кошелёк по умолчанию",
|
||||||
|
"switch_progress_elapsed": "Прошло",
|
||||||
|
"switch_progress_external_wallet": "Внешний кошелёк",
|
||||||
|
"switch_progress_failed_title": "Не удалось переключить кошелёк",
|
||||||
|
"switch_progress_from_label": "из",
|
||||||
|
"switch_progress_hint": "Корректное завершение работы может занять до минуты.",
|
||||||
|
"switch_progress_reconnecting": "Переподключение",
|
||||||
|
"switch_progress_starting": "Запуск узла с новым кошельком",
|
||||||
|
"switch_progress_stopping": "Остановка текущего узла",
|
||||||
|
"switch_progress_title": "Переключение кошелька",
|
||||||
|
"switch_stopnode_body": "Смена кошелька перезапускает узел с выбранным кошельком. Запущенный узел будет остановлен и перезапущен с новым кошельком — если вы намеренно оставили его работать, он запустится снова автоматически.",
|
||||||
|
"switch_stopnode_confirm": "Остановить узел и переключить",
|
||||||
|
"switch_stopnode_title": "Остановить запущенный узел?",
|
||||||
|
"switch_stopnode_warn": "Уже запущен узел, который не был запущен этим кошельком.",
|
||||||
"syncing": "Синхронизация...",
|
"syncing": "Синхронизация...",
|
||||||
"t_address": "T-адрес",
|
"t_address": "T-адрес",
|
||||||
"t_addresses": "T-адреса",
|
"t_addresses": "T-адреса",
|
||||||
@@ -1299,6 +1485,7 @@
|
|||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"theme_effects": "Эффекты темы",
|
"theme_effects": "Эффекты темы",
|
||||||
"theme_language": "ТЕМА И ЯЗЫК",
|
"theme_language": "ТЕМА И ЯЗЫК",
|
||||||
|
"tile_click_to_open": "Нажмите, чтобы открыть",
|
||||||
"time_days_ago": "%d дней назад",
|
"time_days_ago": "%d дней назад",
|
||||||
"time_hours_ago": "%d часов назад",
|
"time_hours_ago": "%d часов назад",
|
||||||
"time_minutes_ago": "%d минут назад",
|
"time_minutes_ago": "%d минут назад",
|
||||||
@@ -1313,7 +1500,9 @@
|
|||||||
"to_upper": "КОМУ",
|
"to_upper": "КОМУ",
|
||||||
"tools": "УТИЛИТЫ",
|
"tools": "УТИЛИТЫ",
|
||||||
"tools_actions": "Инструменты и действия...",
|
"tools_actions": "Инструменты и действия...",
|
||||||
|
"tools_actions_hdr": "ИНСТРУМЕНТЫ И ДЕЙСТВИЯ",
|
||||||
"total": "Итого",
|
"total": "Итого",
|
||||||
|
"total_balance_label": "Общий баланс",
|
||||||
"transaction_id": "ID ТРАНЗАКЦИИ",
|
"transaction_id": "ID ТРАНЗАКЦИИ",
|
||||||
"transaction_sent": "Транзакция успешно отправлена",
|
"transaction_sent": "Транзакция успешно отправлена",
|
||||||
"transaction_sent_msg": "Транзакция отправлена!",
|
"transaction_sent_msg": "Транзакция отправлена!",
|
||||||
@@ -1335,13 +1524,24 @@
|
|||||||
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
|
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
|
||||||
"tt_backup": "Создать резервную копию вашего wallet.dat",
|
"tt_backup": "Создать резервную копию вашего wallet.dat",
|
||||||
"tt_block_explorer": "Открыть обозреватель блоков DragonX в браузере",
|
"tt_block_explorer": "Открыть обозреватель блоков DragonX в браузере",
|
||||||
"tt_blur": "Степень размытия (0%% = выкл., 100%% = максимум)",
|
"tt_blur": "Степень размытия (0% = выкл., 100% = максимум)",
|
||||||
"tt_change_pass": "Сменить пароль шифрования кошелька",
|
"tt_change_pass": "Сменить пароль шифрования кошелька",
|
||||||
"tt_change_pin": "Изменить PIN-код разблокировки",
|
"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_clear_ztx": "Удалить локально кешированную историю z-транзакций",
|
||||||
|
"tt_clock_format": "24- или 12-часовой формат для всего приложения. Чат может переопределить.",
|
||||||
|
"tt_copy_diagnostics": "Копирует сводку для поддержки (версия, состояние демона/кошелька/журналов — без секретов) в буфер обмена",
|
||||||
"tt_custom_fees": "Включить ручной ввод комиссий при отправке транзакций",
|
"tt_custom_fees": "Включить ручной ввод комиссий при отправке транзакций",
|
||||||
"tt_custom_theme": "Пользовательская тема активна",
|
"tt_custom_theme": "Пользовательская тема активна",
|
||||||
"tt_daemon_install_bundled": "Остановить узел, перезаписать установленный dragonxd версией, встроенной в эту сборку кошелька, затем перезапустить",
|
"tt_daemon_install_bundled": "Остановить узел, перезаписать установленный dragonxd версией, встроенной в эту сборку кошелька, затем перезапустить",
|
||||||
|
"tt_daemon_refresh": "Перечитать версию, размер и дату установленного и встроенного dragonxd, показанные выше",
|
||||||
"tt_daemon_update_check": "Скачать и проверить последний полный узел dragonxd из проектного Gitea, затем перезапустить для применения",
|
"tt_daemon_update_check": "Скачать и проверить последний полный узел dragonxd из проектного Gitea, затем перезапустить для применения",
|
||||||
"tt_debug_collapse": "Свернуть параметры журнала отладки",
|
"tt_debug_collapse": "Свернуть параметры журнала отладки",
|
||||||
"tt_debug_expand": "Развернуть параметры журнала отладки",
|
"tt_debug_expand": "Развернуть параметры журнала отладки",
|
||||||
@@ -1359,15 +1559,39 @@
|
|||||||
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
||||||
"tt_language": "Язык интерфейса кошелька",
|
"tt_language": "Язык интерфейса кошелька",
|
||||||
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",
|
"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_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_lock": "Немедленно заблокировать кошелёк",
|
||||||
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
|
"tt_low_spec": "Отключить все тяжёлые визуальные эффекты\\nГорячая клавиша: Ctrl+Shift+Down",
|
||||||
"tt_merge": "Объединить несколько UTXO в один адрес",
|
"tt_merge": "Объединить несколько UTXO в один адрес",
|
||||||
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
|
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
|
||||||
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)",
|
"tt_noise": "Интенсивность зернистой текстуры (0% = выкл., 100% = максимум)",
|
||||||
"tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере",
|
"tt_open_app_dir": "Открыть папку ObsidianDragon (настройки, темы, логи) в файловом менеджере",
|
||||||
"tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна",
|
"tt_open_data_dir": "Открыть в файловом менеджере папку с данными кошелька и блокчейна",
|
||||||
"tt_open_dir": "Нажмите, чтобы открыть в проводнике",
|
"tt_open_dir": "Нажмите, чтобы открыть в проводнике",
|
||||||
|
"tt_open_log_folder": "Открывает папку с журналами отладки и сбоев",
|
||||||
"tt_reduce_motion": "Отключить анимированные переходы и плавное изменение баланса для доступности",
|
"tt_reduce_motion": "Отключить анимированные переходы и плавное изменение баланса для доступности",
|
||||||
"tt_remove_encrypt": "Удалить шифрование и хранить кошелёк без защиты",
|
"tt_remove_encrypt": "Удалить шифрование и хранить кошелёк без защиты",
|
||||||
"tt_remove_pin": "Удалить PIN и требовать пароль для разблокировки",
|
"tt_remove_pin": "Удалить PIN и требовать пароль для разблокировки",
|
||||||
@@ -1380,12 +1604,17 @@
|
|||||||
"tt_rpc_host": "Имя хоста демона DragonX",
|
"tt_rpc_host": "Имя хоста демона DragonX",
|
||||||
"tt_rpc_pass": "Пароль аутентификации RPC",
|
"tt_rpc_pass": "Пароль аутентификации RPC",
|
||||||
"tt_rpc_port": "Порт для RPC-подключений демона",
|
"tt_rpc_port": "Порт для RPC-подключений демона",
|
||||||
|
"tt_rpc_toggle": "Показать или скрыть параметры RPC-подключения только для чтения (хост, порт, пользователь, пароль) для демона",
|
||||||
"tt_rpc_user": "Имя пользователя аутентификации RPC",
|
"tt_rpc_user": "Имя пользователя аутентификации RPC",
|
||||||
"tt_save_settings": "Сохранить все настройки на диск",
|
"tt_save_settings": "Сохранить все настройки на диск",
|
||||||
"tt_save_ztx": "Хранить историю транзакций z-адреса локально для более быстрой загрузки",
|
"tt_save_ztx": "Хранить историю транзакций z-адреса локально для более быстрой загрузки",
|
||||||
"tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s",
|
"tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s",
|
||||||
"tt_scanline": "Эффект развёртки ЭЛТ в консоли",
|
"tt_scanline": "Эффект развёртки ЭЛТ в консоли",
|
||||||
|
"tt_screenshot_open_dir": "Открыть папку скриншотов (в каталоге конфигурации) в вашем файловом менеджере",
|
||||||
|
"tt_screenshot_sweep": "Перебрать каждую тему по всем вкладкам, сохраняя скриншот каждой в папку скриншотов конфигурации (перезаписывает предыдущий проход)",
|
||||||
|
"tt_screenshot_sweep_full": "Как проход по темам, но также захватывает каждое модальное окно / диалог / поток, используя временные офлайн-данные демонстрационного кошелька",
|
||||||
"tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов",
|
"tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов",
|
||||||
|
"tt_seed_demo_chat": "Добавить примеры переписок во вкладку «Чат», чтобы проход захватил её интерфейс; только в памяти, исчезает при перезапуске",
|
||||||
"tt_seed_migrate": "Создать новый кошелёк с сид-фразой и перевести в него ваши средства",
|
"tt_seed_migrate": "Создать новый кошелёк с сид-фразой и перевести в него ваши средства",
|
||||||
"tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки",
|
"tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки",
|
||||||
"tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес",
|
"tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес",
|
||||||
@@ -1397,13 +1626,14 @@
|
|||||||
"tt_theme_hotkey": "Горячая клавиша: Ctrl+Влево/Вправо для переключения тем",
|
"tt_theme_hotkey": "Горячая клавиша: Ctrl+Влево/Вправо для переключения тем",
|
||||||
"tt_tor": "Маршрутизировать подключения демона через сеть Tor для анонимности",
|
"tt_tor": "Маршрутизировать подключения демона через сеть Tor для анонимности",
|
||||||
"tt_tx_url": "Базовый URL для просмотра транзакций в обозревателе блоков",
|
"tt_tx_url": "Базовый URL для просмотра транзакций в обозревателе блоков",
|
||||||
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100%% = полностью непрозрачно, ниже = прозрачнее)",
|
"tt_ui_opacity": "Непрозрачность карточек и боковой панели (100% = полностью непрозрачно, ниже = прозрачнее)",
|
||||||
"tt_validate": "Проверить, действителен ли адрес DragonX",
|
"tt_validate": "Проверить, действителен ли адрес DragonX",
|
||||||
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
|
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
|
||||||
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
|
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
|
||||||
"tt_website": "Открыть сайт DragonX",
|
"tt_website": "Открыть сайт DragonX",
|
||||||
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
||||||
"tt_wizard": "Повторно запустить мастер начальной настройки\\nДемон будет перезапущен",
|
"tt_wizard": "Повторно запустить мастер начальной настройки\\nДемон будет перезапущен",
|
||||||
|
"tx_chat_badge": "Сообщение",
|
||||||
"tx_confirmations": "%d подтверждений",
|
"tx_confirmations": "%d подтверждений",
|
||||||
"tx_details_title": "Детали транзакции",
|
"tx_details_title": "Детали транзакции",
|
||||||
"tx_from_address": "Адрес отправителя:",
|
"tx_from_address": "Адрес отправителя:",
|
||||||
@@ -1449,6 +1679,7 @@
|
|||||||
"validate_not_mine": "Не принадлежит этому кошельку",
|
"validate_not_mine": "Не принадлежит этому кошельку",
|
||||||
"validate_ownership": "Принадлежность:",
|
"validate_ownership": "Принадлежность:",
|
||||||
"validate_results": "Результаты:",
|
"validate_results": "Результаты:",
|
||||||
|
"validate_results_placeholder": "Результаты появятся здесь",
|
||||||
"validate_shielded_type": "Экранированный (z-адрес)",
|
"validate_shielded_type": "Экранированный (z-адрес)",
|
||||||
"validate_status": "Статус:",
|
"validate_status": "Статус:",
|
||||||
"validate_title": "Проверить адрес",
|
"validate_title": "Проверить адрес",
|
||||||
@@ -1472,6 +1703,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ Искать кошельки в другой папке…",
|
"wallets_add_folder_toggle": "+ Искать кошельки в другой папке…",
|
||||||
"wallets_badge_encrypted": "Зашифрован (защищён паролем)",
|
"wallets_badge_encrypted": "Зашифрован (защищён паролем)",
|
||||||
"wallets_badge_encrypted_short": "Зашифрован",
|
"wallets_badge_encrypted_short": "Зашифрован",
|
||||||
|
"wallets_badge_hd": "HD-кошелёк — невозможно подтвердить seed-фразу без открытия",
|
||||||
|
"wallets_badge_hd_short": "HD-кошелёк",
|
||||||
"wallets_badge_legacy": "Устаревший кошелёк (без seed-фразы)",
|
"wallets_badge_legacy": "Устаревший кошелёк (без seed-фразы)",
|
||||||
"wallets_badge_legacy_short": "Устаревший",
|
"wallets_badge_legacy_short": "Устаревший",
|
||||||
"wallets_badge_seed": "Кошелёк с seed-фразой (HD)",
|
"wallets_badge_seed": "Кошелёк с seed-фразой (HD)",
|
||||||
@@ -1587,6 +1820,7 @@
|
|||||||
"xmrig_loading_releases": "Загрузка релизов…",
|
"xmrig_loading_releases": "Загрузка релизов…",
|
||||||
"xmrig_none": "нет",
|
"xmrig_none": "нет",
|
||||||
"xmrig_reinstall": "Переустановить",
|
"xmrig_reinstall": "Переустановить",
|
||||||
|
"xmrig_releases": "релизы xmrig",
|
||||||
"xmrig_stop_mining_first": "Остановите майнинг перед обновлением майнера.",
|
"xmrig_stop_mining_first": "Остановите майнинг перед обновлением майнера.",
|
||||||
"xmrig_unavailable_body": "Для этой платформы нет доступной сборки майнера.",
|
"xmrig_unavailable_body": "Для этой платформы нет доступной сборки майнера.",
|
||||||
"xmrig_unavailable_title": "Обновления майнера недоступны",
|
"xmrig_unavailable_title": "Обновления майнера недоступны",
|
||||||
|
|||||||
272
res/lang/zh.json
272
res/lang/zh.json
@@ -48,6 +48,10 @@
|
|||||||
"advanced": "高级",
|
"advanced": "高级",
|
||||||
"advanced_effects": "高级特效...",
|
"advanced_effects": "高级特效...",
|
||||||
"ago": "前",
|
"ago": "前",
|
||||||
|
"alerts_clear": "清除通知历史",
|
||||||
|
"alerts_history_tooltip": "最近通知",
|
||||||
|
"alerts_none": "暂无通知",
|
||||||
|
"alerts_recent": "最近通知",
|
||||||
"all_filter": "全部",
|
"all_filter": "全部",
|
||||||
"allow_custom_fees": "允许自定义手续费",
|
"allow_custom_fees": "允许自定义手续费",
|
||||||
"amount": "金额",
|
"amount": "金额",
|
||||||
@@ -70,6 +74,9 @@
|
|||||||
"av_title": "Windows Defender 已阻止矿工程序",
|
"av_title": "Windows Defender 已阻止矿工程序",
|
||||||
"available": "可用",
|
"available": "可用",
|
||||||
"backup_backing_up": "正在备份...",
|
"backup_backing_up": "正在备份...",
|
||||||
|
"backup_col_backup": "备份",
|
||||||
|
"backup_col_export": "导出",
|
||||||
|
"backup_col_import": "导入与恢复",
|
||||||
"backup_create": "创建备份",
|
"backup_create": "创建备份",
|
||||||
"backup_created": "钱包备份已创建",
|
"backup_created": "钱包备份已创建",
|
||||||
"backup_data": "备份与数据",
|
"backup_data": "备份与数据",
|
||||||
@@ -88,7 +95,10 @@
|
|||||||
"balance": "余额",
|
"balance": "余额",
|
||||||
"balance_history_collecting": "余额历史——正在收集数据…",
|
"balance_history_collecting": "余额历史——正在收集数据…",
|
||||||
"balance_layout": "余额布局",
|
"balance_layout": "余额布局",
|
||||||
|
"balance_layout_switched": "布局:%s",
|
||||||
|
"balance_mining_rate": "挖矿中 %s",
|
||||||
"balance_shielded_fmt": "屏蔽:%.8f",
|
"balance_shielded_fmt": "屏蔽:%.8f",
|
||||||
|
"balance_syncing_pct": "同步中 %.1f%%",
|
||||||
"balance_transparent_fmt": "透明:%.8f",
|
"balance_transparent_fmt": "透明:%.8f",
|
||||||
"ban": "封禁",
|
"ban": "封禁",
|
||||||
"banned_peers": "已封禁节点",
|
"banned_peers": "已封禁节点",
|
||||||
@@ -128,6 +138,7 @@
|
|||||||
"bootstrap_verifying": "正在验证校验和...",
|
"bootstrap_verifying": "正在验证校验和...",
|
||||||
"bootstrap_wallet_protected": "(wallet.dat 已受保护)",
|
"bootstrap_wallet_protected": "(wallet.dat 已受保护)",
|
||||||
"bootstrap_warning": "现有区块数据(blocks、chainstate、notarizations)将被删除并替换。您的 wallet.dat 不会被修改或删除。",
|
"bootstrap_warning": "现有区块数据(blocks、chainstate、notarizations)将被删除并替换。您的 wallet.dat 不会被修改或删除。",
|
||||||
|
"byte_count_fmt": "%zu / %zu 字节",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"change_pass_confirm": "确认新密码:",
|
"change_pass_confirm": "确认新密码:",
|
||||||
"change_pass_current": "当前密码短语:",
|
"change_pass_current": "当前密码短语:",
|
||||||
@@ -135,26 +146,99 @@
|
|||||||
"change_pass_title": "更改密码短语",
|
"change_pass_title": "更改密码短语",
|
||||||
"characters": "字符",
|
"characters": "字符",
|
||||||
"chat": "聊天",
|
"chat": "聊天",
|
||||||
|
"chat_accent_amber": "琥珀色",
|
||||||
|
"chat_accent_blue": "蓝色",
|
||||||
|
"chat_accent_green": "绿色",
|
||||||
|
"chat_accent_pink": "粉色",
|
||||||
|
"chat_accent_purple": "紫色",
|
||||||
|
"chat_accent_theme": "主题",
|
||||||
|
"chat_add_contact": "添加联系人",
|
||||||
|
"chat_awaiting_key": "等待回复",
|
||||||
|
"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_cancel": "取消",
|
||||||
|
"chat_contact_added": "已添加联系人——可在联系人中重命名",
|
||||||
"chat_contact_request": "联系人请求",
|
"chat_contact_request": "联系人请求",
|
||||||
|
"chat_copy_address_tip": "点击复制地址",
|
||||||
|
"chat_density_comfortable": "宽松",
|
||||||
|
"chat_density_compact": "紧凑",
|
||||||
|
"chat_emoji_color": "彩色",
|
||||||
|
"chat_emoji_mono": "单色",
|
||||||
|
"chat_emoji_search": "搜索表情",
|
||||||
"chat_empty_hint": "暂无对话。您收到的消息将显示在此处。",
|
"chat_empty_hint": "暂无对话。您收到的消息将显示在此处。",
|
||||||
|
"chat_empty_start": "点击\"新建会话\"开始。",
|
||||||
|
"chat_empty_title": "还没有会话",
|
||||||
|
"chat_export": "导出聊天…",
|
||||||
|
"chat_export_done": "会话已导出",
|
||||||
|
"chat_export_failed": "无法写入导出文件。",
|
||||||
|
"chat_export_warn": "将解密后的消息保存为纯文本。请妥善保管该文件。",
|
||||||
|
"chat_filter": "聊天",
|
||||||
|
"chat_hidden_toast": "会话已隐藏——收到新消息后会重新显示",
|
||||||
|
"chat_hide": "隐藏",
|
||||||
|
"chat_hide_hidden": "收起已隐藏",
|
||||||
|
"chat_jump_latest": "最新",
|
||||||
|
"chat_len_over": "消息过长",
|
||||||
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
|
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
|
||||||
"chat_new_button": "新建对话",
|
"chat_mute": "静音",
|
||||||
|
"chat_new_button": "新聊天",
|
||||||
"chat_new_message": "消息",
|
"chat_new_message": "消息",
|
||||||
|
"chat_new_message_toast": "新的加密聊天消息",
|
||||||
"chat_new_send": "发送请求",
|
"chat_new_send": "发送请求",
|
||||||
"chat_new_title": "新建对话",
|
"chat_new_title": "新聊天",
|
||||||
"chat_new_zaddr": "收款方 z 地址",
|
"chat_new_zaddr": "收款方 z 地址",
|
||||||
|
"chat_no_matches": "没有与搜索匹配的会话。",
|
||||||
|
"chat_no_z_contacts": "暂无使用隐私地址的联系人",
|
||||||
|
"chat_opt_bubble_accent": "气泡颜色",
|
||||||
|
"chat_opt_bubble_style": "气泡样式",
|
||||||
|
"chat_opt_density": "消息密度",
|
||||||
|
"chat_opt_emoji": "表情样式",
|
||||||
|
"chat_opt_enter_sends": "回车发送消息",
|
||||||
|
"chat_opt_font_size": "文字大小",
|
||||||
|
"chat_opt_global_clock": "全局时间格式",
|
||||||
|
"chat_opt_poll": "轮询频率",
|
||||||
|
"chat_opt_timestamp": "时间戳",
|
||||||
|
"chat_pick_contact": "从联系人中选择…",
|
||||||
|
"chat_rename": "重命名联系人",
|
||||||
|
"chat_rename_hint": "联系人名称",
|
||||||
|
"chat_renamed": "联系人已重命名",
|
||||||
|
"chat_retry": "重试",
|
||||||
|
"chat_search": "搜索会话",
|
||||||
|
"chat_sec_appearance": "外观",
|
||||||
|
"chat_sec_messaging": "消息",
|
||||||
"chat_select_hint": "选择一个对话以查看。",
|
"chat_select_hint": "选择一个对话以查看。",
|
||||||
"chat_send": "发送",
|
"chat_send": "发送",
|
||||||
"chat_send_failed": "未发送",
|
"chat_send_failed": "未发送",
|
||||||
|
"chat_sending": "发送中…",
|
||||||
|
"chat_settings_done": "完成",
|
||||||
|
"chat_settings_section": "聊天与联系人",
|
||||||
|
"chat_settings_tip": "聊天自定义",
|
||||||
|
"chat_settings_title": "聊天设置",
|
||||||
|
"chat_show_hidden": "显示已隐藏",
|
||||||
|
"chat_time_now": "刚刚",
|
||||||
"chat_toast_compose_failed": "无法编写该消息(内容过长?)。",
|
"chat_toast_compose_failed": "无法编写该消息(内容过长?)。",
|
||||||
"chat_toast_lite_busy": "已有发送正在进行中,或未打开任何钱包。",
|
"chat_toast_lite_busy": "已有发送正在进行中,或未打开任何钱包。",
|
||||||
|
"chat_toast_need_funds": "发送聊天需要少量屏蔽余额(用于支付手续费)。",
|
||||||
"chat_toast_no_zaddr": "没有可用于发送聊天的 z 地址。",
|
"chat_toast_no_zaddr": "没有可用于发送聊天的 z 地址。",
|
||||||
"chat_toast_not_connected": "未连接——聊天消息未发送。",
|
"chat_toast_not_connected": "未连接——聊天消息未发送。",
|
||||||
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
||||||
"chat_toast_request_queued": "联系人请求已排队。",
|
"chat_toast_request_queued": "联系人请求已排队。",
|
||||||
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||||
|
"chat_today": "今天",
|
||||||
|
"chat_ts_12h": "12小时",
|
||||||
|
"chat_ts_24h": "24小时",
|
||||||
|
"chat_ts_global": "跟随全局",
|
||||||
|
"chat_ts_global_short": "全局",
|
||||||
|
"chat_unhide": "取消隐藏",
|
||||||
|
"chat_unmute": "取消静音",
|
||||||
|
"chat_verify_key": "身份密钥 — 对比以验证",
|
||||||
"chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
"chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||||
|
"chat_yesterday": "昨天",
|
||||||
"chat_you": "我",
|
"chat_you": "我",
|
||||||
"choose_icon": "选择图标",
|
"choose_icon": "选择图标",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
@@ -166,6 +250,7 @@
|
|||||||
"click_copy_address": "点击复制地址",
|
"click_copy_address": "点击复制地址",
|
||||||
"click_copy_uri": "点击复制 URI",
|
"click_copy_uri": "点击复制 URI",
|
||||||
"click_to_copy": "点击复制",
|
"click_to_copy": "点击复制",
|
||||||
|
"clock_format": "时间格式",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"conf_count": "%d 确认",
|
"conf_count": "%d 确认",
|
||||||
"confirm_and_send": "确认并发送",
|
"confirm_and_send": "确认并发送",
|
||||||
@@ -203,12 +288,18 @@
|
|||||||
"console_app": "应用",
|
"console_app": "应用",
|
||||||
"console_auto_scroll": "自动滚动",
|
"console_auto_scroll": "自动滚动",
|
||||||
"console_available_commands": "可用命令:",
|
"console_available_commands": "可用命令:",
|
||||||
|
"console_backend_reference": "后端命令参考",
|
||||||
|
"console_backend_unavailable": "无后端",
|
||||||
"console_capturing_output": "正在捕获守护进程输出...",
|
"console_capturing_output": "正在捕获守护进程输出...",
|
||||||
|
"console_cat_advanced": "高级",
|
||||||
"console_cat_blockchain": "区块链",
|
"console_cat_blockchain": "区块链",
|
||||||
"console_cat_control": "控制",
|
"console_cat_control": "控制",
|
||||||
|
"console_cat_keys": "密钥与安全",
|
||||||
"console_cat_mining": "挖矿",
|
"console_cat_mining": "挖矿",
|
||||||
"console_cat_network": "网络",
|
"console_cat_network": "网络",
|
||||||
"console_cat_raw_transactions": "原始交易",
|
"console_cat_raw_transactions": "原始交易",
|
||||||
|
"console_cat_send": "发送",
|
||||||
|
"console_cat_sync": "同步",
|
||||||
"console_cat_utility": "实用工具",
|
"console_cat_utility": "实用工具",
|
||||||
"console_cat_wallet": "钱包",
|
"console_cat_wallet": "钱包",
|
||||||
"console_clear": "清除",
|
"console_clear": "清除",
|
||||||
@@ -242,11 +333,14 @@
|
|||||||
"console_help_help": " help - 显示此帮助信息",
|
"console_help_help": " help - 显示此帮助信息",
|
||||||
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||||
"console_help_stop": " stop - 停止守护进程",
|
"console_help_stop": " stop - 停止守护进程",
|
||||||
|
"console_last_error": "上次错误:",
|
||||||
"console_line_count": "%zu 行",
|
"console_line_count": "%zu 行",
|
||||||
"console_matches": "个匹配",
|
"console_matches": "个匹配",
|
||||||
"console_new_lines": "%d 新行",
|
"console_new_lines": "%d 新行",
|
||||||
"console_no_daemon": "无守护进程",
|
"console_no_daemon": "无守护进程",
|
||||||
|
"console_no_output": "(无输出)",
|
||||||
"console_not_connected": "错误:未连接到守护进程",
|
"console_not_connected": "错误:未连接到守护进程",
|
||||||
|
"console_not_connected_lite": "错误:没有打开的钱包",
|
||||||
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
||||||
"console_ref_builds": "生成",
|
"console_ref_builds": "生成",
|
||||||
"console_ref_cancel": "取消",
|
"console_ref_cancel": "取消",
|
||||||
@@ -262,12 +356,14 @@
|
|||||||
"console_ref_run_confirm": "立即运行 %s?这是一个有重大影响的命令。",
|
"console_ref_run_confirm": "立即运行 %s?这是一个有重大影响的命令。",
|
||||||
"console_ref_search_hint": "按名称或用途搜索…",
|
"console_ref_search_hint": "按名称或用途搜索…",
|
||||||
"console_ref_select_hint": "选择一个命令以查看其功能。",
|
"console_ref_select_hint": "选择一个命令以查看其功能。",
|
||||||
|
"console_ref_value": "值",
|
||||||
"console_rpc_reference": "RPC 命令参考",
|
"console_rpc_reference": "RPC 命令参考",
|
||||||
"console_rpc_trace": "RPC",
|
"console_rpc_trace": "RPC",
|
||||||
"console_scanline": "控制台扫描线",
|
"console_scanline": "控制台扫描线",
|
||||||
"console_search_commands": "搜索命令...",
|
"console_search_commands": "搜索命令...",
|
||||||
"console_select_all": "全选",
|
"console_select_all": "全选",
|
||||||
"console_show_app_output": "显示[应用]钱包日志行",
|
"console_show_app_output": "显示[应用]钱包日志行",
|
||||||
|
"console_show_backend_ref": "显示后端命令参考",
|
||||||
"console_show_daemon_output": "显示守护进程输出",
|
"console_show_daemon_output": "显示守护进程输出",
|
||||||
"console_show_errors_only": "仅显示错误",
|
"console_show_errors_only": "仅显示错误",
|
||||||
"console_show_rpc_ref": "显示 RPC 命令参考",
|
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||||
@@ -280,6 +376,7 @@
|
|||||||
"console_status_stopped": "已停止",
|
"console_status_stopped": "已停止",
|
||||||
"console_status_stopping": "停止中",
|
"console_status_stopping": "停止中",
|
||||||
"console_status_unknown": "未知",
|
"console_status_unknown": "未知",
|
||||||
|
"console_stop_confirm_node": "'stop' 将关闭节点并断开钱包连接。再次输入 'stop' 以确认。",
|
||||||
"console_tab_completion": "Tab 补全",
|
"console_tab_completion": "Tab 补全",
|
||||||
"console_text_colors": "文本颜色",
|
"console_text_colors": "文本颜色",
|
||||||
"console_toggle_accents": "切换行颜色强调",
|
"console_toggle_accents": "切换行颜色强调",
|
||||||
@@ -305,9 +402,17 @@
|
|||||||
"contact_global_tt": "开启:无论您加载哪个钱包,此联系人都保持可见。关闭:它仅属于当前钱包。",
|
"contact_global_tt": "开启:无论您加载哪个钱包,此联系人都保持可见。关闭:它仅属于当前钱包。",
|
||||||
"contact_preview_addr": "地址将显示在此处",
|
"contact_preview_addr": "地址将显示在此处",
|
||||||
"contact_preview_name": "联系人名称",
|
"contact_preview_name": "联系人名称",
|
||||||
|
"contact_wallet_loading": "钱包仍在加载——请选中“在每个钱包中显示”,或稍后再试。",
|
||||||
"contacts": "联系人",
|
"contacts": "联系人",
|
||||||
|
"contacts_avatar_shape": "头像形状",
|
||||||
|
"contacts_list_scale": "列表缩放",
|
||||||
"contacts_search_no_match": "没有匹配的联系人",
|
"contacts_search_no_match": "没有匹配的联系人",
|
||||||
"contacts_search_placeholder": "搜索联系人...",
|
"contacts_search_placeholder": "搜索联系人...",
|
||||||
|
"contacts_settings_tip": "联系人自定义",
|
||||||
|
"contacts_settings_title": "联系人设置",
|
||||||
|
"contacts_shape_circle": "圆形",
|
||||||
|
"contacts_shape_square": "方形",
|
||||||
|
"contacts_shape_tab": "左标签",
|
||||||
"copied": "已复制!",
|
"copied": "已复制!",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"copy_address": "复制完整地址",
|
"copy_address": "复制完整地址",
|
||||||
@@ -321,6 +426,7 @@
|
|||||||
"daemon_bundled": "内置",
|
"daemon_bundled": "内置",
|
||||||
"daemon_install_bundled": "安装内置版本",
|
"daemon_install_bundled": "安装内置版本",
|
||||||
"daemon_installed": "已安装",
|
"daemon_installed": "已安装",
|
||||||
|
"daemon_maintenance_label": "维护",
|
||||||
"daemon_none_bundled": "此版本未内置",
|
"daemon_none_bundled": "此版本未内置",
|
||||||
"daemon_not_installed": "未安装",
|
"daemon_not_installed": "未安装",
|
||||||
"daemon_status_differ": "已安装的程序文件与内置版本不同。",
|
"daemon_status_differ": "已安装的程序文件与内置版本不同。",
|
||||||
@@ -343,6 +449,7 @@
|
|||||||
"daemon_update_latest": "最新:",
|
"daemon_update_latest": "最新:",
|
||||||
"daemon_update_loading": "正在加载版本…",
|
"daemon_update_loading": "正在加载版本…",
|
||||||
"daemon_update_now": "立即更新",
|
"daemon_update_now": "立即更新",
|
||||||
|
"daemon_update_prompt_title": "是否更新节点守护进程?",
|
||||||
"daemon_update_reinstall": "重新安装",
|
"daemon_update_reinstall": "重新安装",
|
||||||
"daemon_update_restart_note": "重启守护进程以开始运行新版本。",
|
"daemon_update_restart_note": "重启守护进程以开始运行新版本。",
|
||||||
"daemon_update_restart_now": "立即重启守护进程",
|
"daemon_update_restart_now": "立即重启守护进程",
|
||||||
@@ -355,8 +462,11 @@
|
|||||||
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
|
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
|
||||||
"daemon_update_verifying": "正在验证…",
|
"daemon_update_verifying": "正在验证…",
|
||||||
"daemon_update_version": "版本:",
|
"daemon_update_version": "版本:",
|
||||||
|
"daemon_updates_label": "更新",
|
||||||
"daemon_version": "守护进程",
|
"daemon_version": "守护进程",
|
||||||
"dark": "深色",
|
"dark": "深色",
|
||||||
|
"data_stale_prefix": "更新于",
|
||||||
|
"data_stale_tooltip": "余额可能已过时 — 钱包最近未收到更新。请检查您的节点连接。",
|
||||||
"date": "日期",
|
"date": "日期",
|
||||||
"date_label": "日期:",
|
"date_label": "日期:",
|
||||||
"debug_logging": "调试日志",
|
"debug_logging": "调试日志",
|
||||||
@@ -385,6 +495,17 @@
|
|||||||
"download_bootstrap": "下载引导程序",
|
"download_bootstrap": "下载引导程序",
|
||||||
"dragonx_green": "DragonX(绿色)",
|
"dragonx_green": "DragonX(绿色)",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
|
"empty_wallet_keys_suffix": "个密钥",
|
||||||
|
"empty_wallet_open_manager": "打开钱包管理器",
|
||||||
|
"empty_wallet_restore": "恢复我的钱包",
|
||||||
|
"empty_wallet_salvage_body": "此钱包为空,因为先前的一次自动修复已将您的原始钱包作为备份保存到一旁。您的币几乎肯定在该备份中,并未丢失。恢复它即可重新加载您的资金——不会删除任何内容;当前文件会先被保存到一旁。",
|
||||||
|
"empty_wallet_salvage_headline": "您的币安全地存放在备份文件中。",
|
||||||
|
"empty_wallet_salvage_title": "您的钱包可能已被修复",
|
||||||
|
"empty_wallet_warning_body": "此钱包没有地址也没有资金,但您的 DragonX 文件夹中的另一个钱包文件包含密钥。您的币很可能在其中,并未丢失。打开钱包管理器以切换到持有您资金的钱包。",
|
||||||
|
"empty_wallet_warning_dismiss": "不再为此钱包提示",
|
||||||
|
"empty_wallet_warning_dismiss_tip": "仅对当前钱包文件停止此提示。如果您以后切换到另一个空钱包,可能会再次提示。",
|
||||||
|
"empty_wallet_warning_headline": "您可能打开了错误的钱包。",
|
||||||
|
"empty_wallet_warning_title": "此钱包为空",
|
||||||
"enc_confirm": "确认:",
|
"enc_confirm": "确认:",
|
||||||
"enc_desc": "加密钱包会用密码短语保护您的私钥。加密后,守护进程将重新启动。",
|
"enc_desc": "加密钱包会用密码短语保护您的私钥。加密后,守护进程将重新启动。",
|
||||||
"enc_encrypting": "正在加密钱包...",
|
"enc_encrypting": "正在加密钱包...",
|
||||||
@@ -546,15 +667,20 @@
|
|||||||
"light": "浅色",
|
"light": "浅色",
|
||||||
"lite_account_label": "账户",
|
"lite_account_label": "账户",
|
||||||
"lite_action": "操作",
|
"lite_action": "操作",
|
||||||
|
"lite_backend_unavailable": "轻量钱包后端不可用",
|
||||||
"lite_backup_keys": "备份与密钥",
|
"lite_backup_keys": "备份与密钥",
|
||||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||||
"lite_birthday_label": "诞生区块",
|
"lite_birthday_label": "诞生区块",
|
||||||
|
"lite_console_backend_commands": "后端命令:",
|
||||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||||
"lite_copy": "复制",
|
"lite_copy": "复制",
|
||||||
|
"lite_could_not_start": "无法启动该操作",
|
||||||
"lite_could_not_write": "无法写入 ",
|
"lite_could_not_write": "无法写入 ",
|
||||||
"lite_encrypt_wallet": "加密钱包",
|
"lite_encrypt_wallet": "加密钱包",
|
||||||
"lite_encryption_removed": "已移除加密",
|
"lite_encryption_removed": "已移除加密",
|
||||||
|
"lite_enter_all_seed_words": "请输入全部 24 个助记词以恢复(已输入 %d 个)",
|
||||||
|
"lite_enter_wallet_path": "请输入钱包路径",
|
||||||
"lite_hide_wipe": "隐藏并清除",
|
"lite_hide_wipe": "隐藏并清除",
|
||||||
"lite_import": "导入",
|
"lite_import": "导入",
|
||||||
"lite_import_key_label": "导入密钥",
|
"lite_import_key_label": "导入密钥",
|
||||||
@@ -567,6 +693,7 @@
|
|||||||
"lite_net_add_url_hint": "https://your-lite-server",
|
"lite_net_add_url_hint": "https://your-lite-server",
|
||||||
"lite_net_checking": "检查中…",
|
"lite_net_checking": "检查中…",
|
||||||
"lite_net_connected": "已连接",
|
"lite_net_connected": "已连接",
|
||||||
|
"lite_net_connecting": "连接中…",
|
||||||
"lite_net_custom": "自定义",
|
"lite_net_custom": "自定义",
|
||||||
"lite_net_disconnected": "未连接",
|
"lite_net_disconnected": "未连接",
|
||||||
"lite_net_hidden_section": "隐藏的服务器",
|
"lite_net_hidden_section": "隐藏的服务器",
|
||||||
@@ -638,6 +765,8 @@
|
|||||||
"lite_working": "处理中…",
|
"lite_working": "处理中…",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"loading_addresses": "正在加载地址...",
|
"loading_addresses": "正在加载地址...",
|
||||||
|
"loading_stall_body": "守护进程已初始化 %.0f 秒。更新后或首次启动时(加载区块索引或重新扫描)这可能是正常现象——就绪后会自动连接。",
|
||||||
|
"loading_stall_title": "耗时超出预期",
|
||||||
"loading_transactions": "正在加载交易",
|
"loading_transactions": "正在加载交易",
|
||||||
"local_hashrate": "本地算力",
|
"local_hashrate": "本地算力",
|
||||||
"low_spec_mode": "低配模式",
|
"low_spec_mode": "低配模式",
|
||||||
@@ -654,6 +783,9 @@
|
|||||||
"market_cap": "市值",
|
"market_cap": "市值",
|
||||||
"market_cap_short": "市值",
|
"market_cap_short": "市值",
|
||||||
"market_chart_loading": "正在加载价格历史",
|
"market_chart_loading": "正在加载价格历史",
|
||||||
|
"market_col_name": "名称",
|
||||||
|
"market_col_trend": "趋势",
|
||||||
|
"market_col_value": "价值",
|
||||||
"market_iv_1d": "1天",
|
"market_iv_1d": "1天",
|
||||||
"market_iv_1h": "1时",
|
"market_iv_1h": "1时",
|
||||||
"market_iv_1m": "1M",
|
"market_iv_1m": "1M",
|
||||||
@@ -662,13 +794,18 @@
|
|||||||
"market_no_history": "无价格历史",
|
"market_no_history": "无价格历史",
|
||||||
"market_no_price": "无价格数据",
|
"market_no_price": "无价格数据",
|
||||||
"market_now": "现在",
|
"market_now": "现在",
|
||||||
|
"market_opt_chart_style": "图表样式",
|
||||||
"market_pct_shielded": "%.0f%% 屏蔽",
|
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||||
"market_portfolio": "投资组合",
|
"market_portfolio": "投资组合",
|
||||||
"market_price_loading": "正在加载价格数据...",
|
"market_price_loading": "正在加载价格数据...",
|
||||||
"market_price_unavailable": "价格数据不可用",
|
"market_price_unavailable": "价格数据不可用",
|
||||||
"market_refresh_price": "刷新价格数据",
|
"market_refresh_price": "刷新价格数据",
|
||||||
|
"market_settings_tip": "市场选项",
|
||||||
|
"market_settings_title": "市场设置",
|
||||||
"market_style_candle": "切换到蜡烛图",
|
"market_style_candle": "切换到蜡烛图",
|
||||||
|
"market_style_candle_label": "K线",
|
||||||
"market_style_line": "切换到折线图",
|
"market_style_line": "切换到折线图",
|
||||||
|
"market_style_line_label": "折线",
|
||||||
"market_trade_on": "在 %s 交易",
|
"market_trade_on": "在 %s 交易",
|
||||||
"market_updated": "\\xc2\\xb7 已更新 %s",
|
"market_updated": "\\xc2\\xb7 已更新 %s",
|
||||||
"market_vol_short": "成交量",
|
"market_vol_short": "成交量",
|
||||||
@@ -762,6 +899,7 @@
|
|||||||
"mining_difficulty_copied": "难度已复制",
|
"mining_difficulty_copied": "难度已复制",
|
||||||
"mining_est_block": "预计区块",
|
"mining_est_block": "预计区块",
|
||||||
"mining_est_daily": "预计日收益",
|
"mining_est_daily": "预计日收益",
|
||||||
|
"mining_est_daily_pool_sub": "粗略的单人挖矿等值,扣除矿池费用前",
|
||||||
"mining_filter_all": "全部",
|
"mining_filter_all": "全部",
|
||||||
"mining_filter_tip_all": "显示所有收益",
|
"mining_filter_tip_all": "显示所有收益",
|
||||||
"mining_filter_tip_pool": "仅显示矿池收益",
|
"mining_filter_tip_pool": "仅显示矿池收益",
|
||||||
@@ -790,10 +928,12 @@
|
|||||||
"mining_open_in_explorer": "在浏览器中打开",
|
"mining_open_in_explorer": "在浏览器中打开",
|
||||||
"mining_payout_address": "支付地址",
|
"mining_payout_address": "支付地址",
|
||||||
"mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。",
|
"mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。",
|
||||||
|
"mining_payout_invalid": "不是有效的 DragonX 地址——启动前请更正,否则挖矿奖励将丢失。",
|
||||||
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
||||||
"mining_pool": "矿池",
|
"mining_pool": "矿池",
|
||||||
"mining_pool_fee": "费用",
|
"mining_pool_fee": "费用",
|
||||||
"mining_pool_hashrate": "矿池算力",
|
"mining_pool_hashrate": "矿池算力",
|
||||||
|
"mining_pool_needs_payout_tooltip": "请先输入收款地址(生成一个 Z 地址)",
|
||||||
"mining_pool_url": "矿池 URL",
|
"mining_pool_url": "矿池 URL",
|
||||||
"mining_pools_header": "矿池",
|
"mining_pools_header": "矿池",
|
||||||
"mining_recent_blocks": "最近区块",
|
"mining_recent_blocks": "最近区块",
|
||||||
@@ -823,6 +963,9 @@
|
|||||||
"mining_syncing_tooltip": "区块链同步中...",
|
"mining_syncing_tooltip": "区块链同步中...",
|
||||||
"mining_tag": " · 挖矿",
|
"mining_tag": " · 挖矿",
|
||||||
"mining_threads": "挖矿线程",
|
"mining_threads": "挖矿线程",
|
||||||
|
"mining_threads_input_tooltip": "输入精确的线程数(按 Enter 应用)",
|
||||||
|
"mining_threads_minus_tooltip": "减少线程",
|
||||||
|
"mining_threads_plus_tooltip": "增加线程",
|
||||||
"mining_to_save": "保存",
|
"mining_to_save": "保存",
|
||||||
"mining_today": "今天",
|
"mining_today": "今天",
|
||||||
"mining_uptime": "运行时间",
|
"mining_uptime": "运行时间",
|
||||||
@@ -849,6 +992,11 @@
|
|||||||
"no_transactions": "未找到交易",
|
"no_transactions": "未找到交易",
|
||||||
"no_transactions_yet": "尚无交易",
|
"no_transactions_yet": "尚无交易",
|
||||||
"node": "节点",
|
"node": "节点",
|
||||||
|
"node_banner_crashed_title": "节点意外停止",
|
||||||
|
"node_banner_lite_open_failed": "无法打开您的钱包",
|
||||||
|
"node_banner_offline_title": "未连接到 DragonX 节点",
|
||||||
|
"node_banner_reconnect": "重新连接",
|
||||||
|
"node_banner_restart": "重启节点",
|
||||||
"node_security": "节点与安全",
|
"node_security": "节点与安全",
|
||||||
"noise": "噪点",
|
"noise": "噪点",
|
||||||
"not_connected": "未连接到守护进程...",
|
"not_connected": "未连接到守护进程...",
|
||||||
@@ -972,11 +1120,12 @@
|
|||||||
"portfolio_spark_min": "分钟",
|
"portfolio_spark_min": "分钟",
|
||||||
"portfolio_spark_month": "月",
|
"portfolio_spark_month": "月",
|
||||||
"portfolio_spark_week": "周",
|
"portfolio_spark_week": "周",
|
||||||
"portfolio_style_compact": "紧凑行",
|
"portfolio_style_compact": "表格",
|
||||||
"portfolio_style_detailed": "详细行",
|
"portfolio_style_detailed": "卡片",
|
||||||
"portfolio_style_featured": "特色行",
|
"portfolio_style_featured": "聚焦",
|
||||||
"portfolio_style_label": "投资组合样式",
|
"portfolio_style_label": "投资组合样式",
|
||||||
"portfolio_untitled": "未命名",
|
"portfolio_untitled": "未命名",
|
||||||
|
"portfolio_wallet_loading": "请等待钱包加载完成后再添加分组。",
|
||||||
"price_chart": "价格图表",
|
"price_chart": "价格图表",
|
||||||
"privacy_great": "隐私性极佳!",
|
"privacy_great": "隐私性极佳!",
|
||||||
"privacy_low": "隐私性低——请屏蔽资金",
|
"privacy_low": "隐私性低——请屏蔽资金",
|
||||||
@@ -986,6 +1135,8 @@
|
|||||||
"qr_failed": "无法生成二维码",
|
"qr_failed": "无法生成二维码",
|
||||||
"qr_title": "二维码",
|
"qr_title": "二维码",
|
||||||
"qr_unavailable": "二维码不可用",
|
"qr_unavailable": "二维码不可用",
|
||||||
|
"quick_receive": "快速接收",
|
||||||
|
"quick_send": "快速发送",
|
||||||
"ram_daemon_gb": "守护进程:%.1f GB (%s)",
|
"ram_daemon_gb": "守护进程:%.1f GB (%s)",
|
||||||
"ram_daemon_mb": "守护进程:%.0f MB (%s)",
|
"ram_daemon_mb": "守护进程:%.0f MB (%s)",
|
||||||
"ram_system_gb": "系统:%.1f / %.0f GB",
|
"ram_system_gb": "系统:%.1f / %.0f GB",
|
||||||
@@ -1035,6 +1186,7 @@
|
|||||||
"rpc_connection": "RPC 连接...",
|
"rpc_connection": "RPC 连接...",
|
||||||
"rpc_host": "RPC 主机",
|
"rpc_host": "RPC 主机",
|
||||||
"rpc_pass": "密码",
|
"rpc_pass": "密码",
|
||||||
|
"rpc_plaintext_remote_warning": "远程 RPC 正在使用明文 HTTP。如果您的守护进程支持 TLS,请在 DRAGONX.conf 中添加 rpctls=1。",
|
||||||
"rpc_port": "端口",
|
"rpc_port": "端口",
|
||||||
"rpc_user": "用户名",
|
"rpc_user": "用户名",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
@@ -1049,6 +1201,8 @@
|
|||||||
"sb_connecting_external": "正在连接外部守护进程...",
|
"sb_connecting_external": "正在连接外部守护进程...",
|
||||||
"sb_connecting_generic": "正在连接守护进程...",
|
"sb_connecting_generic": "正在连接守护进程...",
|
||||||
"sb_daemon_crashed": "守护进程崩溃 %d 次",
|
"sb_daemon_crashed": "守护进程崩溃 %d 次",
|
||||||
|
"sb_daemon_extract_failed": "无法写入守护进程文件——请检查磁盘剩余空间和权限。",
|
||||||
|
"sb_daemon_files_failed": "无法将守护进程文件写入 %s——请检查磁盘剩余空间和权限。",
|
||||||
"sb_daemon_not_found": "未找到守护进程",
|
"sb_daemon_not_found": "未找到守护进程",
|
||||||
"sb_daemon_start_failed": "无法启动 dragonxd",
|
"sb_daemon_start_failed": "无法启动 dragonxd",
|
||||||
"sb_dragonxd_running": "dragonxd 运行中",
|
"sb_dragonxd_running": "dragonxd 运行中",
|
||||||
@@ -1077,6 +1231,7 @@
|
|||||||
"sb_waiting_daemon_err": "等待 dragonxd — %s",
|
"sb_waiting_daemon_err": "等待 dragonxd — %s",
|
||||||
"sb_warming_up": "正在预热...",
|
"sb_warming_up": "正在预热...",
|
||||||
"sb_witness_cache": "正在重建见证",
|
"sb_witness_cache": "正在重建见证",
|
||||||
|
"scale_effects": "缩放与效果",
|
||||||
"screenshot_open_dir": "打开位置",
|
"screenshot_open_dir": "打开位置",
|
||||||
"screenshot_sweep": "运行截图批处理",
|
"screenshot_sweep": "运行截图批处理",
|
||||||
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
|
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
|
||||||
@@ -1141,6 +1296,7 @@
|
|||||||
"send_tooltip_not_connected": "未连接到守护进程",
|
"send_tooltip_not_connected": "未连接到守护进程",
|
||||||
"send_tooltip_select_source": "请先选择来源地址",
|
"send_tooltip_select_source": "请先选择来源地址",
|
||||||
"send_tooltip_syncing": "请等待区块链同步",
|
"send_tooltip_syncing": "请等待区块链同步",
|
||||||
|
"send_tooltip_view_only": "仅查看地址 — 无花费密钥,无法发送",
|
||||||
"send_total": "合计",
|
"send_total": "合计",
|
||||||
"send_transaction": "发送交易",
|
"send_transaction": "发送交易",
|
||||||
"send_tx_failed": "交易失败",
|
"send_tx_failed": "交易失败",
|
||||||
@@ -1160,16 +1316,16 @@
|
|||||||
"sent_filter": "已发送",
|
"sent_filter": "已发送",
|
||||||
"sent_type": "已发送",
|
"sent_type": "已发送",
|
||||||
"sent_upper": "已发送",
|
"sent_upper": "已发送",
|
||||||
"set_label": "设置标签...",
|
"set_label": "设置标签",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
|
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
|
||||||
"settings_acrylic_level": "亚克力级别:",
|
"settings_acrylic_level": "亚克力级别:",
|
||||||
"settings_address_book": "地址簿...",
|
"settings_address_book": "地址簿…",
|
||||||
"settings_auto_detected": "从 DRAGONX.conf 自动检测",
|
"settings_auto_detected": "从 DRAGONX.conf 自动检测",
|
||||||
"settings_auto_lock": "自动锁定",
|
"settings_auto_lock": "自动锁定",
|
||||||
"settings_auto_shield_desc": "自动将透明资金转移到屏蔽地址",
|
"settings_auto_shield_desc": "自动将透明资金转移到屏蔽地址",
|
||||||
"settings_auto_shield_funds": "自动屏蔽透明资金",
|
"settings_auto_shield_funds": "自动屏蔽透明资金",
|
||||||
"settings_backup": "备份...",
|
"settings_backup": "备份…",
|
||||||
"settings_block_explorer_urls": "区块浏览器网址",
|
"settings_block_explorer_urls": "区块浏览器网址",
|
||||||
"settings_builtin": "内置",
|
"settings_builtin": "内置",
|
||||||
"settings_change_passphrase": "更改密码",
|
"settings_change_passphrase": "更改密码",
|
||||||
@@ -1180,53 +1336,62 @@
|
|||||||
"settings_configure_explorer": "配置外部区块浏览器链接",
|
"settings_configure_explorer": "配置外部区块浏览器链接",
|
||||||
"settings_configure_rpc": "配置 dragonxd 守护进程连接",
|
"settings_configure_rpc": "配置 dragonxd 守护进程连接",
|
||||||
"settings_connection": "连接",
|
"settings_connection": "连接",
|
||||||
|
"settings_copy_diagnostics": "复制诊断信息",
|
||||||
"settings_copyright": "版权所有 2024-2026 DragonX 开发者 | GPLv3 许可证",
|
"settings_copyright": "版权所有 2024-2026 DragonX 开发者 | GPLv3 许可证",
|
||||||
"settings_custom": "自定义",
|
"settings_custom": "自定义",
|
||||||
"settings_data_dir": "数据目录:",
|
"settings_data_dir": "数据目录:",
|
||||||
"settings_debug_changed": "调试类别已更改——重启守护进程以应用",
|
"settings_debug_changed": "调试类别已更改——重启守护进程以应用",
|
||||||
"settings_debug_restart_note": "更改将在重启守护进程后生效。",
|
"settings_debug_restart_note": "更改将在重启守护进程后生效。",
|
||||||
"settings_debug_select": "选择要启用的守护进程调试日志类别(-debug= 标志)。",
|
"settings_debug_select": "选择要启用的守护进程调试日志类别(-debug= 标志)。",
|
||||||
|
"settings_diagnostics_copied": "诊断信息已复制到剪贴板",
|
||||||
"settings_encrypt_first_pin": "请先加密钱包以启用 PIN",
|
"settings_encrypt_first_pin": "请先加密钱包以启用 PIN",
|
||||||
"settings_encrypt_wallet": "加密钱包",
|
"settings_encrypt_wallet": "加密钱包",
|
||||||
"settings_explorer_hint": "URL 应包含尾部斜杠。将自动附加 txid/地址。",
|
"settings_explorer_hint": "URL 应包含尾部斜杠。将自动附加 txid/地址。",
|
||||||
"settings_export_all": "全部导出...",
|
"settings_export_all": "全部导出…",
|
||||||
"settings_export_csv": "导出 CSV...",
|
"settings_export_csv": "导出 CSV…",
|
||||||
"settings_export_key": "导出密钥...",
|
"settings_export_key": "导出密钥…",
|
||||||
"settings_gradient_bg": "渐变背景",
|
"settings_gradient_bg": "渐变背景",
|
||||||
"settings_gradient_desc": "用平滑渐变替换纹理背景",
|
"settings_gradient_desc": "用平滑渐变替换纹理背景",
|
||||||
"settings_idle_after": "之后",
|
"settings_idle_after": "之后",
|
||||||
"settings_import_key": "导入私钥...",
|
"settings_import_key": "导入私钥…",
|
||||||
"settings_import_viewkey": "导入查看密钥...",
|
"settings_import_viewkey": "导入查看密钥…",
|
||||||
"settings_language_note": "注意:部分文本需要重启才能更新",
|
"settings_language_note": "注意:部分文本需要重启才能更新",
|
||||||
"settings_lock_now": "立即锁定",
|
"settings_lock_now": "立即锁定",
|
||||||
"settings_locked": "已锁定",
|
"settings_locked": "已锁定",
|
||||||
"settings_merge_to_address": "合并到地址...",
|
"settings_merge_to_address": "合并到地址…",
|
||||||
"settings_noise_opacity": "噪点不透明度:",
|
"settings_noise_opacity": "噪点不透明度:",
|
||||||
|
"settings_not_connected": "未连接到守护进程",
|
||||||
"settings_not_encrypted": "未加密",
|
"settings_not_encrypted": "未加密",
|
||||||
"settings_not_found": "未找到",
|
"settings_not_found": "未找到",
|
||||||
"settings_open_app_dir": "打开应用文件夹",
|
"settings_open_app_dir": "打开应用文件夹",
|
||||||
"settings_open_data_dir": "打开数据文件夹",
|
"settings_open_data_dir": "打开数据文件夹",
|
||||||
|
"settings_open_log_folder": "打开日志文件夹",
|
||||||
"settings_other": "其他",
|
"settings_other": "其他",
|
||||||
"settings_pin_active": "PIN",
|
"settings_pin_active": "PIN",
|
||||||
"settings_privacy": "隐私",
|
"settings_privacy": "隐私",
|
||||||
"settings_quick_unlock_pin": "快速解锁 PIN",
|
"settings_quick_unlock_pin": "快速解锁 PIN",
|
||||||
"settings_reduce_transparency": "降低透明度",
|
"settings_reduce_transparency": "降低透明度",
|
||||||
|
"settings_reloaded": "已从磁盘重新加载设置",
|
||||||
"settings_remove_encryption": "移除加密",
|
"settings_remove_encryption": "移除加密",
|
||||||
"settings_remove_pin": "移除 PIN",
|
"settings_remove_pin": "移除 PIN",
|
||||||
"settings_request_payment": "请求付款...",
|
"settings_request_payment": "请求付款…",
|
||||||
"settings_rescan_desc": "重新扫描区块链以查找丢失的交易",
|
"settings_rescan_desc": "重新扫描区块链以查找丢失的交易",
|
||||||
"settings_restart_daemon": "重启守护进程",
|
"settings_restart_daemon": "重启守护进程",
|
||||||
"settings_rpc_connection": "RPC 连接",
|
"settings_rpc_connection": "RPC 连接",
|
||||||
|
"settings_rpc_error_prefix": "RPC 错误:",
|
||||||
"settings_rpc_note": "注意:连接设置通常从 DRAGONX.conf 自动检测",
|
"settings_rpc_note": "注意:连接设置通常从 DRAGONX.conf 自动检测",
|
||||||
|
"settings_rpc_ok": "RPC 连接正常",
|
||||||
"settings_save_shielded_desc": "将 z-addr 交易存储在本地文件中以供查看",
|
"settings_save_shielded_desc": "将 z-addr 交易存储在本地文件中以供查看",
|
||||||
"settings_save_shielded_local": "将屏蔽交易历史保存到本地",
|
"settings_save_shielded_local": "将屏蔽交易历史保存到本地",
|
||||||
|
"settings_saved": "设置已保存",
|
||||||
"settings_set_pin": "设置 PIN",
|
"settings_set_pin": "设置 PIN",
|
||||||
"settings_shield_mining": "屏蔽挖矿...",
|
"settings_shield_mining": "屏蔽挖矿…",
|
||||||
"settings_solid_colors_desc": "使用纯色代替模糊效果(无障碍功能)",
|
"settings_solid_colors_desc": "使用纯色代替模糊效果(无障碍功能)",
|
||||||
|
"settings_theme_refreshed": "主题列表已刷新",
|
||||||
"settings_tor_desc": "通过 Tor 路由所有连接以增强隐私",
|
"settings_tor_desc": "通过 Tor 路由所有连接以增强隐私",
|
||||||
"settings_unlocked": "已解锁",
|
"settings_unlocked": "已解锁",
|
||||||
"settings_use_tor_network": "使用 Tor 进行网络连接",
|
"settings_use_tor_network": "使用 Tor 进行网络连接",
|
||||||
"settings_validate_address": "验证地址...",
|
"settings_validate_address": "验证地址…",
|
||||||
"settings_visual_effects": "视觉效果",
|
"settings_visual_effects": "视觉效果",
|
||||||
"settings_wallet_file_size": "钱包文件大小:%s",
|
"settings_wallet_file_size": "钱包文件大小:%s",
|
||||||
"settings_wallet_info": "钱包信息",
|
"settings_wallet_info": "钱包信息",
|
||||||
@@ -1234,6 +1399,8 @@
|
|||||||
"settings_wallet_maintenance": "钱包维护",
|
"settings_wallet_maintenance": "钱包维护",
|
||||||
"settings_wallet_not_found": "未找到钱包文件",
|
"settings_wallet_not_found": "未找到钱包文件",
|
||||||
"settings_wallet_size_label": "钱包大小:",
|
"settings_wallet_size_label": "钱包大小:",
|
||||||
|
"settings_ztx_cleared": "Z 交易历史记录已清除",
|
||||||
|
"settings_ztx_not_found": "未找到历史记录文件",
|
||||||
"setup_wizard": "设置向导",
|
"setup_wizard": "设置向导",
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"shield_check_status": "检查状态",
|
"shield_check_status": "检查状态",
|
||||||
@@ -1292,6 +1459,23 @@
|
|||||||
"sweep_to": "归集到:",
|
"sweep_to": "归集到:",
|
||||||
"sweep_toggle": "归集到我的钱包(不保留密钥)",
|
"sweep_toggle": "归集到我的钱包(不保留密钥)",
|
||||||
"sweep_tx": "交易:",
|
"sweep_tx": "交易:",
|
||||||
|
"switch_corrupt_body": "此钱包似乎已损坏——节点无法打开它。请从备份恢复、重新创建,或尝试修复。",
|
||||||
|
"switch_corrupt_repair": "尝试修复(salvage)",
|
||||||
|
"switch_progress_background": "在后台继续",
|
||||||
|
"switch_progress_default_wallet": "默认钱包",
|
||||||
|
"switch_progress_elapsed": "已用时",
|
||||||
|
"switch_progress_external_wallet": "外部钱包",
|
||||||
|
"switch_progress_failed_title": "切换钱包失败",
|
||||||
|
"switch_progress_from_label": "来自",
|
||||||
|
"switch_progress_hint": "正常关闭最多可能需要一分钟。",
|
||||||
|
"switch_progress_reconnecting": "正在重新连接",
|
||||||
|
"switch_progress_starting": "正在以新钱包启动节点",
|
||||||
|
"switch_progress_stopping": "正在停止当前节点",
|
||||||
|
"switch_progress_title": "正在切换钱包",
|
||||||
|
"switch_stopnode_body": "切换钱包会以所选钱包重启节点。正在运行的节点将被停止并以新钱包重新启动——如果你是特意让它保持运行的,它会自动重新启动。",
|
||||||
|
"switch_stopnode_confirm": "停止节点并切换",
|
||||||
|
"switch_stopnode_title": "停止正在运行的节点?",
|
||||||
|
"switch_stopnode_warn": "已有一个并非由此钱包启动的节点正在运行。",
|
||||||
"syncing": "同步中...",
|
"syncing": "同步中...",
|
||||||
"t_address": "T 地址",
|
"t_address": "T 地址",
|
||||||
"t_addresses": "T 地址",
|
"t_addresses": "T 地址",
|
||||||
@@ -1299,6 +1483,7 @@
|
|||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"theme_effects": "主题效果",
|
"theme_effects": "主题效果",
|
||||||
"theme_language": "主题与语言",
|
"theme_language": "主题与语言",
|
||||||
|
"tile_click_to_open": "点击打开",
|
||||||
"time_days_ago": "%d 天前",
|
"time_days_ago": "%d 天前",
|
||||||
"time_hours_ago": "%d 小时前",
|
"time_hours_ago": "%d 小时前",
|
||||||
"time_minutes_ago": "%d 分钟前",
|
"time_minutes_ago": "%d 分钟前",
|
||||||
@@ -1313,7 +1498,9 @@
|
|||||||
"to_upper": "至",
|
"to_upper": "至",
|
||||||
"tools": "工具",
|
"tools": "工具",
|
||||||
"tools_actions": "工具与操作...",
|
"tools_actions": "工具与操作...",
|
||||||
|
"tools_actions_hdr": "工具与操作",
|
||||||
"total": "合计",
|
"total": "合计",
|
||||||
|
"total_balance_label": "总余额",
|
||||||
"transaction_id": "交易 ID",
|
"transaction_id": "交易 ID",
|
||||||
"transaction_sent": "交易发送成功",
|
"transaction_sent": "交易发送成功",
|
||||||
"transaction_sent_msg": "交易已发送!",
|
"transaction_sent_msg": "交易已发送!",
|
||||||
@@ -1335,13 +1522,24 @@
|
|||||||
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
|
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
|
||||||
"tt_backup": "创建 wallet.dat 的备份",
|
"tt_backup": "创建 wallet.dat 的备份",
|
||||||
"tt_block_explorer": "在浏览器中打开 DragonX 区块浏览器",
|
"tt_block_explorer": "在浏览器中打开 DragonX 区块浏览器",
|
||||||
"tt_blur": "模糊程度(0%% = 关闭,100%% = 最大)",
|
"tt_blur": "模糊程度(0% = 关闭,100% = 最大)",
|
||||||
"tt_change_pass": "更改钱包加密密码",
|
"tt_change_pass": "更改钱包加密密码",
|
||||||
"tt_change_pin": "更改您的解锁 PIN",
|
"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_clear_ztx": "删除本地缓存的 z-交易历史",
|
||||||
|
"tt_clock_format": "24 或 12 小时制,应用全局。聊天可覆盖。",
|
||||||
|
"tt_copy_diagnostics": "将支持诊断摘要(版本、守护进程/钱包/日志状态 — 不含机密)复制到剪贴板",
|
||||||
"tt_custom_fees": "发送交易时启用手动费用输入",
|
"tt_custom_fees": "发送交易时启用手动费用输入",
|
||||||
"tt_custom_theme": "自定义主题已激活",
|
"tt_custom_theme": "自定义主题已激活",
|
||||||
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
||||||
|
"tt_daemon_refresh": "重新读取上方显示的已安装及内置 dragonxd 版本、大小和日期",
|
||||||
"tt_daemon_update_check": "从项目 Gitea 下载并验证最新的 dragonxd 全节点,然后重启以应用",
|
"tt_daemon_update_check": "从项目 Gitea 下载并验证最新的 dragonxd 全节点,然后重启以应用",
|
||||||
"tt_debug_collapse": "折叠调试日志选项",
|
"tt_debug_collapse": "折叠调试日志选项",
|
||||||
"tt_debug_expand": "展开调试日志选项",
|
"tt_debug_expand": "展开调试日志选项",
|
||||||
@@ -1359,15 +1557,39 @@
|
|||||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||||
"tt_language": "钱包界面语言",
|
"tt_language": "钱包界面语言",
|
||||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
"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_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_lock": "立即锁定钱包",
|
||||||
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
||||||
"tt_merge": "将多个 UTXO 合并到一个地址",
|
"tt_merge": "将多个 UTXO 合并到一个地址",
|
||||||
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
|
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
|
||||||
"tt_noise": "颗粒纹理强度(0%% = 关闭,100%% = 最大)",
|
"tt_noise": "颗粒纹理强度(0% = 关闭,100% = 最大)",
|
||||||
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
|
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
|
||||||
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
|
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
|
||||||
"tt_open_dir": "点击在文件管理器中打开",
|
"tt_open_dir": "点击在文件管理器中打开",
|
||||||
|
"tt_open_log_folder": "打开包含调试和崩溃日志的文件夹",
|
||||||
"tt_reduce_motion": "禁用动画过渡和余额渐变以提高无障碍性",
|
"tt_reduce_motion": "禁用动画过渡和余额渐变以提高无障碍性",
|
||||||
"tt_remove_encrypt": "移除加密并以未受保护状态存储钱包",
|
"tt_remove_encrypt": "移除加密并以未受保护状态存储钱包",
|
||||||
"tt_remove_pin": "移除 PIN 并要求密码解锁",
|
"tt_remove_pin": "移除 PIN 并要求密码解锁",
|
||||||
@@ -1380,12 +1602,17 @@
|
|||||||
"tt_rpc_host": "DragonX 守护进程主机名",
|
"tt_rpc_host": "DragonX 守护进程主机名",
|
||||||
"tt_rpc_pass": "RPC 认证密码",
|
"tt_rpc_pass": "RPC 认证密码",
|
||||||
"tt_rpc_port": "守护进程 RPC 连接端口",
|
"tt_rpc_port": "守护进程 RPC 连接端口",
|
||||||
|
"tt_rpc_toggle": "显示或隐藏守护进程的只读 RPC 连接详情(主机、端口、用户、密码)",
|
||||||
"tt_rpc_user": "RPC 认证用户名",
|
"tt_rpc_user": "RPC 认证用户名",
|
||||||
"tt_save_settings": "将所有设置保存到磁盘",
|
"tt_save_settings": "将所有设置保存到磁盘",
|
||||||
"tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度",
|
"tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度",
|
||||||
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s",
|
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s",
|
||||||
"tt_scanline": "控制台中的 CRT 扫描线效果",
|
"tt_scanline": "控制台中的 CRT 扫描线效果",
|
||||||
|
"tt_screenshot_open_dir": "在你的文件管理器中打开截图文件夹(位于配置目录下)",
|
||||||
|
"tt_screenshot_sweep": "在每个标签页遍历每种主题,将每种主题的截图保存到配置文件夹的 screenshots 目录(覆盖上一次遍历)",
|
||||||
|
"tt_screenshot_sweep_full": "与主题遍历类似,但同时捕获每个使用临时离线演示钱包数据的模态框 / 对话框 / 流程",
|
||||||
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
|
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
|
||||||
|
"tt_seed_demo_chat": "向聊天标签页注入示例对话,以便遍历能捕获其界面;仅在内存中,重启后消失",
|
||||||
"tt_seed_migrate": "创建一个新的助记词钱包并将您的资金转入其中",
|
"tt_seed_migrate": "创建一个新的助记词钱包并将您的资金转入其中",
|
||||||
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
|
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
|
||||||
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
|
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
|
||||||
@@ -1397,13 +1624,14 @@
|
|||||||
"tt_theme_hotkey": "快捷键:Ctrl+左/右箭头切换主题",
|
"tt_theme_hotkey": "快捷键:Ctrl+左/右箭头切换主题",
|
||||||
"tt_tor": "通过 Tor 网络路由守护进程连接以实现匿名",
|
"tt_tor": "通过 Tor 网络路由守护进程连接以实现匿名",
|
||||||
"tt_tx_url": "在区块浏览器中查看交易的基础 URL",
|
"tt_tx_url": "在区块浏览器中查看交易的基础 URL",
|
||||||
"tt_ui_opacity": "卡片和侧边栏不透明度(100%% = 完全不透明,越低越透明)",
|
"tt_ui_opacity": "卡片和侧边栏不透明度(100% = 完全不透明,越低越透明)",
|
||||||
"tt_validate": "检查 DragonX 地址是否有效",
|
"tt_validate": "检查 DragonX 地址是否有效",
|
||||||
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
||||||
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
||||||
"tt_website": "打开 DragonX 网站",
|
"tt_website": "打开 DragonX 网站",
|
||||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||||
"tt_wizard": "重新运行初始设置向导\\n守护进程将被重启",
|
"tt_wizard": "重新运行初始设置向导\\n守护进程将被重启",
|
||||||
|
"tx_chat_badge": "消息",
|
||||||
"tx_confirmations": "%d 次确认",
|
"tx_confirmations": "%d 次确认",
|
||||||
"tx_details_title": "交易详情",
|
"tx_details_title": "交易详情",
|
||||||
"tx_from_address": "发送地址:",
|
"tx_from_address": "发送地址:",
|
||||||
@@ -1449,6 +1677,7 @@
|
|||||||
"validate_not_mine": "不属于此钱包",
|
"validate_not_mine": "不属于此钱包",
|
||||||
"validate_ownership": "所有权:",
|
"validate_ownership": "所有权:",
|
||||||
"validate_results": "结果:",
|
"validate_results": "结果:",
|
||||||
|
"validate_results_placeholder": "结果将显示在此处",
|
||||||
"validate_shielded_type": "屏蔽(z 地址)",
|
"validate_shielded_type": "屏蔽(z 地址)",
|
||||||
"validate_status": "状态:",
|
"validate_status": "状态:",
|
||||||
"validate_title": "验证地址",
|
"validate_title": "验证地址",
|
||||||
@@ -1472,6 +1701,8 @@
|
|||||||
"wallets_add_folder_toggle": "+ 扫描其他文件夹中的钱包…",
|
"wallets_add_folder_toggle": "+ 扫描其他文件夹中的钱包…",
|
||||||
"wallets_badge_encrypted": "已加密(密码保护)",
|
"wallets_badge_encrypted": "已加密(密码保护)",
|
||||||
"wallets_badge_encrypted_short": "已加密",
|
"wallets_badge_encrypted_short": "已加密",
|
||||||
|
"wallets_badge_hd": "HD 钱包 — 打开钱包才能确认助记词",
|
||||||
|
"wallets_badge_hd_short": "HD 钱包",
|
||||||
"wallets_badge_legacy": "旧版钱包(无助记词)",
|
"wallets_badge_legacy": "旧版钱包(无助记词)",
|
||||||
"wallets_badge_legacy_short": "旧版",
|
"wallets_badge_legacy_short": "旧版",
|
||||||
"wallets_badge_seed": "助记词钱包 (HD)",
|
"wallets_badge_seed": "助记词钱包 (HD)",
|
||||||
@@ -1587,6 +1818,7 @@
|
|||||||
"xmrig_loading_releases": "正在加载发行版…",
|
"xmrig_loading_releases": "正在加载发行版…",
|
||||||
"xmrig_none": "无",
|
"xmrig_none": "无",
|
||||||
"xmrig_reinstall": "重新安装",
|
"xmrig_reinstall": "重新安装",
|
||||||
|
"xmrig_releases": "xmrig 版本",
|
||||||
"xmrig_stop_mining_first": "更新矿工程序前请先停止挖矿。",
|
"xmrig_stop_mining_first": "更新矿工程序前请先停止挖矿。",
|
||||||
"xmrig_unavailable_body": "此平台没有可用的矿工构建版本。",
|
"xmrig_unavailable_body": "此平台没有可用的矿工构建版本。",
|
||||||
"xmrig_unavailable_title": "矿工更新不可用",
|
"xmrig_unavailable_title": "矿工更新不可用",
|
||||||
|
|||||||
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 }
|
||||||
@@ -700,6 +700,12 @@ status-pill-bg-alpha = { size = 30 }
|
|||||||
status-pill-y-offset = { size = 1 }
|
status-pill-y-offset = { size = 1 }
|
||||||
confirmed-threshold = { size = 10 }
|
confirmed-threshold = { size = 10 }
|
||||||
|
|
||||||
|
# Persistent node/RPC error strip at the top of the content column (see App::renderNodeStatusBanner).
|
||||||
|
# Slightly taller than the per-tab sync banner so it comfortably holds the Reconnect/Restart action.
|
||||||
|
[banners.node-status]
|
||||||
|
min-height = { size = 26.0 }
|
||||||
|
height = { size = 30.0 }
|
||||||
|
|
||||||
[tabs.transactions]
|
[tabs.transactions]
|
||||||
search-max-width = 300.0
|
search-max-width = 300.0
|
||||||
search-width-ratio = 0.3
|
search-width-ratio = 0.3
|
||||||
@@ -1503,6 +1509,7 @@ progress-bar = { height = 6.0, radius = 3.0 }
|
|||||||
progress-width = { size = 260.0 }
|
progress-width = { size = 260.0 }
|
||||||
backdrop-alpha = { opacity = 0.80 }
|
backdrop-alpha = { opacity = 0.80 }
|
||||||
vertical-gap = { size = 8.0 }
|
vertical-gap = { size = 8.0 }
|
||||||
|
stall-timeout-sec = { size = 45.0 }
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# First-Run Wizard Screens
|
# First-Run Wizard Screens
|
||||||
|
|||||||
94
scripts/build-freetype-mingw.sh
Executable file
94
scripts/build-freetype-mingw.sh
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Cross-build a MINIMAL static FreeType for the mingw-w64 (Windows) target.
|
||||||
|
#
|
||||||
|
# Why: the wallet's optional color-emoji rendering needs FreeType (to rasterize the COLR/CPAL Twemoji
|
||||||
|
# font). Native Linux/macOS pick up the system FreeType via find_package; the Debian/Ubuntu mingw-w64
|
||||||
|
# cross toolchain ships no FreeType, so we build one here. The Twemoji font is COLRv0 (layered vector),
|
||||||
|
# which FreeType renders WITHOUT libpng / harfbuzz / brotli / zlib — so this is a dependency-free static
|
||||||
|
# build (no external libs to also cross-compile), producing a self-contained libfreetype.a.
|
||||||
|
#
|
||||||
|
# Output: <prefix>/include/freetype2/... + <prefix>/lib/libfreetype.a (default prefix: third_party/freetype-mingw)
|
||||||
|
# build.sh --win-release runs this automatically and passes -DDRAGONX_MINGW_FREETYPE_PREFIX to CMake.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FT_VERSION="2.13.3"
|
||||||
|
FT_SHA256="5c3a8e78f7b24c20b25b54ee575d6daa40007a5f4eea2845861c3409b3021747" # freetype-2.13.3.tar.gz
|
||||||
|
FT_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FT_VERSION}.tar.gz"
|
||||||
|
FT_URL_MIRROR="https://downloads.sourceforge.net/project/freetype/freetype2/${FT_VERSION}/freetype-${FT_VERSION}.tar.gz"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PREFIX="${1:-$SCRIPT_DIR/third_party/freetype-mingw}"
|
||||||
|
WORK="$SCRIPT_DIR/third_party/.freetype-mingw-build"
|
||||||
|
|
||||||
|
# Already built? (libfreetype.a present) → nothing to do.
|
||||||
|
if [[ -f "$PREFIX/lib/libfreetype.a" && -d "$PREFIX/include/freetype2" ]]; then
|
||||||
|
echo "FreeType (mingw) already built at: $PREFIX"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pick the mingw compilers (posix threads variant preferred, matching build.sh).
|
||||||
|
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
|
||||||
|
MINGW_GCC=x86_64-w64-mingw32-gcc-posix; MINGW_GXX=x86_64-w64-mingw32-g++-posix
|
||||||
|
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
|
||||||
|
MINGW_GCC=x86_64-w64-mingw32-gcc; MINGW_GXX=x86_64-w64-mingw32-g++
|
||||||
|
else
|
||||||
|
echo "ERROR: x86_64-w64-mingw32-gcc not found (install mingw-w64)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
cd "$WORK"
|
||||||
|
|
||||||
|
TARBALL="freetype-${FT_VERSION}.tar.gz"
|
||||||
|
if [[ ! -f "$TARBALL" ]]; then
|
||||||
|
echo "Downloading FreeType ${FT_VERSION} ..."
|
||||||
|
curl -fsSL -o "$TARBALL" "$FT_URL" || curl -fsSL -o "$TARBALL" "$FT_URL_MIRROR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Verifying SHA-256 ..."
|
||||||
|
echo "${FT_SHA256} ${TARBALL}" | sha256sum -c - || {
|
||||||
|
echo "ERROR: FreeType tarball checksum mismatch (expected ${FT_SHA256})." >&2
|
||||||
|
echo " got: $(sha256sum "$TARBALL" | cut -d' ' -f1)" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -rf "freetype-${FT_VERSION}"
|
||||||
|
tar xf "$TARBALL"
|
||||||
|
SRC="$WORK/freetype-${FT_VERSION}"
|
||||||
|
|
||||||
|
# Minimal mingw toolchain for FreeType's own CMake.
|
||||||
|
cat > "$WORK/ft-mingw-toolchain.cmake" <<TOOLCHAIN
|
||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||||
|
set(CMAKE_C_COMPILER ${MINGW_GCC})
|
||||||
|
set(CMAKE_CXX_COMPILER ${MINGW_GXX})
|
||||||
|
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
TOOLCHAIN
|
||||||
|
|
||||||
|
echo "Configuring FreeType (static, no external deps) ..."
|
||||||
|
rm -rf "$WORK/build"
|
||||||
|
cmake -S "$SRC" -B "$WORK/build" \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$WORK/ft-mingw-toolchain.cmake" \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DFT_DISABLE_ZLIB=ON \
|
||||||
|
-DFT_DISABLE_BZIP2=ON \
|
||||||
|
-DFT_DISABLE_PNG=ON \
|
||||||
|
-DFT_DISABLE_HARFBUZZ=ON \
|
||||||
|
-DFT_DISABLE_BROTLI=ON
|
||||||
|
|
||||||
|
echo "Building + installing FreeType ..."
|
||||||
|
cmake --build "$WORK/build" -j "$(nproc)"
|
||||||
|
cmake --install "$WORK/build"
|
||||||
|
|
||||||
|
if [[ -f "$PREFIX/lib/libfreetype.a" ]]; then
|
||||||
|
echo "OK: mingw FreeType -> $PREFIX/lib/libfreetype.a"
|
||||||
|
else
|
||||||
|
echo "ERROR: build did not produce libfreetype.a" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -67,25 +79,9 @@ Options:
|
|||||||
--backend-dir PATH SilentDragonXLite/lib source directory.
|
--backend-dir PATH SilentDragonXLite/lib source directory.
|
||||||
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
|
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
|
||||||
--out-dir PATH Output directory for copied artifact and metadata.
|
--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.
|
--reproducible Add deterministic Rust path remaps for clean builds.
|
||||||
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
|
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
|
||||||
--builder NAME Redacted builder/provenance label. Default: local.
|
--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.
|
-j, --jobs N Cargo parallel jobs.
|
||||||
--cargo-arg ARG Extra argument forwarded to cargo build.
|
--cargo-arg ARG Extra argument forwarded to cargo build.
|
||||||
-h, --help Show this help.
|
-h, --help Show this help.
|
||||||
@@ -95,9 +91,13 @@ Outputs:
|
|||||||
<out>/<platform>/lite-backend-symbols.txt
|
<out>/<platform>/lite-backend-symbols.txt
|
||||||
<out>/<platform>/lite-backend-artifact-manifest.json
|
<out>/<platform>/lite-backend-artifact-manifest.json
|
||||||
|
|
||||||
The script captures symbols, checksums, and optional read-only signature
|
The lite backend is always built from the vendored in-tree source
|
||||||
verification metadata only. It does not load the library, resolve function
|
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
|
||||||
pointers, call SDXL, sign, upload, or publish 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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,15 +166,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
OUT_DIR="$(absolute_path "$2")"
|
OUT_DIR="$(absolute_path "$2")"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--artifact)
|
--artifact|--no-build)
|
||||||
[[ $# -ge 2 ]] || die "--artifact requires a value"
|
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."
|
||||||
ARTIFACT_PATH="$(absolute_path "$2")"
|
|
||||||
BUILD_ARTIFACT=false
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--no-build)
|
|
||||||
BUILD_ARTIFACT=false
|
|
||||||
shift
|
|
||||||
;;
|
;;
|
||||||
--reproducible)
|
--reproducible)
|
||||||
REPRODUCIBLE=true
|
REPRODUCIBLE=true
|
||||||
@@ -191,54 +184,11 @@ while [[ $# -gt 0 ]]; do
|
|||||||
BUILDER="$2"
|
BUILDER="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--signature-required)
|
--signature-required|--signature-file|--signature-path|--signature-format|\
|
||||||
SIGNATURE_REQUIRED=true
|
--signature-verification-tool|--signature-tool|--signature-verification-command|\
|
||||||
shift
|
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
|
||||||
;;
|
--signature-transparency-log-url|--signature-verified-sha256)
|
||||||
--signature-file|--signature-path)
|
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."
|
||||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
|
||||||
SIGNATURE_FILE="$(absolute_path "$2")"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-format)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-format requires a value"
|
|
||||||
SIGNATURE_FORMAT="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-verification-tool|--signature-tool)
|
|
||||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
|
||||||
SIGNATURE_VERIFICATION_TOOL="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-verification-command)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
|
|
||||||
SIGNATURE_VERIFICATION_COMMAND="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-key-fingerprint)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
|
|
||||||
SIGNATURE_KEY_FINGERPRINT="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-certificate-identity)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
|
|
||||||
SIGNATURE_CERTIFICATE_IDENTITY="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-certificate-issuer)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
|
|
||||||
SIGNATURE_CERTIFICATE_ISSUER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-transparency-log-url)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
|
|
||||||
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--signature-verified-sha256)
|
|
||||||
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
|
|
||||||
SIGNATURE_VERIFIED_SHA256="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
;;
|
||||||
-j|--jobs)
|
-j|--jobs)
|
||||||
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
||||||
@@ -374,6 +324,9 @@ prepare_backend_source() {
|
|||||||
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
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"
|
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||||
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||||
|
# 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"
|
[[ -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
|
# 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.
|
# "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 ' },\n'
|
||||||
printf ' "provenance": {\n'
|
printf ' "provenance": {\n'
|
||||||
printf ' "owner_ready": true,\n'
|
printf ' "owner_ready": true,\n'
|
||||||
|
printf ' "built_from_source": true,\n'
|
||||||
printf ' "metadata_provided": true,\n'
|
printf ' "metadata_provided": true,\n'
|
||||||
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
||||||
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
||||||
|
|||||||
87
scripts/build_emoji_subset.py
Normal file
87
scripts/build_emoji_subset.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build a monochrome Noto Emoji subset for the chat/message UI.
|
||||||
|
|
||||||
|
Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome
|
||||||
|
(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's
|
||||||
|
*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed)
|
||||||
|
and merge it into the text fonts (see Typography::loadFont, the block after the
|
||||||
|
CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ
|
||||||
|
sequences / regional-indicator flags won't compose — single-codepoint emoji
|
||||||
|
(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use.
|
||||||
|
|
||||||
|
Source is the variable font pinned to wght=400 → static, then subset to the
|
||||||
|
emoji planes plus the higher symbol/star ranges the base UI font doesn't cover.
|
||||||
|
The base Ubuntu font already owns U+2600–26FF etc.; ImGui's MergeMode gives the
|
||||||
|
first-loaded glyph precedence, so those stay text-styled and only the codepoints
|
||||||
|
the base lacks fall through to this font.
|
||||||
|
|
||||||
|
Get the source once (OFL, redistributable):
|
||||||
|
curl -fsSL -o /tmp/NotoEmoji-VF.ttf \
|
||||||
|
'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf'
|
||||||
|
|
||||||
|
Then: python3 scripts/build_emoji_subset.py
|
||||||
|
Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from fontTools import ttLib, subset
|
||||||
|
from fontTools.varLib.instancer import instantiateVariableFont
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
SOURCE_VF = '/tmp/NotoEmoji-VF.ttf'
|
||||||
|
STATIC = '/tmp/NotoEmoji-Static.ttf'
|
||||||
|
OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf')
|
||||||
|
|
||||||
|
# Emoji codepoints to keep. Ranges are inclusive.
|
||||||
|
RANGES = [
|
||||||
|
(0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended)
|
||||||
|
(0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats
|
||||||
|
(0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows
|
||||||
|
(0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style)
|
||||||
|
(0x2194, 0x21AA), # arrows used as emoji
|
||||||
|
(0x231A, 0x231B), # ⌚ ⌛
|
||||||
|
(0x23E9, 0x23FA), # media-control emoji
|
||||||
|
(0x25AA, 0x25FE), # small squares
|
||||||
|
]
|
||||||
|
SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299,
|
||||||
|
0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(SOURCE_VF):
|
||||||
|
raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command")
|
||||||
|
|
||||||
|
# 1. Pin the weight axis so stb_truetype rasterizes a clean static instance.
|
||||||
|
f = ttLib.TTFont(SOURCE_VF)
|
||||||
|
if 'fvar' in f:
|
||||||
|
instantiateVariableFont(f, {'wght': 400}, inplace=True)
|
||||||
|
f.save(STATIC)
|
||||||
|
|
||||||
|
unicodes = list(SINGLES)
|
||||||
|
for lo, hi in RANGES:
|
||||||
|
unicodes.extend(range(lo, hi + 1))
|
||||||
|
|
||||||
|
opts = subset.Options()
|
||||||
|
opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS
|
||||||
|
opts.name_IDs = []
|
||||||
|
opts.notdef_outline = True
|
||||||
|
opts.glyph_names = False
|
||||||
|
opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern']
|
||||||
|
|
||||||
|
font = subset.load_font(STATIC, opts)
|
||||||
|
ss = subset.Subsetter(options=opts)
|
||||||
|
ss.populate(unicodes=unicodes)
|
||||||
|
ss.subset(font)
|
||||||
|
subset.save_font(font, OUTPUT, opts)
|
||||||
|
|
||||||
|
out = ttLib.TTFont(OUTPUT)
|
||||||
|
cmap = out.getBestCmap()
|
||||||
|
color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR'))
|
||||||
|
print(f"Output: {OUTPUT}")
|
||||||
|
print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}")
|
||||||
|
if color:
|
||||||
|
raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -24,18 +24,27 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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=""
|
APPIMAGETOOL=""
|
||||||
if command -v appimagetool &> /dev/null; then
|
if command -v appimagetool &> /dev/null; then
|
||||||
APPIMAGETOOL="appimagetool"
|
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||||
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
|
|
||||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
|
||||||
else
|
else
|
||||||
print_status "Downloading appimagetool..."
|
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||||
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
|
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
print_status "Downloading appimagetool 1.9.0 (pinned)..."
|
||||||
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
wget -q -O "$AT" "$APPIMAGETOOL_URL"
|
||||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
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
|
fi
|
||||||
|
|
||||||
print_status "Creating AppDir structure..."
|
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}"
|
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||||
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
||||||
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
||||||
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
|
||||||
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
|
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
|
||||||
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
|
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
|
||||||
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the
|
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
|
||||||
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset.
|
# 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),
|
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
|
||||||
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
|
||||||
# (the same flow the wallet's unit tests verify for the miner updater).
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
# 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 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 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.
|
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
die() { echo "error: $*" >&2; exit 1; }
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
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.
|
# 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; }
|
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
|
cmd="${1:-}"; shift || true
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
keygen)
|
keygen)
|
||||||
@@ -42,26 +68,90 @@ case "$cmd" in
|
|||||||
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||||
echo " $pub"
|
echo " $pub"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
pubkey)
|
pubkey)
|
||||||
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||||
pubkey_b64 "$1"
|
pubkey_b64 "$1"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
sign)
|
sign)
|
||||||
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||||
key="$1"; shift
|
key="$1"; shift
|
||||||
[ -f "$key" ] || die "no such key: $key"
|
[ -f "$key" ] || die "no such key: $key"
|
||||||
for f in "$@"; do
|
for f in "$@"; do
|
||||||
[ -f "$f" ] || die "no such file: $f"
|
[ -f "$f" ] || die "no such file: $f"
|
||||||
raw="$(mktemp)"
|
sign_file "$key" "$f"
|
||||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
|
||||||
openssl base64 -A -in "$raw" > "$f.sig"
|
|
||||||
printf '\n' >> "$f.sig"
|
|
||||||
rm -f "$raw"
|
|
||||||
echo "signed: $f -> $f.sig"
|
echo "signed: $f -> $f.sig"
|
||||||
done
|
done
|
||||||
echo "Upload each .sig as a release asset next to its archive."
|
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
|
esac
|
||||||
|
|||||||
90
setup.sh
90
setup.sh
@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
|
|||||||
libxkbcommon wayland libsodium curl
|
libxkbcommon wayland libsodium curl
|
||||||
autoconf automake libtool wget python xxd"
|
autoconf automake libtool wget python xxd"
|
||||||
|
|
||||||
pkgs_core_macos="cmake python xxd"
|
pkgs_core_macos="bash cmake python xxd"
|
||||||
|
|
||||||
# Windows cross-compile (from Linux)
|
# Windows cross-compile (from Linux)
|
||||||
pkgs_win_debian="mingw-w64 zip"
|
pkgs_win_debian="mingw-w64 zip"
|
||||||
@@ -284,18 +284,27 @@ fi
|
|||||||
header "Windows Cross-Compile"
|
header "Windows Cross-Compile"
|
||||||
|
|
||||||
if $SETUP_WIN; then
|
if $SETUP_WIN; then
|
||||||
win_pkgs="$(get_pkgs win)"
|
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
|
||||||
if [[ -n "$win_pkgs" ]]; then
|
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
|
||||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
|
||||||
fi
|
# 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
|
# Set posix thread model if available
|
||||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||||
if ! $CHECK_ONLY; then
|
if ! $CHECK_ONLY; then
|
||||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -391,11 +400,26 @@ elif $SETUP_SAPLING; then
|
|||||||
|
|
||||||
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
||||||
OUTPUT_URL="https://z.cash/downloads/sapling-output.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" && \
|
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
|
||||||
ok "Downloaded sapling-spend.params"
|
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
|
||||||
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
ok "Downloaded + verified sapling-spend.params"
|
||||||
ok "Downloaded sapling-output.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
|
fi
|
||||||
else
|
else
|
||||||
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
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"
|
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
|
||||||
header "xmrig-hac Mining Binary"
|
header "drg-xmrig Mining Binary"
|
||||||
|
|
||||||
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
|
||||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
# 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
|
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||||
# Only clean the binary for the platform(s) we are actually building,
|
# Only clean the binary for the platform(s) we are actually building,
|
||||||
@@ -700,14 +726,14 @@ if ! $CHECK_ONLY; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Helper: clone xmrig-hac if not present
|
# Helper: clone drg-xmrig if not present
|
||||||
clone_xmrig_if_needed() {
|
clone_xmrig_if_needed() {
|
||||||
if [[ ! -d "$XMRIG_SRC" ]]; then
|
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||||
info "Cloning xmrig-hac..."
|
info "Cloning drg-xmrig..."
|
||||||
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
|
||||||
else
|
else
|
||||||
ok "xmrig-hac source already present"
|
ok "drg-xmrig source already present"
|
||||||
info "Pulling latest xmrig-hac..."
|
info "Pulling latest drg-xmrig..."
|
||||||
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -728,15 +754,15 @@ else
|
|||||||
rm -rf "$XMRIG_SRC/build"
|
rm -rf "$XMRIG_SRC/build"
|
||||||
|
|
||||||
# Build dependencies (libuv, hwloc, openssl)
|
# 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"
|
cd "$XMRIG_SRC/scripts"
|
||||||
sh build_deps.sh
|
sh build_deps.sh
|
||||||
)
|
)
|
||||||
ok "xmrig-hac dependencies built"
|
ok "drg-xmrig dependencies built"
|
||||||
|
|
||||||
# Build xmrig
|
# Build xmrig
|
||||||
info "Building xmrig-hac (Linux)..."
|
info "Building drg-xmrig (Linux)..."
|
||||||
mkdir -p "$XMRIG_SRC/build"
|
mkdir -p "$XMRIG_SRC/build"
|
||||||
(
|
(
|
||||||
cd "$XMRIG_SRC/build"
|
cd "$XMRIG_SRC/build"
|
||||||
@@ -753,7 +779,7 @@ else
|
|||||||
mkdir -p "$XMRIG_PREBUILT"
|
mkdir -p "$XMRIG_PREBUILT"
|
||||||
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||||
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
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
|
else
|
||||||
err "xmrig (Linux) build failed — binary not found"
|
err "xmrig (Linux) build failed — binary not found"
|
||||||
MISSING=$((MISSING + 1))
|
MISSING=$((MISSING + 1))
|
||||||
@@ -777,7 +803,7 @@ else
|
|||||||
# Clean previous Windows build
|
# Clean previous Windows build
|
||||||
rm -rf "$XMRIG_SRC/build-windows"
|
rm -rf "$XMRIG_SRC/build-windows"
|
||||||
|
|
||||||
info "Building xmrig-hac (Windows cross-compile)..."
|
info "Building drg-xmrig (Windows cross-compile)..."
|
||||||
(
|
(
|
||||||
cd "$XMRIG_SRC/scripts"
|
cd "$XMRIG_SRC/scripts"
|
||||||
bash build_windows.sh
|
bash build_windows.sh
|
||||||
@@ -787,7 +813,7 @@ else
|
|||||||
mkdir -p "$XMRIG_PREBUILT"
|
mkdir -p "$XMRIG_PREBUILT"
|
||||||
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||||
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
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
|
else
|
||||||
err "xmrig.exe (Windows) build failed — binary not found"
|
err "xmrig.exe (Windows) build failed — binary not found"
|
||||||
MISSING=$((MISSING + 1))
|
MISSING=$((MISSING + 1))
|
||||||
@@ -797,7 +823,7 @@ fi
|
|||||||
# ── 8. Binary directories ───────────────────────────────────────────────────
|
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||||
header "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"
|
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||||
if [[ -d "$dir" ]]; then
|
if [[ -d "$dir" ]]; then
|
||||||
# Count actual files (not .gitkeep)
|
# Count actual files (not .gitkeep)
|
||||||
|
|||||||
2083
src/app.cpp
2083
src/app.cpp
File diff suppressed because it is too large
Load Diff
332
src/app.h
332
src/app.h
@@ -13,6 +13,8 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <deque>
|
||||||
|
#include <condition_variable>
|
||||||
#include <nlohmann/json_fwd.hpp>
|
#include <nlohmann/json_fwd.hpp>
|
||||||
#include "data/transaction_history_cache.h"
|
#include "data/transaction_history_cache.h"
|
||||||
#include "data/address_book.h"
|
#include "data/address_book.h"
|
||||||
@@ -70,6 +72,19 @@ enum class EncryptDialogPhase {
|
|||||||
Done // Finished — close dialog
|
Done // Finished — close dialog
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A status string written by a background/worker thread and read every frame by the UI thread. Its
|
||||||
|
// operator= locks, so all the plain `x = "..."` assignment sites stay unchanged; readers call get()
|
||||||
|
// for a consistent per-frame snapshot instead of racing a non-atomic std::string. (M-05, L-06)
|
||||||
|
class GuardedStatus {
|
||||||
|
public:
|
||||||
|
GuardedStatus() = default;
|
||||||
|
GuardedStatus& operator=(std::string v) { std::lock_guard<std::mutex> lk(m_); v_ = std::move(v); return *this; }
|
||||||
|
std::string get() const { std::lock_guard<std::mutex> lk(m_); return v_; }
|
||||||
|
private:
|
||||||
|
mutable std::mutex m_;
|
||||||
|
std::string v_;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Main application class
|
* @brief Main application class
|
||||||
*
|
*
|
||||||
@@ -78,6 +93,7 @@ enum class EncryptDialogPhase {
|
|||||||
class App {
|
class App {
|
||||||
public:
|
public:
|
||||||
App();
|
App();
|
||||||
|
void wipeSecrets(); // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
|
||||||
~App();
|
~App();
|
||||||
|
|
||||||
// Non-copyable
|
// Non-copyable
|
||||||
@@ -139,10 +155,17 @@ public:
|
|||||||
* @brief Whether we are in the shutdown phase
|
* @brief Whether we are in the shutdown phase
|
||||||
*/
|
*/
|
||||||
bool isShuttingDown() const { return shutting_down_; }
|
bool isShuttingDown() const { return shutting_down_; }
|
||||||
|
// True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live
|
||||||
|
// phase/spinner update in real time even when the app is otherwise idle.
|
||||||
|
bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); }
|
||||||
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
|
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
|
||||||
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
|
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
|
||||||
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||||||
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
||||||
|
|
||||||
|
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
|
||||||
|
// for the "Copy diagnostics" action. Contains no secrets.
|
||||||
|
std::string buildDiagnosticsReport();
|
||||||
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
|
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
|
||||||
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
|
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
|
||||||
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
|
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
|
||||||
@@ -152,6 +175,21 @@ public:
|
|||||||
*/
|
*/
|
||||||
void renderShutdownScreen();
|
void renderShutdownScreen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tail the last N lines of the daemon's debug.log (best-effort, reads only the file tail).
|
||||||
|
* Fallback for the shutdown screen when we have no captured stdout — e.g. an external daemon we
|
||||||
|
* attached to rather than spawned — so the user can still see the node flushing/exiting.
|
||||||
|
*/
|
||||||
|
std::vector<std::string> tailDaemonDebugLog(int maxLines) const;
|
||||||
|
|
||||||
|
// True when the daemon's debug.log shows an in-progress Sapling witness-cache rebuild (best-effort
|
||||||
|
// heuristic). Stopping the daemon during one discards it and forces a multi-minute redo next launch.
|
||||||
|
bool daemonWitnessRebuildActive() const;
|
||||||
|
// Whether beginShutdown() should pause and confirm before stopping the daemon (rebuild in progress).
|
||||||
|
bool shouldConfirmDaemonStop() const;
|
||||||
|
// The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render().
|
||||||
|
void renderDaemonStopConfirm();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Render loading overlay in content area while daemon is starting/syncing
|
* @brief Render loading overlay in content area while daemon is starting/syncing
|
||||||
* @param contentH Height of the content area child window
|
* @param contentH Height of the content area child window
|
||||||
@@ -168,6 +206,9 @@ public:
|
|||||||
daemon::EmbeddedDaemon* consoleDaemon();
|
daemon::EmbeddedDaemon* consoleDaemon();
|
||||||
daemon::XmrigManager* consoleXmrig();
|
daemon::XmrigManager* consoleXmrig();
|
||||||
config::Settings* settings() { return settings_.get(); }
|
config::Settings* settings() { return settings_.get(); }
|
||||||
|
// Request a font-atlas rebuild before the next frame (e.g. after toggling color emoji). Handled in
|
||||||
|
// preFrame() via Typography::reload — safe to call from UI code mid-frame.
|
||||||
|
void requestFontRebuild() { font_rebuild_requested_ = true; }
|
||||||
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||||
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||||||
@@ -176,6 +217,9 @@ public:
|
|||||||
// message (to a conversation whose peer key we know) / a new-conversation contact request.
|
// message (to a conversation whose peer key we know) / a new-conversation contact request.
|
||||||
void sendChatMessage(const std::string& conversationId, const std::string& text);
|
void sendChatMessage(const std::string& conversationId, const std::string& text);
|
||||||
void startChatConversation(const std::string& peerZaddr, const std::string& text);
|
void startChatConversation(const std::string& peerZaddr, const std::string& text);
|
||||||
|
// Send a contact request into an existing conversation (used to retry a failed request in place).
|
||||||
|
void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
|
||||||
|
const std::string& text);
|
||||||
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
||||||
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
||||||
// the chat feature is off.
|
// the chat feature is off.
|
||||||
@@ -198,10 +242,16 @@ public:
|
|||||||
data::AddressBook& addressBook() { return address_book_; }
|
data::AddressBook& addressBook() { return address_book_; }
|
||||||
const data::AddressBook& addressBook() const { return address_book_; }
|
const data::AddressBook& addressBook() const { return address_book_; }
|
||||||
|
|
||||||
// Hash of the active wallet's identity (derived from its address list), used to scope
|
// Hash of the active wallet's identity (derived from its address list). This is the tx-history
|
||||||
// per-wallet data (e.g. address-book contacts). Empty until addresses are known (pre-connect).
|
// cache key — it changes when the address set changes, so DON'T scope persistent user data on it.
|
||||||
std::string activeWalletIdentityHash() const;
|
std::string activeWalletIdentityHash() const;
|
||||||
|
|
||||||
|
// Stable per-wallet id ("w:"+hex) for scoping persistent per-wallet data (address-book contacts).
|
||||||
|
// Generated once and persisted in the wallet index (keyed by wallet file), never recomputed from
|
||||||
|
// the mutable address set — so creating addresses / locking / disconnecting never changes it.
|
||||||
|
// Empty only when there is no active wallet file. Establishes + persists the id on first call.
|
||||||
|
std::string activeWalletScopeId();
|
||||||
|
|
||||||
data::WalletIndex& walletIndex() { return wallet_index_; }
|
data::WalletIndex& walletIndex() { return wallet_index_; }
|
||||||
const data::WalletIndex& walletIndex() const { return wallet_index_; }
|
const data::WalletIndex& walletIndex() const { return wallet_index_; }
|
||||||
|
|
||||||
@@ -373,6 +423,9 @@ public:
|
|||||||
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||||||
void startFullUiSweep();
|
void startFullUiSweep();
|
||||||
std::string screenshotFullDir() const;
|
std::string screenshotFullDir() const;
|
||||||
|
// Debug option: restrict either sweep to just the currently-active theme instead of cycling all.
|
||||||
|
bool sweepCurrentThemeOnly() const { return sweep_current_theme_only_; }
|
||||||
|
void setSweepCurrentThemeOnly(bool v) { sweep_current_theme_only_ = v; }
|
||||||
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||||||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||||
@@ -389,6 +442,14 @@ public:
|
|||||||
// True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
|
// True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
|
||||||
// that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
|
// that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
|
||||||
bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
|
bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
|
||||||
|
// Authoritative BIP39-mnemonic status of the ACTIVE wallet, decided at runtime by z_exportmnemonic
|
||||||
|
// (not the offline file probe, which can't tell an HD-but-no-mnemonic wallet from a seed-phrase one).
|
||||||
|
// 0 = unknown/undecidable, 1 = has a seed phrase, 2 = legacy (no mnemonic).
|
||||||
|
int activeWalletSeedBadge() const {
|
||||||
|
if (wallet_seed_status_ == WalletSeedStatus::HasMnemonic) return 1;
|
||||||
|
if (wallet_seed_status_ == WalletSeedStatus::NoMnemonic) return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
void showAboutDialog() { show_about_ = true; }
|
void showAboutDialog() { show_about_ = true; }
|
||||||
|
|
||||||
// Legacy tab compat — maps int to NavPage
|
// Legacy tab compat — maps int to NavPage
|
||||||
@@ -407,6 +468,12 @@ public:
|
|||||||
// Embedded daemon control
|
// Embedded daemon control
|
||||||
bool startEmbeddedDaemon();
|
bool startEmbeddedDaemon();
|
||||||
void stopEmbeddedDaemon();
|
void stopEmbeddedDaemon();
|
||||||
|
// Stop the node specifically for a wallet switch, which MUST free the RPC port to relaunch on
|
||||||
|
// -wallet=<name>. Unlike stopEmbeddedDaemon() (whose DisconnectOnly policy leaves an adopted/external
|
||||||
|
// daemon running), this sends a graceful RPC "stop" — using our own connection creds, so only our
|
||||||
|
// daemon obeys it — even to an adopted daemon, then waits (bounded) for the port to actually release.
|
||||||
|
// Returns true once the RPC port is free (or we're shutting down).
|
||||||
|
bool stopDaemonForWalletSwitch();
|
||||||
bool isEmbeddedDaemonRunning() const;
|
bool isEmbeddedDaemonRunning() const;
|
||||||
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||||||
@@ -449,6 +516,10 @@ public:
|
|||||||
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
||||||
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
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
|
* @brief Reload theme images (background gradient + logo) from new paths
|
||||||
* @param bgPath Path to background image override (empty = use default)
|
* @param bgPath Path to background image override (empty = use default)
|
||||||
@@ -456,6 +527,10 @@ public:
|
|||||||
*/
|
*/
|
||||||
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
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
|
// Wizard / first-run
|
||||||
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
||||||
bool isFirstRun() const;
|
bool isFirstRun() const;
|
||||||
@@ -474,7 +549,11 @@ public:
|
|||||||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||||
void switchToWallet(const std::string& walletFile);
|
// stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from
|
||||||
|
// that dialog). salvage: start the target node with -salvagewallet (repair a corrupt wallet).
|
||||||
|
void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false, bool salvage = false);
|
||||||
|
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
|
||||||
|
void processWalletSwitchRevert();
|
||||||
|
|
||||||
// Wallet encryption helpers
|
// Wallet encryption helpers
|
||||||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
void encryptWalletWithPassphrase(const std::string& passphrase);
|
||||||
@@ -531,6 +610,9 @@ public:
|
|||||||
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||||
void copySecretToClipboard(const std::string& secret);
|
void copySecretToClipboard(const std::string& secret);
|
||||||
void pumpSecretClipboardClear();
|
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 {
|
bool isTransactionRefreshInProgress() const {
|
||||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||||
}
|
}
|
||||||
@@ -572,10 +654,13 @@ private:
|
|||||||
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
|
// 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
|
// 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.
|
// 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,
|
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||||
const std::string& memo, const nlohmann::json& recipients,
|
const std::string& memo, const nlohmann::json& recipients,
|
||||||
const char* traceLabel, bool markFeeGapRetry,
|
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,
|
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||||
const std::string& txid);
|
const std::string& txid);
|
||||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||||
@@ -653,29 +738,135 @@ private:
|
|||||||
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
||||||
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
||||||
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
||||||
|
// Bumped by resetChatSession() on every wallet change; an in-flight identity fetch captures the
|
||||||
|
// value at post time and its completion callback discards its (previous-wallet) secret if the id
|
||||||
|
// no longer matches — so wallet A's seed can't be provisioned under wallet B via a stale job.
|
||||||
|
int chat_session_generation_ = 0;
|
||||||
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
|
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
|
||||||
|
// Per-conversation "last seen" watermark (message timestamp) for unread tracking (Q1). Updated when a
|
||||||
|
// 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.
|
||||||
|
int chatUnreadCount() const;
|
||||||
|
// Mark a conversation read up to latestTs (called by the Chat tab while a thread is displayed).
|
||||||
|
void markChatConversationSeen(const std::string& cid, std::int64_t latestTs);
|
||||||
|
private:
|
||||||
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
|
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
|
||||||
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
|
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
|
||||||
// feature is off, already provisioned, in flight, or unavailable.
|
// feature is off, already provisioned, in flight, or unavailable.
|
||||||
void maybeProvisionChatIdentity();
|
void maybeProvisionChatIdentity();
|
||||||
|
// Drop the in-memory chat identity + decrypted message store, lock the chat DB, and re-arm
|
||||||
|
// provisioning. MUST be called whenever the loaded wallet changes (switch / seed-migration adopt)
|
||||||
|
// so wallet A's private chat can't surface — or be signed with A's keys — under wallet B.
|
||||||
|
void resetChatSession();
|
||||||
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
|
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
|
||||||
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
||||||
void maybeRemindSeedBackup();
|
void maybeRemindSeedBackup();
|
||||||
|
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
|
||||||
|
void maybeWarnLargeWallet(); // full-node: wallet.dat past bloat threshold → one-time toast + clickable alert
|
||||||
|
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
|
||||||
|
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
|
||||||
|
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
|
||||||
|
|
||||||
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
|
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
|
||||||
void beginCreateSeedWallet(); // starts the isolated create on a background thread
|
void beginCreateSeedWallet(); // starts the isolated create on a background thread
|
||||||
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
|
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
|
||||||
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
|
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
|
||||||
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
|
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
|
||||||
|
// W3-3: the terminal callback for the sweep opid, shared by the initial submit and a resume
|
||||||
|
// re-track. `resumed` selects the failure behaviour: a fresh sweep that fails -> Error; a resumed
|
||||||
|
// opid the daemon no longer knows (stale) -> back to the dismissable Sweep gate (re-check balance).
|
||||||
|
std::function<void(bool, const std::string&)> makeSweepCompletionCallback(bool resumed);
|
||||||
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
|
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
|
||||||
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
|
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
|
||||||
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
|
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
|
||||||
void provisionChatIdentityFromSecret(std::string secret);
|
void provisionChatIdentityFromSecret(std::string secret);
|
||||||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
|
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
|
||||||
|
// A spendable z-address that can actually PAY the fee (balance >= fee), preferring the identity
|
||||||
|
// reply address. The reply-to in the memo stays the identity address, so paying from a different
|
||||||
|
// funded note is transport-transparent. Empty if no z-address can cover the fee.
|
||||||
|
std::string chatPayFromZaddr(double fee) const;
|
||||||
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
|
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
|
||||||
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos); // returns true if submitted
|
// Broadcast the memos and, when the async op resolves, flip the echo (echoLocalId) to Sent/Failed.
|
||||||
|
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId); // true if submitted
|
||||||
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
||||||
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
||||||
|
// Full-node 0-conf fast path: re-scan just the chat reply address at minconf=0 every refresh cycle
|
||||||
|
// so incoming messages surface at mempool speed (before a block). Hidden conversations are skipped
|
||||||
|
// (they still un-hide via the normal confirmed harvest). Self-gated; no-op without a chat identity.
|
||||||
|
void fastScanChatMemos();
|
||||||
|
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
|
||||||
|
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
|
||||||
|
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
|
||||||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||||
bool lite_firstrun_dismissed_ = false;
|
bool lite_firstrun_dismissed_ = false;
|
||||||
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||||||
@@ -684,6 +875,16 @@ private:
|
|||||||
bool lite_startup_lock_checked_ = false;
|
bool lite_startup_lock_checked_ = false;
|
||||||
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||||
|
// Serialized async mining-control queue: xmrig start/stop (SIGTERM->SIGKILL->join, up to ~3s) run on
|
||||||
|
// this dedicated FIFO thread instead of the render thread, so the UI never blocks and stop/start
|
||||||
|
// ordering is preserved across the ~13 call sites. (M-03/L-06/L-08/L-09/L-13)
|
||||||
|
std::thread mining_ctl_thread_;
|
||||||
|
std::mutex mining_ctl_mutex_;
|
||||||
|
std::condition_variable mining_ctl_cv_;
|
||||||
|
std::deque<std::function<void()>> mining_ctl_queue_;
|
||||||
|
bool mining_ctl_stop_ = false;
|
||||||
|
void postMiningControl(std::function<void()> job); // enqueue a blocking xmrig op onto the FIFO thread
|
||||||
|
void stopMiningControlThread(); // signal + join the control thread (shutdown)
|
||||||
// Auto-balance runtime state (pool mining, full-node only). The service fetches
|
// Auto-balance runtime state (pool mining, full-node only). The service fetches
|
||||||
// pool hashrates off-thread; the RNG drives the weighted-random pick.
|
// pool hashrates off-thread; the RNG drives the weighted-random pick.
|
||||||
util::PoolStatsService pool_stats_service_;
|
util::PoolStatsService pool_stats_service_;
|
||||||
@@ -716,8 +917,13 @@ private:
|
|||||||
std::atomic<bool> shutting_down_{false};
|
std::atomic<bool> shutting_down_{false};
|
||||||
std::atomic<bool> shutdown_complete_{false};
|
std::atomic<bool> shutdown_complete_{false};
|
||||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||||
std::string shutdown_status_;
|
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
|
||||||
std::thread shutdown_thread_;
|
std::thread shutdown_thread_;
|
||||||
|
// Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm)
|
||||||
|
bool pending_shutdown_confirm_ = false; // a quit is deferred, waiting to open the confirm modal
|
||||||
|
bool daemon_stop_confirm_open_ = false; // the confirm modal is currently showing
|
||||||
|
bool shutdown_confirmed_ = false; // user chose to proceed — bypass the guard on re-entry
|
||||||
|
bool shutdown_keep_daemon_override_ = false; // user chose "keep node running" for this shutdown only
|
||||||
float shutdown_timer_ = 0.0f;
|
float shutdown_timer_ = 0.0f;
|
||||||
bool force_quit_confirm_ = false;
|
bool force_quit_confirm_ = false;
|
||||||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||||||
@@ -725,6 +931,78 @@ private:
|
|||||||
// Daemon restart (e.g. after changing debug log categories)
|
// Daemon restart (e.g. after changing debug log categories)
|
||||||
std::atomic<bool> daemon_restarting_{false};
|
std::atomic<bool> daemon_restarting_{false};
|
||||||
|
|
||||||
|
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
|
||||||
|
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
|
||||||
|
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
|
||||||
|
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
|
||||||
|
std::atomic<bool> wallet_switch_failed_{false};
|
||||||
|
// Distinguishes the failure reason for the revert message: true when the switch failed because the
|
||||||
|
// running node wouldn't release the RPC port in time (not because the target wallet is bad).
|
||||||
|
std::atomic<bool> switch_stop_failed_{false};
|
||||||
|
// True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
|
||||||
|
// instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
|
||||||
|
// loop flags a revert so a broken wallet still can't stick.
|
||||||
|
std::atomic<bool> wallet_switch_pending_confirm_{false};
|
||||||
|
std::string wallet_switch_prev_file_;
|
||||||
|
// Confirm-before-stop for a switch that would stop an ADOPTED (externally-running) node. switchToWallet
|
||||||
|
// sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
|
||||||
|
bool show_switch_stop_daemon_confirm_ = false;
|
||||||
|
std::string pending_switch_wallet_file_;
|
||||||
|
// Block-database recovery: set when the embedded node aborts because its block DB is unreadable
|
||||||
|
// (a daemon-vs-chaindata format mismatch after an update, or a corrupt index). While set, the
|
||||||
|
// connect loop STOPS crash-restarting into the same abort and offers a one-click reindex instead.
|
||||||
|
bool block_db_reindex_available_ = false; // node needs its block DB rebuilt (gates restart loop)
|
||||||
|
bool show_block_db_reindex_confirm_ = false; // auto-shown offer dialog
|
||||||
|
// Wallet auto-recovery: the daemon moved wallet.dat to wallet.<ts>.bak and loaded a salvaged copy
|
||||||
|
// (BDB-verify failure — often a false positive from stale/cross-platform env state). We warn loudly
|
||||||
|
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||||||
|
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||||
|
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||||
|
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||||
|
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
|
||||||
|
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
|
||||||
|
// empty while a sibling wallet file in the datadir still holds keys, warn once so the user's funds
|
||||||
|
// (likely in a wallet.<ts>.bak) aren't mistaken for loss. See maybeWarnEmptyWalletWithFundedSiblings().
|
||||||
|
struct FundedSibling { std::string fileName; int transparentKeys = 0; int shieldedKeys = 0; };
|
||||||
|
bool show_empty_wallet_warning_ = false; // auto-shown warning modal
|
||||||
|
bool empty_wallet_warn_checked_ = false; // evaluated this wallet-open already (reset in onConnected)
|
||||||
|
bool empty_wallet_scan_in_flight_ = false; // a sibling scan is running (main-thread only)
|
||||||
|
bool empty_wallet_has_salvage_bak_ = false; // modal variant: a funded salvage .bak → offer Restore
|
||||||
|
std::vector<FundedSibling> empty_wallet_funded_siblings_; // scan result (main-thread only)
|
||||||
|
// The recovery dialog is the ONE authoritative surface: it stays open through the async rebuild/
|
||||||
|
// restore, driven Offer → Working → Done/Failed (pumpWalletRestore sets the outcome). Presentation
|
||||||
|
// only — the fund-safety file ops in rebuildWalletDatabase()/restoreOriginalWallet() are unchanged.
|
||||||
|
enum class RecoveryPhase { Offer, Working, Done, Failed };
|
||||||
|
RecoveryPhase recovery_phase_ = RecoveryPhase::Offer;
|
||||||
|
int recovery_outcome_sev_ = 0; // 0 ok / 1 warn / 2 error, set at Done/Failed
|
||||||
|
std::string recovery_outcome_msg_; // honest result string for the Done/Failed body
|
||||||
|
bool recovery_last_action_rebuild_ = false; // which handler ran (for "try the other option")
|
||||||
|
// After a successful repair the daemon restarts with a full rescan — minutes long, and it won't
|
||||||
|
// answer RPC yet. This makes the loading overlay show a calm "finishing your wallet repair" screen
|
||||||
|
// (instead of the generic "daemon stuck / RPC timeout / restart daemon" text) and suppresses the
|
||||||
|
// daemon-crash toast. Set on repair success; cleared on connect (onConnected).
|
||||||
|
bool post_recovery_rescan_ = false;
|
||||||
|
double post_recovery_rescan_since_ = 0.0; // stamped on first overlay frame (ImGui::GetTime)
|
||||||
|
// "Restore original wallet" background op: worker sets these under the mutex, pumpWalletRestore()
|
||||||
|
// (main thread) shows the result. 0 = success, 1 = warning, 2 = error.
|
||||||
|
std::mutex wallet_restore_mutex_;
|
||||||
|
bool wallet_restore_done_ = false;
|
||||||
|
int wallet_restore_severity_ = 0;
|
||||||
|
std::string wallet_restore_msg_;
|
||||||
|
// Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
|
||||||
|
// reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
|
||||||
|
// dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
|
||||||
|
// error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI.
|
||||||
|
enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed };
|
||||||
|
std::atomic<int> wallet_switch_phase_{0};
|
||||||
|
std::atomic<bool> wallet_switch_dialog_open_{false};
|
||||||
|
std::string wallet_switch_error_;
|
||||||
|
double wallet_switch_started_time_ = 0.0; // ImGui::GetTime() captured on first progress frame (elapsed)
|
||||||
|
// Set when a failed switch's node output indicates the target wallet is CORRUPT — the Failed modal then
|
||||||
|
// offers a one-click "-salvagewallet" repair, retrying the switch to wallet_switch_target_file_.
|
||||||
|
std::atomic<bool> switch_wallet_corrupt_{false};
|
||||||
|
std::string wallet_switch_target_file_; // the wallet we were switching TO (for a salvage retry)
|
||||||
|
|
||||||
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||||
std::atomic<int> pending_delete_result_{-1};
|
std::atomic<int> pending_delete_result_{-1};
|
||||||
@@ -750,6 +1028,7 @@ private:
|
|||||||
bool seed_backup_loading_ = false;
|
bool seed_backup_loading_ = false;
|
||||||
bool seed_backup_no_mnemonic_ = false;
|
bool seed_backup_no_mnemonic_ = false;
|
||||||
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
||||||
|
bool large_wallet_checked_ = false; // gate: stat wallet.dat for the bloat nudge once per launch
|
||||||
|
|
||||||
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
||||||
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
||||||
@@ -843,10 +1122,22 @@ private:
|
|||||||
bool daemon_start_error_shown_ = false;
|
bool daemon_start_error_shown_ = false;
|
||||||
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
||||||
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
|
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
|
||||||
|
// Sync-settle hysteresis + adaptive balance-poll throttle. Balance polling (z_gettotalbalance) is
|
||||||
|
// O(mapWallet) and holds the daemon's cs_main, which starves block connection on a large shielded
|
||||||
|
// wallet — so we keep the low-impact profile briefly after catching up, and back the balance poll
|
||||||
|
// off in proportion to its own measured cost. See effectivelySyncing() / balanceRefreshDue().
|
||||||
|
bool was_core_syncing_ = false; // previous Core-refresh sync state, to detect the caught-up edge
|
||||||
|
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
|
||||||
|
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
|
||||||
|
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
|
||||||
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
||||||
std::uint64_t clipboard_secret_hash_ = 0;
|
std::uint64_t clipboard_secret_hash_ = 0;
|
||||||
double clipboard_clear_deadline_ = 0.0;
|
double clipboard_clear_deadline_ = 0.0;
|
||||||
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
||||||
|
double connect_stall_since_ = 0.0; // ImGui::GetTime() when the daemon first went "reachable but not ready"; 0 = not stalling (see util/connect_stall.h)
|
||||||
|
bool encryption_incomplete_warned_ = false; // W2-2: once-per-session guard for the "encryption didn't complete" warning
|
||||||
|
bool lock_failure_warned_ = false; // W2-4: guard so a repeatedly-failing auto-lock warns once, not every retry
|
||||||
|
std::uint64_t alerts_seen_total_ = 0; // Notifications::totalPushed() at last alert-panel open; drives the bell's unread dot
|
||||||
|
|
||||||
// Current page (sidebar navigation)
|
// Current page (sidebar navigation)
|
||||||
ui::NavPage current_page_ = ui::NavPage::Overview;
|
ui::NavPage current_page_ = ui::NavPage::Overview;
|
||||||
@@ -856,6 +1147,7 @@ private:
|
|||||||
|
|
||||||
// Debug screenshot sweep state.
|
// Debug screenshot sweep state.
|
||||||
bool screenshot_sweep_active_ = false;
|
bool screenshot_sweep_active_ = false;
|
||||||
|
bool sweep_current_theme_only_ = false; // Debug Options: sweep only the active theme
|
||||||
bool sweep_capture_this_frame_ = false;
|
bool sweep_capture_this_frame_ = false;
|
||||||
int sweep_skin_idx_ = 0;
|
int sweep_skin_idx_ = 0;
|
||||||
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||||||
@@ -915,6 +1207,9 @@ private:
|
|||||||
int logo_h_ = 0;
|
int logo_h_ = 0;
|
||||||
bool logo_loaded_ = false;
|
bool logo_loaded_ = false;
|
||||||
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
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)
|
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
||||||
ImTextureID coin_logo_tex_ = 0;
|
ImTextureID coin_logo_tex_ = 0;
|
||||||
@@ -1042,8 +1337,8 @@ private:
|
|||||||
services::WalletSecurityWorkflow wallet_security_workflow_;
|
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||||||
|
|
||||||
// Wizard: stopping an external daemon before bootstrap
|
// Wizard: stopping an external daemon before bootstrap
|
||||||
bool wizard_stopping_external_ = false;
|
std::atomic<bool> wizard_stopping_external_{false}; // written by the stop worker, read by the UI (L-06)
|
||||||
std::string wizard_stop_status_;
|
GuardedStatus wizard_stop_status_; // thread-safe: written by the stop worker, read by the UI (L-06)
|
||||||
|
|
||||||
// PIN vault
|
// PIN vault
|
||||||
std::unique_ptr<util::SecureVault> vault_;
|
std::unique_ptr<util::SecureVault> vault_;
|
||||||
@@ -1109,6 +1404,13 @@ private:
|
|||||||
|
|
||||||
// Private methods - rendering
|
// Private methods - rendering
|
||||||
void renderStatusBar();
|
void renderStatusBar();
|
||||||
|
// Persistent node/RPC error strip at the top of the content column when the wallet can't
|
||||||
|
// reach its node (or the embedded daemon gave up crashing). Decision logic is the pure
|
||||||
|
// evaluateNodeStatusBanner() in ui/node_status_banner.h; this draws it and wires the action.
|
||||||
|
void renderNodeStatusBanner();
|
||||||
|
// Body of the status-bar alert-history popup: recent alerts (incl. ones whose toast faded),
|
||||||
|
// newest first, with severity icon + relative age + a Clear action. See src/ui/notifications.h.
|
||||||
|
void renderAlertHistoryPanel();
|
||||||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||||
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||||
void renderImportKeyDialog();
|
void renderImportKeyDialog();
|
||||||
@@ -1125,6 +1427,16 @@ private:
|
|||||||
void renderDecryptWalletDialog();
|
void renderDecryptWalletDialog();
|
||||||
void renderPinDialogs();
|
void renderPinDialogs();
|
||||||
void renderAntivirusHelpDialog();
|
void renderAntivirusHelpDialog();
|
||||||
|
void renderSwitchStopDaemonDialog(); // confirm before stopping an adopted node to switch wallets
|
||||||
|
void renderBlockDbReindexDialog(); // offer to rebuild an unreadable block database (-reindex)
|
||||||
|
void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB
|
||||||
|
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
|
||||||
|
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
|
||||||
|
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
|
||||||
|
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
|
||||||
|
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
|
||||||
|
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
|
||||||
|
bool walletRebuildAvailable() const; // the dragonx-wallet-rebuild helper is present
|
||||||
void processDeferredEncryption();
|
void processDeferredEncryption();
|
||||||
|
|
||||||
// Private methods - connection
|
// Private methods - connection
|
||||||
@@ -1155,6 +1467,8 @@ private:
|
|||||||
void refreshPrice();
|
void refreshPrice();
|
||||||
void refreshWalletEncryptionState();
|
void refreshWalletEncryptionState();
|
||||||
void applyRefreshPolicy(ui::NavPage page);
|
void applyRefreshPolicy(ui::NavPage page);
|
||||||
|
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
|
||||||
|
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
|
||||||
bool currentPageNeedsWalletDataRefresh() const;
|
bool currentPageNeedsWalletDataRefresh() const;
|
||||||
bool shouldRunWalletTransactionRefresh() const;
|
bool shouldRunWalletTransactionRefresh() const;
|
||||||
bool shouldRefreshTransactions() const;
|
bool shouldRefreshTransactions() const;
|
||||||
|
|||||||
1979
src/app_network.cpp
1979
src/app_network.cpp
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,10 @@
|
|||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
#include <utility>
|
||||||
|
#include <sodium.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -234,6 +238,41 @@ private:
|
|||||||
// daemon off the main thread (to avoid stalling the UI), or ask the user to
|
// daemon off the main thread (to avoid stalling the UI), or ask the user to
|
||||||
// restart an external daemon. Shared by encryptWalletWithPassphrase() and
|
// restart an external daemon. Shared by encryptWalletWithPassphrase() and
|
||||||
// processDeferredEncryption(); must be called on the main thread.
|
// processDeferredEncryption(); must be called on the main thread.
|
||||||
|
// Zero (overwrite) then delete a plaintext key export so a full cleartext dump of every private key is
|
||||||
|
// never left readable on disk. Idempotent + error-tolerant (safe on a missing/locked file). (H-02)
|
||||||
|
void App::scrubAndRemoveExport(const std::string& path)
|
||||||
|
{
|
||||||
|
if (path.empty()) return;
|
||||||
|
std::error_code ec;
|
||||||
|
const auto sz = std::filesystem::file_size(path, ec);
|
||||||
|
if (!ec && sz > 0) {
|
||||||
|
std::fstream scrub(path, std::ios::binary | std::ios::in | std::ios::out);
|
||||||
|
if (scrub) {
|
||||||
|
const std::vector<char> zeros(static_cast<size_t>(sz), 0);
|
||||||
|
scrub.write(zeros.data(), static_cast<std::streamsize>(sz));
|
||||||
|
scrub.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::filesystem::remove(path, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Startup net for H-02: a crash/kill/early-return between exporting the cleartext keys and scrubbing them
|
||||||
|
// could leave an obsidiandecryptexport* file behind. Purge any found in the data dir on launch.
|
||||||
|
void App::sweepStaleDecryptExports()
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
const std::string dir = util::Platform::getDragonXDataDir();
|
||||||
|
std::filesystem::directory_iterator it(dir, ec), end;
|
||||||
|
for (; it != end; it.increment(ec)) {
|
||||||
|
if (ec) break;
|
||||||
|
const std::string name = it->path().filename().string();
|
||||||
|
if (name.rfind("obsidiandecryptexport", 0) == 0) {
|
||||||
|
scrubAndRemoveExport(it->path().string());
|
||||||
|
DEBUG_LOGF("[decrypt] swept stale plaintext key export: %s\n", name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus) {
|
void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus) {
|
||||||
if (isUsingEmbeddedDaemon()) {
|
if (isUsingEmbeddedDaemon()) {
|
||||||
if (announceRestartStatus) {
|
if (announceRestartStatus) {
|
||||||
@@ -241,16 +280,23 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
|||||||
// the daemon is restarting.
|
// the daemon is restarting.
|
||||||
connection_status_ = TR("restarting_after_encryption");
|
connection_status_ = TR("restarting_after_encryption");
|
||||||
}
|
}
|
||||||
|
// Gate the main-loop reconnect while the daemon is down (so tryConnect can't hit the stopped
|
||||||
|
// node or start a duplicate) and block a concurrent wallet switch/rescan, which check this flag.
|
||||||
|
daemon_restarting_ = true;
|
||||||
// Give daemon a moment to shut down, then restart
|
// Give daemon a moment to shut down, then restart
|
||||||
// (do this off the main thread to avoid stalling the UI)
|
// (do this off the main thread to avoid stalling the UI)
|
||||||
async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) {
|
async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) {
|
||||||
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
// daemon_restarting_ MUST be cleared on every exit (incl. an early-out or a throw), else it
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
// stays stuck true and wedges reconnect + all future switch/rescan/encryption operations.
|
||||||
if (token.cancelled() || shutting_down_) return;
|
try {
|
||||||
stopEmbeddedDaemon();
|
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
||||||
if (token.cancelled() || shutting_down_) return;
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
startEmbeddedDaemon();
|
if (!token.cancelled() && !shutting_down_) {
|
||||||
// tryConnect will be called by the update loop
|
stopEmbeddedDaemon();
|
||||||
|
if (!token.cancelled() && !shutting_down_) startEmbeddedDaemon();
|
||||||
|
}
|
||||||
|
} catch (...) {}
|
||||||
|
daemon_restarting_ = false; // re-arm reconnect (tryConnect runs from the update loop)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
@@ -476,7 +522,17 @@ void App::lockWallet() {
|
|||||||
state_.locked = true;
|
state_.locked = true;
|
||||||
state_.unlocked_until = 0;
|
state_.unlocked_until = 0;
|
||||||
resetTransactionHistoryCacheSession();
|
resetTransactionHistoryCacheSession();
|
||||||
|
lock_failure_warned_ = false;
|
||||||
DEBUG_LOGF("[App] Wallet locked\n");
|
DEBUG_LOGF("[App] Wallet locked\n");
|
||||||
|
} else {
|
||||||
|
// The walletlock RPC failed — the wallet is still UNLOCKED. Surface it (once) rather
|
||||||
|
// than silently leaving an auto-lock unfulfilled and the wallet exposed (W2-4).
|
||||||
|
DEBUG_LOGF("[App] walletlock failed — wallet remains unlocked\n");
|
||||||
|
if (!lock_failure_warned_) {
|
||||||
|
lock_failure_warned_ = true;
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -553,6 +609,12 @@ void App::refreshWalletEncryptionState() {
|
|||||||
state_.unlocked_until = until;
|
state_.unlocked_until = until;
|
||||||
state_.locked = (until == 0);
|
state_.locked = (until == 0);
|
||||||
state_.encryption_state_known = true;
|
state_.encryption_state_known = true;
|
||||||
|
// Wallet is encrypted — any pending deferred-encryption request has now been
|
||||||
|
// satisfied (however it completed). Clear the persisted flag (W2-2).
|
||||||
|
if (settings_ && settings_->getEncryptionPending()) {
|
||||||
|
settings_->setEncryptionPending(false);
|
||||||
|
settings_->save();
|
||||||
|
}
|
||||||
if (state_.locked) {
|
if (state_.locked) {
|
||||||
resetTransactionHistoryCacheSession();
|
resetTransactionHistoryCacheSession();
|
||||||
} else if (state_.transactions.empty()) {
|
} else if (state_.transactions.empty()) {
|
||||||
@@ -565,6 +627,19 @@ void App::refreshWalletEncryptionState() {
|
|||||||
state_.locked = false;
|
state_.locked = false;
|
||||||
state_.unlocked_until = 0;
|
state_.unlocked_until = 0;
|
||||||
state_.encryption_state_known = true;
|
state_.encryption_state_known = true;
|
||||||
|
// W2-2: encryption was requested (persisted flag) but the wallet is NOT encrypted,
|
||||||
|
// and no deferred encryption is pending/in-flight — it was lost to a quit/crash or a
|
||||||
|
// failed connect before it applied. Warn (once/session) instead of silently leaving
|
||||||
|
// an unencrypted wallet the user believes is protected. The flag stays set until the
|
||||||
|
// wallet is actually encrypted, so the warning recurs each launch until resolved.
|
||||||
|
if (settings_ && settings_->getEncryptionPending() &&
|
||||||
|
!wallet_security_.hasDeferredEncryption() && !encrypt_in_progress_ &&
|
||||||
|
!encryption_incomplete_warned_) {
|
||||||
|
encryption_incomplete_warned_ = true;
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
"Wallet encryption did not complete — your wallet is NOT encrypted. "
|
||||||
|
"Open Settings to finish encrypting it.", 30.0f);
|
||||||
|
}
|
||||||
if (state_.transactions.empty()) {
|
if (state_.transactions.empty()) {
|
||||||
loadTransactionHistoryCacheIfAvailable();
|
loadTransactionHistoryCacheIfAvailable();
|
||||||
} else {
|
} else {
|
||||||
@@ -684,6 +759,10 @@ void App::checkIdleMining() {
|
|||||||
// Resolve auto values: active defaults to half, idle defaults to all
|
// Resolve auto values: active defaults to half, idle defaults to all
|
||||||
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
||||||
if (idleThreads <= 0) idleThreads = maxThreads;
|
if (idleThreads <= 0) idleThreads = maxThreads;
|
||||||
|
// Clamp to [1, logical cores] before these reach setgenerate / startPoolMining — a settings field
|
||||||
|
// could otherwise carry an arbitrary count straight past every bound. (M-06)
|
||||||
|
activeThreads = std::clamp(activeThreads, 1, maxThreads);
|
||||||
|
idleThreads = std::clamp(idleThreads, 1, maxThreads);
|
||||||
|
|
||||||
if (systemIdle) {
|
if (systemIdle) {
|
||||||
// System is idle — scale up to idle thread count
|
// System is idle — scale up to idle thread count
|
||||||
@@ -892,8 +971,8 @@ void App::renderLockScreen() {
|
|||||||
cy += captionFont->LegacySize + 12.0f * dp;
|
cy += captionFont->LegacySize + 12.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if PIN vault is available
|
// Check if PIN vault is available (per-wallet vault presence; not the global getPinEnabled flag).
|
||||||
bool hasPinVault = vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled();
|
bool hasPinVault = vault_ && vault_->hasVault();
|
||||||
|
|
||||||
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
||||||
if (hasPinVault) {
|
if (hasPinVault) {
|
||||||
@@ -1186,7 +1265,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||||
|
|
||||||
float barW = ImGui::GetContentRegionAvail().x;
|
float barW = ImGui::GetContentRegionAvail().x;
|
||||||
float barH = 4.0f;
|
float barH = 4.0f * ui::Layout::dpiScale();
|
||||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
||||||
@@ -1236,7 +1315,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
// Indeterminate progress bar
|
// Indeterminate progress bar
|
||||||
{
|
{
|
||||||
float barW = ImGui::GetContentRegionAvail().x;
|
float barW = ImGui::GetContentRegionAvail().x;
|
||||||
float barH = 6.0f;
|
float barH = 6.0f * ui::Layout::dpiScale();
|
||||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
||||||
@@ -1304,9 +1383,13 @@ void App::renderEncryptWalletDialog() {
|
|||||||
enc_dlg_pin_status_.clear();
|
enc_dlg_pin_status_.clear();
|
||||||
std::string savedPass = enc_dlg_saved_passphrase_;
|
std::string savedPass = enc_dlg_saved_passphrase_;
|
||||||
if (worker_ && vault_) {
|
if (worker_ && vault_) {
|
||||||
worker_->post([this, pinStr, savedPass]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, pinStr, savedPass]() mutable -> rpc::RPCWorker::MainCb {
|
||||||
// Argon2id runs here (worker thread)
|
// Argon2id runs here (worker thread)
|
||||||
bool ok = vault_->store(pinStr, savedPass);
|
bool ok = vault_->store(pinStr, savedPass);
|
||||||
|
// Scrub the captured PIN + passphrase copies (they live in the worker's task
|
||||||
|
// queue until this runs); the source member is scrubbed in the MainCb. (L-03)
|
||||||
|
if (!savedPass.empty()) util::SecureVault::secureZero(&savedPass[0], savedPass.size());
|
||||||
|
if (!pinStr.empty()) util::SecureVault::secureZero(&pinStr[0], pinStr.size());
|
||||||
return [this, ok]() {
|
return [this, ok]() {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
settings_->setPinEnabled(true);
|
settings_->setPinEnabled(true);
|
||||||
@@ -1367,6 +1450,11 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
||||||
if (BeginOverlayDialog(ov)) {
|
if (BeginOverlayDialog(ov)) {
|
||||||
|
|
||||||
|
// Same fund-loss consequence as Encrypt/Remove Encryption if the new
|
||||||
|
// passphrase is lost — reuse their warning string/header for consistency.
|
||||||
|
DialogWarningHeader(TR("wiz_encrypt_warning"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||||
ImGui::PushItemWidth(-1);
|
ImGui::PushItemWidth(-1);
|
||||||
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
||||||
@@ -1393,12 +1481,22 @@ void App::renderEncryptWalletDialog() {
|
|||||||
bool valid = strlen(change_old_pass_buf_) > 0 &&
|
bool valid = strlen(change_old_pass_buf_) > 0 &&
|
||||||
strlen(change_new_pass_buf_) >= 8 &&
|
strlen(change_new_pass_buf_) >= 8 &&
|
||||||
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
||||||
|
|
||||||
|
// Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings.
|
||||||
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(btnW, 40))) {
|
||||||
changePassphrase(std::string(change_old_pass_buf_),
|
changePassphrase(std::string(change_old_pass_buf_),
|
||||||
std::string(change_new_pass_buf_));
|
std::string(change_new_pass_buf_));
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
||||||
|
// the !show_change_passphrase_ cleanup block below.
|
||||||
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||||
|
show_change_passphrase_ = false;
|
||||||
|
}
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1471,12 +1569,14 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
// Run entire decrypt flow on worker thread
|
// Run entire decrypt flow on worker thread
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, passphrase = std::move(passphrase)]() mutable -> rpc::RPCWorker::MainCb {
|
||||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||||
[this](rpc::RPCClient& client, const char* context) {
|
[this](rpc::RPCClient& client, const char* context) {
|
||||||
return sendStopCommandSafely(client, context);
|
return sendStopCommandSafely(client, context);
|
||||||
});
|
});
|
||||||
auto unlock = services::WalletSecurityWorkflowExecutor::unlockWallet(passphrase, decryptRpc);
|
auto unlock = services::WalletSecurityWorkflowExecutor::unlockWallet(passphrase, decryptRpc);
|
||||||
|
// Scrub the passphrase — unlock is its only use in this flow.
|
||||||
|
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
|
||||||
if (!unlock.ok) {
|
if (!unlock.ok) {
|
||||||
return [this]() {
|
return [this]() {
|
||||||
wallet_security_workflow_.failEntry("Incorrect passphrase");
|
wallet_security_workflow_.failEntry("Incorrect passphrase");
|
||||||
@@ -1546,6 +1646,11 @@ void App::renderDecryptWalletDialog() {
|
|||||||
std::chrono::steady_clock::now());
|
std::chrono::steady_clock::now());
|
||||||
|
|
||||||
auto restartAndImport = [this, exportPath](const util::AsyncTaskManager::Token& token) {
|
auto restartAndImport = [this, exportPath](const util::AsyncTaskManager::Token& token) {
|
||||||
|
// Scrub + delete the plaintext key export (obsidiandecryptexport…) on EVERY exit path —
|
||||||
|
// success, a restart-failure early return, or an exception. A full cleartext dump of all
|
||||||
|
// private keys must never outlive this step. The startup sweep is a further net for a
|
||||||
|
// crash/kill mid-flight. (H-02)
|
||||||
|
struct ExportScrub { std::string p; ~ExportScrub() { App::scrubAndRemoveExport(p); } } exportScrub{exportPath};
|
||||||
WalletSecurityDaemonAdapter daemonAdapter(*this, token);
|
WalletSecurityDaemonAdapter daemonAdapter(*this, token);
|
||||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||||
[this](rpc::RPCClient& client, const char* context) {
|
[this](rpc::RPCClient& client, const char* context) {
|
||||||
@@ -1599,6 +1704,8 @@ void App::renderDecryptWalletDialog() {
|
|||||||
WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_);
|
WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_);
|
||||||
auto importResult = services::WalletSecurityWorkflowExecutor::importWallet(
|
auto importResult = services::WalletSecurityWorkflowExecutor::importWallet(
|
||||||
importAdapter, exportPath);
|
importAdapter, exportPath);
|
||||||
|
// (exportScrub scrubs + deletes the plaintext key export on scope exit — H-02)
|
||||||
|
|
||||||
if (!importResult.ok) {
|
if (!importResult.ok) {
|
||||||
std::string err = importResult.error;
|
std::string err = importResult.error;
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
@@ -1723,7 +1830,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
// Indeterminate progress bar
|
// Indeterminate progress bar
|
||||||
{
|
{
|
||||||
float barW = ImGui::GetContentRegionAvail().x;
|
float barW = ImGui::GetContentRegionAvail().x;
|
||||||
float barH = 6.0f;
|
float barH = 6.0f * ui::Layout::dpiScale();
|
||||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
||||||
@@ -1853,8 +1960,10 @@ void App::renderPinDialogs() {
|
|||||||
util::SecureVault::isValidPin(pinStr) &&
|
util::SecureVault::isValidPin(pinStr) &&
|
||||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||||
|
|
||||||
|
// Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings.
|
||||||
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = "Verifying passphrase...";
|
pin_status_ = "Verifying passphrase...";
|
||||||
|
|
||||||
@@ -1867,12 +1976,14 @@ void App::renderPinDialogs() {
|
|||||||
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
||||||
|
|
||||||
if (rpc_ && rpc_->isConnected() && worker_) {
|
if (rpc_ && rpc_->isConnected() && worker_) {
|
||||||
worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, passphrase, pin]() mutable -> rpc::RPCWorker::MainCb {
|
||||||
// Verify passphrase via RPC (worker thread)
|
// Verify passphrase via RPC (worker thread)
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Security / PIN setup");
|
rpc::RPCClient::TraceScope trace("Security / PIN setup");
|
||||||
rpc_->call("walletpassphrase", {passphrase, 5});
|
rpc_->call("walletpassphrase", {passphrase, 5});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
||||||
|
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
||||||
return [this]() {
|
return [this]() {
|
||||||
pin_status_ = "Incorrect passphrase";
|
pin_status_ = "Incorrect passphrase";
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
@@ -1881,6 +1992,9 @@ void App::renderPinDialogs() {
|
|||||||
|
|
||||||
// Passphrase correct — store in vault (Argon2id, worker thread)
|
// Passphrase correct — store in vault (Argon2id, worker thread)
|
||||||
bool storeOk = vault_ && vault_->store(pin, passphrase);
|
bool storeOk = vault_ && vault_->store(pin, passphrase);
|
||||||
|
// Captured passphrase + PIN are no longer needed — scrub the worker-queue copies. (M-01)
|
||||||
|
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
||||||
|
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
||||||
|
|
||||||
// Lock wallet back
|
// Lock wallet back
|
||||||
try {
|
try {
|
||||||
@@ -1908,6 +2022,13 @@ void App::renderPinDialogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
||||||
|
// the !show_pin_setup_ cleanup block below.
|
||||||
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||||
|
show_pin_setup_ = false;
|
||||||
|
}
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
|
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
|
||||||
|
|||||||
@@ -170,10 +170,10 @@ void App::installDemoWalletData()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "shielded"; i.label = label; return i;
|
||||||
};
|
};
|
||||||
auto taddr = [](const char* a, double bal, const char* label) {
|
auto taddr = [](const char* a, double bal, const char* label) {
|
||||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
|
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "transparent"; i.label = label; return i;
|
||||||
};
|
};
|
||||||
state_.z_addresses = {
|
state_.z_addresses = {
|
||||||
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
||||||
@@ -376,6 +376,14 @@ void App::buildSweepCatalog()
|
|||||||
add("modal-antivirus", ui::NavPage::Mining,
|
add("modal-antivirus", ui::NavPage::Mining,
|
||||||
[](App& a) { a.pending_antivirus_dialog_ = true; },
|
[](App& a) { a.pending_antivirus_dialog_ = true; },
|
||||||
[](App& a) { a.pending_antivirus_dialog_ = false; });
|
[](App& a) { a.pending_antivirus_dialog_ = false; });
|
||||||
|
add("modal-switch-stopnode", ui::NavPage::Settings,
|
||||||
|
[](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
|
||||||
|
[](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
|
||||||
|
add("modal-switch-progress", ui::NavPage::Settings,
|
||||||
|
[](App& a) { a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::Stopping));
|
||||||
|
a.wallet_switch_dialog_open_.store(true); },
|
||||||
|
[](App& a) { a.wallet_switch_dialog_open_.store(false);
|
||||||
|
a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::None)); });
|
||||||
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
|
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
|
||||||
add("modal-shield", ui::NavPage::Send,
|
add("modal-shield", ui::NavPage::Send,
|
||||||
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
|
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
|
||||||
@@ -696,6 +704,10 @@ void App::startSweepImpl(bool full)
|
|||||||
if (sk.valid) sweep_skins_.push_back(sk.id);
|
if (sk.valid) sweep_skins_.push_back(sk.id);
|
||||||
if (sweep_skins_.empty()) return;
|
if (sweep_skins_.empty()) return;
|
||||||
|
|
||||||
|
// Debug Options "Current theme only": sweep just the active skin instead of cycling every theme.
|
||||||
|
if (sweep_current_theme_only_)
|
||||||
|
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
|
||||||
|
|
||||||
sweep_full_ = full;
|
sweep_full_ = full;
|
||||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||||
buildSweepCatalog();
|
buildSweepCatalog();
|
||||||
|
|||||||
@@ -177,8 +177,36 @@ void App::renderFirstRunWizard() {
|
|||||||
// DPI scale factor — multiply all pixel constants by dp
|
// DPI scale factor — multiply all pixel constants by dp
|
||||||
const float dp = ui::Layout::dpiScale();
|
const float dp = ui::Layout::dpiScale();
|
||||||
|
|
||||||
|
// Vertical scroll: the wizard cards are hand-drawn at absolute Y offsets and grow ~1.5x with the
|
||||||
|
// font-scale setting, so at high scale the focused card's primary button (Continue / Encrypt & Continue
|
||||||
|
// / Skip) can fall below the fixed window. Offset the whole layout by a wheel-driven scroll, clamped to
|
||||||
|
// last frame's measured content height, so every control stays reachable. The window keeps
|
||||||
|
// NoScrollWithMouse, so ImGui doesn't consume the wheel — we read the raw delta and apply our own offset.
|
||||||
|
static float s_wizScroll = 0.0f, s_wizContentH = 0.0f;
|
||||||
|
if (ImGui::IsWindowAppearing()) s_wizScroll = 0.0f;
|
||||||
|
const float wizMaxScroll = std::max(0.0f, s_wizContentH - winSize.y);
|
||||||
|
// Don't steal the wheel from an open combo popup (e.g. the 9-item Language dropdown, which is a
|
||||||
|
// scrollable popup): NoPopupHierarchy stops the popup counting as hovering the wizard, and the
|
||||||
|
// IsPopupOpen guard ensures no wheel is consumed for the whole wizard while any popup is showing.
|
||||||
|
const bool wizPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
|
||||||
|
if (wizMaxScroll > 0.0f && !wizPopupOpen &&
|
||||||
|
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy)) {
|
||||||
|
float wheel = ImGui::GetIO().MouseWheel;
|
||||||
|
if (wheel != 0.0f) s_wizScroll -= wheel * 60.0f * dp;
|
||||||
|
}
|
||||||
|
s_wizScroll = std::max(0.0f, std::min(s_wizScroll, wizMaxScroll));
|
||||||
|
const float scrollY = s_wizScroll;
|
||||||
|
|
||||||
// --- Header: Logo + Welcome ---
|
// --- Header: Logo + Welcome ---
|
||||||
float headerCy = winPos.y + 20.0f * dp;
|
// Vertically center the content when it fits (mirrors the horizontal centering below): on a tall
|
||||||
|
// monitor top-anchoring leaves a large void under the cards. Using last frame's measured block
|
||||||
|
// height, when the content fits inside the window (and we're NOT overflowing, so this doesn't
|
||||||
|
// fight the scroll), push everything down by half the leftover space. No-op once content
|
||||||
|
// fills/exceeds the window (s_wizContentH >= winSize.y ⇒ wizMaxScroll > 0 ⇒ vCenter skipped).
|
||||||
|
float vCenter = 0.0f;
|
||||||
|
if (wizMaxScroll == 0.0f && s_wizContentH > 0.0f && s_wizContentH < winSize.y)
|
||||||
|
vCenter = std::max(0.0f, (winSize.y - s_wizContentH) * 0.5f);
|
||||||
|
float headerCy = winPos.y - scrollY + 20.0f * dp + vCenter;
|
||||||
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
|
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
|
||||||
if (logo_tex_ != 0) {
|
if (logo_tex_ != 0) {
|
||||||
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
||||||
@@ -266,29 +294,43 @@ void App::renderFirstRunWizard() {
|
|||||||
{
|
{
|
||||||
int state = cardState(0);
|
int state = cardState(0);
|
||||||
bool isFocused = (state == 1);
|
bool isFocused = (state == 1);
|
||||||
|
bool isCollapsed = (state == 2); // Completed: minimize to a compact pill (mirrors Card 1)
|
||||||
float cx = leftX + cardPad;
|
float cx = leftX + cardPad;
|
||||||
float cy = card0Top + cardPad;
|
float cy = card0Top + cardPad;
|
||||||
float contentW = colW - 2 * cardPad;
|
float contentW = colW - 2 * cardPad;
|
||||||
|
|
||||||
// Step indicator
|
// Step indicator + title (inline when collapsed)
|
||||||
{
|
if (isCollapsed) {
|
||||||
|
// Compact single-line: check icon + "Step 1" + "Appearance"
|
||||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
float labelX = cx + iconW + 4.0f * dp;
|
||||||
cy += captionFont->LegacySize + 6.0f * dp;
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step1"));
|
||||||
}
|
float step1W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step1")).x;
|
||||||
|
float titleX = labelX + step1W + 12.0f * dp;
|
||||||
|
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_appearance"));
|
||||||
|
cy += captionFont->LegacySize + 4.0f * dp;
|
||||||
|
} else {
|
||||||
|
// Step indicator
|
||||||
|
{
|
||||||
|
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||||
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
||||||
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
{
|
{
|
||||||
const char* t = TR("wiz_appearance");
|
const char* t = TR("wiz_appearance");
|
||||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||||
cy += titleFont->LegacySize + 10.0f * dp;
|
cy += titleFont->LegacySize + 10.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
||||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||||
cy += 14.0f * dp;
|
cy += 14.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
float& wiz_blur_amount = wizardUi.blur_amount;
|
float& wiz_blur_amount = wizardUi.blur_amount;
|
||||||
bool& wiz_theme_effects = wizardUi.theme_effects;
|
bool& wiz_theme_effects = wizardUi.theme_effects;
|
||||||
@@ -324,6 +366,9 @@ void App::renderFirstRunWizard() {
|
|||||||
wiz_appearance_init = true;
|
wiz_appearance_init = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Controls: rendered for the focused and upcoming states so content is visible under
|
||||||
|
// the dim overlay; skipped entirely once completed so the card shrinks to a compact pill.
|
||||||
|
if (!isCollapsed) {
|
||||||
// Render controls always so content is visible under the dim
|
// Render controls always so content is visible under the dim
|
||||||
// overlay when not focused; disable interaction when not active.
|
// overlay when not focused; disable interaction when not active.
|
||||||
ImGui::BeginDisabled(!isFocused);
|
ImGui::BeginDisabled(!isFocused);
|
||||||
@@ -640,13 +685,21 @@ void App::renderFirstRunWizard() {
|
|||||||
cy += btnH;
|
cy += btnH;
|
||||||
}
|
}
|
||||||
|
|
||||||
cy += cardPad;
|
} // if (!isCollapsed)
|
||||||
// Lock card height to the tallest content ever seen
|
|
||||||
float& card0MaxH = wizardUi.card0_max_h;
|
|
||||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
|
||||||
card0Bot = card0Top + card0MaxH;
|
|
||||||
|
|
||||||
// Card 0 finalization deferred until after cards 1+2 are sized
|
cy += cardPad;
|
||||||
|
// Lock card height to the tallest content ever seen (but not when collapsed)
|
||||||
|
float& card0MaxH = wizardUi.card0_max_h;
|
||||||
|
if (isCollapsed) {
|
||||||
|
// Completed: finalize immediately as a compact pill (do not stretch to the
|
||||||
|
// right column height, and skip the deferred stretch below).
|
||||||
|
card0Bot = card0Top + (cy - card0Top);
|
||||||
|
finalizeCard(leftX, colW, card0Top, card0Bot, state);
|
||||||
|
} else {
|
||||||
|
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||||
|
card0Bot = card0Top + card0MaxH;
|
||||||
|
// Card 0 finalization deferred until after cards 1+2 are sized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -889,8 +942,9 @@ void App::renderFirstRunWizard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wizard_stopping_external_) {
|
if (wizard_stopping_external_) {
|
||||||
|
const std::string ws = wizard_stop_status_.get();
|
||||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol,
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol,
|
||||||
wizard_stop_status_.c_str());
|
ws.c_str());
|
||||||
cy += captionFont->LegacySize + 8.0f * dp;
|
cy += captionFont->LegacySize + 8.0f * dp;
|
||||||
} else {
|
} else {
|
||||||
float stopW = 150.0f * dp;
|
float stopW = 150.0f * dp;
|
||||||
@@ -1338,6 +1392,10 @@ void App::renderFirstRunWizard() {
|
|||||||
wallet_security_.beginDeferredEncryption(
|
wallet_security_.beginDeferredEncryption(
|
||||||
std::string(encrypt_pass_buf_),
|
std::string(encrypt_pass_buf_),
|
||||||
(pinEntered && pinOk) ? pinStr : std::string());
|
(pinEntered && pinOk) ? pinStr : std::string());
|
||||||
|
// Persist that encryption was requested (never the passphrase) so a quit/crash or
|
||||||
|
// failed daemon connect before it applies isn't silent — reconciled on the next
|
||||||
|
// connect in refreshWalletEncryptionState (W2-2). Saved with the wizard state below.
|
||||||
|
settings_->setEncryptionPending(true);
|
||||||
|
|
||||||
// Clear sensitive buffers
|
// Clear sensitive buffers
|
||||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
@@ -1372,6 +1430,13 @@ void App::renderFirstRunWizard() {
|
|||||||
encrypt_status_ = TR("wiz_skip_confirm");
|
encrypt_status_ = TR("wiz_skip_confirm");
|
||||||
} else {
|
} else {
|
||||||
s_skipEncConfirm = false;
|
s_skipEncConfirm = false;
|
||||||
|
// Skipping leaves the wallet UNENCRYPTED — wipe the passphrase/PIN the user may have
|
||||||
|
// typed so it doesn't linger in these process-lifetime buffers (only the Encrypt
|
||||||
|
// path cleared them before). (L-07)
|
||||||
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
|
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||||
|
memset(wizard_pin_buf_, 0, sizeof(wizard_pin_buf_));
|
||||||
|
memset(wizard_pin_confirm_buf_, 0, sizeof(wizard_pin_confirm_buf_));
|
||||||
wizard_phase_ = WizardPhase::Done;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
@@ -1407,7 +1472,9 @@ void App::renderFirstRunWizard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Deferred Card 0 finalization: match right column total height ---
|
// --- Deferred Card 0 finalization: match right column total height ---
|
||||||
{
|
// Only for the focused/upcoming Appearance card; a completed one was already finalized
|
||||||
|
// above as a compact pill and must not be re-stretched.
|
||||||
|
if (cardState(0) != 2) {
|
||||||
float rightColBot = card2Bot;
|
float rightColBot = card2Bot;
|
||||||
if (rightColBot > card0Bot) card0Bot = rightColBot;
|
if (rightColBot > card0Bot) card0Bot = rightColBot;
|
||||||
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
|
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
|
||||||
@@ -1416,6 +1483,24 @@ void App::renderFirstRunWizard() {
|
|||||||
// Merge channels: backgrounds → content → overlays
|
// Merge channels: backgrounds → content → overlays
|
||||||
dl->ChannelsMerge();
|
dl->ChannelsMerge();
|
||||||
|
|
||||||
|
// Measure this frame's content height (feeds next frame's scroll clamp) and, when it overflows the
|
||||||
|
// window, draw a slim scroll indicator so the off-screen content is discoverable.
|
||||||
|
{
|
||||||
|
float contentBottom = std::max(card0Bot, std::max(card1Bot, card2Bot));
|
||||||
|
// Subtract vCenter back out: everything below the header was shifted down by it, so the raw
|
||||||
|
// span includes it. We want s_wizContentH to be the true (un-centered) content height, or the
|
||||||
|
// vertical-centering above would feed on itself and oscillate frame-to-frame.
|
||||||
|
s_wizContentH = (contentBottom - winPos.y + scrollY - vCenter) + 24.0f * dp;
|
||||||
|
if (wizMaxScroll > 0.0f && s_wizContentH > 0.0f) {
|
||||||
|
float trackH = winSize.y - 8.0f * dp;
|
||||||
|
float thumbH = std::min(trackH, std::max(32.0f * dp, trackH * (winSize.y / s_wizContentH)));
|
||||||
|
float thumbY = winPos.y + 4.0f * dp + (trackH - thumbH) * (scrollY / wizMaxScroll);
|
||||||
|
float barX = winPos.x + winSize.x - 6.0f * dp;
|
||||||
|
dl->AddRectFilled(ImVec2(barX, thumbY), ImVec2(barX + 3.0f * dp, thumbY + thumbH),
|
||||||
|
ui::material::WithAlpha(ui::material::OnSurface(), 55), 1.5f * dp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,35 @@ bool ChatDatabase::append(const ChatMessage& message)
|
|||||||
return sqlite3_changes(db_) > 0;
|
return sqlite3_changes(db_) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::upsert(const ChatMessage& message)
|
||||||
|
{
|
||||||
|
if (!key_ready_ || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
std::vector<unsigned char> nonce;
|
||||||
|
std::vector<unsigned char> cipher;
|
||||||
|
std::string plain = serialize(message);
|
||||||
|
const bool encrypted = encrypt(plain, nonce, cipher);
|
||||||
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||||
|
if (!encrypted) return false;
|
||||||
|
|
||||||
|
const std::string dedup = dedupHash(message.txid, message.payload_position);
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db_,
|
||||||
|
"INSERT INTO chat_messages (wallet_tag, dedup_hash, nonce, payload) VALUES (?, ?, ?, ?) "
|
||||||
|
"ON CONFLICT(wallet_tag, dedup_hash) DO UPDATE SET nonce=excluded.nonce, payload=excluded.payload",
|
||||||
|
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_blob(stmt, 3, nonce.data(), static_cast<int>(nonce.size()), SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_blob(stmt, 4, cipher.data(), static_cast<int>(cipher.size()), SQLITE_TRANSIENT);
|
||||||
|
const bool done = sqlite3_step(stmt) == SQLITE_DONE;
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ChatMessage> ChatDatabase::load()
|
std::vector<ChatMessage> ChatDatabase::load()
|
||||||
{
|
{
|
||||||
std::vector<ChatMessage> out;
|
std::vector<ChatMessage> out;
|
||||||
@@ -199,6 +228,17 @@ bool ChatDatabase::ensureOpen()
|
|||||||
exec("PRAGMA journal_mode=WAL");
|
exec("PRAGMA journal_mode=WAL");
|
||||||
exec("PRAGMA synchronous=NORMAL");
|
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()) {
|
if (!createSchema()) {
|
||||||
close();
|
close();
|
||||||
return false;
|
return false;
|
||||||
@@ -267,7 +307,10 @@ bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
|||||||
out.body = parsed.value("b", std::string());
|
out.body = parsed.value("b", std::string());
|
||||||
out.timestamp = parsed.value("ts", static_cast<std::int64_t>(0));
|
out.timestamp = parsed.value("ts", static_cast<std::int64_t>(0));
|
||||||
out.payload_position = static_cast<std::size_t>(parsed.value("pos", static_cast<std::uint64_t>(0)));
|
out.payload_position = static_cast<std::size_t>(parsed.value("pos", static_cast<std::uint64_t>(0)));
|
||||||
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent
|
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent (0)
|
||||||
|
// A persisted "Sending" means we crashed mid-broadcast; the outcome is unknown. Resolve it
|
||||||
|
// optimistically to Sent on load so it can't show a stuck spinner forever.
|
||||||
|
if (out.delivery == ChatDelivery::Sending) out.delivery = ChatDelivery::Sent;
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception&) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ public:
|
|||||||
// Returns true if newly inserted; false on duplicate or while locked.
|
// Returns true if newly inserted; false on duplicate or while locked.
|
||||||
bool append(const ChatMessage& message);
|
bool append(const ChatMessage& message);
|
||||||
|
|
||||||
|
// Persist-or-overwrite one message by its (txid+position) dedup key. Unlike append(), this
|
||||||
|
// updates an existing row's payload — used for outgoing echoes whose delivery status changes
|
||||||
|
// (Sending → Sent/Failed). Returns true on success; false while locked / on error.
|
||||||
|
bool upsert(const ChatMessage& message);
|
||||||
|
|
||||||
// Decrypt and return every stored message for the unlocked wallet, in insertion order. Empty
|
// Decrypt and return every stored message for the unlocked wallet, in insertion order. Empty
|
||||||
// while locked or if none. Rows that fail to decrypt/parse are skipped.
|
// while locked or if none. Rows that fail to decrypt/parse are skipped.
|
||||||
std::vector<ChatMessage> load();
|
std::vector<ChatMessage> load();
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ namespace dragonx::chat {
|
|||||||
|
|
||||||
enum class ChatDirection { Incoming, Outgoing };
|
enum class ChatDirection { Incoming, Outgoing };
|
||||||
enum class ChatMessageKind { Message, ContactRequest };
|
enum class ChatMessageKind { Message, ContactRequest };
|
||||||
// Outgoing delivery: Sent = the broadcast was submitted; Failed = it wasn't (not connected, no
|
// Outgoing delivery status. Sending = broadcast in flight (async op not yet resolved); Sent = the
|
||||||
// spendable address, a send already in progress). Always Sent for incoming.
|
// daemon accepted + broadcast the tx; Failed = it didn't (not connected, no funded address, rejected).
|
||||||
enum class ChatDelivery { Sent, Failed };
|
// Always Sent for incoming. NB: the values are persisted (chat DB serializes the int), so Sent MUST
|
||||||
|
// stay 0 and new states are APPENDED — never reordered.
|
||||||
|
enum class ChatDelivery { Sent, Failed, Sending };
|
||||||
|
|
||||||
struct ChatMessage {
|
struct ChatMessage {
|
||||||
ChatDirection direction = ChatDirection::Incoming;
|
ChatDirection direction = ChatDirection::Incoming;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
|||||||
const std::string& conversationId,
|
const std::string& conversationId,
|
||||||
const char* type,
|
const char* type,
|
||||||
const std::string& streamHeaderHex,
|
const std::string& streamHeaderHex,
|
||||||
const std::string& publicKeyHex)
|
const std::string& publicKeyHex,
|
||||||
|
std::int64_t sentAt)
|
||||||
{
|
{
|
||||||
nlohmann::json header;
|
nlohmann::json header;
|
||||||
header["h"] = 1; // header number (>= 1)
|
header["h"] = 1; // header number (>= 1)
|
||||||
@@ -26,6 +27,7 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
|||||||
header["t"] = type; // "Memo" or "Cont"
|
header["t"] = type; // "Memo" or "Cont"
|
||||||
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
||||||
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
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();
|
return header.dump();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +69,8 @@ ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
|||||||
|
|
||||||
OutgoingChatMemos memos;
|
OutgoingChatMemos memos;
|
||||||
memos.recipientZaddr = peerZaddr;
|
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;
|
memos.payloadMemo = ciphertextHex;
|
||||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||||
@@ -95,7 +98,8 @@ ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
|||||||
|
|
||||||
OutgoingChatMemos memos;
|
OutgoingChatMemos memos;
|
||||||
memos.recipientZaddr = peerZaddr;
|
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;
|
memos.payloadMemo = requestText;
|
||||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||||
memos.payloadMemo.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, "t", type, error)) return fail(error);
|
||||||
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
||||||
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
|
if (!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.header_number < 1) return fail("header number must be positive");
|
||||||
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
|
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.sender_public_key_hex = pair.header.public_key_hex;
|
||||||
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
||||||
metadata.payload_memo = pair.payload_memo;
|
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));
|
result.metadata.push_back(std::move(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ struct HushChatHeader {
|
|||||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
std::string secretstream_header_hex;
|
std::string secretstream_header_hex;
|
||||||
std::string public_key_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 {
|
struct HushChatHeaderParseResult {
|
||||||
@@ -80,6 +83,7 @@ struct HushChatTransactionMetadata {
|
|||||||
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
|
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 secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||||
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (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 {
|
struct HushChatTransactionExtractionResult {
|
||||||
|
|||||||
@@ -25,19 +25,41 @@ void ChatService::clearIdentity() {
|
|||||||
|
|
||||||
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||||
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||||
std::int64_t fallbackTimestamp) {
|
std::int64_t fallbackTimestamp,
|
||||||
|
std::vector<std::string>* newIncomingCids) {
|
||||||
if (!has_identity_) return 0;
|
if (!has_identity_) return 0;
|
||||||
|
|
||||||
|
const std::string myPubKey = chatIdentityPublicKeyHex(identity_);
|
||||||
|
|
||||||
int added = 0;
|
int added = 0;
|
||||||
for (const auto& meta : metadata) {
|
for (const auto& meta : metadata) {
|
||||||
|
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
|
||||||
|
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
|
||||||
|
// "from peer" message. (This also collapses same-seed self-chat, where the "peer" wallet shares
|
||||||
|
// our identity, so every message would otherwise loop back.)
|
||||||
|
if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;
|
||||||
|
|
||||||
ChatMessage message;
|
ChatMessage message;
|
||||||
message.direction = ChatDirection::Incoming;
|
message.direction = ChatDirection::Incoming;
|
||||||
message.txid = meta.txid;
|
message.txid = meta.txid;
|
||||||
message.conversation_id = meta.conversation_id;
|
message.conversation_id = meta.conversation_id;
|
||||||
message.peer_zaddr = meta.reply_zaddr;
|
message.peer_zaddr = meta.reply_zaddr;
|
||||||
message.peer_public_key_hex = meta.sender_public_key_hex;
|
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);
|
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;
|
message.payload_position = meta.payload_position;
|
||||||
|
|
||||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||||
@@ -59,6 +81,9 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
|||||||
if (store_.append(message)) {
|
if (store_.append(message)) {
|
||||||
if (db_) db_->append(message);
|
if (db_) db_->append(message);
|
||||||
++added;
|
++added;
|
||||||
|
// Every ingested message is incoming — report its cid so the caller can notify without
|
||||||
|
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
|
||||||
|
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return added;
|
return added;
|
||||||
@@ -105,4 +130,18 @@ bool ChatService::recordOutgoing(const ChatMessage& message) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChatService::recordOutgoingPending(const ChatMessage& message) {
|
||||||
|
// Persist immediately (as Sending) so a send survives an app quit before the broadcast resolves;
|
||||||
|
// resolveOutgoing() then UPSERTS the row to the final status. A stray persisted Sending (crash mid-
|
||||||
|
// broadcast) loads as Sent (see ChatDatabase::deserialize).
|
||||||
|
const bool appended = store_.append(message);
|
||||||
|
if (appended && db_) db_->upsert(message);
|
||||||
|
return appended;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery) {
|
||||||
|
const ChatMessage* updated = store_.updateDelivery(txid, delivery);
|
||||||
|
if (updated && db_) db_->upsert(*updated); // overwrite the Sending row with the final status
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace dragonx::chat
|
} // namespace dragonx::chat
|
||||||
|
|||||||
@@ -40,9 +40,13 @@ public:
|
|||||||
// when the txid isn't present. Newly-added messages are also persisted (if a database is
|
// when the txid isn't present. Newly-added messages are also persisted (if a database is
|
||||||
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
|
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
|
||||||
// Messages are dropped silently (no logging of memo/plaintext).
|
// Messages are dropped silently (no logging of memo/plaintext).
|
||||||
|
// When `newIncomingCids` is non-null it is filled with the conversation ids of the genuinely-new
|
||||||
|
// incoming (non-request) messages appended this call — a reliable "a new message arrived here"
|
||||||
|
// signal for notifications that doesn't depend on any timestamp/seen-watermark comparison.
|
||||||
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||||
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||||
std::int64_t fallbackTimestamp = 0);
|
std::int64_t fallbackTimestamp = 0,
|
||||||
|
std::vector<std::string>* newIncomingCids = nullptr);
|
||||||
|
|
||||||
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
||||||
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
||||||
@@ -72,6 +76,12 @@ public:
|
|||||||
// only local record of what we sent.)
|
// only local record of what we sent.)
|
||||||
bool recordOutgoing(const ChatMessage& message);
|
bool recordOutgoing(const ChatMessage& message);
|
||||||
|
|
||||||
|
// Two-phase echo for delivery tracking: record + persist immediately as Sending (survives an app
|
||||||
|
// quit), then resolveOutgoing() upserts the final status once the broadcast completes. A stray
|
||||||
|
// persisted Sending (crash mid-broadcast) loads back as Sent.
|
||||||
|
bool recordOutgoingPending(const ChatMessage& message);
|
||||||
|
void resolveOutgoing(const std::string& txid, ChatDelivery delivery);
|
||||||
|
|
||||||
const ChatStore& store() const { return store_; }
|
const ChatStore& store() const { return store_; }
|
||||||
ChatStore& store() { return store_; }
|
ChatStore& store() { return store_; }
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include "chat_store.h"
|
#include "chat_store.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace dragonx::chat {
|
namespace dragonx::chat {
|
||||||
|
|
||||||
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
||||||
@@ -19,9 +21,29 @@ std::vector<ChatMessage> ChatStore::conversation(const std::string& conversation
|
|||||||
for (const auto& message : messages_) {
|
for (const auto& message : messages_) {
|
||||||
if (message.conversation_id == conversationId) out.push_back(message);
|
if (message.conversation_id == conversationId) out.push_back(message);
|
||||||
}
|
}
|
||||||
|
// Return chronological (oldest→newest). messages_ is in scan/insertion order, which is NOT
|
||||||
|
// time-ordered (a full scan harvests txids in std::set/unordered_map order) — that would mis-order the
|
||||||
|
// rendered thread AND let the reply-target pin (first-seen peer address) latch onto a non-establishing
|
||||||
|
// message (B2). timestamp is the block/tx time (peer can't set it); tie-break txid + payload_position
|
||||||
|
// for determinism.
|
||||||
|
std::stable_sort(out.begin(), out.end(), [](const ChatMessage& a, const ChatMessage& b) {
|
||||||
|
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
|
||||||
|
if (a.txid != b.txid) return a.txid < b.txid;
|
||||||
|
return a.payload_position < b.payload_position;
|
||||||
|
});
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery) {
|
||||||
|
for (auto& message : messages_) {
|
||||||
|
if (message.txid == txid) {
|
||||||
|
message.delivery = delivery;
|
||||||
|
return &message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> ChatStore::conversationIds() const {
|
std::vector<std::string> ChatStore::conversationIds() const {
|
||||||
std::vector<std::string> ids;
|
std::vector<std::string> ids;
|
||||||
std::unordered_set<std::string> seenIds;
|
std::unordered_set<std::string> seenIds;
|
||||||
|
|||||||
@@ -21,9 +21,23 @@ public:
|
|||||||
// Messages in a conversation, in insertion order.
|
// Messages in a conversation, in insertion order.
|
||||||
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
||||||
|
|
||||||
|
// Update an outgoing echo's delivery status by its local txid. Returns a pointer to the updated
|
||||||
|
// message (for the caller to persist), or nullptr if no message has that txid.
|
||||||
|
const ChatMessage* updateDelivery(const std::string& txid, ChatDelivery delivery);
|
||||||
|
|
||||||
// Distinct conversation ids, in first-seen order.
|
// Distinct conversation ids, in first-seen order.
|
||||||
std::vector<std::string> conversationIds() const;
|
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(); }
|
std::size_t size() const { return messages_.size(); }
|
||||||
bool empty() const { return messages_.empty(); }
|
bool empty() const { return messages_.empty(); }
|
||||||
void clear();
|
void clear();
|
||||||
|
|||||||
@@ -147,6 +147,26 @@ bool Settings::load(const std::string& path)
|
|||||||
loadScalar(j, "language", language_);
|
loadScalar(j, "language", language_);
|
||||||
loadScalar(j, "skin_id", skin_id_);
|
loadScalar(j, "skin_id", skin_id_);
|
||||||
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
||||||
|
if (j.contains("muted_chat_cids") && j["muted_chat_cids"].is_array()) {
|
||||||
|
muted_chat_cids_.clear();
|
||||||
|
for (const auto& c : j["muted_chat_cids"])
|
||||||
|
if (c.is_string()) muted_chat_cids_.push_back(c.get<std::string>());
|
||||||
|
}
|
||||||
|
if (j.contains("hidden_chat_cids") && j["hidden_chat_cids"].is_array()) {
|
||||||
|
hidden_chat_cids_.clear();
|
||||||
|
for (const auto& c : j["hidden_chat_cids"])
|
||||||
|
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
|
||||||
|
}
|
||||||
|
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||||
|
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
||||||
|
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
||||||
|
loadScalar(j, "chat_bubble_style", chat_bubble_style_); setChatBubbleStyle(chat_bubble_style_);
|
||||||
|
loadScalar(j, "chat_bubble_accent", chat_bubble_accent_); setChatBubbleAccent(chat_bubble_accent_);
|
||||||
|
loadScalar(j, "chat_density", chat_density_); setChatDensity(chat_density_);
|
||||||
|
loadScalar(j, "chat_font_scale", chat_font_scale_); setChatFontScale(chat_font_scale_);
|
||||||
|
loadScalar(j, "chat_time_format", chat_time_format_); setChatTimeFormat(chat_time_format_);
|
||||||
|
loadScalar(j, "chat_enter_sends", chat_enter_sends_);
|
||||||
|
loadScalar(j, "time_format", time_format_); setTimeFormat(time_format_);
|
||||||
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||||
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||||
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||||
@@ -176,12 +196,15 @@ bool Settings::load(const std::string& path)
|
|||||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||||
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||||
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
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, "animate_avatars", animate_avatars_);
|
||||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||||
loadScalar(j, "console_text_color", console_text_color_);
|
loadScalar(j, "console_text_color", console_text_color_);
|
||||||
loadScalar(j, "console_zoom", console_zoom_);
|
loadScalar(j, "console_zoom", console_zoom_);
|
||||||
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
|
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
|
||||||
|
loadScalar(j, "console_auto_focus", console_auto_focus_);
|
||||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||||
hidden_addresses_.clear();
|
hidden_addresses_.clear();
|
||||||
for (const auto& a : j["hidden_addresses"])
|
for (const auto& a : j["hidden_addresses"])
|
||||||
@@ -209,12 +232,20 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||||
|
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
|
||||||
|
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
||||||
|
empty_wallet_warning_acked_.clear();
|
||||||
|
for (const auto& w : j["empty_wallet_warning_acked"])
|
||||||
|
if (w.is_string()) empty_wallet_warning_acked_.insert(w.get<std::string>());
|
||||||
|
}
|
||||||
|
loadScalar(j, "encryption_pending", encryption_pending_);
|
||||||
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
|
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
|
||||||
loadScalar(j, "active_wallet_file", active_wallet_file_);
|
loadScalar(j, "active_wallet_file", active_wallet_file_);
|
||||||
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
|
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
|
||||||
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
|
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
|
||||||
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
|
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
|
||||||
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
|
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
|
||||||
|
loadScalar(j, "seed_migration_sweep_opid", seed_migration_sweep_opid_);
|
||||||
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
|
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
|
||||||
loadScalar(j, "unlock_duration", unlock_duration_);
|
loadScalar(j, "unlock_duration", unlock_duration_);
|
||||||
loadScalar(j, "pin_enabled", pin_enabled_);
|
loadScalar(j, "pin_enabled", pin_enabled_);
|
||||||
@@ -422,6 +453,21 @@ bool Settings::save(const std::string& path)
|
|||||||
j["language"] = language_;
|
j["language"] = language_;
|
||||||
j["skin_id"] = skin_id_;
|
j["skin_id"] = skin_id_;
|
||||||
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
||||||
|
j["muted_chat_cids"] = json::array();
|
||||||
|
for (const auto& c : muted_chat_cids_)
|
||||||
|
j["muted_chat_cids"].push_back(c);
|
||||||
|
j["hidden_chat_cids"] = json::array();
|
||||||
|
for (const auto& c : hidden_chat_cids_)
|
||||||
|
j["hidden_chat_cids"].push_back(c);
|
||||||
|
j["chat_emoji_color"] = chat_emoji_color_;
|
||||||
|
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||||
|
j["chat_bubble_style"] = chat_bubble_style_;
|
||||||
|
j["chat_bubble_accent"] = chat_bubble_accent_;
|
||||||
|
j["chat_density"] = chat_density_;
|
||||||
|
j["chat_font_scale"] = chat_font_scale_;
|
||||||
|
j["chat_time_format"] = chat_time_format_;
|
||||||
|
j["chat_enter_sends"] = chat_enter_sends_;
|
||||||
|
j["time_format"] = time_format_;
|
||||||
j["acrylic_enabled"] = acrylic_enabled_;
|
j["acrylic_enabled"] = acrylic_enabled_;
|
||||||
j["acrylic_quality"] = acrylic_quality_;
|
j["acrylic_quality"] = acrylic_quality_;
|
||||||
j["blur_multiplier"] = blur_multiplier_;
|
j["blur_multiplier"] = blur_multiplier_;
|
||||||
@@ -432,11 +478,14 @@ bool Settings::save(const std::string& path)
|
|||||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||||
j["portfolio_style"] = portfolio_style_;
|
j["portfolio_style"] = portfolio_style_;
|
||||||
j["contacts_view_mode"] = contacts_view_mode_;
|
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["animate_avatars"] = animate_avatars_;
|
||||||
j["scanline_enabled"] = scanline_enabled_;
|
j["scanline_enabled"] = scanline_enabled_;
|
||||||
j["console_line_accents"] = console_line_accents_;
|
j["console_line_accents"] = console_line_accents_;
|
||||||
j["console_text_color"] = console_text_color_;
|
j["console_text_color"] = console_text_color_;
|
||||||
j["console_zoom"] = console_zoom_;
|
j["console_zoom"] = console_zoom_;
|
||||||
|
j["console_auto_focus"] = console_auto_focus_;
|
||||||
j["hidden_addresses"] = json::array();
|
j["hidden_addresses"] = json::array();
|
||||||
for (const auto& addr : hidden_addresses_)
|
for (const auto& addr : hidden_addresses_)
|
||||||
j["hidden_addresses"].push_back(addr);
|
j["hidden_addresses"].push_back(addr);
|
||||||
@@ -458,12 +507,18 @@ bool Settings::save(const std::string& path)
|
|||||||
}
|
}
|
||||||
j["wizard_completed"] = wizard_completed_;
|
j["wizard_completed"] = wizard_completed_;
|
||||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||||
|
j["large_wallet_warned"] = large_wallet_warned_;
|
||||||
|
j["empty_wallet_warning_acked"] = json::array();
|
||||||
|
for (const auto& w : empty_wallet_warning_acked_)
|
||||||
|
j["empty_wallet_warning_acked"].push_back(w);
|
||||||
|
j["encryption_pending"] = encryption_pending_;
|
||||||
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
|
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
|
||||||
j["active_wallet_file"] = active_wallet_file_;
|
j["active_wallet_file"] = active_wallet_file_;
|
||||||
j["seed_migration_pending"] = seed_migration_pending_;
|
j["seed_migration_pending"] = seed_migration_pending_;
|
||||||
j["seed_migration_dest"] = seed_migration_dest_;
|
j["seed_migration_dest"] = seed_migration_dest_;
|
||||||
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
|
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
|
||||||
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
|
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
|
||||||
|
j["seed_migration_sweep_opid"] = seed_migration_sweep_opid_;
|
||||||
j["auto_lock_timeout"] = auto_lock_timeout_;
|
j["auto_lock_timeout"] = auto_lock_timeout_;
|
||||||
j["unlock_duration"] = unlock_duration_;
|
j["unlock_duration"] = unlock_duration_;
|
||||||
j["pin_enabled"] = pin_enabled_;
|
j["pin_enabled"] = pin_enabled_;
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ public:
|
|||||||
bool showValue = true; // show the converted/fiat value on the card
|
bool showValue = true; // show the converted/fiat value on the card
|
||||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||||
bool showSparkline = false; // show a price-trend sparkline (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
|
// 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
|
// 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.
|
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||||
@@ -118,6 +119,52 @@ public:
|
|||||||
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
||||||
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
||||||
|
|
||||||
|
// Muted chat conversations (by cid) — muted conversations don't badge or raise a toast (Q10).
|
||||||
|
bool isChatMuted(const std::string& cid) const {
|
||||||
|
return std::find(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid) != muted_chat_cids_.end();
|
||||||
|
}
|
||||||
|
void setChatMuted(const std::string& cid, bool muted) {
|
||||||
|
const bool already = isChatMuted(cid);
|
||||||
|
if (muted && !already) muted_chat_cids_.push_back(cid);
|
||||||
|
else if (!muted && already)
|
||||||
|
muted_chat_cids_.erase(std::remove(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid),
|
||||||
|
muted_chat_cids_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden chat conversations (by cid) — hidden ones are filtered out of the list; a new incoming
|
||||||
|
// message un-hides them (you can't un-receive) so nothing is silently lost.
|
||||||
|
bool isChatHidden(const std::string& cid) const {
|
||||||
|
return std::find(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid) != hidden_chat_cids_.end();
|
||||||
|
}
|
||||||
|
void setChatHidden(const std::string& cid, bool hidden) {
|
||||||
|
const bool already = isChatHidden(cid);
|
||||||
|
if (hidden && !already) hidden_chat_cids_.push_back(cid);
|
||||||
|
else if (!hidden && already)
|
||||||
|
hidden_chat_cids_.erase(std::remove(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid),
|
||||||
|
hidden_chat_cids_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||||
|
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||||
|
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||||
|
float getChatPollRateSec() const { return chat_poll_rate_sec_; }
|
||||||
|
void setChatPollRateSec(float v) { chat_poll_rate_sec_ = std::max(0.5f, std::min(15.0f, v)); }
|
||||||
|
int getChatBubbleStyle() const { return chat_bubble_style_; }
|
||||||
|
void setChatBubbleStyle(int v) { chat_bubble_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
int getChatBubbleAccent() const { return chat_bubble_accent_; }
|
||||||
|
void setChatBubbleAccent(int v) { chat_bubble_accent_ = (v < 0 || v > 5) ? 0 : v; }
|
||||||
|
int getChatDensity() const { return chat_density_; }
|
||||||
|
void setChatDensity(int v) { chat_density_ = (v < 0 || v > 1) ? 0 : v; }
|
||||||
|
float getChatFontScale() const { return chat_font_scale_; }
|
||||||
|
void setChatFontScale(float v) { chat_font_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||||
|
int getChatTimeFormat() const { return chat_time_format_; } // 0=follow global, 1=24h, 2=12h
|
||||||
|
void setChatTimeFormat(int v) { chat_time_format_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
bool getChatEnterSends() const { return chat_enter_sends_; }
|
||||||
|
void setChatEnterSends(bool v) { chat_enter_sends_ = v; }
|
||||||
|
// Global clock format (0=24h, 1=12h) — chat can override it for the Chat tab only.
|
||||||
|
int getTimeFormat() const { return time_format_; }
|
||||||
|
void setTimeFormat(int v) { time_format_ = (v < 0 || v > 1) ? 0 : v; }
|
||||||
|
|
||||||
// Privacy
|
// Privacy
|
||||||
bool getSaveZtxs() const { return save_ztxs_; }
|
bool getSaveZtxs() const { return save_ztxs_; }
|
||||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||||
@@ -180,13 +227,19 @@ public:
|
|||||||
std::string getBalanceLayout() const { return balance_layout_; }
|
std::string getBalanceLayout() const { return balance_layout_; }
|
||||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
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
|
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
|
||||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
|
||||||
int getPortfolioStyle() const { return portfolio_style_; }
|
int getPortfolioStyle() const { return portfolio_style_; }
|
||||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||||
int getContactsViewMode() const { return contacts_view_mode_; }
|
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||||
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
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.
|
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
|
||||||
bool getAnimateAvatars() const { return animate_avatars_; }
|
bool getAnimateAvatars() const { return animate_avatars_; }
|
||||||
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
||||||
@@ -203,6 +256,9 @@ public:
|
|||||||
void setConsoleTextColor(bool v) { console_text_color_ = v; }
|
void setConsoleTextColor(bool v) { console_text_color_ = v; }
|
||||||
float getConsoleZoom() const { return console_zoom_; }
|
float getConsoleZoom() const { return console_zoom_; }
|
||||||
void setConsoleZoom(float v) { console_zoom_ = v; }
|
void setConsoleZoom(float v) { console_zoom_ = v; }
|
||||||
|
// Auto-place the text cursor in the command box when the Console tab is opened.
|
||||||
|
bool getConsoleAutoFocus() const { return console_auto_focus_; }
|
||||||
|
void setConsoleAutoFocus(bool v) { console_auto_focus_ = v; }
|
||||||
|
|
||||||
// Hidden addresses (addresses hidden from the UI by the user)
|
// Hidden addresses (addresses hidden from the UI by the user)
|
||||||
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
|
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
|
||||||
@@ -274,6 +330,24 @@ public:
|
|||||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||||
|
|
||||||
|
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
|
||||||
|
bool getLargeWalletWarned() const { return large_wallet_warned_; }
|
||||||
|
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
|
||||||
|
|
||||||
|
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
|
||||||
|
// warning has been dismissed. Keyed per active wallet file so switching to a different empty
|
||||||
|
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
|
||||||
|
bool isEmptyWalletWarnAcked(const std::string& walletFile) const {
|
||||||
|
return empty_wallet_warning_acked_.count(walletFile) > 0;
|
||||||
|
}
|
||||||
|
void ackEmptyWalletWarn(const std::string& walletFile) { empty_wallet_warning_acked_.insert(walletFile); }
|
||||||
|
|
||||||
|
// Persisted the moment deferred (wizard) encryption is requested; cleared only once the wallet is
|
||||||
|
// observed to be actually encrypted. Lets a quit/crash/failed-connect before it applies be detected
|
||||||
|
// and surfaced (W2-2). NEVER stores the passphrase — only the fact that encryption was requested.
|
||||||
|
bool getEncryptionPending() const { return encryption_pending_; }
|
||||||
|
void setEncryptionPending(bool v) { encryption_pending_ = v; }
|
||||||
|
|
||||||
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
|
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
|
||||||
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
|
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
|
||||||
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
|
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
|
||||||
@@ -297,6 +371,11 @@ public:
|
|||||||
// migration is past the sweep, so a resume goes to the confirm/adopt stage (not sweep again).
|
// migration is past the sweep, so a resume goes to the confirm/adopt stage (not sweep again).
|
||||||
std::string getSeedMigrationSweepTxid() const { return seed_migration_sweep_txid_; }
|
std::string getSeedMigrationSweepTxid() const { return seed_migration_sweep_txid_; }
|
||||||
void setSeedMigrationSweepTxid(const std::string& v) { seed_migration_sweep_txid_ = v; }
|
void setSeedMigrationSweepTxid(const std::string& v) { seed_migration_sweep_txid_ = v; }
|
||||||
|
// W3-3: the async sweep operation id, persisted while the sweep is in flight (before it resolves
|
||||||
|
// to a txid). Lets a resume re-poll a mid-sweep interruption instead of dropping the txid. Cleared
|
||||||
|
// in the same write that persists the txid, so the txid always outranks it (see [[decideSeedMigrationResume]]).
|
||||||
|
std::string getSeedMigrationSweepOpid() const { return seed_migration_sweep_opid_; }
|
||||||
|
void setSeedMigrationSweepOpid(const std::string& v) { seed_migration_sweep_opid_ = v; }
|
||||||
|
|
||||||
// Security — auto-lock timeout (seconds; 0 = disabled)
|
// Security — auto-lock timeout (seconds; 0 = disabled)
|
||||||
int getAutoLockTimeout() const { return auto_lock_timeout_; }
|
int getAutoLockTimeout() const { return auto_lock_timeout_; }
|
||||||
@@ -473,6 +552,18 @@ private:
|
|||||||
std::string theme_ = "dragonx";
|
std::string theme_ = "dragonx";
|
||||||
std::string skin_id_ = "dragonx";
|
std::string skin_id_ = "dragonx";
|
||||||
std::string chat_reply_zaddr_;
|
std::string chat_reply_zaddr_;
|
||||||
|
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||||
|
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
||||||
|
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
||||||
|
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
||||||
|
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
||||||
|
int chat_bubble_style_ = 0; // 0 = rounded, 1 = square, 2 = minimal
|
||||||
|
int chat_bubble_accent_ = 0; // outgoing-bubble accent preset (0 = theme primary)
|
||||||
|
int chat_density_ = 0; // 0 = comfortable, 1 = compact
|
||||||
|
float chat_font_scale_ = 1.0f; // message text scale
|
||||||
|
int chat_time_format_ = 0; // 0 = follow global, 1 = 24h, 2 = 12h (Chat tab only)
|
||||||
|
bool chat_enter_sends_ = true; // Enter sends (vs. inserts newline; Ctrl+Enter sends)
|
||||||
|
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
|
||||||
bool save_ztxs_ = true;
|
bool save_ztxs_ = true;
|
||||||
bool auto_shield_ = true;
|
bool auto_shield_ = true;
|
||||||
bool use_tor_ = false;
|
bool use_tor_ = false;
|
||||||
@@ -495,24 +586,31 @@ private:
|
|||||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||||
#endif
|
#endif
|
||||||
std::string balance_layout_ = "classic";
|
std::string balance_layout_ = "classic";
|
||||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
|
||||||
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
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 animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
|
||||||
bool scanline_enabled_ = true;
|
bool scanline_enabled_ = true;
|
||||||
bool console_line_accents_ = true; // left color accent bars in console output
|
bool console_line_accents_ = true; // left color accent bars in console output
|
||||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||||
float console_zoom_ = 1.0f; // console output font zoom factor
|
float console_zoom_ = 1.0f; // console output font zoom factor
|
||||||
|
bool console_auto_focus_ = false; // focus the command input when the Console tab is opened (opt-in)
|
||||||
std::set<std::string> hidden_addresses_;
|
std::set<std::string> hidden_addresses_;
|
||||||
std::set<std::string> favorite_addresses_;
|
std::set<std::string> favorite_addresses_;
|
||||||
std::map<std::string, AddressMeta> address_meta_;
|
std::map<std::string, AddressMeta> address_meta_;
|
||||||
bool wizard_completed_ = false;
|
bool wizard_completed_ = false;
|
||||||
bool seed_backup_reminded_ = false;
|
bool seed_backup_reminded_ = false;
|
||||||
|
bool large_wallet_warned_ = false;
|
||||||
|
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
||||||
|
bool encryption_pending_ = false;
|
||||||
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
||||||
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
|
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
|
||||||
bool seed_migration_pending_ = false;
|
bool seed_migration_pending_ = false;
|
||||||
std::string seed_migration_dest_;
|
std::string seed_migration_dest_;
|
||||||
std::string seed_migration_temp_dir_;
|
std::string seed_migration_temp_dir_;
|
||||||
std::string seed_migration_sweep_txid_;
|
std::string seed_migration_sweep_txid_;
|
||||||
|
std::string seed_migration_sweep_opid_;
|
||||||
int auto_lock_timeout_ = 900; // 15 minutes
|
int auto_lock_timeout_ = 900; // 15 minutes
|
||||||
int unlock_duration_ = 600; // 10 minutes
|
int unlock_duration_ = 600; // 10 minutes
|
||||||
bool pin_enabled_ = false;
|
bool pin_enabled_ = false;
|
||||||
|
|||||||
@@ -61,14 +61,21 @@ bool DaemonController::externalDaemonDetected() const
|
|||||||
return daemon_->externalDaemonDetected();
|
return daemon_->externalDaemonDetected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DaemonController::clearExternalDaemonDetected()
|
||||||
|
{
|
||||||
|
daemon_->clearExternalDaemonDetected();
|
||||||
|
}
|
||||||
|
|
||||||
DaemonController::State DaemonController::state() const
|
DaemonController::State DaemonController::state() const
|
||||||
{
|
{
|
||||||
return daemon_->getState();
|
return daemon_->getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& DaemonController::lastError() const
|
std::string DaemonController::lastError() const
|
||||||
{
|
{
|
||||||
return daemon_->getLastError();
|
// By value — getLastError() now returns a mutex-locked COPY, so forwarding it by reference would
|
||||||
|
// dangle (bind a reference to that temporary). (M-04 follow-through)
|
||||||
|
return daemon_ ? daemon_->getLastError() : std::string();
|
||||||
}
|
}
|
||||||
|
|
||||||
int DaemonController::crashCount() const
|
int DaemonController::crashCount() const
|
||||||
@@ -116,6 +123,16 @@ void DaemonController::setZapOnNextStart(bool enabled)
|
|||||||
daemon_->setZapOnNextStart(enabled);
|
daemon_->setZapOnNextStart(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DaemonController::setSalvageOnNextStart(bool enabled)
|
||||||
|
{
|
||||||
|
daemon_->setSalvageOnNextStart(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::setReindexOnNextStart(bool enabled)
|
||||||
|
{
|
||||||
|
daemon_->setReindexOnNextStart(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
bool DaemonController::zapOnNextStart() const
|
bool DaemonController::zapOnNextStart() const
|
||||||
{
|
{
|
||||||
return daemon_->zapOnNextStart();
|
return daemon_->zapOnNextStart();
|
||||||
|
|||||||
@@ -93,8 +93,9 @@ public:
|
|||||||
|
|
||||||
bool isRunning() const;
|
bool isRunning() const;
|
||||||
bool externalDaemonDetected() const;
|
bool externalDaemonDetected() const;
|
||||||
|
void clearExternalDaemonDetected();
|
||||||
State state() const;
|
State state() const;
|
||||||
const std::string& lastError() const;
|
std::string lastError() const; // by value: EmbeddedDaemon::getLastError() returns a locked copy (M-04)
|
||||||
int crashCount() const;
|
int crashCount() const;
|
||||||
int lastBlockHeight() const;
|
int lastBlockHeight() const;
|
||||||
double memoryUsageMB() const;
|
double memoryUsageMB() const;
|
||||||
@@ -106,6 +107,8 @@ public:
|
|||||||
bool rescanOnNextStart() const;
|
bool rescanOnNextStart() const;
|
||||||
void setZapOnNextStart(bool enabled);
|
void setZapOnNextStart(bool enabled);
|
||||||
bool zapOnNextStart() const;
|
bool zapOnNextStart() const;
|
||||||
|
void setSalvageOnNextStart(bool enabled);
|
||||||
|
void setReindexOnNextStart(bool enabled); // -reindex: rebuild the block DB from raw blocks on next start
|
||||||
|
|
||||||
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
||||||
bool externalDaemonDetected,
|
bool externalDaemonDetected,
|
||||||
|
|||||||
97
src/daemon/daemon_startup_diagnosis.h
Normal file
97
src/daemon/daemon_startup_diagnosis.h
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// daemon_startup_diagnosis.h — pure classifiers over a crashed daemon's captured console output,
|
||||||
|
// so the app can offer a targeted one-click fix instead of a bare "daemon crashed" / silent no-funds.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
// True when dragonxd aborted because its BLOCK DATABASE could not be loaded — either a
|
||||||
|
// daemon-vs-chaindata serialization-format mismatch after a daemon update (the deterministic
|
||||||
|
// "non-canonical optional discriminant" → "Error loading block database" → "Aborted block database
|
||||||
|
// rebuild. Exiting." sequence) or a genuinely corrupt/incomplete block index. In BOTH cases the fix
|
||||||
|
// is the same: `-reindex` rebuilds the index + chainstate from the intact raw blocks (blk*.dat).
|
||||||
|
// This is what otherwise silently presents as a wallet with zero balance — the node never starts.
|
||||||
|
inline bool blockDbOutputLooksBroken(const std::string& out)
|
||||||
|
{
|
||||||
|
return out.find("Error loading block database") != std::string::npos
|
||||||
|
|| out.find("non-canonical optional discriminant") != std::string::npos
|
||||||
|
|| out.find("Aborted block database rebuild") != std::string::npos
|
||||||
|
|| out.find("LoadBlockIndex()") != std::string::npos; // "... : failed to read value"
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when dragonxd AUTO-RECOVERED the wallet on startup: on any BDB-verify failure it moves the
|
||||||
|
// original wallet.dat to "wallet.{timestamp}.bak", salvages readable keys into a fresh wallet.dat, and
|
||||||
|
// keeps running — no flag required (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)).
|
||||||
|
// The salvage can be incomplete (or a false positive from stale/cross-platform BDB env state), so the
|
||||||
|
// node silently comes up on a possibly-empty wallet — which reads as fund loss unless we surface it.
|
||||||
|
inline bool walletAutoRecovered(const std::string& out)
|
||||||
|
{
|
||||||
|
// Cover BOTH salvage outcomes. A successful salvage prints the "data salvaged"/"saved as wallet.<ts>.bak"
|
||||||
|
// warning; a FAILED one (e.g. an inconsistent-but-readable file where aggressive salvage finds no
|
||||||
|
// records) prints "salvage failed"/"found no records". In every case CWalletDB::Recover first logs
|
||||||
|
// "Renamed <wallet> to wallet.<ts>.bak" and CDBEnv::Salvage logs its own banner — those two fire the
|
||||||
|
// instant a salvage begins, before the daemon may abort, so they're the earliest reliable signal.
|
||||||
|
return out.find("CDBEnv::Salvage") != std::string::npos // salvage is running
|
||||||
|
|| out.find("wallet.dat corrupt, data salvaged") != std::string::npos // RECOVER_OK
|
||||||
|
|| out.find("Original wallet.dat saved as wallet.") != std::string::npos
|
||||||
|
|| out.find("wallet.dat corrupt, salvage failed") != std::string::npos // RECOVER_FAIL
|
||||||
|
|| out.find("found no records in wallet") != std::string::npos // aggressive salvage empty
|
||||||
|
|| (out.find("Renamed ") != std::string::npos && out.find(" to wallet.") != std::string::npos
|
||||||
|
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside
|
||||||
|
}
|
||||||
|
|
||||||
|
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
|
||||||
|
inline long long parseWalletSalvageBakTs(const std::string& name)
|
||||||
|
{
|
||||||
|
if (name.rfind("wallet.", 0) != 0) return -1; // must start "wallet."
|
||||||
|
if (name.size() < 12 || name.compare(name.size() - 4, 4, ".bak") != 0) return -1; // ...and end ".bak"
|
||||||
|
const std::string mid = name.substr(7, name.size() - 7 - 4); // digits between the dots
|
||||||
|
if (mid.empty() || mid.size() > 18) return -1;
|
||||||
|
for (char c : mid) if (c < '0' || c > '9') return -1;
|
||||||
|
long long ts = 0;
|
||||||
|
for (char c : mid) ts = ts * 10 + (c - '0');
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most RECENT salvage backup (highest timestamp). Pure, testable.
|
||||||
|
inline std::string newestWalletSalvageBak(const std::vector<std::string>& filenames)
|
||||||
|
{
|
||||||
|
long long best = -1;
|
||||||
|
std::string bestName;
|
||||||
|
for (const auto& f : filenames) {
|
||||||
|
const long long ts = parseWalletSalvageBakTs(f);
|
||||||
|
if (ts > best) { best = ts; bestName = f; }
|
||||||
|
}
|
||||||
|
return bestName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LARGEST salvage backup, from (filename, fileSize) pairs — the least-salvaged one, i.e. the original.
|
||||||
|
// This is what "Restore original wallet" should use: a salvage CASCADE shrinks the wallet each round, so
|
||||||
|
// the newest .bak is the WORST and the largest is the pristine pre-salvage original (an emptied salvage
|
||||||
|
// is tiny; a real wallet is large). Ties break toward the newest timestamp. Returns "" if none present.
|
||||||
|
inline std::string largestWalletSalvageBak(const std::vector<std::pair<std::string, unsigned long long>>& files)
|
||||||
|
{
|
||||||
|
std::string bestName;
|
||||||
|
unsigned long long bestSize = 0;
|
||||||
|
long long bestTs = -1;
|
||||||
|
for (const auto& fp : files) {
|
||||||
|
const long long ts = parseWalletSalvageBakTs(fp.first);
|
||||||
|
if (ts < 0) continue;
|
||||||
|
if (fp.second > bestSize || (fp.second == bestSize && ts > bestTs)) {
|
||||||
|
bestSize = fp.second; bestTs = ts; bestName = fp.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestName;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
@@ -224,10 +224,9 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
|||||||
void EmbeddedDaemon::setState(State s, const std::string& message)
|
void EmbeddedDaemon::setState(State s, const std::string& message)
|
||||||
{
|
{
|
||||||
state_ = s;
|
state_ = s;
|
||||||
if (!message.empty()) {
|
if (!message.empty() && s == State::Error) {
|
||||||
if (s == State::Error) {
|
std::lock_guard<std::mutex> lk(error_mutex_); // dedicated mutex — never taken with output_mutex_ held
|
||||||
last_error_ = message;
|
last_error_ = message;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state_callback_) {
|
if (state_callback_) {
|
||||||
@@ -386,52 +385,80 @@ static std::string getPortOwnerInfo(int port)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a TCP port is already in use (something is LISTENING)
|
// Check if a TCP port is already in use (something is LISTENING). The daemon binds BOTH 127.0.0.1 (IPv4)
|
||||||
|
// and ::1 (IPv6); during shutdown one can linger after the other releases (and a "Binding RPC on ::1 …
|
||||||
|
// failed" is fatal to a fresh start), so we treat the port as in use if EITHER localhost family has it.
|
||||||
static bool isPortInUse(int port)
|
static bool isPortInUse(int port)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
WSADATA wsa;
|
WSADATA wsa;
|
||||||
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
|
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
|
||||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
bool inUse = false;
|
||||||
if (sock == INVALID_SOCKET) { WSACleanup(); return false; }
|
{ // IPv4 127.0.0.1
|
||||||
struct sockaddr_in addr;
|
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
addr.sin_family = AF_INET;
|
if (sock != INVALID_SOCKET) {
|
||||||
addr.sin_port = htons(static_cast<u_short>(port));
|
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
|
||||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
addr.sin_family = AF_INET;
|
||||||
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
addr.sin_port = htons(static_cast<u_short>(port));
|
||||||
closesocket(sock);
|
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||||
|
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
|
||||||
|
closesocket(sock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inUse) { // IPv6 ::1
|
||||||
|
SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
|
||||||
|
if (sock != INVALID_SOCKET) {
|
||||||
|
struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin6_family = AF_INET6;
|
||||||
|
addr.sin6_port = htons(static_cast<u_short>(port));
|
||||||
|
addr.sin6_addr = in6addr_loopback; // ::1 — avoids inet_pton's _WIN32_WINNT gating on mingw
|
||||||
|
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
|
||||||
|
closesocket(sock);
|
||||||
|
}
|
||||||
|
}
|
||||||
WSACleanup();
|
WSACleanup();
|
||||||
return (result == 0);
|
return inUse;
|
||||||
#else
|
#else
|
||||||
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid
|
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The
|
||||||
// creating sockets. Fall back to connect() if /proc is unavailable.
|
// parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port.
|
||||||
FILE* fp = fopen("/proc/net/tcp", "r");
|
auto scanProc = [port](const char* path) -> bool {
|
||||||
if (fp) {
|
FILE* fp = fopen(path, "r");
|
||||||
char line[256];
|
if (!fp) return false;
|
||||||
|
char line[512];
|
||||||
unsigned int localPort, state;
|
unsigned int localPort, state;
|
||||||
bool found = false;
|
bool found = false;
|
||||||
while (fgets(line, sizeof(line), fp)) {
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
|
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
|
||||||
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
|
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { found = true; break; }
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
return found;
|
return found;
|
||||||
|
};
|
||||||
|
if (FILE* probe = fopen("/proc/net/tcp", "r")) { // /proc available → authoritative LISTEN check
|
||||||
|
fclose(probe);
|
||||||
|
return scanProc("/proc/net/tcp") || scanProc("/proc/net/tcp6");
|
||||||
}
|
}
|
||||||
// Fallback (macOS): try to connect
|
// Fallback (macOS): connect() probe on both loopback families.
|
||||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
auto connProbe = [port](int family, const char* addr) -> bool {
|
||||||
if (sock < 0) return false;
|
int sock = socket(family, SOCK_STREAM, 0);
|
||||||
struct sockaddr_in addr;
|
if (sock < 0) return false;
|
||||||
memset(&addr, 0, sizeof(addr));
|
bool ok = false;
|
||||||
addr.sin_family = AF_INET;
|
if (family == AF_INET) {
|
||||||
addr.sin_port = htons(static_cast<uint16_t>(port));
|
struct sockaddr_in a; memset(&a, 0, sizeof(a));
|
||||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
a.sin_family = AF_INET; a.sin_port = htons(static_cast<uint16_t>(port));
|
||||||
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
close(sock);
|
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
|
||||||
return (result == 0);
|
} else {
|
||||||
|
struct sockaddr_in6 a; memset(&a, 0, sizeof(a));
|
||||||
|
a.sin6_family = AF_INET6; a.sin6_port = htons(static_cast<uint16_t>(port));
|
||||||
|
inet_pton(AF_INET6, addr, &a.sin6_addr);
|
||||||
|
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
|
||||||
|
}
|
||||||
|
close(sock);
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +488,34 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
}
|
}
|
||||||
external_daemon_detected_ = false;
|
external_daemon_detected_ = false;
|
||||||
|
|
||||||
|
// A previous dragonxd can release the RPC port well before it releases the datadir
|
||||||
|
// .lock — a graceful shutdown can take up to ~90s (see isDaemonProcessRunning). Starting
|
||||||
|
// into a still-held lock spawns a process that dies instantly with "Cannot obtain a lock
|
||||||
|
// on data directory"; the crash monitor reports that generically and, three times in
|
||||||
|
// ~12s, that is enough to trip the 3-strike restart cap before the lock's ~90s life
|
||||||
|
// elapses. Gate on the process actually still being alive, with a SHORT bounded wait
|
||||||
|
// (not the full ~90s — start() runs on the UI thread). Isolated starts (migrate-to-seed:
|
||||||
|
// skip_port_check_ / -datadir override) are exempt; they run their own datadir+port.
|
||||||
|
{
|
||||||
|
constexpr int kDatadirLockWaitPollMs = 100;
|
||||||
|
constexpr int kDatadirLockWaitMaxPolls = 3; // ~300ms total, breaks early on exit
|
||||||
|
bool stillRunning = false;
|
||||||
|
if (!skip_port_check_ && override_datadir_.empty()) {
|
||||||
|
stillRunning = isDaemonProcessRunning();
|
||||||
|
for (int i = 0; stillRunning && i < kDatadirLockWaitMaxPolls; ++i) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(kDatadirLockWaitPollMs));
|
||||||
|
stillRunning = isDaemonProcessRunning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const StartLockGateDecision gate =
|
||||||
|
evaluateDatadirLockGate(skip_port_check_, !override_datadir_.empty(), stillRunning);
|
||||||
|
if (!gate.proceed) {
|
||||||
|
VERBOSE_LOGF("[INFO] %s\n", gate.errorMessage);
|
||||||
|
setState(State::Error, gate.errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState(State::Starting, "Looking for dragonxd binary...");
|
setState(State::Starting, "Looking for dragonxd binary...");
|
||||||
|
|
||||||
std::string daemon_path = binary_path;
|
std::string daemon_path = binary_path;
|
||||||
@@ -496,9 +551,16 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
args.push_back("-wallet=" + wallet_file_);
|
args.push_back("-wallet=" + wallet_file_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
|
// Add wallet-repair flag if requested (one-shot). Precedence: salvage > zap > rescan; each implies a
|
||||||
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
|
// rescan in the daemon, so we don't stack them.
|
||||||
if (zap_on_next_start_.exchange(false)) {
|
if (salvage_on_next_start_.exchange(false)) {
|
||||||
|
// -salvagewallet recovers readable keypairs from a corrupt wallet.dat; the daemon then implies -rescan.
|
||||||
|
DEBUG_LOGF("[INFO] Adding -salvagewallet flag to recover a corrupt wallet\n");
|
||||||
|
args.push_back("-salvagewallet");
|
||||||
|
zap_on_next_start_.store(false);
|
||||||
|
rescan_on_next_start_.store(false);
|
||||||
|
} else if (zap_on_next_start_.exchange(false)) {
|
||||||
|
// -zapwallettxes=2 wipes all wallet tx/note records and rebuilds them from the chain (implies -rescan).
|
||||||
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
|
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
|
||||||
args.push_back("-zapwallettxes=2");
|
args.push_back("-zapwallettxes=2");
|
||||||
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
|
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
|
||||||
@@ -508,6 +570,14 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
args.push_back("-rescan");
|
args.push_back("-rescan");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -reindex rebuilds the block index + chainstate from the raw blocks (fixes an unreadable/format-
|
||||||
|
// mismatched block DB). It's about the CHAIN, not the wallet, so it's independent of the wallet-repair
|
||||||
|
// chain above (and implies its own wallet rescan). One-shot, consumed here.
|
||||||
|
if (reindex_on_next_start_.exchange(false)) {
|
||||||
|
DEBUG_LOGF("[INFO] Adding -reindex flag to rebuild the block database from raw blocks\n");
|
||||||
|
args.push_back("-reindex");
|
||||||
|
}
|
||||||
|
|
||||||
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
|
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
|
||||||
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
|
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
|
||||||
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
|
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
|
||||||
@@ -522,8 +592,14 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
override_extra_args_.clear();
|
override_extra_args_.clear();
|
||||||
|
|
||||||
if (!startProcess(daemon_path, args)) {
|
if (!startProcess(daemon_path, args)) {
|
||||||
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
|
// startProcess() sets a precise last_error_ (e.g. "dragonxd could not be executed:
|
||||||
setState(State::Error, "Failed to start dragonxd process");
|
// ... not executable or wrong architecture"). Surface THAT via setState — which also
|
||||||
|
// stores the Error message into last_error_ — instead of clobbering it with a generic
|
||||||
|
// string that would then be all getLastError()/the UI ever sees.
|
||||||
|
std::string detail = last_error_.empty() ? std::string("Failed to start dragonxd process")
|
||||||
|
: last_error_;
|
||||||
|
DEBUG_LOGF("[ERROR] %s\n", detail.c_str());
|
||||||
|
setState(State::Error, detail);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,12 +620,28 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
// Forward declaration — defined after startProcess
|
// Forward declaration — defined after startProcess
|
||||||
static DWORD findProcessByName(const char* name);
|
static DWORD findProcessByName(const char* name);
|
||||||
|
|
||||||
|
// Quote a single argument per the CommandLineToArgvW rules (MSDN) so a value containing a space or a
|
||||||
|
// quote is delivered as ONE argv token to the daemon instead of splitting/corrupting argv (L-02).
|
||||||
|
static std::string quoteWinArg(const std::string& arg) {
|
||||||
|
if (!arg.empty() && arg.find_first_of(" \t\n\v\"") == std::string::npos) return arg;
|
||||||
|
std::string out = "\"";
|
||||||
|
for (size_t i = 0; ; ++i) {
|
||||||
|
size_t nbs = 0;
|
||||||
|
while (i < arg.size() && arg[i] == '\\') { ++nbs; ++i; }
|
||||||
|
if (i == arg.size()) { out.append(nbs * 2, '\\'); break; }
|
||||||
|
if (arg[i] == '"') { out.append(nbs * 2 + 1, '\\'); out.push_back('"'); }
|
||||||
|
else { out.append(nbs, '\\'); out.push_back(arg[i]); }
|
||||||
|
}
|
||||||
|
out.push_back('"');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
|
bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
|
||||||
{
|
{
|
||||||
// Build command line
|
// Build command line (binary path always quoted; each arg quoted/escaped per Windows rules — L-02)
|
||||||
std::string cmd = "\"" + binary_path + "\"";
|
std::string cmd = "\"" + binary_path + "\"";
|
||||||
for (const auto& arg : args) {
|
for (const auto& arg : args) {
|
||||||
cmd += " " + arg;
|
cmd += " " + quoteWinArg(arg);
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[INFO] Starting daemon: %s\n", cmd.c_str());
|
DEBUG_LOGF("[INFO] Starting daemon: %s\n", cmd.c_str());
|
||||||
|
|
||||||
@@ -597,7 +689,10 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
debug_log_path_.c_str(), debug_log_offset_);
|
debug_log_path_.c_str(), debug_log_offset_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE).
|
// Launch daemon windowless. Use CREATE_NO_WINDOW (NOT CREATE_NEW_CONSOLE): CREATE_NEW_CONSOLE
|
||||||
|
// allocates a console window that briefly flashes on screen before SW_HIDE can hide it, which is
|
||||||
|
// visible as a console-window flash on wallet launch. CREATE_NO_WINDOW gives the console child no
|
||||||
|
// window at all (same approach as the xmrig launcher). The daemon logs to debug.log, not a console.
|
||||||
// The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
|
// The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
|
||||||
// — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
|
// — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
|
||||||
STARTUPINFOA si;
|
STARTUPINFOA si;
|
||||||
@@ -615,7 +710,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
FALSE,
|
FALSE,
|
||||||
CREATE_NEW_CONSOLE,
|
CREATE_NO_WINDOW,
|
||||||
NULL,
|
NULL,
|
||||||
work_dir.c_str(),
|
work_dir.c_str(),
|
||||||
&si,
|
&si,
|
||||||
@@ -656,17 +751,24 @@ static DWORD findProcessByName(const char* name)
|
|||||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
if (snap == INVALID_HANDLE_VALUE) return 0;
|
if (snap == INVALID_HANDLE_VALUE) return 0;
|
||||||
|
|
||||||
PROCESSENTRY32 entry;
|
// Use the explicit WIDE Toolhelp API + a wide compare so this is correct regardless of the UNICODE
|
||||||
|
// macro. (The non-suffixed PROCESSENTRY32/Process32First map to the wide variants when UNICODE is
|
||||||
|
// defined, in which case szExeFile is WCHAR[] and an ANSI _stricmp would compare garbage and NEVER
|
||||||
|
// match — silently making findProcessByName a no-op that returns 0 for a running process.)
|
||||||
|
wchar_t wname[MAX_PATH];
|
||||||
|
if (MultiByteToWideChar(CP_ACP, 0, name, -1, wname, MAX_PATH) == 0) { CloseHandle(snap); return 0; }
|
||||||
|
|
||||||
|
PROCESSENTRY32W entry;
|
||||||
entry.dwSize = sizeof(entry);
|
entry.dwSize = sizeof(entry);
|
||||||
|
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
if (Process32First(snap, &entry)) {
|
if (Process32FirstW(snap, &entry)) {
|
||||||
do {
|
do {
|
||||||
if (_stricmp(entry.szExeFile, name) == 0) {
|
if (lstrcmpiW(entry.szExeFile, wname) == 0) { // Win32 case-insensitive wide compare
|
||||||
pid = entry.th32ProcessID;
|
pid = entry.th32ProcessID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} while (Process32Next(snap, &entry));
|
} while (Process32NextW(snap, &entry));
|
||||||
}
|
}
|
||||||
CloseHandle(snap);
|
CloseHandle(snap);
|
||||||
return pid;
|
return pid;
|
||||||
@@ -921,17 +1023,37 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self-pipe used purely as an exec-success/failure handshake, separate from
|
||||||
|
// the stdout pipe above. Both ends are close-on-exec, so a successful execv()
|
||||||
|
// closes the write end for free (parent reads EOF); on execv() failure the
|
||||||
|
// child writes errno here, so the parent learns synchronously instead of
|
||||||
|
// reporting State::Running for a child that never became dragonxd. We use
|
||||||
|
// pipe()+FD_CLOEXEC (not pipe2) because this POSIX branch is shared with
|
||||||
|
// macOS, which has no pipe2().
|
||||||
|
int execpipe[2];
|
||||||
|
if (pipe(execpipe) == -1) {
|
||||||
|
last_error_ = "Failed to create exec-status pipe: " + std::string(strerror(errno));
|
||||||
|
close(pipefd[0]);
|
||||||
|
close(pipefd[1]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fcntl(execpipe[0], F_SETFD, FD_CLOEXEC);
|
||||||
|
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid == -1) {
|
if (pid == -1) {
|
||||||
last_error_ = "Fork failed: " + std::string(strerror(errno));
|
last_error_ = "Fork failed: " + std::string(strerror(errno));
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
|
close(execpipe[0]);
|
||||||
|
close(execpipe[1]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
// Child process
|
// Child process
|
||||||
close(pipefd[0]); // Close read end
|
close(pipefd[0]); // Close read end of the stdout pipe
|
||||||
|
close(execpipe[0]); // Child only writes the exec-status pipe
|
||||||
|
|
||||||
// Put child in its own process group so we can kill the entire
|
// Put child in its own process group so we can kill the entire
|
||||||
// group later (including dragonxd spawned by a wrapper script).
|
// group later (including dragonxd spawned by a wrapper script).
|
||||||
@@ -998,17 +1120,56 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
execv(binary_path.c_str(), argv.data());
|
execv(binary_path.c_str(), argv.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, exec failed
|
// If we get here, execv() failed — the child never became dragonxd.
|
||||||
fprintf(stderr, "execv failed: %s\n", strerror(errno));
|
// Capture errno before fprintf/strerror can clobber it, report it to
|
||||||
|
// the parent over the exec-status pipe (EINTR-safe), then exit.
|
||||||
|
int exec_errno = errno;
|
||||||
|
fprintf(stderr, "execv failed: %s\n", strerror(exec_errno));
|
||||||
|
ssize_t w;
|
||||||
|
do {
|
||||||
|
w = write(execpipe[1], &exec_errno, sizeof(exec_errno));
|
||||||
|
} while (w < 0 && errno == EINTR);
|
||||||
_exit(127);
|
_exit(127);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent process
|
// Parent process
|
||||||
close(pipefd[1]); // Close write end
|
close(pipefd[1]); // Close our copy of the stdout write end
|
||||||
|
close(execpipe[1]); // Must close our copy, or the read() below never sees EOF
|
||||||
|
|
||||||
|
// Exec-status handshake: EOF => execv() succeeded (its write end was closed
|
||||||
|
// on exec); a full sizeof(int) => execv() failed and the child sent errno.
|
||||||
|
int child_errno = 0;
|
||||||
|
size_t got = 0;
|
||||||
|
char* ep = reinterpret_cast<char*>(&child_errno);
|
||||||
|
for (;;) {
|
||||||
|
ssize_t n = read(execpipe[0], ep + got, sizeof(child_errno) - got);
|
||||||
|
if (n == 0) break; // EOF: exec succeeded
|
||||||
|
if (n < 0) { if (errno == EINTR) continue; break; } // other error: assume success
|
||||||
|
got += static_cast<size_t>(n);
|
||||||
|
if (got >= sizeof(child_errno)) break; // full errno: exec failed
|
||||||
|
}
|
||||||
|
close(execpipe[0]);
|
||||||
|
|
||||||
|
if (got >= sizeof(child_errno)) {
|
||||||
|
// execv() never replaced the child; it fprintf'd and _exit(127)'d. Reap
|
||||||
|
// the already-dead zombie here — monitorProcess() is only started after
|
||||||
|
// this function returns true, so there is no competing reaper.
|
||||||
|
close(pipefd[0]);
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
last_error_ = "dragonxd could not be executed: " + std::string(strerror(child_errno)) +
|
||||||
|
" — not executable or wrong architecture";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
stdout_fd_ = pipefd[0];
|
stdout_fd_ = pipefd[0];
|
||||||
|
|
||||||
// Also set process group from parent side (race with child's setpgid)
|
// Best-effort: the child already calls setpgid(0, 0); this parent-side call
|
||||||
setpgid(pid, pid);
|
// just closes the fork/exec race window. A failure here is not fatal to
|
||||||
|
// startup, so we log rather than abort.
|
||||||
|
if (setpgid(pid, pid) != 0) {
|
||||||
|
DEBUG_LOGF("[WARN] setpgid(%d) from parent failed: %s\n", (int)pid, strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
// Set non-blocking
|
// Set non-blocking
|
||||||
int flags = fcntl(stdout_fd_, F_GETFL, 0);
|
int flags = fcntl(stdout_fd_, F_GETFL, 0);
|
||||||
@@ -1093,17 +1254,21 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
|||||||
|
|
||||||
bool EmbeddedDaemon::isRunning() const
|
bool EmbeddedDaemon::isRunning() const
|
||||||
{
|
{
|
||||||
|
// Read the atomic state_ instead of calling waitpid() here. monitorProcess()
|
||||||
|
// is the sole thread allowed to waitpid() process_pid_ during normal operation.
|
||||||
|
// Calling waitpid() from this method too (as it used to, and this is invoked
|
||||||
|
// from the UI thread nearly every frame) meant whichever thread reaped the
|
||||||
|
// child's exit first consumed the status; if isRunning() won that race,
|
||||||
|
// monitorProcess() never saw the exit, so crash_count_ / the decoded exit
|
||||||
|
// code / the State::Error transition were all silently lost. Mirrors the
|
||||||
|
// fix already in XmrigManager::isRunning().
|
||||||
if (process_pid_ <= 0) return false;
|
if (process_pid_ <= 0) return false;
|
||||||
|
|
||||||
int status;
|
const State s = state_.load(std::memory_order_relaxed);
|
||||||
pid_t result = waitpid(process_pid_, &status, WNOHANG);
|
// State::Stopping is included: stop()'s graceful/SIGTERM wait loops poll
|
||||||
|
// isRunning() while state_ == Stopping — before the process has actually
|
||||||
if (result == 0) {
|
// terminated — and must keep seeing "alive" to wait/escalate correctly.
|
||||||
// Still running
|
return (s == State::Running || s == State::Stopping);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmbeddedDaemon::drainOutput()
|
void EmbeddedDaemon::drainOutput()
|
||||||
@@ -1247,5 +1412,28 @@ bool EmbeddedDaemon::tcpPortInUse(int port)
|
|||||||
return isPortInUse(port);
|
return isPortInUse(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EmbeddedDaemon::isDaemonProcessRunning()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
return findProcessByName("dragonxd.exe") != 0;
|
||||||
|
#elif defined(__linux__)
|
||||||
|
// Scan /proc for a process whose comm is exactly "dragonxd". Iterate with an error_code so a proc
|
||||||
|
// entry vanishing mid-scan (a process exiting) can't throw.
|
||||||
|
std::error_code ec;
|
||||||
|
fs::directory_iterator it("/proc", ec), end;
|
||||||
|
for (; !ec && it != end; it.increment(ec)) {
|
||||||
|
const std::string pid = it->path().filename().string();
|
||||||
|
if (pid.empty() || pid[0] < '0' || pid[0] > '9') continue; // numeric pid dirs only
|
||||||
|
std::ifstream f((it->path() / "comm").string());
|
||||||
|
std::string comm;
|
||||||
|
if (f && std::getline(f, comm) && comm == "dragonxd") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
// macOS has no /proc; fall back to the RPC-port probe (best-effort).
|
||||||
|
return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace daemon
|
} // namespace daemon
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief Get last error message
|
* @brief Get last error message
|
||||||
*/
|
*/
|
||||||
const std::string& getLastError() const { return last_error_; }
|
// Copy under lock: last_error_ is written from the monitor thread (setState on an unexpected exit)
|
||||||
|
// while the UI thread reads it — a reference would be a torn-read / use-after-free race (M-04).
|
||||||
|
std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get dragonxd process output (thread-safe copy)
|
* @brief Get dragonxd process output (thread-safe copy)
|
||||||
@@ -142,6 +144,10 @@ public:
|
|||||||
* When true the wallet should connect to it instead of showing an error.
|
* When true the wallet should connect to it instead of showing an error.
|
||||||
*/
|
*/
|
||||||
bool externalDaemonDetected() const { return external_daemon_detected_; }
|
bool externalDaemonDetected() const { return external_daemon_detected_; }
|
||||||
|
// Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that
|
||||||
|
// stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also
|
||||||
|
// clears it on the port-free fall-through, but only if not short-circuited by an early guard.
|
||||||
|
void clearExternalDaemonDetected() { external_daemon_detected_ = false; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set callback for state changes
|
* @brief Set callback for state changes
|
||||||
@@ -197,6 +203,18 @@ public:
|
|||||||
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
||||||
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
||||||
|
|
||||||
|
// -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in
|
||||||
|
// the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt.
|
||||||
|
void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; }
|
||||||
|
bool salvageOnNextStart() const { return salvage_on_next_start_.load(); }
|
||||||
|
|
||||||
|
// -reindex: rebuild the block index + chainstate from the raw blocks (blk*.dat) on startup. One-shot,
|
||||||
|
// consumed on the next start. Offered when the node aborts on an unreadable block database (a
|
||||||
|
// daemon-vs-chaindata format mismatch after an update, or a corrupt index). It implies a wallet
|
||||||
|
// rescan, so it's the block-DB analogue of -salvagewallet and coexists with the wallet-repair flags.
|
||||||
|
void setReindexOnNextStart(bool v) { reindex_on_next_start_ = v; }
|
||||||
|
bool reindexOnNextStart() const { return reindex_on_next_start_.load(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
|
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
|
||||||
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
|
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
|
||||||
@@ -217,6 +235,41 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
|
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief True while ANY dragonxd process is running (by process name), regardless of who started it.
|
||||||
|
* Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a
|
||||||
|
* graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the
|
||||||
|
* process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released
|
||||||
|
* the datadir before starting a replacement. Matches the daemon binary name on all platforms.
|
||||||
|
*/
|
||||||
|
static bool isDaemonProcessRunning();
|
||||||
|
|
||||||
|
/** Decision returned by evaluateDatadirLockGate(): whether start() may spawn now. */
|
||||||
|
struct StartLockGateDecision {
|
||||||
|
bool proceed = true; // false => bail before spawning
|
||||||
|
const char* errorMessage = ""; // set (a string literal) when proceed == false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pure decision for start(): bail because a previous dragonxd still holds the
|
||||||
|
* shared datadir lock? Isolated instances (skip_port_check_ / an active -datadir
|
||||||
|
* override) are exempt — they run their own throwaway datadir+port and can coexist
|
||||||
|
* with the main daemon. Does no process/fs I/O itself (the caller does the probing),
|
||||||
|
* so it is directly unit-testable; defined inline so tests need only this header.
|
||||||
|
*/
|
||||||
|
static StartLockGateDecision evaluateDatadirLockGate(bool skipPortCheck,
|
||||||
|
bool isolatedOverride,
|
||||||
|
bool stillRunningAfterWait)
|
||||||
|
{
|
||||||
|
if (skipPortCheck || isolatedOverride) return {true, ""};
|
||||||
|
if (stillRunningAfterWait) {
|
||||||
|
return {false,
|
||||||
|
"A previous dragonxd is still shutting down and holding the data "
|
||||||
|
"directory lock. Retrying shortly…"};
|
||||||
|
}
|
||||||
|
return {true, ""};
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
|
/** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
|
||||||
static bool tcpPortInUse(int port);
|
static bool tcpPortInUse(int port);
|
||||||
|
|
||||||
@@ -235,6 +288,7 @@ private:
|
|||||||
std::atomic<State> state_{State::Stopped};
|
std::atomic<State> state_{State::Stopped};
|
||||||
std::atomic<bool> external_daemon_detected_{false};
|
std::atomic<bool> external_daemon_detected_{false};
|
||||||
std::string last_error_;
|
std::string last_error_;
|
||||||
|
mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads)
|
||||||
mutable std::mutex output_mutex_; // protects process_output_
|
mutable std::mutex output_mutex_; // protects process_output_
|
||||||
std::string process_output_;
|
std::string process_output_;
|
||||||
StateCallback state_callback_;
|
StateCallback state_callback_;
|
||||||
@@ -261,6 +315,8 @@ private:
|
|||||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||||
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||||
|
std::atomic<bool> salvage_on_next_start_{false}; // -salvagewallet flag for next start
|
||||||
|
std::atomic<bool> reindex_on_next_start_{false}; // -reindex flag for next start (rebuild block DB)
|
||||||
std::string override_datadir_; // one-shot: -datadir for the next start
|
std::string override_datadir_; // one-shot: -datadir for the next start
|
||||||
std::vector<std::string> override_extra_args_; // one-shot: extra args for the next start
|
std::vector<std::string> override_extra_args_; // one-shot: extra args for the next start
|
||||||
bool skip_port_check_ = false; // isolated instance on a non-default port
|
bool skip_port_check_ = false; // isolated instance on a non-default port
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
|||||||
// RPC port. So the wallet lives in <base>/DRAGONX; `base` is the migration root we clean up.
|
// RPC port. So the wallet lives in <base>/DRAGONX; `base` is the migration root we clean up.
|
||||||
const std::string base = util::Platform::getConfigDir() + "/seed-migrate";
|
const std::string base = util::Platform::getConfigDir() + "/seed-migrate";
|
||||||
const std::string dataDir = base + "/DRAGONX";
|
const std::string dataDir = base + "/DRAGONX";
|
||||||
|
// W3-2: never blindly wipe a pre-existing temp seed wallet. A prior migration that swept funds into
|
||||||
|
// it but was abandoned or crashed before adopting would otherwise have its (fund-bearing) wallet
|
||||||
|
// destroyed here. A completed migration removes this dir on adopt, so a leftover means an unfinished
|
||||||
|
// one — refuse and point the user at it rather than silently destroying it.
|
||||||
|
if (fs::exists(dataDir + "/wallet.dat")) {
|
||||||
|
r.error = "A previous seed migration looks unfinished — its temporary wallet is still at\n" + base +
|
||||||
|
"\nResume or cancel it first. If you are certain its funds are already in your main "
|
||||||
|
"wallet, delete that folder and try again.";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
fs::remove_all(base, ec);
|
fs::remove_all(base, ec);
|
||||||
fs::create_directories(dataDir, ec);
|
fs::create_directories(dataDir, ec);
|
||||||
if (ec) { r.error = "Could not create the temporary wallet directory."; return r; }
|
if (ec) { r.error = "Could not create the temporary wallet directory."; return r; }
|
||||||
@@ -110,9 +120,13 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
|||||||
|
|
||||||
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
|
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
|
||||||
try {
|
try {
|
||||||
auto m = cli.call("z_exportmnemonic");
|
auto m = cli.callSecret("z_exportmnemonic"); // zero the raw body too (B7)
|
||||||
if (m.contains("mnemonic") && m["mnemonic"].is_string())
|
if (m.contains("mnemonic") && m["mnemonic"].is_string()) {
|
||||||
r.seedPhrase = m["mnemonic"].get<std::string>();
|
// Take our copy, then scrub the json node's own copy so it isn't freed in the clear (B7).
|
||||||
|
auto& mn = m["mnemonic"].get_ref<std::string&>();
|
||||||
|
r.seedPhrase = mn;
|
||||||
|
if (!mn.empty()) sodium_memzero(&mn[0], mn.size());
|
||||||
|
}
|
||||||
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
|
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
|
||||||
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
|
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
|
||||||
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
|
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
|
||||||
@@ -132,6 +146,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).
|
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
|
||||||
cli.disconnect();
|
cli.disconnect();
|
||||||
temp.stop(20000);
|
temp.stop(20000);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Copyright 2024-2026 The Hush Developers
|
// Copyright 2024-2026 The Hush Developers
|
||||||
// Released under the GPLv3
|
// 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.
|
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||||
|
|
||||||
#include "xmrig_manager.h"
|
#include "xmrig_manager.h"
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
#include "../util/pool_registry.h"
|
#include "../util/pool_registry.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -89,8 +90,32 @@ static std::string getConfigDir() {
|
|||||||
// libcurl write callback
|
// libcurl write callback
|
||||||
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
||||||
auto* s = static_cast<std::string*>(userdata);
|
auto* s = static_cast<std::string*>(userdata);
|
||||||
s->append(static_cast<char*>(ptr), sz * n);
|
const size_t add = sz * n;
|
||||||
return sz * n;
|
// Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or
|
||||||
|
// MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02)
|
||||||
|
constexpr size_t kMaxStatsBytes = 1u << 20; // 1 MiB
|
||||||
|
if (s->size() + add > kMaxStatsBytes) return 0;
|
||||||
|
s->append(static_cast<char*>(ptr), add);
|
||||||
|
return add;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target
|
||||||
|
// that a public mining pool would never be — used to refuse a background stats GET to it (M-09).
|
||||||
|
static bool hostLooksInternal(const std::string& host) {
|
||||||
|
if (host.empty() || host == "localhost") return true;
|
||||||
|
if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 ||
|
||||||
|
host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true;
|
||||||
|
if (host.rfind("172.", 0) == 0) { // 172.16.0.0 - 172.31.255.255
|
||||||
|
const int second = std::atoi(host.c_str() + 4);
|
||||||
|
if (second >= 16 && second <= 31) return true;
|
||||||
|
}
|
||||||
|
if (host.find(':') != std::string::npos) { // IPv6 literal: loopback / ULA / link-local
|
||||||
|
if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 ||
|
||||||
|
host.rfind("fe80", 0) == 0) return true;
|
||||||
|
}
|
||||||
|
if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true;
|
||||||
|
if (host.find('.') == std::string::npos) return true; // bare single-label name = LAN/hosts, not a pool
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -100,9 +125,14 @@ static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
|||||||
XmrigManager::XmrigManager() = default;
|
XmrigManager::XmrigManager() = default;
|
||||||
|
|
||||||
XmrigManager::~XmrigManager() {
|
XmrigManager::~XmrigManager() {
|
||||||
|
should_stop_ = true;
|
||||||
if (isRunning()) {
|
if (isRunning()) {
|
||||||
stop(3000);
|
stop(3000);
|
||||||
}
|
}
|
||||||
|
// Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is
|
||||||
|
// false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04)
|
||||||
|
if (monitor_thread_.joinable())
|
||||||
|
monitor_thread_.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -116,32 +146,18 @@ std::string XmrigManager::findXmrigBinary() {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: system PATH
|
// Fallback: system PATH — windowless so it never flashes a console.
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
FILE* f = _popen("where xmrig.exe 2>nul", "r");
|
std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
|
||||||
#else
|
#else
|
||||||
FILE* f = popen("which xmrig 2>/dev/null", "r");
|
std::string out = util::Platform::runHiddenCapture("which xmrig");
|
||||||
#endif
|
|
||||||
if (f) {
|
|
||||||
char line[512];
|
|
||||||
if (fgets(line, sizeof(line), f)) {
|
|
||||||
std::string s(line);
|
|
||||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
|
|
||||||
s.pop_back();
|
|
||||||
if (!s.empty() && fs::exists(s)) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
_pclose(f);
|
|
||||||
#else
|
|
||||||
pclose(f);
|
|
||||||
#endif
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ifdef _WIN32
|
|
||||||
_pclose(f);
|
|
||||||
#else
|
|
||||||
pclose(f);
|
|
||||||
#endif
|
#endif
|
||||||
|
{
|
||||||
|
std::string s = out;
|
||||||
|
const auto nl = s.find_first_of("\r\n"); // first line only
|
||||||
|
if (nl != std::string::npos) s.erase(nl);
|
||||||
|
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
|
||||||
|
if (!s.empty() && fs::exists(s)) return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
@@ -208,22 +224,43 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
fs::create_directories(fs::path(outPath).parent_path());
|
fs::create_directories(fs::path(outPath).parent_path());
|
||||||
std::ofstream ofs(outPath);
|
const std::string dumped = j.dump(4);
|
||||||
if (!ofs.is_open()) {
|
#ifndef _WIN32
|
||||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
// Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never
|
||||||
|
// in a world/group-readable file — even for a local attacker who opened it in the old
|
||||||
|
// create-then-chmod window and held the fd open across the chmod. (L-01)
|
||||||
|
int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||||
|
if (fd < 0) {
|
||||||
|
setLastError("Cannot write xmrig config: " + outPath);
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ofs << j.dump(4);
|
size_t off = 0;
|
||||||
ofs.close();
|
bool wrote = true;
|
||||||
|
while (off < dumped.size()) {
|
||||||
#ifndef _WIN32
|
ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off);
|
||||||
// 0600 permissions — only owner can read/write
|
if (nw <= 0) { wrote = false; break; }
|
||||||
chmod(outPath.c_str(), 0600);
|
off += static_cast<size_t>(nw);
|
||||||
#endif
|
}
|
||||||
|
::close(fd);
|
||||||
|
if (!wrote) {
|
||||||
|
setLastError("Cannot write xmrig config: " + outPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
#else
|
||||||
|
std::ofstream ofs(outPath, std::ios::trunc);
|
||||||
|
if (!ofs.is_open()) {
|
||||||
|
setLastError("Cannot write xmrig config: " + outPath);
|
||||||
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ofs << dumped;
|
||||||
|
ofs.close();
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
last_error_ = std::string("Config write error: ") + e.what();
|
setLastError(std::string("Config write error: ") + e.what());
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -235,19 +272,22 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
|||||||
|
|
||||||
bool XmrigManager::start(const Config& cfg) {
|
bool XmrigManager::start(const Config& cfg) {
|
||||||
if (state_ == State::Running || state_ == State::Starting) {
|
if (state_ == State::Running || state_ == State::Starting) {
|
||||||
last_error_ = "Already running";
|
setLastError("Already running");
|
||||||
DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
|
DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state_ = State::Starting;
|
state_ = State::Starting;
|
||||||
should_stop_ = false;
|
should_stop_ = false;
|
||||||
last_error_.clear();
|
setLastError(std::string());
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||||
process_output_.clear();
|
process_output_.clear();
|
||||||
}
|
}
|
||||||
stats_ = PoolStats{};
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||||
|
stats_ = PoolStats{};
|
||||||
|
}
|
||||||
|
|
||||||
// Extract pool hostname for stats API queries
|
// Extract pool hostname for stats API queries
|
||||||
{
|
{
|
||||||
@@ -264,7 +304,7 @@ bool XmrigManager::start(const Config& cfg) {
|
|||||||
// Find binary
|
// Find binary
|
||||||
std::string binary = findXmrigBinary();
|
std::string binary = findXmrigBinary();
|
||||||
if (binary.empty()) {
|
if (binary.empty()) {
|
||||||
last_error_ = "xmrig binary not found";
|
setLastError("xmrig binary not found");
|
||||||
state_ = State::Error;
|
state_ = State::Error;
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n");
|
DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n");
|
||||||
return false;
|
return false;
|
||||||
@@ -292,7 +332,11 @@ bool XmrigManager::start(const Config& cfg) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start monitor thread
|
// Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set
|
||||||
|
// State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable
|
||||||
|
// std::thread calls std::terminate() and aborts the whole wallet. (M-04)
|
||||||
|
if (monitor_thread_.joinable())
|
||||||
|
monitor_thread_.join();
|
||||||
monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
|
monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
|
||||||
state_ = State::Running;
|
state_ = State::Running;
|
||||||
DEBUG_LOGF("[INFO] XmrigManager: started\n");
|
DEBUG_LOGF("[INFO] XmrigManager: started\n");
|
||||||
@@ -367,7 +411,7 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
|||||||
|
|
||||||
HANDLE hRead = nullptr, hWrite = nullptr;
|
HANDLE hRead = nullptr, hWrite = nullptr;
|
||||||
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
|
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
|
||||||
last_error_ = "CreatePipe failed";
|
setLastError("CreatePipe failed");
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -399,7 +443,7 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
|||||||
char errBuf[256];
|
char errBuf[256];
|
||||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
||||||
last_error_ = "CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf;
|
setLastError("CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf);
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str());
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -450,14 +494,14 @@ void XmrigManager::drainOutput() {
|
|||||||
bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
|
bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
|
||||||
int pipefd[2];
|
int pipefd[2];
|
||||||
if (pipe(pipefd) != 0) {
|
if (pipe(pipefd) != 0) {
|
||||||
last_error_ = "pipe() failed";
|
setLastError("pipe() failed");
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid < 0) {
|
if (pid < 0) {
|
||||||
last_error_ = "fork() failed";
|
setLastError("fork() failed");
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
@@ -628,7 +672,7 @@ void XmrigManager::monitorProcess() {
|
|||||||
if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) {
|
if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) {
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode);
|
DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode);
|
||||||
state_ = State::Error;
|
state_ = State::Error;
|
||||||
last_error_ = "xmrig process exited unexpectedly";
|
setLastError("xmrig process exited unexpectedly");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,7 +683,7 @@ void XmrigManager::monitorProcess() {
|
|||||||
if (ret == process_pid_ || ret < 0) {
|
if (ret == process_pid_ || ret < 0) {
|
||||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret);
|
DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret);
|
||||||
state_ = State::Error;
|
state_ = State::Error;
|
||||||
last_error_ = "xmrig process exited unexpectedly";
|
setLastError("xmrig process exited unexpectedly");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,6 +823,11 @@ void XmrigManager::fetchPoolApiStats() {
|
|||||||
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
|
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
|
||||||
// /api/pools); unknown/custom hosts fall back to the .is convention.
|
// /api/pools); unknown/custom hosts fall back to the .is convention.
|
||||||
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
|
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
|
||||||
|
// SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a
|
||||||
|
// loopback/private/link-local/single-label target — those aren't public mining pools, and a
|
||||||
|
// paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted
|
||||||
|
// registry statsUrl and are exempt. (M-09)
|
||||||
|
if (!known && hostLooksInternal(pool_host_)) return;
|
||||||
const std::string url = known ? known->statsUrl
|
const std::string url = known ? known->statsUrl
|
||||||
: ("https://" + pool_host_ + "/api/stats");
|
: ("https://" + pool_host_ + "/api/stats");
|
||||||
|
|
||||||
@@ -857,25 +906,18 @@ void XmrigManager::startVersionDetection()
|
|||||||
std::thread([]() {
|
std::thread([]() {
|
||||||
const std::string bin = findXmrigBinary();
|
const std::string bin = findXmrigBinary();
|
||||||
std::string ver;
|
std::string ver;
|
||||||
if (!bin.empty()) {
|
// Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an
|
||||||
const std::string cmd = "\"" + bin + "\" --version 2>&1";
|
// app-controlled path, but this closes command injection if it ever isn't. (M-10)
|
||||||
#ifdef _WIN32
|
// Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"")
|
||||||
FILE* fp = _popen(cmd.c_str(), "r");
|
// on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows
|
||||||
#else
|
// paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10)
|
||||||
FILE* fp = popen(cmd.c_str(), "r");
|
const bool binShellSafe =
|
||||||
#endif
|
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
|
||||||
if (fp) {
|
if (binShellSafe) {
|
||||||
std::string out;
|
// Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
|
||||||
char buf[256];
|
const std::string cmd = "\"" + bin + "\" --version";
|
||||||
size_t n;
|
const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
|
||||||
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
|
if (!out.empty()) ver = parseMinerVersion(out);
|
||||||
#ifdef _WIN32
|
|
||||||
_pclose(fp);
|
|
||||||
#else
|
|
||||||
pclose(fp);
|
|
||||||
#endif
|
|
||||||
ver = parseMinerVersion(out);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||||
g_installed_ver = ver;
|
g_installed_ver = ver;
|
||||||
|
|||||||
@@ -86,8 +86,10 @@ public:
|
|||||||
bool isRunning() const;
|
bool isRunning() const;
|
||||||
State getState() const { return state_.load(std::memory_order_relaxed); }
|
State getState() const { return state_.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
const PoolStats& getStats() const { return stats_; }
|
// Return COPIES under lock: stats_ and last_error_ are mutated by the monitor thread while the UI
|
||||||
const std::string& getLastError() const { return last_error_; }
|
// thread reads them, so handing out a reference is a torn-read / use-after-free race (M-03, M-04).
|
||||||
|
PoolStats getStats() const { std::lock_guard<std::mutex> lk(stats_mutex_); return stats_; }
|
||||||
|
std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
|
||||||
|
|
||||||
/// Thread count requested at start() — available immediately, unlike
|
/// Thread count requested at start() — available immediately, unlike
|
||||||
/// PoolStats::threads_active which requires an API response.
|
/// PoolStats::threads_active which requires an API response.
|
||||||
@@ -156,11 +158,14 @@ private:
|
|||||||
void monitorProcess();
|
void monitorProcess();
|
||||||
void drainOutput();
|
void drainOutput();
|
||||||
void appendOutput(const char* data, size_t len);
|
void appendOutput(const char* data, size_t len);
|
||||||
|
// Set last_error_ under error_mutex_ (writers run on both the main thread and the monitor thread).
|
||||||
|
void setLastError(std::string e) { std::lock_guard<std::mutex> lk(error_mutex_); last_error_ = std::move(e); }
|
||||||
void fetchStatsHttp(); // Blocking HTTP call — runs on monitor thread only
|
void fetchStatsHttp(); // Blocking HTTP call — runs on monitor thread only
|
||||||
void fetchPoolApiStats(); // Fetch pool-side stats (hashrate) from pool HTTP API
|
void fetchPoolApiStats(); // Fetch pool-side stats (hashrate) from pool HTTP API
|
||||||
|
|
||||||
std::atomic<State> state_{State::Stopped};
|
std::atomic<State> state_{State::Stopped};
|
||||||
std::string last_error_;
|
std::string last_error_;
|
||||||
|
mutable std::mutex error_mutex_; // guards last_error_ (written by main + monitor threads)
|
||||||
|
|
||||||
mutable std::mutex output_mutex_;
|
mutable std::mutex output_mutex_;
|
||||||
std::string process_output_;
|
std::string process_output_;
|
||||||
|
|||||||
@@ -46,20 +46,25 @@ bool AddressBook::load()
|
|||||||
entries_.clear();
|
entries_.clear();
|
||||||
|
|
||||||
if (j.contains("entries") && j["entries"].is_array()) {
|
if (j.contains("entries") && j["entries"].is_array()) {
|
||||||
|
size_t skipped = 0;
|
||||||
for (const auto& entry : j["entries"]) {
|
for (const auto& entry : j["entries"]) {
|
||||||
AddressBookEntry e;
|
// W6-3: skip (and count) a malformed element rather than letting one bad entry throw and
|
||||||
e.label = entry.value("label", "");
|
// abort the whole load — which would discard EVERY contact (entries_ was already cleared).
|
||||||
e.address = entry.value("address", "");
|
if (!entry.is_object()) { ++skipped; continue; }
|
||||||
e.notes = entry.value("notes", "");
|
try {
|
||||||
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
|
AddressBookEntry e;
|
||||||
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
|
e.label = entry.value("label", "");
|
||||||
e.scope = entry.value("scope", "global");
|
e.address = entry.value("address", "");
|
||||||
e.avatar = entry.value("avatar", "");
|
e.notes = entry.value("notes", "");
|
||||||
|
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
|
||||||
if (!e.address.empty()) {
|
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
|
||||||
entries_.push_back(e);
|
e.scope = entry.value("scope", "global");
|
||||||
}
|
e.avatar = entry.value("avatar", "");
|
||||||
|
if (!e.address.empty()) entries_.push_back(e);
|
||||||
|
} catch (const std::exception&) { ++skipped; }
|
||||||
}
|
}
|
||||||
|
if (skipped > 0)
|
||||||
|
DEBUG_LOGF("Address book: skipped %zu malformed entr%s\n", skipped, skipped == 1 ? "y" : "ies");
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("Address book loaded: %zu entries\n", entries_.size());
|
DEBUG_LOGF("Address book loaded: %zu entries\n", entries_.size());
|
||||||
@@ -144,6 +149,20 @@ bool AddressBook::removeEntry(size_t index)
|
|||||||
return save();
|
return save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AddressBook::reattachLegacyScopes(const std::string& scopeId)
|
||||||
|
{
|
||||||
|
if (scopeId.empty()) return 0;
|
||||||
|
int rescoped = 0;
|
||||||
|
for (auto& e : entries_) {
|
||||||
|
if (e.isGlobal()) continue; // global stays global
|
||||||
|
if (e.scope.rfind("w:", 0) == 0) continue; // already a stable scope
|
||||||
|
e.scope = scopeId;
|
||||||
|
++rescoped;
|
||||||
|
}
|
||||||
|
if (rescoped > 0) save();
|
||||||
|
return rescoped;
|
||||||
|
}
|
||||||
|
|
||||||
int AddressBook::findByAddress(const std::string& address) const
|
int AddressBook::findByAddress(const std::string& address) const
|
||||||
{
|
{
|
||||||
for (size_t i = 0; i < entries_.size(); i++) {
|
for (size_t i = 0; i < entries_.size(); i++) {
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool removeEntry(size_t index);
|
bool removeEntry(size_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Re-attach contacts stuck on a legacy (drifting address-hash) scope to a stable wallet id.
|
||||||
|
* Rewrites every non-global entry whose scope is NOT already a stable "w:"-prefixed id to
|
||||||
|
* `scopeId`, and saves once if anything changed. Recovery for contacts orphaned when the
|
||||||
|
* old address-set-hash scope shifted (e.g. after creating a new address).
|
||||||
|
* @return number of entries re-scoped.
|
||||||
|
*/
|
||||||
|
int reattachLegacyScopes(const std::string& scopeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
||||||
* @param address Address to search for
|
* @param address Address to search for
|
||||||
|
|||||||
37
src/data/seed_migration_resume.h
Normal file
37
src/data/seed_migration_resume.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Pure routing decision for resuming a pending migrate-to-seed flow (finding W3-3). Kept free of
|
||||||
|
// App/UI/RPC state so the highest-risk branch — where a reopened migration lands — is unit-testable
|
||||||
|
// and reviewable in isolation. App::showSeedMigrationDialog feeds it the persisted state + live
|
||||||
|
// connectivity and switches on the result. See src/app_network.cpp.
|
||||||
|
namespace dragonx {
|
||||||
|
|
||||||
|
enum class MigrationResume {
|
||||||
|
Intro, // no pending migration → start fresh at the intro
|
||||||
|
Confirming, // a sweep txid is persisted → resume at the confirm/adopt gate (re-derived from chain)
|
||||||
|
RetrackOpid, // a sweep opid (but no txid yet) is persisted AND we're connected → re-poll it
|
||||||
|
SweepGate, // otherwise → the dismissable Sweep step (reload balance, offer re-sweep)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decide where reopening the migration dialog lands.
|
||||||
|
//
|
||||||
|
// Invariant: the txid outranks the opid — once a sweep resolves to a txid the opid is cleared in the
|
||||||
|
// same settings write, so a persisted txid always means "past the sweep". A persisted opid is only
|
||||||
|
// re-tracked when connected, because the buttonless "Sweeping" spinner relies on the opid poller
|
||||||
|
// (which needs an RPC connection) to ever exit; disconnected, we fall back to the dismissable Sweep
|
||||||
|
// gate (which reloads the balance and, if the earlier sweep already drained it, short-circuits to
|
||||||
|
// adopt) — never trapping the user.
|
||||||
|
inline MigrationResume decideSeedMigrationResume(bool pending,
|
||||||
|
bool haveDest,
|
||||||
|
const std::string& sweepTxid,
|
||||||
|
const std::string& sweepOpid,
|
||||||
|
bool connected) {
|
||||||
|
if (!pending || !haveDest) return MigrationResume::Intro;
|
||||||
|
if (!sweepTxid.empty()) return MigrationResume::Confirming;
|
||||||
|
if (!sweepOpid.empty() && connected) return MigrationResume::RetrackOpid;
|
||||||
|
return MigrationResume::SweepGate;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx
|
||||||
@@ -25,6 +25,7 @@ bool sameEntry(const WalletIndexEntry& a, const WalletIndexEntry& b)
|
|||||||
return a.fileName == b.fileName
|
return a.fileName == b.fileName
|
||||||
&& a.displayName == b.displayName
|
&& a.displayName == b.displayName
|
||||||
&& a.walletIdentityHash == b.walletIdentityHash
|
&& a.walletIdentityHash == b.walletIdentityHash
|
||||||
|
&& a.scopeId == b.scopeId
|
||||||
&& a.cachedBalance == b.cachedBalance
|
&& a.cachedBalance == b.cachedBalance
|
||||||
&& a.cachedAddressCount == b.cachedAddressCount
|
&& a.cachedAddressCount == b.cachedAddressCount
|
||||||
&& a.lastOpenedEpoch == b.lastOpenedEpoch
|
&& a.lastOpenedEpoch == b.lastOpenedEpoch
|
||||||
@@ -61,6 +62,7 @@ bool WalletIndex::load()
|
|||||||
if (w.fileName.empty()) continue;
|
if (w.fileName.empty()) continue;
|
||||||
w.displayName = e.value("name", w.fileName);
|
w.displayName = e.value("name", w.fileName);
|
||||||
w.walletIdentityHash = e.value("identity", "");
|
w.walletIdentityHash = e.value("identity", "");
|
||||||
|
w.scopeId = e.value("scopeId", "");
|
||||||
w.cachedBalance = e.value("balance", -1.0);
|
w.cachedBalance = e.value("balance", -1.0);
|
||||||
w.cachedAddressCount = e.value("addresses", (long long)-1);
|
w.cachedAddressCount = e.value("addresses", (long long)-1);
|
||||||
w.lastOpenedEpoch = e.value("lastOpened", (long long)0);
|
w.lastOpenedEpoch = e.value("lastOpened", (long long)0);
|
||||||
@@ -96,6 +98,7 @@ bool WalletIndex::save()
|
|||||||
e["file"] = w.fileName;
|
e["file"] = w.fileName;
|
||||||
e["name"] = w.displayName;
|
e["name"] = w.displayName;
|
||||||
e["identity"] = w.walletIdentityHash;
|
e["identity"] = w.walletIdentityHash;
|
||||||
|
e["scopeId"] = w.scopeId;
|
||||||
e["balance"] = w.cachedBalance;
|
e["balance"] = w.cachedBalance;
|
||||||
e["addresses"] = w.cachedAddressCount;
|
e["addresses"] = w.cachedAddressCount;
|
||||||
e["lastOpened"] = w.lastOpenedEpoch;
|
e["lastOpened"] = w.lastOpenedEpoch;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ struct WalletIndexEntry {
|
|||||||
std::string fileName; // plain wallet filename in the datadir (e.g. "wallet.dat")
|
std::string fileName; // plain wallet filename in the datadir (e.g. "wallet.dat")
|
||||||
std::string displayName; // user-facing name (defaults to fileName)
|
std::string displayName; // user-facing name (defaults to fileName)
|
||||||
std::string walletIdentityHash; // address-derived identity of the last load; "" = unknown
|
std::string walletIdentityHash; // address-derived identity of the last load; "" = unknown
|
||||||
|
std::string scopeId; // stable per-wallet id ("w:"+hex) for scoping contacts etc.;
|
||||||
|
// generated once, never recomputed from the (mutable) address set
|
||||||
double cachedBalance = -1.0; // last-known total balance; < 0 = unknown (never opened)
|
double cachedBalance = -1.0; // last-known total balance; < 0 = unknown (never opened)
|
||||||
long long cachedAddressCount = -1;// < 0 = unknown
|
long long cachedAddressCount = -1;// < 0 = unknown
|
||||||
long long lastOpenedEpoch = 0; // unix seconds of last open; 0 = never opened
|
long long lastOpenedEpoch = 0; // unix seconds of last open; 0 = never opened
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
|
|
||||||
#include "wallet_state.h"
|
#include "wallet_state.h"
|
||||||
|
#include "../util/text_format.h" // util::formatClockDateTime (app-wide 24h/12h clock)
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -18,12 +19,13 @@ std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>
|
|||||||
for (size_t i = 0; i < addresses.size(); ++i) {
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||||
const auto& address = addresses[i];
|
const auto& address = addresses[i];
|
||||||
if (!address.isSpendable()) continue;
|
if (!address.isSpendable()) continue;
|
||||||
if (requirePositiveBalance && address.balance <= 0.0) continue;
|
// Rank/filter by the CONFIRMED balance — an address holding only 0-conf change can't be sent from.
|
||||||
|
if (requirePositiveBalance && address.spendableBalance <= 0.0) continue;
|
||||||
indices.push_back(i);
|
indices.push_back(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
|
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
|
||||||
return addresses[lhs].balance > addresses[rhs].balance;
|
return addresses[lhs].spendableBalance > addresses[rhs].spendableBalance;
|
||||||
});
|
});
|
||||||
return indices;
|
return indices;
|
||||||
}
|
}
|
||||||
@@ -33,8 +35,8 @@ int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
|||||||
int bestIndex = -1;
|
int bestIndex = -1;
|
||||||
double bestBalance = 0.0;
|
double bestBalance = 0.0;
|
||||||
for (size_t i = 0; i < addresses.size(); ++i) {
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||||
if (addresses[i].isSpendable() && addresses[i].balance > bestBalance) {
|
if (addresses[i].isSpendable() && addresses[i].spendableBalance > bestBalance) {
|
||||||
bestBalance = addresses[i].balance;
|
bestBalance = addresses[i].spendableBalance;
|
||||||
bestIndex = static_cast<int>(i);
|
bestIndex = static_cast<int>(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,13 +46,7 @@ int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
|||||||
std::string TransactionInfo::getTimeString() const
|
std::string TransactionInfo::getTimeString() const
|
||||||
{
|
{
|
||||||
if (timestamp == 0) return "Unknown";
|
if (timestamp == 0) return "Unknown";
|
||||||
|
return util::formatClockDateTime(timestamp);
|
||||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
|
||||||
std::tm* tm = std::localtime(&t);
|
|
||||||
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
|
|
||||||
return ss.str();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TransactionInfo::getTypeDisplay() const
|
std::string TransactionInfo::getTypeDisplay() const
|
||||||
@@ -77,13 +73,7 @@ std::string PeerInfo::getConnectionTime() const
|
|||||||
std::string BannedPeer::getBannedUntilString() const
|
std::string BannedPeer::getBannedUntilString() const
|
||||||
{
|
{
|
||||||
if (banned_until == 0) return "Never";
|
if (banned_until == 0) return "Never";
|
||||||
|
return util::formatClockDateTime(banned_until);
|
||||||
std::time_t t = static_cast<std::time_t>(banned_until);
|
|
||||||
std::tm* tm = std::localtime(&t);
|
|
||||||
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
|
|
||||||
return ss.str();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -21,13 +21,17 @@ namespace dragonx {
|
|||||||
*/
|
*/
|
||||||
struct AddressInfo {
|
struct AddressInfo {
|
||||||
std::string address;
|
std::string address;
|
||||||
double balance = 0.0;
|
double balance = 0.0; // DISPLAY total incl. pending 0-conf change (minconf=0)
|
||||||
std::string type; // "shielded" or "transparent"
|
std::string type; // "shielded" or "transparent"
|
||||||
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
||||||
|
|
||||||
// For display
|
// For display
|
||||||
std::string label;
|
std::string label;
|
||||||
|
|
||||||
|
// CONFIRMED balance (minconf>=1) — what z_sendmany can actually spend now. Kept last so positional
|
||||||
|
// brace-init of the leading fields (used in tests) still compiles.
|
||||||
|
double spendableBalance = 0.0;
|
||||||
|
|
||||||
// Derived
|
// Derived
|
||||||
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
||||||
bool isShielded() const { return type == "shielded"; }
|
bool isShielded() const { return type == "shielded"; }
|
||||||
@@ -252,11 +256,17 @@ struct WalletState {
|
|||||||
// Sync status
|
// Sync status
|
||||||
SyncInfo sync;
|
SyncInfo sync;
|
||||||
|
|
||||||
// Balances (named to match UI usage)
|
// Balances (named to match UI usage). These are the DISPLAY totals — minconf=0, so they include the
|
||||||
double privateBalance = 0.0; // shielded balance
|
// user's own pending change and don't crater during an unconfirmed send.
|
||||||
|
double privateBalance = 0.0; // shielded balance (display, incl. pending change)
|
||||||
double transparentBalance = 0.0;
|
double transparentBalance = 0.0;
|
||||||
double totalBalance = 0.0;
|
double totalBalance = 0.0;
|
||||||
double unconfirmedBalance = 0.0;
|
double unconfirmedBalance = 0.0; // = totalBalance - spendableTotalBalance (the pending portion)
|
||||||
|
// CONFIRMED / spendable totals (minconf>=1) — what can actually be sent right now. z_sendmany runs at
|
||||||
|
// minconf=1, so the Send form / Max / spend validation must size off these, never the display totals.
|
||||||
|
double spendablePrivateBalance = 0.0;
|
||||||
|
double spendableTransparentBalance = 0.0;
|
||||||
|
double spendableTotalBalance = 0.0;
|
||||||
|
|
||||||
// Aliases for backward compatibility
|
// Aliases for backward compatibility
|
||||||
double& shielded_balance = privateBalance;
|
double& shielded_balance = privateBalance;
|
||||||
@@ -302,6 +312,7 @@ struct WalletState {
|
|||||||
|
|
||||||
// Timestamps for refresh logic
|
// Timestamps for refresh logic
|
||||||
int64_t last_balance_update = 0;
|
int64_t last_balance_update = 0;
|
||||||
|
int64_t last_address_update = 0; // set when an address-list refresh applies; 0 = never loaded yet
|
||||||
int64_t last_tx_update = 0;
|
int64_t last_tx_update = 0;
|
||||||
int64_t last_peer_update = 0;
|
int64_t last_peer_update = 0;
|
||||||
int64_t last_mining_update = 0;
|
int64_t last_mining_update = 0;
|
||||||
@@ -325,6 +336,7 @@ struct WalletState {
|
|||||||
sync = SyncInfo{};
|
sync = SyncInfo{};
|
||||||
privateBalance = transparentBalance = totalBalance = 0.0;
|
privateBalance = transparentBalance = totalBalance = 0.0;
|
||||||
unconfirmedBalance = 0.0;
|
unconfirmedBalance = 0.0;
|
||||||
|
spendablePrivateBalance = spendableTransparentBalance = spendableTotalBalance = 0.0;
|
||||||
encrypted = false;
|
encrypted = false;
|
||||||
locked = false;
|
locked = false;
|
||||||
unlocked_until = 0;
|
unlocked_until = 0;
|
||||||
@@ -335,6 +347,15 @@ struct WalletState {
|
|||||||
transactions.clear();
|
transactions.clear();
|
||||||
peers.clear();
|
peers.clear();
|
||||||
bannedPeers.clear();
|
bannedPeers.clear();
|
||||||
|
// W6-1: reset node-level mining state too — the daemon restarts on a wallet switch (mining
|
||||||
|
// stops), so leaving the previous wallet's hashrate/blocks would show stale mining stats.
|
||||||
|
mining = MiningInfo{};
|
||||||
|
pool_mining = PoolMiningState{};
|
||||||
|
// After a disconnect / wallet switch nothing is freshly known, so drop the "last successful
|
||||||
|
// refresh" stamps. Otherwise the pre-teardown time survives and, on reconnect, the staleness
|
||||||
|
// badge (and any "updated X ago" reader) briefly reports it as current until the first refresh
|
||||||
|
// re-stamps it. All readers treat 0 as "never" (formatTimeAgoShort/timeAgo return "").
|
||||||
|
last_balance_update = last_address_update = last_tx_update = last_peer_update = last_mining_update = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild combined addresses list from z/t lists
|
// Rebuild combined addresses list from z/t lists
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ INCBIN(ubuntu_mono, "@CMAKE_SOURCE_DIR@/res/fonts/UbuntuMono-R.ttf");
|
|||||||
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
||||||
INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
|
INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
|
||||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
||||||
|
INCBIN(noto_emoji_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoEmoji-Subset.ttf");
|
||||||
|
INCBIN(twemoji_color, "@CMAKE_SOURCE_DIR@/res/fonts/TwemojiMozilla-Color.ttf");
|
||||||
|
|||||||
@@ -35,4 +35,11 @@ extern "C" {
|
|||||||
|
|
||||||
extern const unsigned char g_noto_cjk_subset_data[];
|
extern const unsigned char g_noto_cjk_subset_data[];
|
||||||
extern const unsigned int g_noto_cjk_subset_size;
|
extern const unsigned int g_noto_cjk_subset_size;
|
||||||
|
|
||||||
|
extern const unsigned char g_noto_emoji_subset_data[];
|
||||||
|
extern const unsigned int g_noto_emoji_subset_size;
|
||||||
|
|
||||||
|
// Twemoji COLR/CPAL color-emoji font (used only when color emoji is enabled + FreeType is available).
|
||||||
|
extern const unsigned char g_twemoji_color_data[];
|
||||||
|
extern const unsigned int g_twemoji_color_size;
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
116
src/main.cpp
116
src/main.cpp
@@ -439,8 +439,16 @@ static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context);
|
|||||||
static void Shutdown(SDL_Window* window, SDL_GLContext gl_context);
|
static void Shutdown(SDL_Window* window, SDL_GLContext gl_context);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Global single instance lock
|
// Global single instance lock. Keyed PER VARIANT so the full node and Lite can run side by side —
|
||||||
|
// their config dirs are already separate (DRAGONX_APP_NAME), so only this lock kept them apart.
|
||||||
|
// NB: DRAGONX_LITE_BUILD is ALWAYS defined (0 for the full node, 1 for Lite) via $<BOOL:...>, so this
|
||||||
|
// must be #if (value), not #ifdef (existence) — #ifdef is true for both and made the full node grab
|
||||||
|
// the Lite lock.
|
||||||
|
#if DRAGONX_LITE_BUILD
|
||||||
|
static dragonx::util::SingleInstance g_single_instance("obsidiandragonlite");
|
||||||
|
#else
|
||||||
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");
|
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");
|
||||||
|
#endif
|
||||||
|
|
||||||
// Check for payment URI in command line args
|
// Check for payment URI in command line args
|
||||||
static std::string findPaymentURI(int argc, char* argv[])
|
static std::string findPaymentURI(int argc, char* argv[])
|
||||||
@@ -713,20 +721,105 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#if defined(__has_include)
|
||||||
|
# if __has_include(<execinfo.h>)
|
||||||
|
# include <execinfo.h>
|
||||||
|
# define DRAGONX_HAVE_BACKTRACE 1
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Absolute path to the crash log, filled at install time so the async-signal handler needs no
|
||||||
|
// allocation. (POSIX counterpart of the Windows SEH CrashHandler above — W7-3.)
|
||||||
|
static char g_crashLogPath[1024] = {0};
|
||||||
|
|
||||||
|
// Async-signal-safe crash handler: only open()/write()/backtrace_symbols_fd()/raise() are used —
|
||||||
|
// no stdio, std::filesystem or malloc (all unsafe inside a signal handler).
|
||||||
|
static void PosixCrashHandler(int sig)
|
||||||
|
{
|
||||||
|
int fd = g_crashLogPath[0] ? open(g_crashLogPath, O_WRONLY | O_CREAT | O_APPEND, 0600) : -1;
|
||||||
|
if (fd >= 0) {
|
||||||
|
auto put = [fd](const char* s) { ssize_t n = write(fd, s, std::strlen(s)); (void)n; };
|
||||||
|
put("\n=== CRASH: signal ");
|
||||||
|
char num[16]; int i = 0, v = sig; // signal number -> decimal, no stdio
|
||||||
|
if (v == 0) { num[i++] = '0'; }
|
||||||
|
else { char tmp[16]; int t = 0; while (v > 0) { tmp[t++] = char('0' + v % 10); v /= 10; }
|
||||||
|
while (t > 0) num[i++] = tmp[--t]; }
|
||||||
|
num[i] = '\n';
|
||||||
|
ssize_t nn = write(fd, num, i + 1); (void)nn;
|
||||||
|
#ifdef DRAGONX_HAVE_BACKTRACE
|
||||||
|
void* frames[64];
|
||||||
|
int nframes = backtrace(frames, 64);
|
||||||
|
backtrace_symbols_fd(frames, nframes, fd); // async-signal-safe
|
||||||
|
#endif
|
||||||
|
put("=== END CRASH ===\n");
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
// Restore the default disposition and re-raise so we still get a core dump / normal termination.
|
||||||
|
signal(sig, SIG_DFL);
|
||||||
|
raise(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void installPosixCrashHandler(const std::string& crashLogPath)
|
||||||
|
{
|
||||||
|
std::snprintf(g_crashLogPath, sizeof(g_crashLogPath), "%s", crashLogPath.c_str());
|
||||||
|
struct sigaction sa;
|
||||||
|
std::memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = PosixCrashHandler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
for (int sig : {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL}) {
|
||||||
|
sigaction(sig, &sa, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // !_WIN32
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
||||||
{
|
{
|
||||||
std::string odDir = dragonx::util::Platform::getObsidianDragonDir();
|
std::string odDir = dragonx::util::Platform::getObsidianDragonDir();
|
||||||
std::error_code ec;
|
std::string odErr;
|
||||||
std::filesystem::create_directories(odDir, ec);
|
if (!dragonx::util::Platform::ensureDirectory(odDir, &odErr)) {
|
||||||
|
// Pre-App-init: nothing (ini, logs, config) can persist if this fails, and the
|
||||||
|
// Windows log redirect below isn't set up yet — report loudly before any setup.
|
||||||
|
std::fprintf(stderr, "%s\n", odErr.c_str());
|
||||||
|
#ifdef _WIN32
|
||||||
|
MessageBoxA(nullptr, odErr.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONERROR);
|
||||||
|
#endif
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
// W7-2: initialize the app-level Logger's file sink on ALL platforms so LOG/LOGF/VERBOSE_LOGF are
|
||||||
// Redirect stdout/stderr to a log file so diagnostic output is visible
|
// actually persisted to dragonx-debug.log. Previously init() was never called, so on Linux/macOS the
|
||||||
// even when built as a GUI app (WIN32_EXECUTABLE hides the console).
|
// file never existed at all (the Windows-only stdout freopen below is a separate mechanism).
|
||||||
{
|
{
|
||||||
std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string();
|
const std::string logPath =
|
||||||
|
(std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string();
|
||||||
|
dragonx::util::Logger::instance().init(logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
// W7-3: install the POSIX crash handler (the Windows SEH filter is installed below). A segfault or
|
||||||
|
// abort now leaves a backtrace in dragonx-crash.log instead of vanishing silently on Linux/macOS.
|
||||||
|
{
|
||||||
|
const std::string crashPath =
|
||||||
|
(std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string();
|
||||||
|
installPosixCrashHandler(crashPath);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Redirect raw stdout/stderr (library / daemon-pipe writes) to a log file so it's visible even when
|
||||||
|
// built as a GUI app (WIN32_EXECUTABLE hides the console). Separate file from the structured Logger
|
||||||
|
// above so the two writers don't interleave/contend on one file.
|
||||||
|
{
|
||||||
|
std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-stdout.log").string();
|
||||||
freopen(logPath.c_str(), "w", stdout);
|
freopen(logPath.c_str(), "w", stdout);
|
||||||
freopen(logPath.c_str(), "a", stderr);
|
freopen(logPath.c_str(), "a", stderr);
|
||||||
}
|
}
|
||||||
@@ -764,11 +857,12 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
// Check for existing instance
|
// Check for existing instance
|
||||||
if (!g_single_instance.tryLock()) {
|
if (!g_single_instance.tryLock()) {
|
||||||
fprintf(stderr, "Another instance of ObsidianDragon is already running.\n");
|
fprintf(stderr, "Another instance of %s is already running.\n", DRAGONX_APP_NAME);
|
||||||
DEBUG_LOGF("Please close the existing instance first.\n");
|
DEBUG_LOGF("Please close the existing instance first.\n");
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.",
|
const std::string msg = std::string("Another instance of ") + DRAGONX_APP_NAME +
|
||||||
L"ObsidianDragon", MB_OK | MB_ICONINFORMATION);
|
" is already running.\nPlease close it first.";
|
||||||
|
MessageBoxA(nullptr, msg.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONINFORMATION);
|
||||||
#endif
|
#endif
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -1966,6 +2060,7 @@ int main(int argc, char* argv[])
|
|||||||
bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
|
bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
|
||||||
&& !opaqueBackground;
|
&& !opaqueBackground;
|
||||||
bool animating = app.isShuttingDown()
|
bool animating = app.isShuttingDown()
|
||||||
|
|| app.isWalletSwitchInProgress()
|
||||||
|| backdropNeedsFrames
|
|| backdropNeedsFrames
|
||||||
|| app.hasTransactionSendProgress()
|
|| app.hasTransactionSendProgress()
|
||||||
|| app.isTransactionRefreshInProgress()
|
|| app.isTransactionRefreshInProgress()
|
||||||
@@ -2032,6 +2127,7 @@ int main(int argc, char* argv[])
|
|||||||
// deadlocks waiting for detached pthreads. On Linux, static
|
// deadlocks waiting for detached pthreads. On Linux, static
|
||||||
// destructors and atexit handlers can also block. _Exit() bypasses
|
// destructors and atexit handlers can also block. _Exit() bypasses
|
||||||
// all of that.
|
// all of that.
|
||||||
|
app.wipeSecrets(); // _Exit() below bypasses ~App(), so scrub secret buffers here (L-05)
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
fflush(stderr);
|
fflush(stderr);
|
||||||
_Exit(0);
|
_Exit(0);
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ static const EmbeddedResource s_resources[] = {
|
|||||||
{ g_dragonx_cli_exe_data, g_dragonx_cli_exe_size, RESOURCE_DRAGONX_CLI },
|
{ g_dragonx_cli_exe_data, g_dragonx_cli_exe_size, RESOURCE_DRAGONX_CLI },
|
||||||
{ g_dragonx_tx_exe_data, g_dragonx_tx_exe_size, RESOURCE_DRAGONX_TX },
|
{ g_dragonx_tx_exe_data, g_dragonx_tx_exe_size, RESOURCE_DRAGONX_TX },
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||||
|
{ g_dragonx_wallet_rebuild_exe_data, g_dragonx_wallet_rebuild_exe_size, RESOURCE_DRAGONX_WALLET_REBUILD },
|
||||||
|
#endif
|
||||||
#ifdef HAS_EMBEDDED_XMRIG
|
#ifdef HAS_EMBEDDED_XMRIG
|
||||||
{ g_xmrig_exe_data, g_xmrig_exe_size, RESOURCE_XMRIG },
|
{ g_xmrig_exe_data, g_xmrig_exe_size, RESOURCE_XMRIG },
|
||||||
#endif
|
#endif
|
||||||
@@ -436,6 +439,24 @@ bool extractEmbeddedResources()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||||
|
// Offline wallet-rebuild recovery helper — extracted next to the daemon so a bare, self-extracting
|
||||||
|
// ObsidianDragon.exe still offers "Repair automatically" (findWalletRebuildHelper() checks this dir).
|
||||||
|
const EmbeddedResource* rebuildRes = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD);
|
||||||
|
if (rebuildRes) {
|
||||||
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_WALLET_REBUILD;
|
||||||
|
if (!std::filesystem::exists(dest)) {
|
||||||
|
DEBUG_LOGF("[INFO] Extracting dragonx-wallet-rebuild (%zu MB)...\n", rebuildRes->size / (1024*1024));
|
||||||
|
if (!extractResource(rebuildRes, dest)) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
#ifndef _WIN32
|
||||||
|
else { chmod(dest.c_str(), 0755); }
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement.
|
// Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement.
|
||||||
// Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly;
|
// Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly;
|
||||||
// if it's still running, the remove fails harmlessly and we retry on the next startup.
|
// if it's still running, the remove fails harmlessly and we retry on the next startup.
|
||||||
@@ -450,6 +471,32 @@ bool extractEmbeddedResources()
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string ensureWalletRebuildHelperExtracted()
|
||||||
|
{
|
||||||
|
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||||
|
const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD);
|
||||||
|
if (!res || res->size == 0) return {};
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char sep = '\\';
|
||||||
|
#else
|
||||||
|
const char sep = '/';
|
||||||
|
#endif
|
||||||
|
const std::string dir = getDaemonDirectory();
|
||||||
|
const std::string dest = dir + sep + RESOURCE_DRAGONX_WALLET_REBUILD;
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(dest, ec)) return dest; // already extracted
|
||||||
|
std::filesystem::create_directories(dir, ec);
|
||||||
|
if (!extractResource(res, dest)) return {};
|
||||||
|
#ifndef _WIN32
|
||||||
|
chmod(dest.c_str(), 0755);
|
||||||
|
#endif
|
||||||
|
DEBUG_LOGF("[INFO] Extracted wallet-rebuild helper on demand: %s\n", dest.c_str());
|
||||||
|
return dest;
|
||||||
|
#else
|
||||||
|
return {};
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string getDaemonDirectory()
|
std::string getDaemonDirectory()
|
||||||
{
|
{
|
||||||
// Daemon binaries live in %APPDATA%/ObsidianDragon/dragonx/ (Windows) or
|
// Daemon binaries live in %APPDATA%/ObsidianDragon/dragonx/ (Windows) or
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ BundledDaemonInfo getBundledDaemonInfo();
|
|||||||
// caller should stop the daemon first. Returns true if all present resources were written.
|
// caller should stop the daemon first. Returns true if all present resources were written.
|
||||||
bool reextractBundledDaemon();
|
bool reextractBundledDaemon();
|
||||||
|
|
||||||
|
// Ensure the embedded offline wallet-rebuild recovery helper is extracted to the daemon dir, and
|
||||||
|
// return its path ("" if not embedded in this build or extraction failed). Idempotent — extracts only
|
||||||
|
// when missing. Unlike the first-run extractEmbeddedResources() (gated on needsParamsExtraction()),
|
||||||
|
// this runs on demand so recovery works from a self-contained exe on ANY run, not just the first.
|
||||||
|
std::string ensureWalletRebuildHelperExtracted();
|
||||||
|
|
||||||
// Resource names
|
// Resource names
|
||||||
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
||||||
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
||||||
@@ -62,6 +68,7 @@ constexpr const char* RESOURCE_ASMAP = "asmap.dat";
|
|||||||
constexpr const char* RESOURCE_DRAGONXD = "dragonxd.exe";
|
constexpr const char* RESOURCE_DRAGONXD = "dragonxd.exe";
|
||||||
constexpr const char* RESOURCE_DRAGONX_CLI = "dragonx-cli.exe";
|
constexpr const char* RESOURCE_DRAGONX_CLI = "dragonx-cli.exe";
|
||||||
constexpr const char* RESOURCE_DRAGONX_TX = "dragonx-tx.exe";
|
constexpr const char* RESOURCE_DRAGONX_TX = "dragonx-tx.exe";
|
||||||
|
constexpr const char* RESOURCE_DRAGONX_WALLET_REBUILD = "dragonx-wallet-rebuild.exe";
|
||||||
constexpr const char* RESOURCE_XMRIG = "xmrig.exe";
|
constexpr const char* RESOURCE_XMRIG = "xmrig.exe";
|
||||||
constexpr const char* RESOURCE_DARK_GRADIENT = "dark_gradient.png";
|
constexpr const char* RESOURCE_DARK_GRADIENT = "dark_gradient.png";
|
||||||
constexpr const char* RESOURCE_LOGO = "logo_ObsidianDragon_dark.png";
|
constexpr const char* RESOURCE_LOGO = "logo_ObsidianDragon_dark.png";
|
||||||
|
|||||||
@@ -14,8 +14,12 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
#include "../util/xmrig_updater.h" // util::sha256Hex
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
@@ -120,30 +124,121 @@ std::string Connection::getSaplingParamsDir()
|
|||||||
return resources::getDaemonDirectory();
|
return resources::getDaemonDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Connection::verifySaplingParams()
|
namespace {
|
||||||
|
|
||||||
|
std::string joinParamPath(const std::string& dir, const std::string& file) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return dir + "\\" + file;
|
||||||
|
#else
|
||||||
|
return dir + "/" + file;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// "<size>:<mtime>" fingerprint used to skip re-hashing an unchanged file. Empty on error.
|
||||||
|
std::string paramStatLine(const std::string& path) {
|
||||||
|
std::error_code ec;
|
||||||
|
auto sz = fs::file_size(path, ec);
|
||||||
|
if (ec) return {};
|
||||||
|
auto mtime = fs::last_write_time(path, ec);
|
||||||
|
long long ticks = ec ? 0 :
|
||||||
|
std::chrono::duration_cast<std::chrono::seconds>(mtime.time_since_epoch()).count();
|
||||||
|
return std::to_string(static_cast<unsigned long long>(sz)) + ":" + std::to_string(ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool paramHashMatches(const std::string& path, const std::string& expectedHex) {
|
||||||
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!f) return false;
|
||||||
|
std::streamsize sz = f.tellg();
|
||||||
|
if (sz <= 0) return false;
|
||||||
|
f.seekg(0, std::ios::beg);
|
||||||
|
std::vector<char> buf(static_cast<size_t>(sz));
|
||||||
|
if (!f.read(buf.data(), sz)) return false;
|
||||||
|
std::string got = util::sha256Hex(buf.data(), buf.size());
|
||||||
|
return !got.empty() && got == expectedHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The verification cache: <params_dir>/.sapling_verified holds one paramStatLine per param,
|
||||||
|
// in list order, from the last successful hash check.
|
||||||
|
bool saplingMarkerMatches(const std::string& markerPath, const std::vector<std::string>& expected) {
|
||||||
|
for (const auto& s : expected) if (s.empty()) return false; // couldn't stat -> don't trust
|
||||||
|
std::ifstream f(markerPath);
|
||||||
|
if (!f) return false;
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::string l;
|
||||||
|
while (std::getline(f, l)) lines.push_back(l);
|
||||||
|
return lines == expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeSaplingMarker(const std::string& markerPath, const std::vector<std::string>& lines) {
|
||||||
|
std::ofstream f(markerPath, std::ios::trunc);
|
||||||
|
if (!f) return;
|
||||||
|
for (const auto& l : lines) f << l << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical Zcash-family Sapling trusted-setup param digests — identical bytes across every
|
||||||
|
// fork/platform. Source of truth: scripts/build-lite-backend-artifact.sh ensure_sapling_params().
|
||||||
|
// Keep in sync if the params are ever rotated.
|
||||||
|
const std::pair<std::string, std::string> kSaplingParamDigests[] = {
|
||||||
|
{ "sapling-spend.params", "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13" },
|
||||||
|
{ "sapling-output.params", "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4" },
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool Connection::verifySaplingParamsIn(
|
||||||
|
const std::string& dir,
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& digests)
|
||||||
{
|
{
|
||||||
std::string params_dir = getSaplingParamsDir();
|
if (dir.empty()) {
|
||||||
if (params_dir.empty()) {
|
|
||||||
DEBUG_LOGF("verifySaplingParams: params dir is empty\n");
|
DEBUG_LOGF("verifySaplingParams: params dir is empty\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (digests.empty()) return false;
|
||||||
|
|
||||||
#ifdef _WIN32
|
// 1) Every param must exist.
|
||||||
std::string spend_path = params_dir + "\\sapling-spend.params";
|
std::vector<std::string> paths;
|
||||||
std::string output_path = params_dir + "\\sapling-output.params";
|
paths.reserve(digests.size());
|
||||||
#else
|
for (const auto& d : digests) {
|
||||||
std::string spend_path = params_dir + "/sapling-spend.params";
|
std::string p = joinParamPath(dir, d.first);
|
||||||
std::string output_path = params_dir + "/sapling-output.params";
|
if (!fs::exists(p)) {
|
||||||
#endif
|
DEBUG_LOGF("verifySaplingParams: %s MISSING\n", p.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
paths.push_back(std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
bool spend_exists = fs::exists(spend_path);
|
// 2) Fast path: if the cached marker matches the current size:mtime of every param, trust
|
||||||
bool output_exists = fs::exists(output_path);
|
// the previous successful hash instead of re-hashing ~48MB on every startup.
|
||||||
|
const std::string markerPath = joinParamPath(dir, ".sapling_verified");
|
||||||
|
std::vector<std::string> current;
|
||||||
|
current.reserve(paths.size());
|
||||||
|
for (const auto& p : paths) current.push_back(paramStatLine(p));
|
||||||
|
if (saplingMarkerMatches(markerPath, current)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("verifySaplingParams: dir=%s\n", params_dir.c_str());
|
// 3) Integrity-check each param against its pinned SHA-256. A truncated or corrupt param
|
||||||
DEBUG_LOGF(" spend: %s -> %s\n", spend_path.c_str(), spend_exists ? "found" : "MISSING");
|
// (a partial extraction, or a Linux bundle where the file merely *exists*) is rejected
|
||||||
DEBUG_LOGF(" output: %s -> %s\n", output_path.c_str(), output_exists ? "found" : "MISSING");
|
// here instead of being handed to the daemon and failing later on a shielded operation.
|
||||||
|
for (size_t i = 0; i < paths.size(); ++i) {
|
||||||
|
if (!paramHashMatches(paths[i], digests[i].second)) {
|
||||||
|
DEBUG_LOGF("verifySaplingParams: %s FAILED integrity check (truncated or corrupt)\n",
|
||||||
|
paths[i].c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return spend_exists && output_exists;
|
// 4) Record the verified state so later startups take the fast path.
|
||||||
|
writeSaplingMarker(markerPath, current);
|
||||||
|
DEBUG_LOGF("verifySaplingParams: %zu params verified (sha256)\n", paths.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Connection::verifySaplingParams()
|
||||||
|
{
|
||||||
|
std::vector<std::pair<std::string, std::string>> digests;
|
||||||
|
for (const auto& d : kSaplingParamDigests) digests.emplace_back(d.first, d.second);
|
||||||
|
return verifySaplingParamsIn(getSaplingParamsDir(), digests);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionConfig Connection::parseConfFile(const std::string& path)
|
ConnectionConfig Connection::parseConfFile(const std::string& path)
|
||||||
@@ -195,6 +290,8 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
|
|||||||
config.proxy = value;
|
config.proxy = value;
|
||||||
} else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") {
|
} else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") {
|
||||||
config.use_tls = parseBoolValue(value);
|
config.use_tls = parseBoolValue(value);
|
||||||
|
} else if (key == "rpcallowplaintext") {
|
||||||
|
config.allow_plaintext_remote = parseBoolValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,11 +306,14 @@ ConnectionConfig Connection::autoDetectConfig()
|
|||||||
{
|
{
|
||||||
ConnectionConfig config;
|
ConnectionConfig config;
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Ensure the data directory exists. Use the non-throwing helper and report any failure
|
||||||
|
// via config.dir_error so callers can surface it — the old throwing create_directories()
|
||||||
|
// overload could raise an uncaught filesystem_error straight through autoDetectConfig()'s
|
||||||
|
// callers (read-only home, permission denied, etc.).
|
||||||
std::string data_dir = getDefaultDataDir();
|
std::string data_dir = getDefaultDataDir();
|
||||||
if (!fs::exists(data_dir)) {
|
if (!util::Platform::ensureDirectory(data_dir, &config.dir_error)) {
|
||||||
DEBUG_LOGF("Creating data directory: %s\n", data_dir.c_str());
|
DEBUG_LOGF("[ERROR] autoDetectConfig: %s\n", config.dir_error.c_str());
|
||||||
fs::create_directories(data_dir);
|
return config; // data dir unusable — bail early with dir_error set
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find DRAGONX.conf
|
// Try to find DRAGONX.conf
|
||||||
@@ -268,6 +368,31 @@ bool Connection::buildCookieAuthConfig(const ConnectionConfig& base, ConnectionC
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True only for a well-formed IPv4 loopback literal (127.0.0.0/8): exactly four dot-separated
|
||||||
|
// 0-255 octets with the first == 127. Rejects "127.evil.com", "127.0.0.1.attacker",
|
||||||
|
// "127.300.0.1", "1270.0.0.1", etc. — the old rfind("127.",0)==0 prefix matched all of those.
|
||||||
|
static bool isExactIPv4Loopback(const std::string& host)
|
||||||
|
{
|
||||||
|
int octets = 0, value = 0, digits = 0;
|
||||||
|
bool firstIs127 = false;
|
||||||
|
for (size_t i = 0; i <= host.size(); ++i) {
|
||||||
|
const char c = (i < host.size()) ? host[i] : '.'; // trailing sentinel flushes the last octet
|
||||||
|
if (c == '.') {
|
||||||
|
if (digits == 0 || digits > 3 || value > 255) return false;
|
||||||
|
if (octets == 0) firstIs127 = (value == 127);
|
||||||
|
++octets;
|
||||||
|
value = 0;
|
||||||
|
digits = 0;
|
||||||
|
} else if (c >= '0' && c <= '9') {
|
||||||
|
value = value * 10 + (c - '0');
|
||||||
|
++digits;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return octets == 4 && firstIs127;
|
||||||
|
}
|
||||||
|
|
||||||
bool Connection::isLocalHost(const std::string& host)
|
bool Connection::isLocalHost(const std::string& host)
|
||||||
{
|
{
|
||||||
std::string lowered = lowercase(host);
|
std::string lowered = lowercase(host);
|
||||||
@@ -277,7 +402,7 @@ bool Connection::isLocalHost(const std::string& host)
|
|||||||
|
|
||||||
return lowered == "localhost" || lowered == "localhost." ||
|
return lowered == "localhost" || lowered == "localhost." ||
|
||||||
lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" ||
|
lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" ||
|
||||||
lowered == "127.0.0.1" || lowered.rfind("127.", 0) == 0;
|
isExactIPv4Loopback(lowered);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
|
bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
|
||||||
@@ -285,6 +410,13 @@ bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
|
|||||||
return !config.use_tls && !isLocalHost(config.host);
|
return !config.use_tls && !isLocalHost(config.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Connection::allowsPlaintextRemote(const ConnectionConfig& config)
|
||||||
|
{
|
||||||
|
// Explicit opt-in (DRAGONX.conf: rpcallowplaintext=1) to send credentials over a plaintext
|
||||||
|
// link to a remote host. Off by default — see usesPlaintextRemote().
|
||||||
|
return config.allow_plaintext_remote;
|
||||||
|
}
|
||||||
|
|
||||||
const char* Connection::authSourceName(AuthSource source)
|
const char* Connection::authSourceName(AuthSource source)
|
||||||
{
|
{
|
||||||
switch (source) {
|
switch (source) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace rpc {
|
namespace rpc {
|
||||||
@@ -27,7 +29,11 @@ struct ConnectionConfig {
|
|||||||
std::string proxy; // SOCKS5 proxy for Tor
|
std::string proxy; // SOCKS5 proxy for Tor
|
||||||
bool use_embedded = true;
|
bool use_embedded = true;
|
||||||
bool use_tls = false;
|
bool use_tls = false;
|
||||||
|
bool allow_plaintext_remote = false; // rpcallowplaintext=1 — opt in to plaintext creds to a remote host
|
||||||
AuthSource auth_source = AuthSource::Missing;
|
AuthSource auth_source = AuthSource::Missing;
|
||||||
|
// Non-empty when autoDetectConfig() could not create the data directory; callers
|
||||||
|
// should surface it and abort the connect rather than proceeding blindly.
|
||||||
|
std::string dir_error;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +75,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
static bool verifySaplingParams();
|
static bool verifySaplingParams();
|
||||||
|
|
||||||
|
// Verify the Sapling params in `dir` against a { filename, expected-sha256-hex } list.
|
||||||
|
// Exposed with an injectable dir + digest list so the integrity + marker-cache logic is
|
||||||
|
// unit-testable without the real ~48MB params; verifySaplingParams() calls it with the
|
||||||
|
// pinned production digests and getSaplingParamsDir().
|
||||||
|
static bool verifySaplingParamsIn(
|
||||||
|
const std::string& dir,
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& digests);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the Sapling params directory
|
* @brief Get the Sapling params directory
|
||||||
*/
|
*/
|
||||||
@@ -119,6 +133,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
static bool usesPlaintextRemote(const ConnectionConfig& config);
|
static bool usesPlaintextRemote(const ConnectionConfig& config);
|
||||||
|
|
||||||
|
// Whether plaintext credentials to a remote host are explicitly allowed (opt-in via the
|
||||||
|
// DRAGONX.conf rpcallowplaintext key). Off by default: usesPlaintextRemote() && !this
|
||||||
|
// means the connect is refused.
|
||||||
|
static bool allowsPlaintextRemote(const ConnectionConfig& config);
|
||||||
|
|
||||||
static const char* authSourceName(AuthSource source);
|
static const char* authSourceName(AuthSource source);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ namespace rpc {
|
|||||||
|
|
||||||
namespace {
|
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.
|
||||||
|
// 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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::mutex g_trace_mutex;
|
std::mutex g_trace_mutex;
|
||||||
RPCClient::TraceCallback g_trace_callback;
|
RPCClient::TraceCallback g_trace_callback;
|
||||||
std::atomic_bool g_trace_enabled{false};
|
std::atomic_bool g_trace_enabled{false};
|
||||||
@@ -85,6 +98,10 @@ void RPCClient::setTraceSource(std::string source)
|
|||||||
// Callback for libcurl to write response data
|
// Callback for libcurl to write response data
|
||||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
||||||
size_t totalSize = size * nmemb;
|
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);
|
userp->append((char*)contents, totalSize);
|
||||||
return totalSize;
|
return totalSize;
|
||||||
}
|
}
|
||||||
@@ -129,7 +146,11 @@ RPCClient::RPCClient() : impl_(std::make_unique<Impl>())
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
RPCClient::~RPCClient() = default;
|
RPCClient::~RPCClient() {
|
||||||
|
// Scrub the persistent Basic-auth secret on destruction (disconnect() may not have run). impl_ is
|
||||||
|
// still destroyed normally afterward (curl cleanup unchanged). (L-04)
|
||||||
|
if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size());
|
||||||
|
}
|
||||||
|
|
||||||
bool RPCClient::connect(const std::string& host, const std::string& port,
|
bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||||
const std::string& user, const std::string& password)
|
const std::string& user, const std::string& password)
|
||||||
@@ -149,6 +170,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
// Create Basic auth header with proper base64 encoding, then wipe the plaintext
|
// Create Basic auth header with proper base64 encoding, then wipe the plaintext
|
||||||
// "user:password" temporary (std::string does not zero its buffer on destruction).
|
// "user:password" temporary (std::string does not zero its buffer on destruction).
|
||||||
std::string credentials = user + ":" + password;
|
std::string credentials = user + ":" + password;
|
||||||
|
if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size()); // wipe any prior secret before overwrite (L-04)
|
||||||
auth_ = util::base64_encode(credentials);
|
auth_ = util::base64_encode(credentials);
|
||||||
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
||||||
|
|
||||||
@@ -176,6 +198,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
|
impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
|
||||||
std::string auth_header = "Authorization: Basic " + auth_;
|
std::string auth_header = "Authorization: Basic " + auth_;
|
||||||
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
|
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
|
||||||
|
if (!auth_header.empty()) sodium_memzero(auth_header.data(), auth_header.size()); // curl copied it (L-04)
|
||||||
|
|
||||||
// Configure curl
|
// Configure curl
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
||||||
@@ -191,6 +214,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).
|
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
|
||||||
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
|
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
|
// 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
|
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
|
||||||
@@ -278,6 +305,7 @@ void RPCClient::disconnect()
|
|||||||
curl_slist_free_all(impl_->headers);
|
curl_slist_free_all(impl_->headers);
|
||||||
impl_->headers = nullptr;
|
impl_->headers = nullptr;
|
||||||
}
|
}
|
||||||
|
if (!auth_.empty()) { sodium_memzero(auth_.data(), auth_.size()); auth_.clear(); } // scrub Basic-auth secret (L-04)
|
||||||
}
|
}
|
||||||
|
|
||||||
json RPCClient::makePayload(const std::string& method, const json& params)
|
json RPCClient::makePayload(const std::string& method, const json& params)
|
||||||
@@ -317,7 +345,7 @@ std::string RPCClient::performCall(const std::string& method, const json& params
|
|||||||
return response_data;
|
return response_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
json RPCClient::parseRpcResult(long httpCode, const std::string& body)
|
json RPCClient::parseRpcResult(long httpCode, const std::string& body, bool scrubSource)
|
||||||
{
|
{
|
||||||
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
|
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
|
||||||
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
||||||
@@ -355,7 +383,9 @@ json RPCClient::parseRpcResult(long httpCode, const std::string& body)
|
|||||||
throw RpcError(errCode, "RPC error: " + err_msg);
|
throw RpcError(errCode, "RPC error: " + err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response["result"];
|
json result = response["result"]; // a COPY of the subobject (operator[] yields an lvalue ref)
|
||||||
|
if (scrubSource) scrubJsonSecrets(response); // zero the discarded tree's secret before it frees (B7)
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
json RPCClient::call(const std::string& method, const json& params)
|
json RPCClient::call(const std::string& method, const json& params)
|
||||||
@@ -370,6 +400,56 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
return parseRpcResult(http_code, response_data);
|
return parseRpcResult(http_code, response_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json RPCClient::callSecret(const std::string& method, const json& params)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
|
if (!impl_->curl) {
|
||||||
|
throw std::runtime_error("Not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero the raw response body whether parsing succeeds or throws — it holds the secret in the
|
||||||
|
// clear (the curl write buffer performCall returns), and would otherwise be freed un-wiped (B7).
|
||||||
|
long http_code = 0;
|
||||||
|
std::string response_data = performCall(method, params, http_code);
|
||||||
|
try {
|
||||||
|
json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
|
||||||
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||||
|
return result;
|
||||||
|
} catch (...) {
|
||||||
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RPCClient::callSecretString(const std::string& method, const json& params)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
|
if (!impl_->curl) {
|
||||||
|
throw std::runtime_error("Not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
long http_code = 0;
|
||||||
|
std::string response_data = performCall(method, params, http_code);
|
||||||
|
try {
|
||||||
|
json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
|
||||||
|
std::string out;
|
||||||
|
if (result.is_string()) {
|
||||||
|
// Copy the secret out, then zero the json node's OWN heap buffer — parseRpcResult's
|
||||||
|
// json::parse allocates this independently of response_data, so scrubbing the raw body
|
||||||
|
// alone would leave it behind (B7). `out` is now the only live copy; the caller wipes it.
|
||||||
|
auto& s = result.get_ref<std::string&>();
|
||||||
|
out = s;
|
||||||
|
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
||||||
|
}
|
||||||
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||||
|
if (!result.is_string()) throw std::runtime_error("RPC result is not a string");
|
||||||
|
return out;
|
||||||
|
} catch (...) {
|
||||||
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
@@ -445,14 +525,22 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto& result = oj["result"];
|
auto& result = oj["result"];
|
||||||
|
std::string out;
|
||||||
if (result.is_null()) {
|
if (result.is_null()) {
|
||||||
return "null";
|
out = "null";
|
||||||
} else if (result.is_string()) {
|
} else if (result.is_string()) {
|
||||||
// Return the raw string (not JSON-encoded) — caller wraps as needed
|
// Return the raw string (not JSON-encoded) — caller wraps as needed
|
||||||
return result.get<std::string>();
|
out = result.get<std::string>();
|
||||||
} else {
|
} 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)
|
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
|
||||||
|
|||||||
@@ -133,6 +133,26 @@ public:
|
|||||||
*/
|
*/
|
||||||
json call(const std::string& method, const json& params = json::array());
|
json call(const std::string& method, const json& params = json::array());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Like call(), but zeroes the raw HTTP response body after parsing (B7).
|
||||||
|
*
|
||||||
|
* For RPCs whose response carries a secret (z_exportmnemonic / z_exportkey), the parsed
|
||||||
|
* value is scrubbed by the caller — but the raw response string this method builds also
|
||||||
|
* holds that secret and is otherwise freed without zeroing. Use this variant so the largest,
|
||||||
|
* longest-lived plaintext copy doesn't linger in freed heap. Slightly slower (an extra wipe);
|
||||||
|
* only worth it for secret-bearing calls.
|
||||||
|
*/
|
||||||
|
json callSecret(const std::string& method, const json& params = json::array());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief callSecret() for RPCs whose result is a bare secret string (z_exportkey / dumpprivkey /
|
||||||
|
* z_exportviewingkey). Returns the result string with BOTH the raw response body AND the
|
||||||
|
* parsed json's own copy of the secret zeroed — so no un-scrubbed heap copy survives the
|
||||||
|
* call. The returned std::string is the only remaining copy; the caller owns it and must
|
||||||
|
* wipe it (sodium_memzero) when done. Throws if the result is not a JSON string.
|
||||||
|
*/
|
||||||
|
std::string callSecretString(const std::string& method, const json& params = json::array());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Make a raw RPC call with a custom timeout
|
* @brief Make a raw RPC call with a custom timeout
|
||||||
* @param method RPC method name
|
* @param method RPC method name
|
||||||
@@ -250,8 +270,10 @@ private:
|
|||||||
// hold curl_mutex_ and have verified impl_->curl.
|
// hold curl_mutex_ and have verified impl_->curl.
|
||||||
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
||||||
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
||||||
// response["result"] on success.
|
// response["result"] on success. When scrubSource is true (secret-bearing calls), the intermediate
|
||||||
static json parseRpcResult(long httpCode, const std::string& body);
|
// parse tree's string values are zeroed before it is discarded — parseRpcResult returns a COPY of
|
||||||
|
// the result node, so its own tree would otherwise free the secret un-wiped (B7).
|
||||||
|
static json parseRpcResult(long httpCode, const std::string& body, bool scrubSource = false);
|
||||||
|
|
||||||
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
|
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
|
||||||
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
|
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -37,16 +38,27 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
|||||||
{
|
{
|
||||||
if (!unspent.is_array()) return;
|
if (!unspent.is_array()) return;
|
||||||
|
|
||||||
std::map<std::string, double> balances;
|
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
||||||
|
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
||||||
|
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
||||||
|
std::map<std::string, double> total;
|
||||||
|
std::map<std::string, double> spendable;
|
||||||
for (const auto& output : unspent) {
|
for (const auto& output : unspent) {
|
||||||
auto address = readOptional<std::string>(output, "address");
|
auto address = readOptional<std::string>(output, "address");
|
||||||
auto amount = readOptional<double>(output, "amount");
|
auto amount = readOptional<double>(output, "amount");
|
||||||
if (address && amount) balances[*address] += *amount;
|
if (!address || !amount) continue;
|
||||||
|
total[*address] += *amount;
|
||||||
|
auto conf = readOptional<int>(output, "confirmations");
|
||||||
|
if (conf && *conf >= 1) spendable[*address] += *amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
||||||
|
// notes in this set is 0, and spendableBalance is always a subset sum of balance.
|
||||||
for (auto& info : addresses) {
|
for (auto& info : addresses) {
|
||||||
auto balance = balances.find(info.address);
|
auto t = total.find(info.address);
|
||||||
if (balance != balances.end()) info.balance = balance->second;
|
auto s = spendable.find(info.address);
|
||||||
|
info.balance = (t != total.end()) ? t->second : 0.0;
|
||||||
|
info.spendableBalance = (s != spendable.end()) ? s->second : 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,20 +180,6 @@ void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetada
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
|
||||||
const std::string& txid,
|
|
||||||
const NetworkRefreshService::TransactionViewCacheEntry& entry)
|
|
||||||
{
|
|
||||||
if (!chat::hushChatFeatureEnabledAtBuild()) return;
|
|
||||||
|
|
||||||
std::vector<chat::HushChatMemoOutput> outputs;
|
|
||||||
outputs.reserve(entry.outgoing_outputs.size());
|
|
||||||
for (const auto& output : entry.outgoing_outputs) {
|
|
||||||
if (!output.memo.empty()) outputs.push_back(chat::HushChatMemoOutput{output.position, output.memo});
|
|
||||||
}
|
|
||||||
appendExtractedHushChatMetadata(destination, txid, outputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
||||||
const HushChatMemoOutputMap& outputsByTxid)
|
const HushChatMemoOutputMap& outputsByTxid)
|
||||||
{
|
{
|
||||||
@@ -263,7 +261,7 @@ NetworkRefreshService::ConnectionInitResult NetworkRefreshService::collectConnec
|
|||||||
}
|
}
|
||||||
|
|
||||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefreshResult(
|
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefreshResult(
|
||||||
const json& totalBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
const json& totalBalance, const json& spendableBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
||||||
{
|
{
|
||||||
CoreRefreshResult result;
|
CoreRefreshResult result;
|
||||||
result.balanceOk = balanceOk && totalBalance.is_object();
|
result.balanceOk = balanceOk && totalBalance.is_object();
|
||||||
@@ -272,6 +270,11 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
|||||||
result.transparentBalance = readBalanceString(totalBalance, "transparent");
|
result.transparentBalance = readBalanceString(totalBalance, "transparent");
|
||||||
result.totalBalance = readBalanceString(totalBalance, "total");
|
result.totalBalance = readBalanceString(totalBalance, "total");
|
||||||
}
|
}
|
||||||
|
if (spendableBalance.is_object()) { // confirmed totals (minconf=1); left unset on old daemons
|
||||||
|
result.spendableShieldedBalance = readBalanceString(spendableBalance, "private");
|
||||||
|
result.spendableTransparentBalance = readBalanceString(spendableBalance, "transparent");
|
||||||
|
result.spendableTotalBalance = readBalanceString(spendableBalance, "total");
|
||||||
|
}
|
||||||
|
|
||||||
result.blockchainOk = blockOk && blockInfo.is_object();
|
result.blockchainOk = blockOk && blockInfo.is_object();
|
||||||
if (result.blockchainOk) {
|
if (result.blockchainOk) {
|
||||||
@@ -288,17 +291,28 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
|||||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance)
|
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance)
|
||||||
{
|
{
|
||||||
json totalBalance;
|
json totalBalance;
|
||||||
|
json spendableBalance;
|
||||||
json blockInfo;
|
json blockInfo;
|
||||||
bool balanceOk = false;
|
bool balanceOk = false;
|
||||||
bool blockOk = false;
|
bool blockOk = false;
|
||||||
|
double balanceScanMs = 0.0;
|
||||||
|
|
||||||
if (includeBalance) {
|
if (includeBalance) {
|
||||||
try {
|
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
|
||||||
totalBalance = rpc.call("z_gettotalbalance", json::array());
|
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
|
||||||
|
// (balanceRefreshDue()), keeping balance scans from starving block connection.
|
||||||
|
const auto balanceStart = std::chrono::steady_clock::now();
|
||||||
|
try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater
|
||||||
|
totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
|
||||||
balanceOk = true;
|
balanceOk = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||||
}
|
}
|
||||||
|
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
|
||||||
|
spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
|
||||||
|
} catch (...) {}
|
||||||
|
balanceScanMs = std::chrono::duration<double, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - balanceStart).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -308,7 +322,9 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
|||||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseCoreRefreshResult(totalBalance, balanceOk, blockInfo, blockOk);
|
auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
|
||||||
|
result.balanceScanMs = balanceScanMs;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
|
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
|
||||||
@@ -440,16 +456,32 @@ std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::
|
|||||||
if (!parsed.contains("dragonx-2")) return std::nullopt;
|
if (!parsed.contains("dragonx-2")) return std::nullopt;
|
||||||
|
|
||||||
const auto& data = parsed["dragonx-2"];
|
const auto& data = parsed["dragonx-2"];
|
||||||
|
// CoinGecko emits JSON null (not an omitted key) for fields it can't currently compute —
|
||||||
|
// commonly usd_24h_change on illiquid/newly-listed tokens — while still returning a valid
|
||||||
|
// spot price in the same object. .value(key, default) throws type_error on a PRESENT null,
|
||||||
|
// which the outer catch turns into "no price update at all", so read null-tolerantly and
|
||||||
|
// keep the valid usd/btc rather than discarding the whole refresh.
|
||||||
|
auto num = [&data](const char* key, double def) {
|
||||||
|
auto it = data.find(key);
|
||||||
|
return (it != data.end() && it->is_number()) ? it->get<double>() : def;
|
||||||
|
};
|
||||||
PriceRefreshResult result;
|
PriceRefreshResult result;
|
||||||
result.market.price_usd = data.value("usd", 0.0);
|
result.market.price_usd = num("usd", 0.0);
|
||||||
result.market.price_btc = data.value("btc", 0.0);
|
result.market.price_btc = num("btc", 0.0);
|
||||||
result.market.change_24h = data.value("usd_24h_change", 0.0);
|
result.market.change_24h = num("usd_24h_change", 0.0);
|
||||||
result.market.volume_24h = data.value("usd_24h_vol", 0.0);
|
result.market.volume_24h = num("usd_24h_vol", 0.0);
|
||||||
result.market.market_cap = data.value("usd_market_cap", 0.0);
|
result.market.market_cap = num("usd_market_cap", 0.0);
|
||||||
|
|
||||||
char buf[64];
|
char buf[64];
|
||||||
std::tm* tm = std::localtime(&fetchedAt);
|
// Runs on the RPC worker thread — std::localtime shares a process-wide static tm, so use the
|
||||||
if (tm && std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm) > 0) {
|
// 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;
|
result.market.last_updated = buf;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -606,18 +638,23 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
|||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
||||||
|
result.addressListOk = false; // enumeration failed → the shielded list may be falsely short
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json unspent = rpc.call("z_listunspent", json::array());
|
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
|
||||||
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||||
for (auto& info : result.shieldedAddresses) {
|
for (auto& info : result.shieldedAddresses) {
|
||||||
try {
|
try { // display total (minconf=0, includes pending change)
|
||||||
json balance = rpc.call("z_getbalance", json::array({info.address}));
|
json total = rpc.call("z_getbalance", json::array({info.address, 0}));
|
||||||
if (!balance.is_null()) info.balance = balance.get<double>();
|
if (!total.is_null()) info.balance = total.get<double>();
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
|
try { // spendable (minconf=1); degrade to the display value on old daemons
|
||||||
|
json conf = rpc.call("z_getbalance", json::array({info.address, 1}));
|
||||||
|
info.spendableBalance = (!conf.is_null()) ? conf.get<double>() : info.balance;
|
||||||
|
} catch (...) { info.spendableBalance = info.balance; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,10 +663,11 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
|||||||
result.transparentAddresses = parseTransparentAddressList(tList);
|
result.transparentAddresses = parseTransparentAddressList(tList);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
||||||
|
result.addressListOk = false; // enumeration failed → the transparent list may be falsely short
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json unspent = rpc.call("listunspent", json::array());
|
json unspent = rpc.call("listunspent", json::array({0})); // minconf=0 → include 0-conf change
|
||||||
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
|
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||||
@@ -920,7 +958,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
if (cached != snapshot.viewTxCache.end()) {
|
if (cached != snapshot.viewTxCache.end()) {
|
||||||
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
||||||
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
||||||
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, cached->second);
|
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
||||||
|
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
||||||
|
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
||||||
|
// recorded by the local echo. (The recent-refresh variant already omits this.)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -945,7 +986,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
|
|
||||||
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
||||||
appendViewTransactionOutputs(result.transactions, txid, entry);
|
appendViewTransactionOutputs(result.transactions, txid, entry);
|
||||||
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, entry);
|
// (Chat metadata is harvested only from RECEIVED notes, not our own outgoing memos — see
|
||||||
|
// the cached-view branch above.)
|
||||||
|
|
||||||
json rawTransaction;
|
json rawTransaction;
|
||||||
bool hasRawTransaction = false;
|
bool hasRawTransaction = false;
|
||||||
@@ -1104,12 +1146,16 @@ NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOpe
|
|||||||
std::set<std::string> reported;
|
std::set<std::string> reported;
|
||||||
for (const auto& op : result) {
|
for (const auto& op : result) {
|
||||||
if (!op.is_object()) continue;
|
if (!op.is_object()) continue;
|
||||||
std::string opid = op.value("id", std::string());
|
// Type-checked reads: .value(key, default) throws if the key is PRESENT with a non-string
|
||||||
|
// type, which would abort the whole poll (and wedge it for the session — see the call site).
|
||||||
|
if (!op.contains("id") || !op["id"].is_string()) continue;
|
||||||
|
std::string opid = op["id"].get<std::string>();
|
||||||
if (opid.empty()) continue;
|
if (opid.empty()) continue;
|
||||||
if (requested.find(opid) == requested.end()) continue; // not one of ours — ignore
|
if (requested.find(opid) == requested.end()) continue; // not one of ours — ignore
|
||||||
reported.insert(opid);
|
reported.insert(opid);
|
||||||
|
|
||||||
std::string status = op.value("status", std::string());
|
std::string status = (op.contains("status") && op["status"].is_string())
|
||||||
|
? op["status"].get<std::string>() : std::string();
|
||||||
if (status == "success") {
|
if (status == "success") {
|
||||||
parsed.doneOpids.push_back(opid);
|
parsed.doneOpids.push_back(opid);
|
||||||
parsed.anySuccess = true;
|
parsed.anySuccess = true;
|
||||||
@@ -1187,6 +1233,12 @@ void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
|
|||||||
if (result.shieldedBalance) state.shielded_balance = *result.shieldedBalance;
|
if (result.shieldedBalance) state.shielded_balance = *result.shieldedBalance;
|
||||||
if (result.transparentBalance) state.transparent_balance = *result.transparentBalance;
|
if (result.transparentBalance) state.transparent_balance = *result.transparentBalance;
|
||||||
if (result.totalBalance) state.total_balance = *result.totalBalance;
|
if (result.totalBalance) state.total_balance = *result.totalBalance;
|
||||||
|
// Confirmed/spendable totals; if the minconf=1 call was unavailable (old daemon) degrade to the
|
||||||
|
// display value so nothing is *over*-reported as spendable (z_sendmany stays the final gate).
|
||||||
|
state.spendablePrivateBalance = result.spendableShieldedBalance.value_or(state.privateBalance);
|
||||||
|
state.spendableTransparentBalance = result.spendableTransparentBalance.value_or(state.transparentBalance);
|
||||||
|
state.spendableTotalBalance = result.spendableTotalBalance.value_or(state.totalBalance);
|
||||||
|
state.unconfirmedBalance = std::max(0.0, state.totalBalance - state.spendableTotalBalance);
|
||||||
state.last_balance_update = updatedAt;
|
state.last_balance_update = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,9 +98,12 @@ public:
|
|||||||
|
|
||||||
struct CoreRefreshResult {
|
struct CoreRefreshResult {
|
||||||
bool balanceOk = false;
|
bool balanceOk = false;
|
||||||
std::optional<double> shieldedBalance;
|
std::optional<double> shieldedBalance; // display (minconf=0, incl. pending change)
|
||||||
std::optional<double> transparentBalance;
|
std::optional<double> transparentBalance;
|
||||||
std::optional<double> totalBalance;
|
std::optional<double> totalBalance;
|
||||||
|
std::optional<double> spendableShieldedBalance; // confirmed (minconf=1)
|
||||||
|
std::optional<double> spendableTransparentBalance;
|
||||||
|
std::optional<double> spendableTotalBalance;
|
||||||
bool blockchainOk = false;
|
bool blockchainOk = false;
|
||||||
std::optional<int> blocks;
|
std::optional<int> blocks;
|
||||||
std::optional<int> headers;
|
std::optional<int> headers;
|
||||||
@@ -108,6 +111,7 @@ public:
|
|||||||
std::optional<double> verificationProgress;
|
std::optional<double> verificationProgress;
|
||||||
std::optional<int> longestChain;
|
std::optional<int> longestChain;
|
||||||
std::optional<int> notarized;
|
std::optional<int> notarized;
|
||||||
|
double balanceScanMs = 0.0; // wall-clock spent in z_gettotalbalance this refresh (0 if balance skipped)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MiningRefreshResult {
|
struct MiningRefreshResult {
|
||||||
@@ -146,6 +150,10 @@ public:
|
|||||||
struct AddressRefreshResult {
|
struct AddressRefreshResult {
|
||||||
std::vector<AddressInfo> shieldedAddresses;
|
std::vector<AddressInfo> shieldedAddresses;
|
||||||
std::vector<AddressInfo> transparentAddresses;
|
std::vector<AddressInfo> transparentAddresses;
|
||||||
|
// False if either address-enumeration RPC (z_listaddresses / getaddressesbyaccount) threw, so the
|
||||||
|
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. the
|
||||||
|
// empty-wallet warning) must not trust a 0 count unless this is true.
|
||||||
|
bool addressListOk = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AddressRefreshSnapshot {
|
struct AddressRefreshSnapshot {
|
||||||
@@ -227,6 +235,7 @@ public:
|
|||||||
RefreshRpcGateway& rpc,
|
RefreshRpcGateway& rpc,
|
||||||
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
|
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
|
||||||
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
|
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
|
||||||
|
const nlohmann::json& spendableBalance,
|
||||||
bool balanceOk,
|
bool balanceOk,
|
||||||
const nlohmann::json& blockInfo,
|
const nlohmann::json& blockInfo,
|
||||||
bool blockOk);
|
bool blockOk);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#include "wallet_security_controller.h"
|
#include "wallet_security_controller.h"
|
||||||
#include "../util/secure_vault.h"
|
#include "../util/secure_vault.h"
|
||||||
|
#include "../util/address_validation.h"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace services {
|
namespace services {
|
||||||
@@ -108,18 +111,35 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
|
|||||||
|
|
||||||
bool WalletSecurityController::isViewingKey(const std::string& key)
|
bool WalletSecurityController::isViewingKey(const std::string& key)
|
||||||
{
|
{
|
||||||
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
|
// DragonX's z_exportviewingkey returns a Sapling *incoming* viewing key (mainnet HRP "zivks");
|
||||||
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
|
// z_importviewingkey only decodes that form. Recognize it structurally — a valid Bech32 checksum
|
||||||
return key.rfind("zxview", 0) == 0;
|
// plus a known HRP — instead of a bare prefix, and cover testnet/regtest too. (The old check
|
||||||
|
// looked for Zcash's "zxview" extended-FVK HRP, which DragonX never emits, so every real viewing
|
||||||
|
// key was rejected client-side.) Watch-only: reveals the address's funds but cannot spend them.
|
||||||
|
const std::string hrp = util::bech32Hrp(key);
|
||||||
|
return hrp == "zivks" // mainnet
|
||||||
|
|| hrp == "zivktestsapling" // testnet
|
||||||
|
|| hrp == "zivkregtestsapling"; // regtest
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||||
{
|
{
|
||||||
|
// Sapling z spending key (HRP "secret-extended-key-{main,test,regtest}"). These run ~300 chars,
|
||||||
|
// past the Bech32 length cap, so match by HRP prefix and let the daemon vet the payload.
|
||||||
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
|
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
|
||||||
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return true; // Sprout z spending key
|
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return true; // Sprout z spending key
|
||||||
// Transparent WIF: base58, ~51-52 chars, common version prefixes.
|
// Transparent WIF: decode Base58Check and confirm it is actually a secret key — version byte plus
|
||||||
if (key.size() >= 51 && key.size() <= 52 &&
|
// a 32-byte key, optionally a compression flag (payload 33 or 34 bytes). This accepts BOTH the
|
||||||
(key[0] == '5' || key[0] == 'K' || key[0] == 'L' || key[0] == 'U')) return true;
|
// compressed ("U…") and uncompressed ("7…") mainnet forms and the testnet form, and rejects
|
||||||
|
// addresses / typos via the real checksum — the old length+first-char heuristic dropped the
|
||||||
|
// uncompressed mainnet key (which starts with '7', not one of 5/K/L/U).
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
if (util::decodeBase58Check(key, payload) &&
|
||||||
|
(payload.size() == 33 || payload.size() == 34) &&
|
||||||
|
(payload[0] == 188 /* DragonX main/regtest SECRET_KEY */ ||
|
||||||
|
payload[0] == 128 /* DragonX testnet SECRET_KEY */)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public:
|
|||||||
std::size_t minLength = 4);
|
std::size_t minLength = 4);
|
||||||
static KeyKind classifyAddress(const std::string& address);
|
static KeyKind classifyAddress(const std::string& address);
|
||||||
static KeyKind classifyPrivateKey(const std::string& key);
|
static KeyKind classifyPrivateKey(const std::string& key);
|
||||||
// True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only).
|
// True if `key` is a shielded viewing key (Sapling incoming viewing key, "zivks…" — watch-only).
|
||||||
static bool isViewingKey(const std::string& key);
|
static bool isViewingKey(const std::string& key);
|
||||||
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
|
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
|
||||||
static bool isRecognizedPrivateKey(const std::string& key);
|
static bool isRecognizedPrivateKey(const std::string& key);
|
||||||
|
|||||||
@@ -160,6 +160,10 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
|
|
||||||
// ---- Gradient Border Shift ----
|
// ---- Gradient Border Shift ----
|
||||||
gradient_border_.enabled = eff("gradient-border-enabled").sizeOr(0.0f) > 0.5f;
|
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_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
||||||
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
||||||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
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,
|
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;
|
if (!enabled_ || !gradient_border_.enabled) return;
|
||||||
|
|
||||||
// Smooth sinusoidal oscillation between color A and color B
|
// 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;
|
// 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
|
// Extract RGBA from both colors and lerp
|
||||||
RGB ca = unpackRGB(gradient_border_.colorA);
|
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 r = ca.r + (int)((cb.r - ca.r) * phase);
|
||||||
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
||||||
int b = ca.b + (int)((cb.b - ca.b) * 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
|
// Draw the shifting border
|
||||||
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
|
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 {
|
float rounding) const {
|
||||||
if (!enabled_ || effects::isLowSpecMode()) return;
|
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
|
// Edge trace on panels — use position-based phase offset so each
|
||||||
// panel's tracer is at a different position around the border
|
// panel's tracer is at a different position around the border
|
||||||
if (edge_trace_.enabled) {
|
if (edge_trace_.enabled) {
|
||||||
|
|||||||
@@ -69,9 +69,12 @@ public:
|
|||||||
void drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
void drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||||
float rounding) const;
|
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,
|
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)
|
/// Draw ember particles that rise from an element (fire theme)
|
||||||
void drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const;
|
void drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const;
|
||||||
@@ -186,6 +189,7 @@ private:
|
|||||||
|
|
||||||
struct GradientBorderConfig {
|
struct GradientBorderConfig {
|
||||||
bool enabled = false;
|
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 speed = 0.15f; ///< full color shift cycles per second
|
||||||
float thickness = 1.5f; ///< border line thickness in pixels
|
float thickness = 1.5f; ///< border line thickness in pixels
|
||||||
float alpha = 0.6f; ///< peak alpha
|
float alpha = 0.6f; ///< peak alpha
|
||||||
|
|||||||
@@ -173,6 +173,24 @@ inline float kSidePanelMinWidth() { return schema::UI().drawElement("panels",
|
|||||||
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
|
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
|
||||||
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
|
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
|
||||||
|
|
||||||
|
// Overall content-column cap: the max width a tab's content occupies before it is centered in wider
|
||||||
|
// windows. <= 0 disables the cap so tab content fills ALL available horizontal width (the default —
|
||||||
|
// requested so large windows don't leave a big empty gutter on the right). Set a positive
|
||||||
|
// ui.toml [layout] content-max-width to re-enable a centered readable column.
|
||||||
|
inline float kContentMaxWidth() { return schema::UI().drawElement("layout", "content-max-width").sizeOr(0.0f) * dpiScale(); }
|
||||||
|
|
||||||
|
// Shared compose-card envelope for the Send + Receive tabs (and any tab wanting the same box): fill the
|
||||||
|
// available column up to the content-max-width cap, then center the leftover as margin. Both tabs MUST
|
||||||
|
// derive their card width/offset from this so the two envelopes stay byte-for-byte identical — they
|
||||||
|
// previously drifted (Send capped at 760dp, Receive at 860dp), so the Send card rendered narrower than
|
||||||
|
// Receive on any window wider than ~860dp. Returns {width, offsetX} in the same units as availW.
|
||||||
|
struct CardBox { float width; float offsetX; };
|
||||||
|
inline CardBox mainComposeCardBox(float availW) {
|
||||||
|
float cap = kContentMaxWidth();
|
||||||
|
float w = (cap > 0.0f) ? std::min(availW, cap) : availW; // cap <= 0 -> fill full width
|
||||||
|
return CardBox{ w, std::max(0.0f, (availW - w) * 0.5f) };
|
||||||
|
}
|
||||||
|
|
||||||
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
|
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
|
||||||
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
|
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,26 @@ inline ImVec4 WarningVec4() { return ImGui::ColorConvertU32ToFloat4(Warni
|
|||||||
// Convenience Functions for Common Patterns
|
// Convenience Functions for Common Patterns
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Theme-aware translucent overlay for tracks / hover fills / dividers.
|
||||||
|
*
|
||||||
|
* A raw white overlay (IM_COL32(255,255,255,a)) reads on dark skins but vanishes
|
||||||
|
* on light/pastel skins (white-on-white). This picks a dark overlay on light
|
||||||
|
* themes and a white overlay on dark themes so the alpha reads either way.
|
||||||
|
* (Self-contained luminance check so colors.h stays free of draw_helpers.h.)
|
||||||
|
*
|
||||||
|
* @param alpha 0-255 opacity of the overlay
|
||||||
|
*/
|
||||||
|
inline ImU32 SurfaceOverlay(int alpha)
|
||||||
|
{
|
||||||
|
ImU32 bg = Background();
|
||||||
|
float r = ((bg >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
||||||
|
float g = ((bg >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
||||||
|
float b = ((bg >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
||||||
|
bool light = (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f;
|
||||||
|
return light ? IM_COL32(0, 0, 0, alpha) : IM_COL32(255, 255, 255, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get color with applied state overlay
|
* @brief Get color with applied state overlay
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -57,6 +57,21 @@ inline ImU32 ReadableError() {
|
|||||||
return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF);
|
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.
|
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
|
||||||
inline const char* LoadingDots() {
|
inline const char* LoadingDots() {
|
||||||
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||||
@@ -64,6 +79,63 @@ inline const char* LoadingDots() {
|
|||||||
return kDots[n];
|
return kDots[n];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Centered empty state ─────────────────────────────────────────────────
|
||||||
|
// A big muted icon + title + optional wrapped hint, centered on BOTH axes within
|
||||||
|
// GetContentRegionAvail(). Mirrors chat_tab's centeredEmptyState so list-empty states
|
||||||
|
// read the same across tabs. Call at the start of the region you want it centered in
|
||||||
|
// (e.g. right after a BeginChild / a leading Dummy). Font metrics use the live font
|
||||||
|
// scale (LegacySize * FontScaleMain) and PushFont draws at that same scale, so this is
|
||||||
|
// crisp at HiDPI / font_scale 1.5 without any manual dpiScale multiply on the metrics.
|
||||||
|
inline void DrawEmptyState(const char* iconGlyph, const char* title, const char* hint = nullptr)
|
||||||
|
{
|
||||||
|
auto scaled = [](ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; };
|
||||||
|
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||||
|
const ImVec2 origin = ImGui::GetCursorPos();
|
||||||
|
ImFont* iconF = Type().iconXL();
|
||||||
|
ImFont* titleF = Type().subtitle1();
|
||||||
|
ImFont* hintF = Type().body2();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
const float gap = 8.0f * dp;
|
||||||
|
const float wrap = std::min(avail.x - 40.0f * dp, 360.0f * dp);
|
||||||
|
const float iconSz = iconF ? scaled(iconF) : 40.0f;
|
||||||
|
const float iconH = (iconF && iconGlyph) ? iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, iconGlyph).y : 0.0f;
|
||||||
|
const float titleH = titleF->CalcTextSizeA(scaled(titleF), FLT_MAX, 0.0f, title).y;
|
||||||
|
const float hintH = hint ? hintF->CalcTextSizeA(scaled(hintF), wrap, wrap, hint).y : 0.0f;
|
||||||
|
const float totalH = iconH + (iconH > 0.0f ? gap : 0.0f) + titleH + (hint ? gap + hintH : 0.0f);
|
||||||
|
float y = origin.y + std::max(0.0f, (avail.y - totalH) * 0.5f);
|
||||||
|
|
||||||
|
if (iconF && iconGlyph && iconGlyph[0]) {
|
||||||
|
const float iw = iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, iconGlyph).x;
|
||||||
|
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - iw) * 0.5f, y));
|
||||||
|
ImGui::PushFont(iconF);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(OnSurface(), 70));
|
||||||
|
ImGui::TextUnformatted(iconGlyph);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
y += iconH + gap;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const float tw = titleF->CalcTextSizeA(scaled(titleF), FLT_MAX, 0.0f, title).x;
|
||||||
|
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - tw) * 0.5f, y));
|
||||||
|
ImGui::PushFont(titleF);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(title);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
y += titleH + gap;
|
||||||
|
}
|
||||||
|
if (hint) {
|
||||||
|
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - wrap) * 0.5f, y));
|
||||||
|
ImGui::PushFont(hintF);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(OnSurface(), 120));
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap);
|
||||||
|
ImGui::TextUnformatted(hint);
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Text Drop Shadow
|
// Text Drop Shadow
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -472,11 +544,16 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0),
|
|||||||
ImVec2 bMin = ImGui::GetItemRectMin();
|
ImVec2 bMin = ImGui::GetItemRectMin();
|
||||||
ImVec2 bMax = ImGui::GetItemRectMax();
|
ImVec2 bMax = ImGui::GetItemRectMax();
|
||||||
|
|
||||||
// For icon fonts, manually draw centered icon after getting button rect
|
// For icon fonts, manually draw centered icon after getting button rect. Measure/draw only the
|
||||||
|
// VISIBLE label (up to the "##id" separator): CalcTextSizeA/AddText don't strip "##" the way
|
||||||
|
// ImGui's own text render does, so an id suffix like "##pickContact" would inflate textSz and
|
||||||
|
// shove the glyph left off-center (and try to draw the notdef id chars).
|
||||||
if (isIconFont && size.x > 0 && size.y > 0) {
|
if (isIconFont && size.x > 0 && size.y > 0) {
|
||||||
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label);
|
const char* labelEnd = label;
|
||||||
|
while (*labelEnd && !(labelEnd[0] == '#' && labelEnd[1] == '#')) ++labelEnd;
|
||||||
|
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label, labelEnd);
|
||||||
ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f);
|
ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f);
|
||||||
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label);
|
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label, labelEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
float rounding = ImGui::GetStyle().FrameRounding;
|
float rounding = ImGui::GetStyle().FrameRounding;
|
||||||
@@ -1347,10 +1424,17 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
|||||||
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
|
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
|
||||||
WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f);
|
WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f);
|
||||||
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, labels[i]);
|
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,
|
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()),
|
active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||||
labels[i]);
|
labels[i]);
|
||||||
|
dl->PopClipRect();
|
||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
ImGui::SetCursorScreenPos(cMin);
|
ImGui::SetCursorScreenPos(cMin);
|
||||||
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;
|
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;
|
||||||
@@ -1381,6 +1465,7 @@ struct OverlayCardState {
|
|||||||
int stableCount = 0; // consecutive frames the height held steady (within 1px)
|
int stableCount = 0; // consecutive frames the height held steady (within 1px)
|
||||||
int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap
|
int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap
|
||||||
bool shown = false; // revealed (centered) at least once this open; don't re-hide after
|
bool shown = false; // revealed (centered) at least once this open; don't re-hide after
|
||||||
|
bool overflow = false; // content once exceeded the viewport → clamp to viewport + scroll (sticky/open)
|
||||||
};
|
};
|
||||||
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
||||||
inline std::string g_overlayCurrentKey;
|
inline std::string g_overlayCurrentKey;
|
||||||
@@ -1506,6 +1591,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
||||||
float cardY, cardBottomY;
|
float cardY, cardBottomY;
|
||||||
bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame
|
bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame
|
||||||
|
bool autoOverflow = false; // auto-height content taller than the viewport → clamp + scroll
|
||||||
const bool fixedHeight = (spec.cardHeight > 0.0f);
|
const bool fixedHeight = (spec.cardHeight > 0.0f);
|
||||||
if (fixedHeight) {
|
if (fixedHeight) {
|
||||||
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
|
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
|
||||||
@@ -1515,9 +1601,16 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
} else {
|
} else {
|
||||||
g_overlayCurrentKey = childId;
|
g_overlayCurrentKey = childId;
|
||||||
OverlayCardState& cs = g_overlayCardHeights[childId];
|
OverlayCardState& cs = g_overlayCardHeights[childId];
|
||||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; }
|
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; cs.overflow = false; }
|
||||||
if (!cs.shown) cs.appearFrames++;
|
if (!cs.shown) cs.appearFrames++;
|
||||||
const float measuredH = cs.height;
|
const float measuredH = cs.height;
|
||||||
|
const float maxCardH = vp_size.y - 32.0f;
|
||||||
|
// Once the measured content is taller than the viewport, lock the card to the viewport height and
|
||||||
|
// let its content child scroll (autoOverflow) so the footer/actions stay reachable. Sticky for this
|
||||||
|
// open: clamping makes next frame's measured height the clamped value, so re-deciding from it would
|
||||||
|
// oscillate — decide once and hold until the dialog re-opens.
|
||||||
|
if (measuredH > maxCardH) cs.overflow = true;
|
||||||
|
autoOverflow = cs.overflow;
|
||||||
// Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's
|
// Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's
|
||||||
// already been shown this open (don't re-hide on a mid-dialog content change); a frame cap
|
// already been shown this open (don't re-hide on a mid-dialog content change); a frame cap
|
||||||
// guarantees a pathological ever-changing height can't hide the dialog forever.
|
// guarantees a pathological ever-changing height can't hide the dialog forever.
|
||||||
@@ -1525,11 +1618,18 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
(cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8);
|
(cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8);
|
||||||
if (ready) {
|
if (ready) {
|
||||||
cs.shown = true;
|
cs.shown = true;
|
||||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
if (autoOverflow) {
|
||||||
cardY = (measuredH < vp_size.y - 32.0f)
|
// Taller than the screen: top-anchor at the 16px margin, clamp to the viewport; the
|
||||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
// content child (below) becomes the scroll region so the footer/actions stay reachable.
|
||||||
: vp_pos.y + 16.0f;
|
cardY = vp_pos.y + 16.0f;
|
||||||
cardBottomY = cardY + measuredH;
|
cardBottomY = cardY + maxCardH;
|
||||||
|
} else {
|
||||||
|
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||||
|
cardY = (measuredH < maxCardH)
|
||||||
|
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||||
|
: vp_pos.y + 16.0f;
|
||||||
|
cardBottomY = cardY + measuredH;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Still settling: lay the content out (so the auto-height child gets measured) but keep
|
// Still settling: lay the content out (so the auto-height child gets measured) but keep
|
||||||
// the card hidden (hideForMeasure below) so it never flashes off-center — it appears,
|
// the card hidden (hideForMeasure below) so it never flashes off-center — it appears,
|
||||||
@@ -1545,7 +1645,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
||||||
if (!floating && !hideForMeasure) {
|
if (!floating && !hideForMeasure) {
|
||||||
GlassPanelSpec cardGlass;
|
GlassPanelSpec cardGlass;
|
||||||
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
|
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
|
||||||
|
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
|
||||||
|
// tiles, chat) while staying translucent rather than opaque.
|
||||||
|
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1562,14 +1665,21 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
||||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (fixedHeight ? 0 : ImGuiChildFlags_AutoResizeY);
|
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
|
||||||
|
// content overflowed the viewport); otherwise the child auto-resizes to its content.
|
||||||
|
const bool clampedCard = fixedHeight || autoOverflow;
|
||||||
|
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (clampedCard ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||||
// NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift
|
// NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift
|
||||||
// the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll
|
// the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll
|
||||||
// the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on
|
// the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on
|
||||||
// their own; auto-height cards resize to content so they never overflow anyway.
|
// their own; auto-height cards resize to content so they normally never overflow — EXCEPT when the
|
||||||
|
// content is taller than the viewport (autoOverflow), where the card itself IS the scroll region.
|
||||||
|
ImGuiWindowFlags childScroll = autoOverflow
|
||||||
|
? ImGuiWindowFlags_None
|
||||||
|
: (ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||||
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
||||||
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
|
ImVec2(cardWidth, clampedCard ? (cardBottomY - cardY) : 0.0f),
|
||||||
cflags, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
cflags, childScroll);
|
||||||
// Floating (portfolio-style) cards: the padding applies to this content child only, so pop it
|
// Floating (portfolio-style) cards: the padding applies to this content child only, so pop it
|
||||||
// now (nested children mustn't inherit it), and center button labels. Net style-var count stays
|
// now (nested children mustn't inherit it), and center button labels. Net style-var count stays
|
||||||
// at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged.
|
// at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged.
|
||||||
@@ -1692,7 +1802,7 @@ inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = Wa
|
|||||||
inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
|
inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
|
||||||
bool danger, bool& outCancel, bool& outConfirm)
|
bool danger, bool& outCancel, bool& outConfirm)
|
||||||
{
|
{
|
||||||
float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f);
|
float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f) * Layout::dpiScale();
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) {
|
if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) {
|
||||||
outCancel = true;
|
outCancel = true;
|
||||||
|
|||||||
195
src/ui/material/settings_controls.h
Normal file
195
src/ui/material/settings_controls.h
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// 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 = 9.0f * dp, gap = 6.0f * dp; // mockup .btn padding: 9px horizontal
|
||||||
|
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 = 7.0f * dp; // mockup .btn radius: 7px (softer than the global 4px frame)
|
||||||
|
const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
|
||||||
|
|
||||||
|
ImU32 bg = 0, border = 0, fg = OnSurface();
|
||||||
|
bool glass = false;
|
||||||
|
switch (tier) {
|
||||||
|
case ActionTier::Primary:
|
||||||
|
// Mockup .btn.acc: a dark accent-tinted chip with accent TEXT — not a bright filled button.
|
||||||
|
bg = WithAlpha(Primary(), hov ? 52 : 38);
|
||||||
|
border = WithAlpha(Primary(), hov ? 150 : 110);
|
||||||
|
fg = Primary();
|
||||||
|
break;
|
||||||
|
case ActionTier::Secondary:
|
||||||
|
bg = WithAlpha(OnSurface(), hov ? 30 : 20);
|
||||||
|
border = WithAlpha(OnSurface(), 48);
|
||||||
|
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
|
||||||
@@ -15,6 +15,11 @@
|
|||||||
#include "../embedded/IconsMaterialDesign.h" // Icon codepoint defines
|
#include "../embedded/IconsMaterialDesign.h" // Icon codepoint defines
|
||||||
#include "../../util/logger.h"
|
#include "../../util/logger.h"
|
||||||
|
|
||||||
|
#ifdef DRAGONX_HAVE_FREETYPE
|
||||||
|
#include "imgui_internal.h" // ImFontAtlasGetFontLoaderForStbTruetype
|
||||||
|
#include "misc/freetype/imgui_freetype.h" // ImGuiFreeType::GetFontLoader + LoadColor flag
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
namespace material {
|
namespace material {
|
||||||
@@ -126,6 +131,15 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
|||||||
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, userFontScale: %.2f, combined: %.2f)\n",
|
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, userFontScale: %.2f, combined: %.2f)\n",
|
||||||
dpiScale, Layout::kFontScale(), Layout::userFontScale(), scale);
|
dpiScale, Layout::kFontScale(), Layout::userFontScale(), scale);
|
||||||
|
|
||||||
|
#ifdef DRAGONX_HAVE_FREETYPE
|
||||||
|
// Choose the atlas font loader BEFORE any font is added: FreeType (required to rasterize COLR
|
||||||
|
// color-emoji glyphs) when color emoji is enabled, else the default stb_truetype loader. Toggling
|
||||||
|
// the setting + reload() flips this cleanly.
|
||||||
|
io.Fonts->SetFontLoader(color_emoji_ ? ImGuiFreeType::GetFontLoader()
|
||||||
|
: ImFontAtlasGetFontLoaderForStbTruetype());
|
||||||
|
DEBUG_LOGF("Typography: font loader = %s\n", color_emoji_ ? "FreeType (color emoji)" : "stb_truetype");
|
||||||
|
#endif
|
||||||
|
|
||||||
// For ImGui, we need to load fonts at specific pixel sizes.
|
// For ImGui, we need to load fonts at specific pixel sizes.
|
||||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||||
|
|
||||||
@@ -325,6 +339,63 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
|||||||
name, g_noto_cjk_subset_size);
|
name, g_noto_cjk_subset_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge monochrome emoji for chat + user text (Q12). Only into the small text fonts — baking
|
||||||
|
// ~1400 emoji at heading sizes would bloat the atlas for glyphs no heading needs. ImGui does no
|
||||||
|
// shaping, so single-codepoint emoji render but ZWJ sequences / flags won't compose. The
|
||||||
|
// >0xFFFF ranges require IMGUI_USE_WCHAR32 (imconfig.h).
|
||||||
|
static const char* const kEmojiFonts[] = {
|
||||||
|
"Body1", "Body2", "Subtitle1", "Subtitle2", "Caption", "Overline", "Button", "ButtonSm"
|
||||||
|
};
|
||||||
|
bool wantEmoji = false;
|
||||||
|
for (const char* n : kEmojiFonts) if (strcmp(name, n) == 0) { wantEmoji = true; break; }
|
||||||
|
|
||||||
|
// Emoji blob: the COLR/CPAL color font (FreeType-rendered) when color emoji is enabled and this
|
||||||
|
// is a FreeType build, else the monochrome subset (default / non-FreeType path).
|
||||||
|
const unsigned char* emojiData = g_noto_emoji_subset_data;
|
||||||
|
unsigned int emojiSize = g_noto_emoji_subset_size;
|
||||||
|
bool colorGlyphs = false;
|
||||||
|
#ifdef DRAGONX_HAVE_FREETYPE
|
||||||
|
if (color_emoji_ && g_twemoji_color_size > 0) {
|
||||||
|
emojiData = g_twemoji_color_data;
|
||||||
|
emojiSize = g_twemoji_color_size;
|
||||||
|
colorGlyphs = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (wantEmoji && emojiSize > 0) {
|
||||||
|
void* emojiCopy = IM_ALLOC(emojiSize);
|
||||||
|
memcpy(emojiCopy, emojiData, emojiSize);
|
||||||
|
|
||||||
|
ImFontConfig emojiCfg;
|
||||||
|
emojiCfg.FontDataOwnedByAtlas = true;
|
||||||
|
emojiCfg.MergeMode = true; // merge into the text font just loaded
|
||||||
|
emojiCfg.OversampleH = 1;
|
||||||
|
emojiCfg.OversampleV = 1;
|
||||||
|
emojiCfg.PixelSnapH = true;
|
||||||
|
emojiCfg.GlyphMinAdvanceX = 0;
|
||||||
|
#ifdef DRAGONX_HAVE_FREETYPE
|
||||||
|
if (colorGlyphs) emojiCfg.FontLoaderFlags |= ImGuiFreeTypeLoaderFlags_LoadColor; // render COLR in color
|
||||||
|
#endif
|
||||||
|
// The base Ubuntu font already owns U+2600–26FF etc.; MergeMode keeps the first-loaded glyph,
|
||||||
|
// so its text-style symbols win and only the codepoints it lacks fall through to emoji.
|
||||||
|
static const ImWchar emojiRanges[] = {
|
||||||
|
0x2600, 0x27BF, // Misc Symbols + Dingbats
|
||||||
|
0x2B00, 0x2BFF, // stars (⭐) + arrows
|
||||||
|
0x1F000, 0x1FAFF, // emoji planes (emoticons, pictographs, transport, supplement, extended)
|
||||||
|
0,
|
||||||
|
};
|
||||||
|
emojiCfg.GlyphRanges = emojiRanges;
|
||||||
|
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)",
|
||||||
|
colorGlyphs ? "Twemoji" : "NotoEmoji", size);
|
||||||
|
|
||||||
|
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, emojiSize, size, &emojiCfg);
|
||||||
|
if (emojiMerge) {
|
||||||
|
DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n",
|
||||||
|
colorGlyphs ? "color" : "mono", emojiSize, name);
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
||||||
IM_FREE(fontDataCopy);
|
IM_FREE(fontDataCopy);
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
float getDpiScale() const { return dpiScale_; }
|
float getDpiScale() const { return dpiScale_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select color vs monochrome emoji for the next (re)load. Color needs a FreeType-enabled
|
||||||
|
* build (DRAGONX_HAVE_FREETYPE); otherwise this is inert and monochrome is always used.
|
||||||
|
* Set before load()/reload() (App does this from the chat_emoji_color setting).
|
||||||
|
*/
|
||||||
|
void setColorEmoji(bool enabled) { color_emoji_ = enabled; }
|
||||||
|
bool colorEmoji() const { return color_emoji_; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get font for a type style
|
* @brief Get font for a type style
|
||||||
*
|
*
|
||||||
@@ -261,6 +269,7 @@ private:
|
|||||||
|
|
||||||
bool loaded_ = false;
|
bool loaded_ = false;
|
||||||
float dpiScale_ = 1.0f;
|
float dpiScale_ = 1.0f;
|
||||||
|
bool color_emoji_ = false; // when true + FreeType present, merge the COLR color-emoji font
|
||||||
|
|
||||||
// Fonts for each type style
|
// Fonts for each type style
|
||||||
ImFont* fonts_[15] = {};
|
ImFont* fonts_[15] = {};
|
||||||
|
|||||||
111
src/ui/node_status_banner.h
Normal file
111
src/ui/node_status_banner.h
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Persistent node-connectivity banner shown at the top of the content column when the wallet
|
||||||
|
// cannot reach its node. Distinct from the transient toast notifications: it stays visible for
|
||||||
|
// as long as the fault persists, so an offline wallet is never silently mistaken for a working
|
||||||
|
// one. The decision (whether to show, how severe, which action) is a pure function of a state
|
||||||
|
// snapshot so it can be unit-tested; App::renderNodeStatusBanner() feeds it the live state and
|
||||||
|
// draws the strip. See src/app.cpp.
|
||||||
|
namespace dragonx::ui {
|
||||||
|
|
||||||
|
// Visual weight. Warning (amber) = recoverable / a reconnect is offered; Error (red) = a hard
|
||||||
|
// fault the user must act on (the daemon gave up crashing, or a lite wallet failed to open).
|
||||||
|
enum class NodeBannerSeverity {
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
// What the banner's action button does. App maps this to the concrete call.
|
||||||
|
enum class NodeBannerAction {
|
||||||
|
None, // no button — nothing the user can usefully do from here
|
||||||
|
Reconnect, // full node: re-run the RPC connect state machine (App::tryConnect)
|
||||||
|
RestartNode, // full node: the embedded daemon crashed & auto-restart gave up (App::restartDaemon)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Why the banner is up. App maps this to a translated headline; `detail` carries the live,
|
||||||
|
// already-human-readable status text (connection_status_ / daemon lastError / lite open error).
|
||||||
|
enum class NodeBannerReason {
|
||||||
|
None,
|
||||||
|
FullNodeOffline, // a reachable node was lost, or never came up; reconnect offered
|
||||||
|
DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped
|
||||||
|
LiteOpenFailed, // lite build: the wallet failed to open
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NodeBannerState {
|
||||||
|
bool show = false;
|
||||||
|
NodeBannerSeverity severity = NodeBannerSeverity::Warning;
|
||||||
|
NodeBannerReason reason = NodeBannerReason::None;
|
||||||
|
NodeBannerAction action = NodeBannerAction::None;
|
||||||
|
std::string detail; // passthrough status/error text (may be empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Snapshot of the connection state the banner reads. Plain values so the decision is testable
|
||||||
|
// without an App instance.
|
||||||
|
struct NodeBannerInputs {
|
||||||
|
bool lite = false; // lite build (no embedded daemon / RPC)
|
||||||
|
bool connected = false; // state_.connected — the master "online" flag
|
||||||
|
bool warming_up = false; // daemon reachable, RPC warmup (code -28)
|
||||||
|
bool daemon_initializing = false; // daemon launching / block index loading
|
||||||
|
bool connection_in_progress = false; // a connect attempt is actively running
|
||||||
|
|
||||||
|
// Full-node embedded-daemon crash signal.
|
||||||
|
bool using_embedded_daemon = false;
|
||||||
|
bool has_daemon_controller = false;
|
||||||
|
bool daemon_running = false;
|
||||||
|
int daemon_crash_count = 0;
|
||||||
|
|
||||||
|
std::string connection_status; // human-readable status line (already translated)
|
||||||
|
std::string daemon_last_error; // DaemonController::lastError() (may be empty)
|
||||||
|
std::string lite_open_error; // lite: last wallet-open failure reason
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-restart give-up threshold — mirrors the crash cap in app_network.cpp's connect loop.
|
||||||
|
inline constexpr int kNodeBannerCrashGiveUpCount = 3;
|
||||||
|
|
||||||
|
inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
|
||||||
|
NodeBannerState s;
|
||||||
|
|
||||||
|
if (in.lite) {
|
||||||
|
// Lite has no daemon/RPC; "online" == wallet open. Only a genuine open failure is a
|
||||||
|
// fault worth a persistent banner (a not-yet-created wallet is handled by the normal
|
||||||
|
// "No wallet open" prompt, and leaves lite_open_error empty).
|
||||||
|
if (!in.connected && !in.lite_open_error.empty()) {
|
||||||
|
s.show = true;
|
||||||
|
s.severity = NodeBannerSeverity::Error;
|
||||||
|
s.reason = NodeBannerReason::LiteOpenFailed;
|
||||||
|
s.action = NodeBannerAction::None;
|
||||||
|
s.detail = in.lite_open_error;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full node. Connected, or in an expected startup phase → the loading/warmup overlay owns
|
||||||
|
// the screen, so no banner. An active connect attempt likewise shows progress, not an
|
||||||
|
// error — don't flicker a banner over it.
|
||||||
|
if (in.connected) return s;
|
||||||
|
if (in.warming_up || in.daemon_initializing) return s;
|
||||||
|
if (in.connection_in_progress) return s;
|
||||||
|
|
||||||
|
// Genuinely offline. Distinguish "the embedded daemon crashed and we stopped retrying" (a
|
||||||
|
// hard fault needing a manual restart) from an ordinary lost/failed connection (retryable).
|
||||||
|
if (in.using_embedded_daemon && in.has_daemon_controller && !in.daemon_running &&
|
||||||
|
in.daemon_crash_count >= kNodeBannerCrashGiveUpCount) {
|
||||||
|
s.show = true;
|
||||||
|
s.severity = NodeBannerSeverity::Error;
|
||||||
|
s.reason = NodeBannerReason::DaemonCrashed;
|
||||||
|
s.action = NodeBannerAction::RestartNode;
|
||||||
|
s.detail = !in.daemon_last_error.empty() ? in.daemon_last_error : in.connection_status;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.show = true;
|
||||||
|
s.severity = NodeBannerSeverity::Warning;
|
||||||
|
s.reason = NodeBannerReason::FullNodeOffline;
|
||||||
|
s.action = NodeBannerAction::Reconnect;
|
||||||
|
s.detail = in.connection_status;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::ui
|
||||||
@@ -32,19 +32,22 @@ void Notifications::render()
|
|||||||
return v >= 0 ? v : fb;
|
return v >= 0 ? v : fb;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Status bar geometry
|
// Status bar geometry. These are logical-px schema values; the icon/text drawn into the pill
|
||||||
float sbHeight = S.window("components.status-bar").height;
|
// are DPI-baked, so scale the box by dpiScale to match the (also DPI-scaled) rendered status bar
|
||||||
if (sbHeight <= 0.0f) sbHeight = 30.0f;
|
// and keep the icon/text inside the pill at HiDPI.
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
float sbHeight = S.window("components.status-bar").height * dp;
|
||||||
|
if (sbHeight <= 0.0f) sbHeight = 30.0f * dp;
|
||||||
|
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
float viewBottom = viewport->WorkPos.y + viewport->WorkSize.y;
|
float viewBottom = viewport->WorkPos.y + viewport->WorkSize.y;
|
||||||
float viewCenterX = viewport->WorkPos.x + viewport->WorkSize.x * 0.5f;
|
float viewCenterX = viewport->WorkPos.x + viewport->WorkSize.x * 0.5f;
|
||||||
|
|
||||||
// Toast pill sizing — fit inside status bar with margin
|
// Toast pill sizing — fit inside status bar with margin
|
||||||
float pillMarginY = nde("pill-margin-y", 3.0f);
|
float pillMarginY = nde("pill-margin-y", 3.0f) * dp;
|
||||||
float pillHeight = sbHeight - pillMarginY * 2.0f;
|
float pillHeight = sbHeight - pillMarginY * 2.0f;
|
||||||
float pillPadX = nde("padding-x", 12.0f);
|
float pillPadX = nde("padding-x", 12.0f) * dp;
|
||||||
float pillRounding = nde("pill-rounding", 12.0f);
|
float pillRounding = nde("pill-rounding", 12.0f) * dp;
|
||||||
|
|
||||||
// Get accent color based on type — resolved from theme palette
|
// Get accent color based on type — resolved from theme palette
|
||||||
ImVec4 accent_color, text_color;
|
ImVec4 accent_color, text_color;
|
||||||
@@ -89,7 +92,7 @@ void Notifications::render()
|
|||||||
ImFont* textFont = material::Type().caption();
|
ImFont* textFont = material::Type().caption();
|
||||||
ImFont* iconFont = material::Type().iconSmall();
|
ImFont* iconFont = material::Type().iconSmall();
|
||||||
float iconW = iconFont ? iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0.0f, icon).x : 0.0f;
|
float iconW = iconFont ? iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0.0f, icon).x : 0.0f;
|
||||||
float iconGap = 4.0f;
|
float iconGap = 4.0f * dp;
|
||||||
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
||||||
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
||||||
// Clamp to reasonable bounds
|
// Clamp to reasonable bounds
|
||||||
@@ -122,7 +125,7 @@ void Notifications::render()
|
|||||||
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
||||||
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
||||||
// bottom-left progress strip so both bottom corners are respected.
|
// bottom-left progress strip so both bottom corners are respected.
|
||||||
float progH = nde("progress-bar-height", 2.0f);
|
float progH = nde("progress-bar-height", 2.0f) * dp;
|
||||||
float progW = pillWidth * (1.0f - progress);
|
float progW = pillWidth * (1.0f - progress);
|
||||||
if (progW > 0.0f) {
|
if (progW > 0.0f) {
|
||||||
ImVec2 clipMin(pillX, pMax.y - progH);
|
ImVec2 clipMin(pillX, pMax.y - progH);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
#include "schema/ui_schema.h"
|
#include "schema/ui_schema.h"
|
||||||
|
|
||||||
@@ -22,6 +24,17 @@ enum class NotificationType {
|
|||||||
Error
|
Error
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A retained alert for the persistent history panel. Unlike a live Notification (which fades and is
|
||||||
|
// erased within seconds), this keeps a wall-clock epoch so its age can be shown as "3m ago" long
|
||||||
|
// after the toast is gone. See App::renderAlertHistoryPanel.
|
||||||
|
struct AlertRecord {
|
||||||
|
std::string message;
|
||||||
|
NotificationType type;
|
||||||
|
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display
|
||||||
|
std::function<void()> onClick; // optional: makes this bell-panel entry actionable
|
||||||
|
std::string actionHint; // optional: accent link label rendered for the action
|
||||||
|
};
|
||||||
|
|
||||||
struct Notification {
|
struct Notification {
|
||||||
std::string message;
|
std::string message;
|
||||||
NotificationType type;
|
NotificationType type;
|
||||||
@@ -82,9 +95,29 @@ public:
|
|||||||
push(message, NotificationType::Error, duration);
|
push(message, NotificationType::Error, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void push(const std::string& message, NotificationType type, float duration = 5.0f) {
|
// An actionable alert: a normal toast PLUS a clickable entry in the bell/alert-history panel.
|
||||||
|
// onClick fires when the user clicks the accent `actionHint` link in that panel.
|
||||||
|
void action(const std::string& message, NotificationType type, std::function<void()> onClick,
|
||||||
|
const std::string& actionHint, float duration = -1.0f) {
|
||||||
|
if (duration < 0.0f) duration = schemaDuration("duration-warning", 3.5f);
|
||||||
|
push(message, type, duration, std::move(onClick), actionHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void push(const std::string& message, NotificationType type, float duration = 5.0f,
|
||||||
|
std::function<void()> onClick = nullptr, const std::string& actionHint = "") {
|
||||||
notifications_.emplace_back(message, type, duration);
|
notifications_.emplace_back(message, type, duration);
|
||||||
|
|
||||||
|
// Retain a copy in the persistent history (the toast above will fade in seconds; this
|
||||||
|
// survives so the user can review what happened). Thread note: every push is on the UI
|
||||||
|
// thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock,
|
||||||
|
// consistent with the rest of this class. Do NOT push from a raw worker thread.
|
||||||
|
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr)),
|
||||||
|
std::move(onClick), actionHint});
|
||||||
|
++total_pushed_;
|
||||||
|
while (history_.size() > kMaxHistory) {
|
||||||
|
history_.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
// Log errors and warnings (debug-only output)
|
// Log errors and warnings (debug-only output)
|
||||||
if (type == NotificationType::Error) {
|
if (type == NotificationType::Error) {
|
||||||
DEBUG_LOGF("[ERROR] Notification: %s\n", message.c_str());
|
DEBUG_LOGF("[ERROR] Notification: %s\n", message.c_str());
|
||||||
@@ -127,6 +160,15 @@ public:
|
|||||||
max_notifications_ = max;
|
max_notifications_ = max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Persistent alert history (for the status-bar bell panel) ──
|
||||||
|
/// Retained alerts, oldest first (capped at kMaxHistory; the toast deque is separate).
|
||||||
|
const std::deque<AlertRecord>& history() const { return history_; }
|
||||||
|
bool hasHistory() const { return !history_.empty(); }
|
||||||
|
void clearHistory() { history_.clear(); }
|
||||||
|
/// Monotonic count of every alert ever pushed this session — survives capping/clearing, so it is
|
||||||
|
/// the correct basis for an "unseen since last opened" count (deque size is not).
|
||||||
|
std::uint64_t totalPushed() const { return total_pushed_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Notifications() = default;
|
Notifications() = default;
|
||||||
~Notifications() = default;
|
~Notifications() = default;
|
||||||
@@ -137,6 +179,10 @@ private:
|
|||||||
size_t max_notifications_ = 5;
|
size_t max_notifications_ = 5;
|
||||||
std::function<void(const std::string&, bool)> console_callback_;
|
std::function<void(const std::string&, bool)> console_callback_;
|
||||||
|
|
||||||
|
std::deque<AlertRecord> history_;
|
||||||
|
std::uint64_t total_pushed_ = 0;
|
||||||
|
static constexpr size_t kMaxHistory = 100;
|
||||||
|
|
||||||
static float schemaDuration(const char* key, float fallback) {
|
static float schemaDuration(const char* key, float fallback) {
|
||||||
float v = schema::UI().drawElement("components.notifications", key).size;
|
float v = schema::UI().drawElement("components.notifications", key).size;
|
||||||
return v > 0.0f ? v : fallback;
|
return v > 0.0f ? v : fallback;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -149,6 +149,7 @@ struct SidebarStatus {
|
|||||||
int unconfirmedTxCount = 0; // badge on History
|
int unconfirmedTxCount = 0; // badge on History
|
||||||
bool miningActive = false; // green dot on Mining
|
bool miningActive = false; // green dot on Mining
|
||||||
int peerCount = 0; // badge on Peers
|
int peerCount = 0; // badge on Peers
|
||||||
|
int chatUnreadCount = 0; // badge on Chat (unread incoming messages)
|
||||||
// Exit
|
// Exit
|
||||||
bool exitClicked = false;
|
bool exitClicked = false;
|
||||||
// Branding logo (optional — loaded at startup)
|
// Branding logo (optional — loaded at startup)
|
||||||
@@ -537,11 +538,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
float exitRelY = curY + bottomPadding;
|
float exitRelY = curY + bottomPadding;
|
||||||
float panelH = exitRelY + stripH;
|
float panelH = exitRelY + stripH;
|
||||||
|
|
||||||
// Vertical centering — offset so panel is centered in the child window
|
|
||||||
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
|
|
||||||
if (centerOffset + panelH > contentHeight)
|
|
||||||
centerOffset = std::max(0.0f, contentHeight - panelH);
|
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// PASS 2: Render using computed positions
|
// PASS 2: Render using computed positions
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -551,6 +547,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImVec2 wp = ImGui::GetWindowPos();
|
ImVec2 wp = ImGui::GetWindowPos();
|
||||||
|
|
||||||
|
// Vertical centering — center the panel within the child. app.cpp sizes the child (contentHeight)
|
||||||
|
// to the visible area (child top -> status-bar top) using window-local geometry, so this yields
|
||||||
|
// equal top/bottom gaps at any height on every platform, no viewport dependency.
|
||||||
|
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
|
||||||
|
if (centerOffset + panelH > contentHeight)
|
||||||
|
centerOffset = std::max(0.0f, contentHeight - panelH);
|
||||||
|
|
||||||
float panelLeft = wp.x + glassMarginL;
|
float panelLeft = wp.x + glassMarginL;
|
||||||
float panelRight = wp.x + sidebarWidth - glassMarginR;
|
float panelRight = wp.x + sidebarWidth - glassMarginR;
|
||||||
float panelTopY = wp.y + centerOffset;
|
float panelTopY = wp.y + centerOffset;
|
||||||
@@ -675,17 +678,33 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
|
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
|
||||||
|
|
||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
|
// Reserve room for a badge (if this item will draw one) so the
|
||||||
|
// label centers in the space to the left of it instead of
|
||||||
|
// running underneath the badge circle.
|
||||||
|
bool itemHasBadge =
|
||||||
|
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
|
||||||
|
(item.page == NavPage::Mining && status.miningActive) ||
|
||||||
|
(item.page == NavPage::Peers && status.peerCount > 0) ||
|
||||||
|
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
|
||||||
|
float badgeReserve = 0.0f;
|
||||||
|
if (itemHasBadge) {
|
||||||
|
bool dotOnlyReserve = (item.page == NavPage::Mining);
|
||||||
|
float badgeRReserve = dotOnlyReserve ? badgeRadiusDot : badgeRadiusNumber;
|
||||||
|
float badgeInsetXReserve = sde("badge-inset-x", 6.0f);
|
||||||
|
badgeReserve = badgeRReserve * 2.0f + badgeInsetXReserve;
|
||||||
|
}
|
||||||
|
|
||||||
ImFont* font = selected ? Type().subtitle2() : Type().body2();
|
ImFont* font = selected ? Type().subtitle2() : Type().body2();
|
||||||
float lblFsz = ScaledFontSize(font);
|
float lblFsz = ScaledFontSize(font);
|
||||||
float btnW = indMax.x - indMin.x;
|
float btnW = indMax.x - indMin.x;
|
||||||
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2;
|
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2 - badgeReserve;
|
||||||
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
||||||
if (labelSz.x > maxLabelW && maxLabelW > 0) {
|
if (labelSz.x > maxLabelW && maxLabelW > 0) {
|
||||||
lblFsz *= maxLabelW / labelSz.x;
|
lblFsz *= maxLabelW / labelSz.x;
|
||||||
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
||||||
}
|
}
|
||||||
float totalW = iconS * 2.0f + iconLabelGap + labelSz.x;
|
float totalW = iconS * 2.0f + iconLabelGap + labelSz.x;
|
||||||
float btnCX = (indMin.x + indMax.x) * 0.5f;
|
float btnCX = (indMin.x + indMax.x - badgeReserve) * 0.5f;
|
||||||
float startX = btnCX - totalW * 0.5f;
|
float startX = btnCX - totalW * 0.5f;
|
||||||
|
|
||||||
DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol);
|
DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol);
|
||||||
@@ -715,12 +734,16 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
dotOnly = true; badgeCol = Success();
|
dotOnly = true; badgeCol = Success();
|
||||||
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
|
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
|
||||||
badgeCount = status.peerCount;
|
badgeCount = status.peerCount;
|
||||||
|
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
|
||||||
|
badgeCount = status.chatUnreadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (badgeCount > 0 || dotOnly) {
|
if (badgeCount > 0 || dotOnly) {
|
||||||
float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber;
|
float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber;
|
||||||
float bx = indMax.x - badgeR - 6.0f;
|
float badgeInsetX = sde("badge-inset-x", 6.0f);
|
||||||
float by = indMin.y + badgeR + 5.0f;
|
float badgeInsetY = sde("badge-inset-y", 5.0f);
|
||||||
|
float bx = indMax.x - badgeR - badgeInsetX;
|
||||||
|
float by = indMin.y + badgeR + badgeInsetY;
|
||||||
dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol);
|
dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol);
|
||||||
if (!dotOnly && showLabels) {
|
if (!dotOnly && showLabels) {
|
||||||
char buf[16];
|
char buf[16];
|
||||||
|
|||||||
48
src/ui/staleness_badge.h
Normal file
48
src/ui/staleness_badge.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Refresh-staleness badge (finding W6-2). The wallet stamps WalletState::last_balance_update only on
|
||||||
|
// a *successful* balance fetch (see services/network_refresh_service.cpp), so a busy daemon that fails
|
||||||
|
// z_gettotalbalance without dropping the whole connection leaves the old balance on screen with a
|
||||||
|
// frozen timestamp — and the node-status banner (which only fires on a full disconnect) stays hidden.
|
||||||
|
// This badge is the surface that reflects that "connected but the number may be out of date" state.
|
||||||
|
//
|
||||||
|
// The decision is a pure function of (last-success timestamp, now, connected) so it is unit-testable;
|
||||||
|
// balance_tab.cpp draws the pill. Both use the same std::time(nullptr) wall-clock the refresh path
|
||||||
|
// stamps with, so age = now - last_update is consistent.
|
||||||
|
namespace dragonx::ui {
|
||||||
|
|
||||||
|
enum class StalenessSeverity {
|
||||||
|
Warning, // amber — noticeably behind
|
||||||
|
Error, // red — very stale, something is likely wrong
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StalenessBadge {
|
||||||
|
bool show = false;
|
||||||
|
StalenessSeverity severity = StalenessSeverity::Warning;
|
||||||
|
std::int64_t seconds_old = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Balance refreshes every ~2s on the Overview profile (and ~10s while syncing), so tens of seconds
|
||||||
|
// with no successful update means refreshes are failing, not merely slow.
|
||||||
|
inline constexpr std::int64_t kStaleAfterSeconds = 45;
|
||||||
|
inline constexpr std::int64_t kVeryStaleAfterSeconds = 180;
|
||||||
|
|
||||||
|
inline StalenessBadge evaluateStalenessBadge(std::int64_t last_update, std::int64_t now, bool connected) {
|
||||||
|
StalenessBadge b;
|
||||||
|
// Offline is the node-status banner's job; don't double up. A zero stamp means "never updated
|
||||||
|
// this session" (fresh start) or "reset on disconnect" — nothing to be stale about yet.
|
||||||
|
if (!connected || last_update <= 0) return b;
|
||||||
|
|
||||||
|
std::int64_t age = now - last_update;
|
||||||
|
if (age < 0) age = 0; // clock skew guard
|
||||||
|
if (age < kStaleAfterSeconds) return b;
|
||||||
|
|
||||||
|
b.show = true;
|
||||||
|
b.seconds_old = age;
|
||||||
|
b.severity = (age >= kVeryStaleAfterSeconds) ? StalenessSeverity::Error : StalenessSeverity::Warning;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::ui
|
||||||
@@ -125,29 +125,26 @@ void RenderAboutDialog(App* app, bool* p_open)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("about_license_text"));
|
ImGui::TextWrapped("%s", TR("about_license_text"));
|
||||||
|
|
||||||
ImGui::Spacing();
|
// Links — 3-button action row, centered via the shared footer helper (draws its own
|
||||||
ImGui::Separator();
|
// Spacing/Separator/Spacing above the row, replacing the hand-rolled divider block).
|
||||||
ImGui::Spacing();
|
const float linksTotalW = linkW * 3.0f + ImGui::GetStyle().ItemSpacing.x * 2.0f;
|
||||||
|
material::BeginOverlayDialogFooter(linksTotalW);
|
||||||
// Links
|
if (material::TactileButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||||
if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
|
||||||
util::Platform::openUrl("https://dragonx.is");
|
util::Platform::openUrl("https://dragonx.is");
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::StyledButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
if (material::TactileButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
|
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
if (material::TactileButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
// Close button — lone dismiss action, centered via the shared footer helper (no extra
|
||||||
|
// divider above it, so it sits directly under the links row).
|
||||||
// Close button
|
material::BeginOverlayDialogFooter(closeW, false);
|
||||||
float button_width = closeW;
|
if (material::TactileButton(TR("close"), ImVec2(closeW, 0), S.resolveFont(closeBtn.font))) {
|
||||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
|
|
||||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
|
||||||
*p_open = false;
|
*p_open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,13 @@ public:
|
|||||||
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
|
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
|
||||||
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
|
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp);
|
||||||
|
// Scrollbar visible (not NoScrollbar) — the icon set exceeds the fixed-height
|
||||||
|
// grid, so a real scrollbar is the discoverable way to reach the rest.
|
||||||
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
|
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
|
||||||
ImGuiWindowFlags_NoScrollbar);
|
ImGuiWindowFlags_NoScrollWithMouse);
|
||||||
|
ApplySmoothScroll();
|
||||||
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
|
||||||
@@ -185,6 +190,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
if (ImGui::GetCursorPosY() < controlsTopY) {
|
if (ImGui::GetCursorPosY() < controlsTopY) {
|
||||||
|
|||||||
@@ -169,9 +169,9 @@ public:
|
|||||||
}
|
}
|
||||||
if (amountValid && newFromBal < 1e-9) {
|
if (amountValid && newFromBal < 1e-9) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
// Full-balance send: same warning-icon treatment as the de-shielding header above,
|
||||||
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
|
// so this stakes-bearing line reads as distinct from the neutral preview text.
|
||||||
ImGui::PopStyleColor();
|
DialogWarningHeader(TR("sends_full_balance_warning"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
@@ -180,16 +180,14 @@ public:
|
|||||||
const char* sendingLabel = TR("sending");
|
const char* sendingLabel = TR("sending");
|
||||||
ImFont* buttonFont = Type().button();
|
ImFont* buttonFont = Type().button();
|
||||||
float buttonFontSize = ScaledFontSize(buttonFont);
|
float buttonFontSize = ScaledFontSize(buttonFont);
|
||||||
float minBtnW = 120.0f * dp;
|
|
||||||
float confirmMinW = 160.0f * dp;
|
float confirmMinW = 160.0f * dp;
|
||||||
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
|
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
|
||||||
float cancelW = std::max(minBtnW,
|
// Both footer buttons share one width (equal-width primary/Close pair), sized to fit the
|
||||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
|
// widest label — the "Sending…" swap label included — so nothing clips.
|
||||||
float confirmTextW = std::max(
|
float confirmTextW = std::max(
|
||||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
|
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
|
||||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
|
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
|
||||||
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
|
float btnW = std::max(confirmMinW, confirmTextW + buttonPadW);
|
||||||
float totalW = cancelW + confirmW + Layout::spacingMd();
|
|
||||||
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed
|
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed
|
||||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
|
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
|
||||||
@@ -201,19 +199,18 @@ public:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
// Standardized primary + Close footer (centered, no divider). The primary is the
|
||||||
|
// Confirm/"Sending…" action; the Close button dismisses the dialog.
|
||||||
|
bool outConfirm = false;
|
||||||
|
bool outClose = false;
|
||||||
|
DialogActionFooter(s_sending ? sendingLabel : confirmLabel,
|
||||||
|
amountValid && !s_sending,
|
||||||
|
cancelLabel, outConfirm, outClose, btnW);
|
||||||
|
|
||||||
float rowStartX = ImGui::GetCursorPosX();
|
if (outClose) {
|
||||||
float contentW = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
|
|
||||||
|
|
||||||
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
|
|
||||||
s_open = false;
|
s_open = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
if (outConfirm) {
|
||||||
|
|
||||||
ImGui::BeginDisabled(!amountValid || s_sending);
|
|
||||||
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
|
|
||||||
s_sending = true;
|
s_sending = true;
|
||||||
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
||||||
amount, s_fee, "",
|
amount, s_fee, "",
|
||||||
@@ -231,7 +228,6 @@ public:
|
|||||||
// state, and when the async callback sets s_resultMsg the in-dialog result screen shows
|
// state, and when the async callback sets s_resultMsg the in-dialog result screen shows
|
||||||
// (with its own Close button). Previously closing here made that result screen dead code.
|
// (with its own Close button). Previously closing here made that result screen dead code.
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user